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 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.
See also:
Modifications enable changes to existing classes for which you do not have the source. For example, if 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.
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 snippet, 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 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. 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 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 {
}
You access modification fields using the 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 snippet shows how to set and get the Logo modification fields on an Article
object. If 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();
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 withlogo
.. The prefix distinguishes theLogo
fields from the ones declared by theArticle
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 theLogo
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 returnnull
.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 snippet, the
Sluggable
interface includes a default method for getting theSluggableData
class, a modification of theSluggable
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 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>Modification
, such asArticleModification
. - For modification of a concrete type that has a specific purpose, name the class
<purpose>
<type>Modification
, such asAnalyticsArticleModification
. - 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 calledVideoObjectModification
.
- For a modification of a concrete type, name the class