Modifications

Modifications allow you to add new fields to existing classes. Modifications are most often used to apply a common set of fields and behaviors to a group of classes. Modifications can also be used to add fields to an existing class for which you do not have access to the source code, such as Brightspot system classes.

A Modification extends a class with the target of the modification, specified as a generic. When you create a Modification-derived class, Dari automatically modifies the object type definitions of all classes impacted by the <T> parameter. Modification data can then be accessed using the State#as method.

Modifying a Class without the Source

Modifications enable changes to existing classes for which you do not have the source. For example, say that you want to add a field to the Brightspot CMS class com.psddev.cms.db.Site. You need only create a Modification-derived class that specifies Site as the target class:

public class SiteModification extends Modification<com.psddev.cms.db.Site> {

   private String customSiteField;

   public String getcustomSiteField() {
     return customSiteField;
   }

   public void setcustomSiteField(String customSiteField) {
     this.customSiteField = customSiteField;
   }

}

The new field, customSiteField, then appears in the Sites page of the CMS.

../../_images/sites.png

Modifying a Group of Classes

Another common use case for modifications is to add a common set of fields to a group of classes.

The following example shows a modification that is intended to add a company logo. Because Object is specified as the modified class, all classes that inherit from Object will be modified, and the modification can be applied to all existing objects.

public class Logo extends Modification<Object> {

   private LocalStorageItem logoImage;
   private Date releaseDate;


   public LocalStorageItem getLogoImage() {
     return logoImage;
   }

   public void setLogoImage(LocalStorageItem logoImage) {
     this.logoImage = logoImage;
   }

   public Date getReleaseDate() {
     return releaseDate;
   }

   public void setReleaseDate(Date date) {
         this.releaseDate = date;
   }
}

Narrowing the Modification Scope

In the previous code example, all classes will be modified because the <T> parameter specifies Object. If you want to modify only specific classes, you can apply the @Modification.Classes annotation on the modification class. This annotation overrides the <T> parameter. As shown in the code snippet, only Article and BlogPost classes will carry the logo image.

@Modification.Classes({ Article.class, BlogPost.class })
public class Logo extends Modification<Object> {

...

}

For more information, see @Modification.Classes.

Using a Common Interface

In the previous code examples, the <T> parameter of the Modification<T> generic class specifies the classes to be modified. As an alternative to this technique, you can instead specify an interface for the parameter. You then implement the interface on the classes that you want to modify.

To use this technique, first create an interface that will be common to classes that implement it. Note that interfaces used for modifications must extend com.psddev.dari.db.Recordable.

In the following example, the Logo class is implemented as a static inner class of the Branding interface. Note the Branding interface is specified in the Modification parameter.

public interface Branding extends Recordable {

   public static class Logo extends Modification<Branding> {

      private LocalStorageItem logoImage;
      private Date releaseDate;

      public LocalStorageItem getLogoImage() {
        return logoImage;
      }

      public void setLogoImage(LocalStorageItem logoImage) {
        this.logoImage = logoImage;
      }

      public Date getReleaseDate() {
        return releaseDate;
      }

      public void setReleaseDate(Date date) {
        this.releaseDate = date;
      }
   }
}

Next, implement the interface on all classes to be modified.

As shown in the code snippet, Article and BlogPost implement the Branding interface, and will therefore carry the Logo fields:

public class Article extends Record implements Branding { ... }

public class BlogPost extends Record implements Branding { ... }

Accessing Modification Fields

You access modification fields using the State.as method. It returns an instance of the specified Modification-derived class linked to the state, from which you can invoke getter and setter methods.

To continue with the Logo modification example, the following code snippet shows how to set and get the Logo modification fields on an Article object. Note that in the case where you specify an interface as the target of a modification rather than a class, the interface must qualify the modification inner class specified in the State.as parameter.

// When class specified in modification:
// setters
article.as(Logo.class).setLogoImage(storageItemObject);
article.as(Logo.class).setReleaseDate(date);
article.save();

// getters
LocalStorageItem storageItemObject = article.as(Logo.class).getLogoImage();
Date date = article.as(Logo.class).getReleaseDate();

// When interface specified in modification:
// setters
article.as(Branding.Logo.class).setLogoImage(storageItemObject);
article.as(Branding.Logo.class).setReleaseDate(date);
article.save();

// getters
LocalStorageItem storageItemObject = article.as(Branding.Logo.class).getLogoImage();
Date date = article.as(Branding.Logo.class).getReleaseDate();

Best Practices

  • Use the @Recordable.FieldInternalNamePrefix annotation on modifications to prevent naming conflicts in your data model. Specify a unique prefix so that the internal names of modification fields do not conflict with fields declared by classes that are targets of the modification.

    For example, in the following snippet the annotation specifies that the Logo fields be prefixed with “logo.”. The prefix distinguishes the Logo fields from the ones declared by the Article class, the target of the modification.

    @Recordable.FieldInternalNamePrefix("logo.")
    public class Logo extends Modification<Article> {
       private LocalStorageItem logoImage;
       private Date releaseDate;
    
       // getters and setters
    
     }
    

    The internal field names are reflected in the object type definition for Article, as shown in this JSON fragment. The fields for the Logo modification include the specified prefix, whereas the headline field in the core Article class does not.

    ...
    "fields" : [ {
     ...
     "name" : "headline",,
     "java.field" : "headline",
     "java.declaringClass" : "brightspot.tutorial.article.Article",
     ...
    },
    {
     ...
     "name" : "logo.logoImage",
     "java.field" : "logoImage",
     "java.declaringClass" : "brightspot.tutorial.logo.Logo",
     ...
    }
    {
     ...
     "name" : "logo.releaseDate",
     "java.field" : "releaseDate",
     "java.declaringClass" : "brightspot.tutorial.logo.Logo",
     ...
    } ],
    
  • For interfaces that are targets of modifications, use default methods as a convenience for implementing classes. For example, you can have a default method that returns a null fallback value. This ensures that implementing classes without a fallback do not have to declare the method in their class definition to simply return null.

    Include a default method that returns the modification class. This allows a caller to get the modification from the interface, instead of the caller using the State.as method to get the modification linked to the state. This provides type safety.

    In the following code snippet, the Sluggable interface includes a default method for getting the SluggableData class, a modification of the Sluggable interface.

    public interface Sluggable extends Recordable {
    
      /**
       * Completely optional to include in your implementation. It serves as a
       * compile-time type-safe convenience method for accessing the Data class.
       */
       default SluggableData asSluggableData() {
          return as(SluggableData.class);
    }
    
  • Implement com.psddev.cms.db.Copyable on modifications with field values that you do not want to copy to new objects. For example, in a data migration scenario where you are moving from a legacy environment, you might have values that are obsolete in the new environment, such as a legacy object ID. Use the Copyable#onCopy method to clear such fields. You generally want to clear hidden fields as well.

  • To maintain naming consistency with Brightspot system modifications, follow these guidelines for your modifications:

    • For a modification of a concrete type, name the class <type>Modificaton, such as ArticleModification.
    • For modification of a concrete type that has a specific purpose, name the class <purpose><type>Modification, such as AnalyticsArticleModification.
    • For modification of the Object type, which allows any type to invoke methods on the modification (Modification<Object>), name the class <prefix>ObjectModification, where <prefix> distinguishes one modification from another that can be invoked from any object type. For example, a modification that provides video-related functionality might be called VideoObjectModification.