Annotation Interface OnChange


Annotates methods to be invoked whenever some target field in some target object changes during a transaction.

Overview

When the value of a matching field in a matching object changes, a change event is created and the annotated method is invoked. The change event's type will be some sub-type of FieldChange appropriate for the type of field and the change that occurred. Only change events whose types are compatible with the method's parameter are delivered.

A "matching object" is one that is found at the end of the reference path specified by path(), starting from the object to be notified; see ReferencePath for more information about reference paths.

By default, the reference path is empty, which means changes in the target object itself are monitored.

A "matching field" is one named in value(), or every event-generating field if value() is empty.

A class may have multiple @OnChange methods, each with a specific purpose.

Examples

   @PermazenType
   public abstract class Account implements PermazenObject {

   // Database fields

       public abstract boolean isEnabled();
       public abstract void setEnabled(boolean enabled);

       @NotNull
       public abstract String getName();
       public abstract void setName(String name);

       public abstract NavigableSet<AccessLevel> getAccessLevels();

   // @OnChange methods

       @OnChange
       private void handleAnyChange1(FieldChange<Account> change) {
           // Sees any change to ANY field of THIS account
       }

       @OnChange
       private static void handleAnyChange2(FieldChange<Account> change) {
           // Sees any change to ANY field of ANY account (note static method)
       }

       @OnChange("accessLevels")
       private void handleAccessLevelsChange(SetFieldAdd<Account, AccessLevel> change) {
           // Sees any addition to THIS accounts access levels
       }

       @OnChange
       private void handleSimpleChange(SimpleFieldChange<Account, ?> change) {
           // Sees any change to any SIMPLE field of THIS account (i.e., "enabled", "name")
       }
   }

   @PermazenType
   public abstract class User implements PermazenObject {

   // Database fields

       @NotNull
       @PermazenField(indexed = true, unique = true)
       public abstract String getUsername();
       public abstract void setUsername(String username);

       @NotNull
       public abstract Account getAccount();
       public abstract void setAccount(Account account);

       public abstract NavigableSet<User> getFriends();

   // @OnChange methods

       @OnChange("username")
       private void handleUsernameChange(SimpleFieldChange<User, String> change) {
           // Sees any change to THIS user's username
       }

       @OnChange(path = "account", value = "enabled")
       private void handleUsernameChange(SimpleFieldChange<Account, Boolean> change) {
           // Sees any change to THIS user's account's enabled status
       }

       @OnChange(path = "->friends->friends->account")
       private void handleFOFAccountNameChange(SimpleFieldChange<Account, ?> change) {
           // Sees any change to ANY simple field in ANY friend-of-a-friend's Account
       }

       @OnChange(path = "->account<-User.account", value = "username")
       private void handleSameAccountUserUsernameChange(SimpleFieldChange<User, String> change) {
           // Sees changes to the username of any User having the same Account as this User.
           // Note the use of the inverse step "<-User.account" from Account back to User
       }

       @OnChange("account")
       private static void handleMembershipChange(SimpleFieldChange<User, Account> change) {
           // Sees any change to ANY user's account
       }
   }
 

Method Parameter Types

In all cases the annotated method must return void and take one parameter whose type must be compatible with at least one of the FieldChange sub-types appropriate for the/a field being monitored. The parameter type can be narrowed to restrict which notifications are delivered. For example, a method with a SetFieldChange parameter will receive notifications about all changes to a set field, but a method with a SetFieldAdd parameter will receive notification only when an element is added to the set.

The method may have any level of access, including private, and multiple independent @OnChange methods are allowed.

Multiple fields in the target object may be specified; if so, all of the fields are monitored together, and they all must emit FieldChanges compatible with the method's parameter type. Therefore, when multiple fields are monitored by the same method, the method's parameter type may need to be widened to accomodate them all.

If value() is empty (the default), then every field in the target object is monitored, though again only changes compatible with the method's parameter type will be delivered. So for example, a method taking a SetFieldChange would receive notifications about changes to all Set fields in the class, but not any other fields.

Currently, due to type erasure, only the parameter's raw type is taken into consideration and an error is generated if the parameter's generic type and its raw type don't match the same events.

Instance vs. Static Methods

For an instance method, the method will be invoked on each object for which the changed field is found at the end of the specified reference path starting from that object. So if there are three Child objects, and the Child class has an instance method annotated with @OnChange(path = "parent", value = "name"), then all three Child objects will be notified when the parent's name changes.

If the instance is a static method, then the method is invoked once when any instance of the class containing the method exists for which the changed field is found at the end of the specified reference path, no matter how many such instances there are. So in the previous example, making the method static would cause it to be invoked once when the parent's name changes.

Notification Delivery

Notifications are delivered synchronously within the thread the made the change, after the change is made and just prior to returning to the original caller. Additional changes made within an @OnChange handler that themselves result in notifications are also handled prior to returning to the original caller. Put another way, the queue of outstanding notifications triggered by invoking a method that changes any field is emptied before that method returns. Therefore, infinite loops are possible if an @OnChange handler method modifies the field it's monitoring (directly, or indirectly via other @OnChange handler methods).

@OnChange functions within a single transaction; it does not notify about changes that occur in other transactions.

Fields of Sub-Types

The same field can appear in multiple sub-types, e.g., when implementing a Java interface containing a Permazen field. This can lead to some subtleties: for example, in some cases, a field may not exist in a Java object type, but it does exist in a some sub-type of that type:

 @PermazenType
 public abstract class Person {

     public abstract Set<Person> getFriends();

     @OnChange(path = "friends", field="name")
     private void friendNameChanged(SimpleFieldChange<NamedPerson, String> change) {
         // ... do whatever
     }
 }

 @PermazenType
 public abstract class NamedPerson extends Person {

     public abstract String getName();
     public abstract void setName(String name);
 }
 
Here the path "friends.name" seems incorrect because "friends" has type Person, while "name" is a field of NamedPerson, a narrower type than Person. However, this will still work as long as there is no ambiguity, i.e., in this example, there are no other sub-types of Person with a different field named "name".

Note also in the example above the SimpleFieldChange parameter to the method friendNameChanged() necessarily has generic type NamedPerson, not Person.

Other Notes

Counter fields do not generate change notifications.

No notifications are delivered for "changes" that do not actually change anything (e.g., setting a simple field to the value already contained in that field, or adding an element to a set which is already contained in the set).

For any given field change and path, only one notification will be delivered per recipient object, even if the changed field is seen through the path in multiple ways (e.g., via reference path "mylist.myfield" where the changed object containing myfield appears multiple times in mylist).

Some notifications may need to be ignored by objects in detached transactions; you can use this.isDetached() to detect that situation.

When handing change events, any action that has effects visible to the outside world should be made contingent on successful transaction commit, for example, by wrapping it in Transaction.addCallback().

See Transaction.addSimpleFieldChangeListener() for further information on other special corner cases.

Meta-Annotations

This annotation may be configured indirectly as a Spring meta-annotation when spring-core is on the classpath.

See Also:
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Specify the reference path to the target object(s) that should be monitored for changes.
    Specify the fields in the target object(s) that should be monitored for changes.
  • Element Details

    • path

      String path
      Specify the reference path to the target object(s) that should be monitored for changes. See ReferencePath for information on reference paths and their proper syntax.

      The default empty path means the monitored object and the notified object are the same.

      In the case of static methods, a non-empty path restricts notification from being delivered unless there exists at least one object for whom the monitored object is found at the other end of the path.

      Returns:
      reference path leading to monitored objects
      See Also:
      Default:
      ""
    • value

      String[] value
      Specify the fields in the target object(s) that should be monitored for changes.

      Multiple fields may be specified; if so, each field is handled as a separate independent listener registration, and for each field, the method's parameter type must be compatible with at least one of the FieldChange event sub-types emitted by that field.

      If zero paths are specified (the default), every field in the target object(s) that emits FieldChanges compatible with the method's parameter type will be monitored for changes.

      Returns:
      the names of the fields to monitored in the target objects
      Default:
      {}