Annotation Type OnChange


  • @Retention(RUNTIME)
    @Target({ANNOTATION_TYPE,METHOD})
    @Documented
    public @interface OnChange
    Annotation for methods that are to be invoked whenever a simple or complex target field in some target object changes during a transaction, where the target object containing the changed field is found at the end of a path of references starting from the object to be notified. See ReferencePath for more information about reference paths.

    Overview

    There several ways to control which changes are delivered to the annotated method:
    • By specifying a path of object references, via value(), to the target object and field
    • By widening or narrowing the type of the FieldChange method parameter (or omitting it altogether)
    • By declaring an instance method, to monitor changes from the perspective of the associated object, or a static method, to monitor changes from a global perspective
    • By allowing or disallowing notifications that occur within snapshot transactions.
    A class may have multiple @OnChange methods, each with a specific purpose.

    Examples

       @PermazenType
       public abstract class Account implements JObject {
    
       // 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      // equivalent to @OnChange("*")
           private void handleAnyChange1() {
               // Sees any change to ANY field of THIS account
           }
    
           @OnChange("*")
           private void handleAnyChange2(FieldChange<Account> change) {
               // Sees any change to ANY field of THIS account
           }
    
           @OnChange("*")
           private static void handleAnyChange3(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 (e.g., enabled, name)
           }
    
           @OnChange(startType = User.class, value = "account")
           private static void handleMembershipChange(SimpleFieldChange<User, Account> change) {
               // Sees any change to which users are associated with ANY account
           }
       }
    
       @PermazenType
       public abstract class User implements JObject {
    
       // Database fields
    
           @NotNull
           @JField(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("account.enabled")
           private void handleUsernameChange(SimpleFieldChange<Account, Boolean> change) {
               // Sees any change to THIS user's account's enabled status
           }
    
           @OnChange("friends.element.friends.element.account.*")
           private void handleFOFAccountNameChange(SimpleFieldChange<Account, ?> change) {
               // Sees any change to any simple field in any friend-of-a-friend's Account
           }
    
           @OnChange("account.^User:account^.username")
           private void handleSameAccountUserUsernameChange(SimpleFieldChange<User, String> change) {
               // Sees changes to the username of any User with the same Account as this instance
               // Note the use of the inverse step "^User:account^" from Account back to User
           }
       }
     

    Method Parameter Types

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

    A method with zero parameters is delivered all possible notifications, which is equivalent to having an ignored parameter of type FieldChange<?>.

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

    Multiple reference paths may be specified; if so, all of the specified paths are monitored together, and they all must emit FieldChanges compatible with the method's parameter type. Therefore, when multiple fields are monitored, the method's parameter type may need to be widened (either in raw type, generic type parameters, or both).

    As a special case, if the last field is "*" (wildcard), then every field in the target object is matched. However, only fields that emit changes compatible with the method's parameter type will be monitored. 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.

    Instance vs. Static Methods

    If the method is an instance method, then startType() must be left unset; if the instance is a static method, then startType() may be explicitly set, or if left unset it defaults to the class containing the annotated method.

    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.

    If the annotated method is a static method, the method is invoked once if any instance exists for which the changed field is found at the end of the specified reference path, no matter how many such instances there are. Otherwise the behavior is the same.

    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. 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 may have occurred in a different transaction.

    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 class Person {
    
         public abstract Set<Person> getFriends();
    
         @OnChange("friends.element.name")
         private void friendNameChanged(SimpleFieldChange<NamedPerson, String> change) {
             // ... do whatever
         }
     }
    
     @PermazenType
     public class NamedPerson extends Person {
    
         public abstract String getName();
         public abstract void setName(String name);
     }
     
    Here the path "friends.element.name" seems incorrect because "friends.element" 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 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.element.myfield" where the changed object containing myfield appears multiple times in mylist).

    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:
    ReferencePath, io.permazen.change
    • Element Detail

      • value

        String[] value
        Specifies the path(s) to the target field(s) to watch for changes. See ReferencePath for information on the proper syntax.

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

        If zero paths are specified (the default), every field in the class (including superclasses) that emits FieldChanges compatible with the method parameter will be monitored for changes.

        Returns:
        reference path leading to the changed field
        See Also:
        ReferencePath
        Default:
        {}
      • startType

        Class<?> startType
        Specifies the starting type for the ReferencePath specified by value().

        This property must be left unset for instance methods. For static methods, if this property is left unset, then then class containing the annotated method is assumed.

        Returns:
        Java type at which the reference path starts
        See Also:
        ReferencePath
        Default:
        void.class
      • snapshotTransactions

        boolean snapshotTransactions
        Determines whether this annotation should also be enabled for snapshot transaction objects. If unset, notifications will only be delivered to non-snapshot (i.e., normal) database instances.
        Returns:
        whether enabled for snapshot transactions
        See Also:
        SnapshotJTransaction
        Default:
        false