Annotations

The Dari framework provides Java annotations that enhances the behavior or the processing of Model classes or fields. For example, the @DisplayName annotation substitutes descriptive names in place of actual field or class names in the UI. The @Indexed annotation triggers runtime indexing of fields or methods so that they can be queried.

This topic shows you how to use Dari annotations. For a comprehensive list of Dari annotations, see the Dari Javadoc. Refer to “Annotation Types” in the com.psddev.dari.db and com.psddev.dari.util packages.

As shown in the following example, a class annotation is placed above the class declaration, whereas a field annotation can be placed above the target field or beside the field declaration.

@Recordable.BootstrapPackages("System Activities")
public class Activity extends Record {

   @Indexed
   private Date activityDate;

   @Required private User activityUser;

   private Project project;
   private String type;
   ...

 }

@LazyLoad

Applies to: Class

Denotes that the objects referenced by fields in the target type are lazily loaded. For example, to facilitate smooth browsing through a photo gallery with several images, image initialization is deferred until the point at which it is needed.

@Modification.Classes

Applies to: Class

Specifies an array of classes that the target type should modify. This takes precedence over the T type argument in Modification. In the following, Bar would apply only to Foo, not Object:

@Modification.Classes({ Foo.class })
class Bar extends Modification<Object> {
   ...
}

For more information, see Modifications.

@ObjectField.AnnotationProcessorClass

Applies to: Annotation

Specifies the processor class that can manipulate a field definition using the target annotation. For an annotation applied to a field, ObjectField.AnnotationProcessorClass specifies the implemented class that Dari will use to processes the field. For a custom annotation, you implement ObjectField.AnnotationProcessor.

@ObjectType.AnnotationProcessorClass

Applies to: Annotation

Specifies the processor class that can manipulate a class using the target annotation. For an annotation applied to a class, ObjectType.AnnotationProcessorClass specifies the implementation that Dari will use to processes the class. For a custom annotation, you implement ObjectType.AnnotationProcessor.

@Recordable.Abstract

Applies to: Class

Similar to specifying a Java class as abstract, this annotation prevents instances of the target type from being written to the database. The underlying ObjectType will be created, but only when ObjectType#isConcrete is true can an object be saved to the database.

In the example below, the Project class can be created, but only subclassed instances of Project can be saved to the database:

@Abstract
public class Project extends Record {

  ...
}

@Recordable.BeanProperty

Applies to: Class

For use with JSPs, this annotation specifies the JavaBeans property name that can be used to access an instance of the target type as a modification. It is useful if you need to add new fields to existing objects and the fields will be rendered.

For example, say that you need to add two fields, promoTitle and promoImage, to a group of objects. You would create an interface that classes requiring the new fields will implement. The interface includes a static class that extends Modification and declares the new fields. Note that the @BeanProperty annotation specifies the name by which the static class will be accessed.

public interface Promotable extends Recordable {

         @BeanProperty("promotable")
         public static class DefaultPromotable extends Modification<Promotable> {

            @Indexed
            private String promoTitle;
            private Image promoImage;

            // Getters Setters

         }
}

Any classes that require the new fields must be updated to implement Promotable, with the new fields added.

public class Blog extends Content implements Promotable {

       private String promoTitle;
       private Image promoTitle;

   // Getters Setters
}

To access the newly added fields when rendering the content, the @BeanProperty("promotable") is used:

<cms:render value="${content.promotable.promoTitle}" />
<cms:img src="${content.promotable.promoImage}" />

@Recordable.BootstrapPackages

Applies to: Class

Enables objects of the target class to be exported to a custom package identified by the value argument. This annotation is used in conjunction with the Database Bootstrap tool, which performs export and import operations between Brightspot environments with the same underlying data Model. Target classes to which this annotation is applied are listed in the Database Bootstrap tool.

The required value argument specifies one or more packages to export object data. A package is a JSON file that can be downloaded from the Database Bootstrap tool. A separate JSON file is created for each package specified in the annotation.

The optional depends argument specifies data types on which the target class is dependent. The specified data types will appear in a selection list in the Database Bootstrap tool. The selection list is associated with the packages set in the value argument. This allows the tool user to select the dependent types to export with the objects of the target class.

Note

All objects of the types specified in the depends argument are eligible for export, which could be many more objects than are actually referenced by instances of the target class. For example, if the target class references two object types, then each instance of the class references two objects. If you specify those same object types in the depends argument, all objects of those types can be selected for export, even if some of those objects are not referenced by instances of the target class. To limit dependencies to only referenced objects, see the @BootstrapFollowReferences annotation.

In the following example, the annotation enables Activity objects to be exported to a package called “System Activities”. In addition, the User and Project types will appear in a selection list in the Database Bootstrap tool. If the tool user selects those types, then all User and Project objects will be exported, including those that are not referenced by Activity objects.

 @Recordable.BootstrapPackages(value={"System Activities"}, depends={User.class, Project.class}
 public class Activity extends Record {

    private Date activityDate;
    private String activityType;

    private User activityUser;
    private Project project;

    // Getters and Setters
 }

@Recordable.BootstrapFollowReferences

Applies to: Field

When the @Recordable.BootstrapPackages annotation is applied to a class, @BootstrapFollowReferences can be applied to fields that reference Record-derived objects. This annotation enables the referenced objects to be exported to the package specified in the @BootstrapPackages annotation. The Database Bootstrap tool performs export and import operations between Brightspot environments with the same underlying data Model.

Note

@BootstrapFollowReferences enables only referenced objects to be exported to the package specified in the @BootstrapPackages annotation. Other objects of the same data type as the referenced objects are not exported. This export behavior contrasts with the use of the depends argument in the @BootstrapPackages annotation.

In the following example, the @BootstrapPackages annotation enables Activity objects to be exported to a package called “System Activities”. Two fields of the target class reference Record-derived types, User and Project, and are targets of the @BootstrapFollowReferences annotation. Therefore, all objects of type User and Project that are referenced by Activity objects will be exported along with the Activity objects.

 @Recordable.BootstrapPackages("System Activities")
 public class Activity extends Record {

    private Date activityDate;
    private String activityType;

    @Recordable.BootstrapFollowReferences
    private User activityUser;

    @Recordable.BootstrapFollowReferences
    @private Project project;

    // Getters and Setters
 }

@Recordable.CollectionMaximum

Applies to: Field

Supported on any subclass of Collection, this annotation specifies the maximum number of items in the target collection. In the following example, no more than 8 Slide objects can be added to the list.

public class Gallery extends Content {

  @CollectionMaximum(8)
  private List<Slide> slides;

  ...
}

@Recordable.CollectionMinimum

Applies to: Field

Supported on any subclass of Collection, this annotation specifies the minimum number of items in the target collection. In the following example, at least one Slide object must be added to the list.

public class Gallery extends Content {

  @CollectionMinimum(1)
  private List<Slide> slides;

  ...
}

@Recordable.Denormalized

Applies to: Field, Class

For Solr indexing purposes only, this annotation specifies whether a reference field is denormalized within instances of a referring class. Denormalizing forces Dari to copy data of referenced objects to instances of the referring class. The denormalized data is saved in the Solr index; it is not visible on the referring object. If set on a class, the annotation applies to all of the reference fields in the class.

Note

The annotation is reserved for advanced cases. Only use when it is absolutely necessary.

This annotation is useful in site searches where the query criteria specifies data from both referenced and referring objects. In the followiing example, the Person class references the State class with a field marked with the @Denormalized annotation.

// referenced class
public class State extends Record {

   @Indexed
   private String stateName;

   @Indexed(unique = true)
   private String stateAbbreviation;

  // getters and setters
}

// referring class
public class Person extends Record {

   @Indexed
   private String firstName;

   @Indexed
   private String lastName;

   @Indexed
   @Denormalized
   private State state;

   // getters and setters
}

In the Solr index, Dari copies the data of State objects to the referring Person objects. This allows Solr to find Person objects based on text stored in Person objects and on text in referenced State objects. For example, the following query searches the Solr index for Person objects with a string of “Gena Roberts”, which is stored in a Person object, and a string of “North Dakota”, which is stored in a State object that is referenced by the Person object.

"Gena Roberts AND North Dakota"

If the @Denormalized annotation is not applied in the Person class, then the above Solr query would fail to retrieve any Person objects.

@Recordable.DisplayName

Applies to: Field, Class, Method

Specifies the target’s name, which the frontend of an application can optionally display to end users. In the following example, the annotation is applied to the title field, which can be displayed as “Headline” to end users.

public class Article extends Content {

   @Recordable.DisplayName("Headline")
   private String title;
}

When the annotation is applied to a class, then the specified name can be used for display in place of the class name.

The annotation can also be applied to an @Indexed method that returns a calculated value (not one entered by a user). By default, values returned by indexed methods are unavailable for display in the frontend. However, if @DisplayName is applied to an indexed method, a frontend can display the calculated value with the specified name. For more information on indexing, see Indexes.

@Recordable.Embedded

Applies to: Field, Class

Specifies whether the target class type is embedded within another class type. That is, an embedded type is not stored as a separate record in the underlying database, but is embedded in the record of the containing object. Embedded objects cannot be directly instantiated, saved, or queried. They must be accessed through the containing object.

In the following example, Contact is embedded within Company.

public class Contact extends Record {

   private String poBox;
   private String city;
   private String state;
   private String zip;

   ...
}

public class Company extends Record {

   @Embedded
   private Contact contact;

   ...

}

As an alternative to placing the annotation on an object field, you can apply the @Embedded annotation on a nested class.

public class Company extends Record {

   private String name;
   private Contact contact;

   @Embedded
   public class Contact extends Record {

      private String poBox;
      private String city;
      private String state;
      private String zip;
   }

}

For more information on embedded objects, see Relationships.

@Recordable.FieldInternalNamePrefix

Applies to: Field, Class, Method

Specifies the prefix for the internal names of all fields in the target type. For example, if you have an annotated class…

@Recordable.FieldInternalNamePrefix("company-")
public class Company extends Record {

   private String name;
   private String city;
   private String zip;

   ...
}

…the name of each field in a class is prefaced with the specified annotation value, as in this JSON representation of a Company object:

{
   "company-name": "Acme, Inc.",
   "company-city": "Detroit",
   "company-zip": "20611",
   "_id": "0000015b-2022-d3a6-afdb-aba3d90f0000",
   "_type": "0000015b-2015-dfff-abdf-f89d49330000"
}

To query a class using this annotation, you specify the prefixed name of the fields, for example:

return Query.from(Company.class).where("company-city = 'Detroit'").first();

This annotation can be used in conjunction with the @InternalName annotation.

Note

For modifications, always use the FieldInternalNamePrefix annotation to avoid naming conflicts in your data model. For more information, see Modification Best Practices.

See also:

@Recordable.Ignored

Applies to: Field, Method

Specifies whether the target field or method should be ignored by the frontend of an application (the default is false for fields and true for methods). For fields, the @Ignored annotation has a similar impact as the Java transient keyword. An ignored field is excluded from the ObjectType defintion for the class, from data validation, and from queries. Values of an ignored field are not saved to the database.

You can also add @Ignored(false) to a method, which creates an ObjectMethod.

@Recordable.Indexed

Applies to: Field, Method

Specifies whether the target field value is indexed, allowing the field to be queried. Fields can be returned and rendered without indexing, but to query on a field, it must be indexed.

Note

Apply the @Indexed annotation only to fields likely to be queried. Adding this annotation to all the fields on every class potentially creates unnecessary rows in the underlying database, and can lead to poor performance in systems with large amounts of data.

In the example below, the @Indexed annotation is applied to the userName field.

public class User extends Record
{
       @Indexed
       private String userName;
   ...
}

Indexing this field allows User instances to be queried for specified names.

User user = Query.from(User.class).where("userName = 'Curly'".trim()).first();

You can apply the @Indexed annotation to getter methods or any method that returns a value. Indexing methods provides a means to index and query values that are not directly set by users, but are generated from other values stored in an object. Applying the @Indexed annotation to a method creates an ObjectMethod.

For more information on indexing, see Indexes. To reindex existing objects, see Database Bulk Operations. To change the default time interval when Dari updates method indexes, see @Recordable.Recalculate.

Optional Elements

caseSensitive
Default=false
If true, only case-sensitive searches are performed on the field.
unique
Default=false
If true, only a unique value can be set on the field.
visibility
Default=false
If true, an object’s visibility in a query is determined by whether its visibility-indexed fields are set to null. The @Indexed(visibility=true) annotation is typically applied to fields of type Boolean. In the following code snippet, status is a visibility-indexed field.
public class Application extends Record {

   @Indexed(visibility = true)
   private Boolean status; // set to null by default

   ...
}

If any visibility-indexed field of an object is set to a non-null value, then that object is not returned in a query for published objects. If all of the visibility-indexed fields of the object are null, then that object is returned in a query.

For more information, see indexes.

@Recordable.InternalName

Applies to: Field, Method, Class

Specifies the identifier Dari uses to store the annotated item in its internal representation.

Without this annotation, Dari stores a class and its fields using native Java identifiers as described in the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package content.blog;

import com.psddev.cms.db.Content;

public class Author extends Content {

    private String lastName;

    private String firstName;

    /* Getters and setters */

}

In the previous snippet—

  • Line 1 declares the package.
  • Line 5 declares the class.
  • Lines 7–9 declare fields.

Dari’s JSON representation of this class uses the native Java identifiers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "name": "Author",
  "internalName": "content.blog.Author",
  "fields": [ {
    "name": "lastName"
  }, {
    "name": "firstName"
  } ],
  "java.objectClass": "content.blog.Author"
}

In the previous snippet—

  • Line 2 shows the class’s name.
  • Line 3 shows the class’s internal name constructed as a standard fully qualified class name.
  • Lines 5 and 7 show the class’s fields.
  • Line 9 shows the class’s native fully qualified Java name.

The following snippet adds @Recordable.InternalName annotations to the class name and a field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package content.blog;

import com.psddev.cms.db.Content;
import com.psddev.dari.db.Recordable;

@Recordable.InternalName("romance")
public class Author extends Content {

    @Recordable.InternalName("pseudonym")
    private String lastName;

    private String firstName;

}

In the previous snippet, lines 6 and 9 apply the annotation @Recordable.InternalName to the class’s name and one of its fields. As a result, Dari adjusts its internal representation of the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "name": "Author",
  "internalName": "romance",
  "fields": [ {
    "name": "pseudonym"
  }, {
    "name": "firstName"
  } ],
  "java.objectClass": "content.blog.Author"
}

In the previous snippet—

  • Line 2 shows the class’s name.
  • Line 3 shows the class’s internal name constructed from the value of the annotation @Recordable.InternalName at the class level.
  • Lines 5 shows the field’s internal name constructed from the value of the annotation @Recordable.InternalName at the field level.
  • Line 9 shows the class’s native Java fully qualified name.

Note that if you use @InternalName on an indexed field, you must use the internal name to query the field.

You can also annotate indexed methods with @InternalName.

In the following example, the getAddress method is indexed in the Company class. When a Company object is created, Dari automatically executes the method and saves the full address value in the database.

...

@Indexed
@InternalName("fullAddress")
public String getAddress() {
   return (getCity()+ " " + getZip());
 }

To query an indexed method that is annotated with an internal name, you must specify the internal name of the method to test field values. For example, to search for a Company object where the full address is “Detroit 48236”, you would specify the method’s internal name:

return Query.from(Company.class).where("fullAddress = 'Detroit 48236'").first();

The strings used with @InternalName on methods and fields within a class must be unique, otherwise the application fails to load.

See also:

@Recordable.JunctionField

Applies to: Field

Specifies the name of the field in a junction query to be used to populate the target field. This annotation enhances database performance for retrieval of many objects that are contained within another object.

In the following example, a Season object potentially references many Episode objects. Therefore, a junction field is added to the Episode class, and, in the Season class, a @JunctionField annotation references the junction field in Episode. This allows the population of a list of episodes for a season by having each Episode object contain a reference to the Season it is in.

// Season:
public class Season extends Record {

  @Indexed
  private int year;

  @JunctionField("season")
  private List<Episode> episodes;

  ...
}

// Episode:
public class Episode extends Record {

  @Indexed
  private String title;

  private int index;

  @Required
  private Season season;

  ...
}

@Recordable.JunctionPositionField

Applies to: Field

Enables ordering of @JunctionField. In the following code example, the @JunctionPositionField annotation is added. It targets the index field in Episode, which is used to number Episode objects. Retrieved Episode objects are listed in the order of their index value.

// Season:
public class Season extends Record {

  @Indexed
  private int year;

  @JunctionField("season")
  @JunctionPositionField("index")
  private List<Episode> episodes;

  ...
}

// Episode:
public class Episode extends Record {

  @Indexed
  private String title;

  private int index;

  @Required
  private Season season;

  ...
}

@Recordable.LabelFields

Applies to: Class

Specifies one or more field names that are used to construct a displayable label for retrieved objects. By default, Dari uses the value of the first field in the class. For more information, see Object Labels.

@Recordable.Maximum

Applies to: Field

Specifies either the maximum numeric value or string length of the target field. For example, the rate field can have a value no greater than 10%.

public class Interest extends Record {

   private boolean dealerCharged;

   @Maximum(.10)
   private double rate;

   ...
}

This annotation can also be used with @Recordable.Minimum and @Recordable.Step.

@Recordable.MimeTypes

Applies to: Field

Specifies the valid MIME type for the target StorageItem field using the SparseSet representation. For example, to specify that the StorageItem field must be a pdf or image type:

@MimeTypes("+application/pdf +image/")
private StorageItem file;

@Recordable.Minimum

Applies to: Field

Specifies either the minimum numeric value or string length of the target field. For example, the rate field can have a value no less than 1%.

public class Interest extends Record {

   private boolean dealerCharged;

   @Maximum(.10)
   @Minimum(.01)
   private double rate;

   ...
}

This annotation can also be used with @Recordable.Maximum and @Recordable.Step.

Note

Minimum or maximum field validation is only enforced on non-null values. If you want to require a value, also use the @Required annotation.

@Recordable.PreviewField

Applies to: Class

Indicates the StorageItem field that should be used by Brightspot to preview a Record instance. The StorageItem to use for preview can then be accessed via State#getPreview. The annotation is typically used on image or video classes.

For example, @PreviewField is used on the Image class, specifying the file field to use in Brightspot preview:

@Recordable.PreviewField("file")
public class Image extends Content {

  private String name;
  private StorageItem file;
  private String altText;

  ...
}

@Recordable.Recalculate

Applies to: Method

Specifies how often the Dari background task, RecalculationTask, recalculates an indexed method and updates the method index with the returned values. An indexed method is one that returns a calculated value (not one entered by a user). Settings are based on the com.psddev.dari.db.RecalculationDelay interface. If you do not apply this annotation, Dari recalculates the method only when an object containing the method is created or saved. If you apply this annotation without parameters, the default setting for an indexed method is RecalculationDelay.Hour. That is, Dari recalculates the return value of the method and updates the index in hourly intervals. Alternatively, you can change the delay value of the annotation.

In the following example, the getFullName method is recalculated in hourly intervals.

   // Creates a full name value from the firstName and lastName values.
   @Indexed
   @Recalculate
   public String getFullName() {
     return (site.getFirstName()+ " " + site.getLastName());
   }
}

For more information about indexing, see @Recordable.Indexed. For configuring the Dari Recalculation task, see Recalculation Task.

@Recordable.Regex

Applies to: Field

This annotation specifies a regular expression pattern for a target string field. The input value on the field must match the pattern; otherwise, Brightspot displays an error. The regular expression pattern is set on the required value element. Optionally, you can include the validationMessage element to specify a custom error message in Brightspot.

In the following code snippet, the Regex annotation specifies the required pattern for the email field. The validationMessage element is set to the Brightspot error message.

public class Author extends Content
{
   private String name;

   @Recordable.Regex(value=".+\\@.+\\..+", validationMessage="Use email format 'myemail@address.com'")
   private String email;

   //getters and setters
}

If a user enters a non-compliant string on the email field, Brightspot displays the message set on the validationMessage element.

../../_images/regex-validation-message.png

@Recordable.Required

Applies to: Field

Specifies whether the target field value is required. For example, the activityUser and project fields are required for an Activity.

public class Activity extends Record {

   @Indexed
   private Date activityDate;

   @Required
   private User activityUser;

   @Required
   private Project project;

   ...
 }

@Recordable.SourceDatabaseClass

Applies to: Class

Specifies the source database class for the target type. The annotation takes a value of an implementing class of com.psddev.dari.db.Database.

For example, you might have a large dataset that you don’t want to be included in your default MySQL database, so you specify ElasticSearch storage:

@SourceDatabaseClass(com.psddev.dari.elasticsearch.ElasticsearchDatabase.class)
public static class Order extends Record {

   ...
}

@Recordable.SourceDatabaseName

Applies to: Class

Specifies the source database name for the target type. This annotation is an alternative to @SourceDatabaseClass. Instead of specifying the database class, you specify the name of the database as configured in the Tomcat context.xml file:

@SourceDatabaseName("OrderProcessing")
public static class Order extends Record {

   ...
}

@Recordable.Step

Applies to: Field

Specifies the incremental step between the minimum and the maximum that the target field must match. The @Step annotation must be used with the @Minimum and @Maximum annotations.

Based on the following annotations, the rate field accepts a value range of 1 to 10 percent in half-percent increments.

public class Interest extends Record {

   private boolean dealerCharged;

   @Minimum(.01)
   @Maximum(.10)
   @Step(.005)
   private double rate;

   ...
}

@Recordable.TypeId

Applies to: Class

Forces the type ID to the specified value instead of automatic generation of the ID. A type ID conflict with another class has no impact.

This annotation is typically used to facilitate code refactoring, allowing you to maintain existing type IDs for classes that you relocate to different packages. For example, the following snippet defines a class Article prior to refactoring.

package content.article;

import com.psddev.cms.db.Content;

public class Article extends Content {

    /* Fields, getters, setters */

}

Dari’s internal representation of the class is as follows:

1
2
3
4
5
6
{
  "name": "Article",
  "internalName": "content.article.Article",
  "_id": "a3489571-fd7e-3ca4-b850-61909ef172cd",
  "_type": "982a8b2a-7600-3bb0-ae68-740f77cd85d3"
}

In the previous snippet—

  • Line 2 shows the class’s name.
  • Line 3 shows the class’s fully qualified name.
  • Line 4 shows the class’s instance ID.
  • Line 5 shows the class’s type ID.

As your project evolves, you decide to create a new content type Blog in a different package that is identical to the content type Article. Using the annotation @Recordable.TypeId you can force Dari to store the blog as an article.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package content.blog;

import com.psddev.cms.db.Content;
import com.psddev.dari.db.Recordable;

@Recordable.TypeId("982a8b2a-7600-3bb0-ae68-740f77cd85d3")
public class Blog extends Content  {

    /* Fields, getters, setters */

}

In the previous snippet, line 6 applies the annotation @Recordable.TypeId to assigned the article’s type ID to the blog. Dari’s internal representation for the blog reflects the internal name and type ID for Article.

1
2
3
4
5
6
{
  "name": "Article",
  "internalName": "content.blog.Article",
  "_id": "daba47fb-df6e-3e05-bfa3-d7d943cc346d",
  "_type": "982a8b2a-7600-3bb0-ae68-740f77cd85d3"
}

In the previous snippet—

  • Lines 2 and 5 are identical to the values for the class Article.
  • Line 3 provides the class’s fully qualified name.
  • Line 4 shows the class’s instance ID.

See also:

@Recordable.TypePostProcessorClasses

Applies to: Class

Specifies an array of processor classes to run after the type is initialized. When an instance of the target class is created, your implementation of ObjectType.PostProcessor executes.

@Recordable.Types

Applies to: Field

Specifies the valid class types for the target field value. In the example below, only Image and Video types, which derive from Media, can be added to the list of items.

public class Gallery extends Content {

  @Types( {Image.class, Video.class} )
  private List<Media> items;
}

@Recordable.TypesExclude

Applies to: Field

Specifies the class types to be excluded for the target field value. In the example below, all types that derive from Promotable can be set on the item field except for Image.

public class Promo extends Content {

  @TypesExclude( {Image.class} )
  private Promotable item;
}

@Recordable.Values

Applies to: Field

Specifies the valid values for the target field. In the example below, only one of the listed colors can be set on the teamColor field.

public class Team extends Content {

   private String teamName;

   @Values({"red", "blue", "yellow", "green"})
   private String teamColor;
}

@Recordable.Where

Applies to: Field

Allows you to specify a predicate for a reference field of type extending from Record. At validation time, the predicate is evaluated against the value. If the value does not match the predicate, a validation error is thrown. (For information about validation time and when it occurs, see Save Lifecycle.)

The following example illustrates how to use @Where for data validation. In this example, when creating a new article, Dari checks that the author associated with the article has an expertise of Lifestyle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package content.article;

import com.psddev.cms.db.Content;
import com.psddev.cms.db.Directory;

public class Author extends Content {

    private String name;

    @Indexed
    private String expertise;

}

In the previous snippet—

  • Line 11 declares a property expertise associated with an Author.
  • Line 10 applies the annotation @Recordable.Indexed, allowing Dari to search on the property expertise.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package content.article;

import com.psddev.cms.db.Content;
import com.psddev.dari.db.Recordable;

public class Article extends Content {

    private String headline;

    @Where("expertise = 'Lifestyle'")
    private Author author;

}

In the previous snippet—

  • Line 11 adds a property Author to the Model Article.
  • Line 10 applies the annotation @Where that requires all authors associated with articles to have an expertise of Lifestyle.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package content.article;

import com.psddev.dari.db.ValidationException;

public class ArticleGenerator {

    public static Article addArticle(Author author) throws Throwable {
        Article article = new Article();
        article.setHeadline("Is Your Destination Wedding on an Island Impacted by Global Warming?");
        article.setAuthor(author);

        try {
            article.saveImmediately();
        } catch (ValidationException e) {
            /* Error handling...*/
            return null;
        }
        return article;
    }
}

In the previous snippet—

  • Lines 7–19 implement a method for programmatically adding articles to a database.
    • Line 10 associates the passed author with the new article.
    • Lines 12–17 initiate the article’s save lifecycle, at which time Dari performs data validation. If the passed author has an expertise Lifestyle, the try-catch block passes; if the author has an expertise different from Lifestyle, the try-catch block throws a ValidationException error.

In addition to data validation, you can use the annotation @Where to filter results in a content picker. For example, the following snippet adds an annotation @Where that filters available authors to those whose expertise is Lifestyle.

@Where("expertise = 'Lifestyle'")
private Author author;

When a user opens the content picker, the available authors are those whose expertise is Lifestyle.

../../_images/content-picker-with-static-where.svg

You can also have a dynamic predicate for the @Where annotation, allowing for dynamic lookups in the Content Picker.

1
2
@Where("_id = ?/getArticles")
private Set<Article> relatedStories;

In the previous snippet, Brightspot renders the Content Edit Form with a Selection Field for items of type Article. When the user clicks the selection field, Brightspot populates the content picker with articles in a set returned by the method getArticles.

Line 1 in the previous snippet uses the path ?/getArticles. The question mark ? represents the current object (similar to Java’s keyword this), so the method getArticles is a member of the current object.

The following example shows how to implement a dynamic predicate that populates the content picker with articles having at least one tag shared with the current article in the content edit form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package content.article;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

import com.psddev.cms.db.Content;
import com.psddev.cms.db.ToolUi;
import com.psddev.dari.db.Recordable;

public class Article extends Content {

    @Recordable.Required
    private String headline;

    @ToolUi.RichText
    private String body;

    @Indexed
    private Set<Tag> tags;

    @Where("tags = ?/getTags")
    private Article relatedStory;

    /* Getters and Setters */

    @ToolUi.Hidden
    @Ignored(false)
    public Set<Tag> getTags() {
        if (tags != null) {
            return tags;
        }
        return new HashSet();
    }
}

In the previous snippet—

  • Lines 11–23 declare a content type Article with four fields: headline, body, tags, and a selection field for a related story.

    ../../_images/content-edit-form-with-where.png
  • Line 19 applies an @Indexed annotation, allowing Dari to search for all the tags associated with a given article.

  • Line 22 applies a dynamic @Where annotation. The annotation populates the content picker with articles whose tags are in the set returned by the method getTags.

  • Lines 27–34 implement the method getTags:

    • Line 27 applies the @ToolUi.Hidden annotation, which prevents the method’s results from appearing in the content edit form.

    • Line 28 applies the @Ignored(false) annotation, which ensures the method is run when the user clicks on the selection field.

    • Line 29 declares the method’s return type, a set of tags.

    • Lines 31 returns a set of tags in the current article (if any). Brightspot uses this list in the @Where annotation to populate the content picker.

      ../../_images/content-picker-with-dynamic-where.svg
    • Line 33 returns an empty set if the current article has no tags.

See also:

@RoutingFilter.Path

Applies to: Class

Specifies the application path for the target servlet. The purpose of the annotation is to group servlets together under a particular path segment. The annotation is equivalent of the Java @WebServlet annotation, but includes an optional application argument to organize servlets into logical groups.

In the following example, the annotation specifies the path of the servlet using the required and optional arguments. The optional application argument is the group path for logically related servlets. The required value argument specifies the rest of the URL path where the target servlet is located.

@RoutingFilter.Path(application = "auth", value = "/admin/employees")
public class AuthenticateUsers extends PageServlet {

...

}

If the routing filter is set in Tomcat’s context.xml, that setting overrides the application path specified in the annotation. For example, the following application path in context.xml would be used instead of the value specified in the above annotation:

<Environment name="dari/routingFilter/applicationPath/auth" type="java.lang.String" value="/admin/users" />

@UpdateTrackable.Names

Applies to: Class

Specifies the fields in the target class for which you want to track updates. Dari provides an API that indicates whether instances of a class were updated within a specified period of time. This includes new instances that are created as well as existing instances that are changed.

In the following code snippet, the @UpdateTrackable.Names annotation specifies tracking of the activityUser and project fields in the Activity class.

@UpdateTrackable.Names( {"activityUser", "project"})
public class Activity extends Record {

   @Indexed
   private User activityUser;

   @Indexed
   private Project project;

   private Date activityDate;
   private String activityType;

 // Getters and Setters
}

The UpdateTrackable.Static.isUpdated method indicates whether instances of a specified class were updated within a specified time period. In the following example, a boolean is returned that indicates if activityUser or project values were created or changed for any Activity instances over the last hour.

public class Code {
   public static Object main() throws Throwable {
     long time = 3600000;
     String[] type = {"Activity"};
     return (UpdateTrackable.Static.isUpdated(type[0], time));
   }
}