io.permazen.annotation.OnChange Maven / Gradle / Ivy
Show all versions of permazen-main Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.annotation;
import io.permazen.Counter;
import io.permazen.DetachedPermazenTransaction;
import io.permazen.ReferencePath;
import io.permazen.change.FieldChange;
import io.permazen.change.SetFieldAdd;
import io.permazen.change.SetFieldChange;
import io.permazen.change.SetFieldClear;
import io.permazen.change.SimpleFieldChange;
import io.permazen.core.Transaction;
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;
/**
* Annotates methods to be invoked whenever some target field in some target object changes during a transaction.
*
*
*
*
*
* Overview
*
*
* Annotated methods accept a single change event parameter whose type must be a sub-type of {@link FieldChange}.
* When a matching change in a matching object occurs, an event of the appropriate type is created
* and delivered to the annotated method.
*
*
* A matching change is one that originates from one of the fields specified by {@link #value} (or every event-generating
* field if {@link #value} is empty), and that has a type compatible with the method's parameter type. This type
* compatibility check is based on the parameter's generic type, not just its raw type.
*
*
* For instance methods, a matching object is one that is found at the end of the {@linkplain ReferencePath reference path}
* specified by {@link #path}, starting from the object to be notified. By default, {@link #path} is empty, which means
* changes in the object itself are monitored. See {@link ReferencePath} for more information about reference paths.
*
*
* For static methods, {@link #path} must be empty and every object is a matching object, so any change event compatible
* with the method's parameter type will be delivered.
*
*
* A class may have multiple {@link OnChange @OnChange} methods, each with a specific purpose.
*
*
Method Parameter Types
*
*
* In all cases the annotated method must return void and take one parameter whose type is compatible with least one
* of the {@link FieldChange} sub-types appropriate for the field(s) being monitored. The parameter type can be narrowed
* to restrict which notifications are delivered. For example, a method with a {@link SetFieldChange} parameter will
* receive notifications about all changes to a set field, but a method with a {@link SetFieldAdd} parameter will receive
* notification only when an element is added to the set. Similarly, a {@link SetFieldClear SetFieldClear<Animal>} will
* receive notifications when a set associated with any {@code Animal} is cleared, but a {@link SetFieldClear
* SetFieldClear<Cat>} will only receive notifications when a set associated with a {@code Cat} is cleared.
*
*
* The method may have any level of access, including {@code private}.
*
*
* Multiple fields may be specified in {@link #value}; if so, all of the fields are monitored together, and they all
* must emit {@link FieldChange}s compatible with the method's parameter type. When multiple fields are monitored
* by the same method, the method's parameter type may need to be widened to accomodate them all.
*
*
* If {@link #value} is empty (the default), then every event-generating field in the target object is monitored,
* though again only changes compatible with the method's parameter type will be delivered.
*
*
Instance vs. Static Methods
*
*
* An instance method 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. For example, if there are three child {@code Node}'s
* pointing to the same parent {@code Node}, and the {@code Node} class has an instance method annotated with
* {@link OnChange @OnChange}{@code (path = "parent", value = "name")}, then all three child {@code Node}'s
* will be notified when the parent's name changes.
*
*
* A static method is invoked once for any matching change event; the {@link path} is ignored and must be empty.
*
*
Examples
*
*
* This example shows how the annotation and the method parameter work together to determine which events are delivered:
*
*
* @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 instance methods
*
* @OnChange
* private void handleAnyChange1(FieldChange<Account> change) {
* // Sees any change to ANY field of THIS account
* }
*
* @OnChange("accessLevels")
* private void handleAccessLevelsChange(SetFieldAdd<Account, AccessLevel> change) {
* // Sees any addition to THIS account's access levels
* }
*
* @OnChange
* private void handleSimpleChange(SimpleFieldChange<Account, ?> change) {
* // Sees any change to any SIMPLE field of THIS account (i.e., "enabled", "name")
* }
*
* // @OnChange static methods
*
* @OnChange
* private static void handleAnyChange2(FieldChange<Account> change) {
* // Sees any change to ANY field of ANY account
* }
* }
*
*
*
* This example shows how to use the {@link #path} property to track changes in other objects:
*
*
* @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 instance 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 static methods
*
* @OnChange("account")
* private static void handleAccountChange(SimpleFieldChange<User, Account> change) {
* // Sees any change to ANY user's account
* }
*
* @OnChange("name")
* private static void handleAccountNameChange(SimpleFieldChange<Account, String> change) {
* // Sees any change to ANY account's name
* }
* }
*
*
* Use Case: Custom Indexes
*
*
* {@link OnChange @OnChange} annotations are useful for creating custom database indexes. In the most general
* sense, an "index" is a secondary data structure that (a) is entirely derived from some primary data structure,
* (b) can be efficiently updated when the primary data changes, and (c) provides a quick answer to a question that
* would otherwise require an extensive calculation if computed from scratch.
*
*
* The code below shows an example index that tracks the average and variance in {@code House} prices.
* See Welford's
* Algorithm for an explanation of how {@code HouseStats.adjust()} works.
*
*
* @PermazenType
* public abstract class House implements PermazenObject {
*
* public abstract String getAddress();
* public abstract void setAddress(String address);
*
* public abstract double getPrice();
* public abstract void setPrice(double price);
* }
*
* @PermazenType(singleton = true)
* public abstract class HouseStats implements PermazenObject {
*
* // Public Methods
*
* public static HouseStats getInstance() {
* return PermazenTransaction.getCurrent().getSingleton(HouseStats.class);
* }
*
* // The total number of houses
* public abstract long getCount();
*
* // The average house price
* public abstract double getAverage();
*
* // The variance in house price
* public double getVariance() {
* return this.getCount() > 1 ? this.getM2() / this.getCount() : 0.0;
* }
*
* // Listener Methods
*
* @OnCreate
* private static void handleAddition(House house) {
* getInstance().adjust(true, house.getPrice()); // price is always zero here
* }
*
* @OnDelete
* private static void handleRemoval(House house) {
* getInstance().adjust(false, house.getPrice());
* }
*
* @OnChange("price")
* private static void handlePriceChange(SimpleFieldChange<House, Double> change) {
* HouseStats stats = getInstance();
* stats.adjust(false, change.getOldValue());
* stats.adjust(true, change.getNewValue());
* }
*
* // Non-public Methods
*
* protected abstract void setCount(long count);
*
* protected abstract void setAverage(double average);
*
* protected abstract double getM2();
* protected abstract void setM2(double m2);
*
* private void adjust(boolean add, double price) {
* long count = this.getCount();
* double mean = this.getAverage();
* double m2 = this.getM2();
* double delta;
* double delta2;
* if (add) {
* count++;
* delta = price - mean;
* mean += delta / count;
* delta2 = price - mean;
* m2 += delta * delta2;
* } else if (count == 1) {
* count = 0;
* mean = 0;
* m2 = 0;
* } else { // reverse the above steps
* delta2 = price - mean;
* mean = (count * mean - price) / (count - 1);
* delta = price - mean;
* m2 -= delta * delta2;
* count--;
* }
* this.setCount(count);
* this.setAverage(mean);
* this.setM2(m2);
* }
* }
*
*
* Use Case: Dependent Objects
*
*
* {@link OnChange @OnChange} annotations can be used to automatically garbage collect dependent objects.
* A dependent object is one that is only useful or meaningful in the context of some other object(s) that reference it.
* You can combine {@link OnChange @OnChange} with {@link OnDelete @OnDelete} and {@link ReferencePath @ReferencePath}
* to keep track of these references.
*
*
* For example, suppose multiple {@code Person}'s can share a common {@code Address}. You only want {@code Address} objects
* in your database when they are referred to by at least one {@code Person}; as soon as an {@code Address} is no longer
* referenced, you want it to be automaticaly garbage collected.
*
*
* You can use {@link OnChange @OnChange} annotations to implement a simple reference counting scheme. Actually, you don't
* need to count references, you just need to check whether any references still exist at the appropriate times.
*
*
* You could do something like the example below. It defers the cleanup until validation time to avoid objects being deleted
* accidentally due to transient reference manipulation.
*
*
* @PermazenType
* public abstract class Person implements PermazenObject {
*
* @NotNull
* public abstract Address getAddress();
* public abstract void setAddress(Address address);
* }
*
* @PermazenType
* public abstract class Address implements PermazenObject {
*
* @NotNull
* public abstract String getNumberAndStreet();
* public abstract void setNumberAndStreet(String ns);
*
* @NotNull
* public abstract String getZip();
* public abstract void setZip(String zip);
*
* // Index queries
*
* @ReferencePath("<-Person.address")
* @Size(min = 1)
* public abstract NavigableSet<Person> getOccupants();
*
* // Dependent Object Checks
*
* @OnDelete(path = "<-Person.address")
* private void onOccupantDelete(Person person) {
* this.revalidate();
* }
*
* @OnChange(path = "<-Person.address", value = "address")
* private void onOccupantChange(SimpleFieldChange<Person, Address> change) {
* this.revalidate();
* }
*
* @OnValidate(early = true)
* private void deleteIfOrphan() {
* if (this.getOccupants().isEmpty())
* this.delete();
* }
* }
*
*
* Notification Delivery
*
*
* Notifications are delivered synchronously within the thread that made the change, after the change is made and just
* prior to returning to the original caller who invoked the method that changed the field.
* If an {@link OnChange @OnChange} method itself makes changes that generate additional change notifications,
* these new notifications are also handled prior to returning to the original caller. Put another way, the queue of
* outstanding notifications triggered by field changes is always emptied before the original method returns. Therefore,
* infinite loops are possible, e.g., if an {@link OnChange @OnChange} method modifies the field it's monitoring
* either directly, or indirectly via other {@link OnChange @OnChange} methods.
*
*
* {@link OnChange @OnChange} operates 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. For example, consider this class:
*
*
* @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);
* }
*
*
* The path {@code "->friends"} implies type {@code Person}, but the field {@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; in this example,
* "no ambiguity" would mean no other sub-types of {@code Person} exist that have a non-{@link String} {@code "name"} field.
*
*
* Note that in the example above, the {@link SimpleFieldChange} parameter to the method {@code friendNameChanged()}
* necessarily has generic type {@code NamedPerson}, not {@code Person}.
*
*
Other Notes
*
*
* {@link 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 {@code "->mylist"} where the changed object
* appears multiple times in {@code mylist}).
*
*
* Some notifications may need to be ignored by objects in {@linkplain DetachedPermazenTransaction detached} transactions;
* you can use {@code 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 {@link Transaction#addCallback Transaction.addCallback()}.
*
*
* See {@link 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 ReferencePath
* @see io.permazen.change
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Documented
public @interface OnChange {
/**
* Specify the reference path to the target object(s) that should be monitored for changes.
* See {@link 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.
*
*
* When annotating static methods, this property is unused and must be left unset.
*
* @return reference path leading to monitored objects
* @see ReferencePath
*/
String path() default "";
/**
* 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 {@link FieldChange}
* event sub-types emitted by that field.
*
*
* If zero fields are specified (the default), every field in the target object(s) that emits
* {@link FieldChange}s compatible with the method's parameter type will be monitored for changes.
*
* @return the names of the fields to monitored in the target objects
*/
String[] value() default { };
}