org.jsimpledb.annotation.OnChange Maven / Gradle / Ivy
Show all versions of jsimpledb-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for methods that are to be invoked whenever a 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 {@link org.jsimpledb.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 {@link #value}, to the target object and field
* - By widening or narrowing the type of the {@link org.jsimpledb.change.FieldChange} method parameter
* (or omitting it algtogether)
* - 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 {@linkplain #snapshotTransactions allowing or disallowing} notifications that occur within
* {@linkplain org.jsimpledb.SnapshotJTransaction snapshot transactions}.
*
* A class may have multiple {@link OnChange @OnChange} methods, each with a specific purpose.
*
* Examples
*
*
* @JSimpleClass
* 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
* }
* }
*
* @JSimpleClass
* 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 {@link org.jsimpledb.change.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 {@link org.jsimpledb.change.SetFieldChange} will receive notifications about all changes to a set field,
* while a method taking a {@link org.jsimpledb.change.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 {@link org.jsimpledb.change.FieldChange FieldChange<?>}.
*
*
* The method may have any level of access, including {@code private}, and multiple independent {@link OnChange @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 {@link org.jsimpledb.change.FieldChange}s compatible with the method's parameter type. Therefore, when
* mutiple 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 {@code "*"} (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 {@link org.jsimpledb.change.SetFieldChange} would receive notifications about
* changes to all {@code 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 {@link #startType} must be left unset; if the instance is a static
* method, then {@link #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 {@link OnChange @OnChange} handler that themselves result in notifications
* are also handled prior to returning to the original caller. Therefore, infinite loops are possible if an
* {@link OnChange @OnChange} handler method modifies the field it's monitoring (directly, or indirectly via
* other {@link OnChange @OnChange} handler methods).
*
*
* {@link OnChange @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 JSimpleDB 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:
*
*
* @JSimpleClass
* public class Person {
*
* public abstract Set<Person> getFriends();
*
* @OnChange("friends.element.name")
* private void friendNameChanged(SimpleFieldChange<NamedPerson, String> change) {
* // ... do whatever
* }
* }
*
* @JSimpleClass
* public class NamedPerson extends Person {
*
* public abstract String getName();
* public abstract void setName(String name);
* }
*
*
* Here the path {@code "friends.element.name"} seems incorrect because {@code "friends.element"} has type {@code Person},
* while {@code "name"} is a field of {@code NamedPerson}, a narrower type than {@code 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 {@code Person} with a field
* named {@code "name"}. Note also in the example above the {@link org.jsimpledb.change.SimpleFieldChange} parameter to the
* method {@code friendNameChanged()} necessarily has generic type {@code NamedPerson}, not {@code Person}.
*
* Other Notes
*
*
* 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 {@code "mylist.element.myfield"} where the changed object
* containing {@code myfield} appears multiple times in {@code mylist}).
*
*
* See {@link org.jsimpledb.core.Transaction#addSimpleFieldChangeListener Transaction.addSimpleFieldChangeListener()}
* for further information on other special corner cases.
*
*
Meta-Annotations
*
*
* This annotation may be configured indirectly as a Spring
* meta-annotation
* when {@code spring-core} is on the classpath.
*
* @see org.jsimpledb.ReferencePath
* @see org.jsimpledb.change
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Documented
public @interface OnChange {
/**
* Specifies the path(s) to the target field(s) to watch for changes.
* See {@link org.jsimpledb.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 {@link org.jsimpledb.change.FieldChange}
* sub-types emitted by each field.
*
*
* If zero paths are specified (the default), every field in the class (including superclasses) that emits
* {@link org.jsimpledb.change.FieldChange}s compatible with the method parameter will be monitored for changes.
*
* @return reference path leading to the changed field
* @see org.jsimpledb.ReferencePath
*/
String[] value() default { };
/**
* Specifies the starting type for the {@link org.jsimpledb.ReferencePath} specified by {@link #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.
*
* @return Java type at which the reference path starts
* @see org.jsimpledb.ReferencePath
*/
Class> startType() default void.class;
/**
* Determines whether this annotation should also be enabled for
* {@linkplain org.jsimpledb.SnapshotJTransaction snapshot transaction} objects.
* If unset, notifications will only be delivered to non-snapshot (i.e., normal) database instances.
*
* @return whether enabled for snapshot transactions
* @see org.jsimpledb.SnapshotJTransaction
*/
boolean snapshotTransactions() default false;
}