All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.permazen.annotation.OnChange Maven / Gradle / Ivy

The newest version!

/*
 * 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 { }; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy