Class PersistentObject<T>

java.lang.Object
org.dellroad.stuff.pobj.PersistentObject<T>
Type Parameters:
T - type of the root object

public class PersistentObject<T> extends Object
Main class for Simple XML Persistence Objects (POBJ).

Overview

A PersistentObject instance manages an in-memory database with ACID semantics and strict validation that is represented by a regular Java object graph (i.e., a root Java object and all of the other objects that it (indirectly) references). The object graph can be (de)serialized to/from XML, and this XML representation is used for persistence in the file system. The backing XML file is read in at initialization time and updated after each change.

All changes required copying the entire object graph, and are atomic and strictly serialized. In other words, the entire object graph is read and written by value. On the filesystem, the persistent XML file is updated by writing out a new, temporary copy and renaming the copy onto the original, using File.renameTo() so that changes are also atomic from a filesystem perspective, i.e., it's not possible to open an invalid or partial XML file (assuming filesystem renames are atomic, e.g., all UNIX variants). This class also supports information flowing in the other direction, where we pick up "out of band" updates to the XML file (see below).

This class is most appropriate for use with information that must be carefully controlled and validated, but that doesn't change frequently. Configuration information for an application stored in a config.xml file is a typical use case; beans whose behavior is determined by the configured information can subclass AbstractConfiguredBean and have their lifecycles managed automatically.

Because each change involves copying of the entire graph, an efficient graph copy operation is desirable. The default method is to serialize and then deserialize the object graph to/from XML in memory. See GraphCloneable for a much more efficient approach.

Validation

Validation is peformed in Java (not in XML) and defined by the provided PersistentObjectDelegate. This delegate guarantees that only a valid root Java object can be read or written. Setting an invalid Java root object via setRoot() with throw an exception; reading an invalid XML file on disk will generate an error (or be ignored; see "Empty Starts" below).

Update Details

When the object graph is updated, it must pass validation checks, and then the persistent XML file is updated and listener notifications are sent out. Listeners are always notified in a separate thread from the one that invoked setRoot(). Support for delayed write-back of the persistent XML file is included: this allows modifications that occur in rapid succession to be consolidated into a single filesystem write operation.

Support for optimistic locking is included. There is a "current version" number which is incremented each time the object graph is updated; writes may optionally specify this number to ensure no intervening changes have occurred. If concurrent updates are expected, applications may choose to implement a 3-way merge algorithm of some kind to handle optimistic locking failures.

To implement a truly atomic read-modify-write operation without the possibility of locking failure, simply synchronize on this instance, e.g.:


  synchronized (pobj) {
      MyRoot root = pboj.getRoot();
      root.setFoobar("new value");    // or whatever else we want to modify
      pobj.setRoot(root);
  }
  

Instances can also be configured to automatically preserve one or more backup copies of the persistent file on systems that support hard links (see FileStreamRepository). Set the numBackups property to enable.

"Out-of-band" Writes

When a non-zero check interval is configured, instances support "out-of-band" writes directly to the XML persistent file by some other process. This can be handy in cases where the other process (perhaps hand edits) is updating the persistent file and you want to have the running Java process pick up the changes just as if setRoot() had been invoked. In particular, instances will detect the appearance of a newly appearing persistent file after has starting without one (see "empty starts" below). In all cases, persistent objects must properly validate. To avoid reading a partial file the external process should write the file atomically by creating a temporary file and renaming it; however, this race window is small and in any case the problem is self-correcting because a partially written XML file will not validate, and so it will be ignored and retried after another check interval milliseconds has passed.

A special case of this is effected when setRoot() is never explicitly invoked by the application. Then some other process must be responsible for all database updates via XML file, and this class will automatically pick them up, validate them, and send out notifications to listeners.

Empty Starts and Stops

An "empty start" occurs when an instance is started but the persistent XML file is either missing, does not validate, or cannot be read for some other reason. In such cases, the instance will start with no object graph, and getRoot() will return null. This situation will correct itself as soon as the object graph is written via setRoot() or the persistent file appears (effecting an "out-of-band" update). At that time, listeners will be notified for the first time, with PersistentObjectEvent.getOldRoot() returning null.

Whether empty starts are allowed is determined by the allowEmptyStart property (default false). When empty starts are disallowed and the persistent XML file cannot be successfully read, then start() will instead throw an immediate PersistentObjectException.

Similarly, "empty stops" are allowed when the allowEmptyStop property is set to true (by default it is false). An "empty stop" occurs when a null value is passed to setRoot() (note, an invalid XML file appearing on disk does not cause an empty start). Subsequent invocations of getRoot() will return null. The persistent file is not modified when null is passed to setRoot(T, long, boolean). When empty stops are disallowed, then invoking setRoot(T, long, boolean) with a null object will result in an IllegalArgumentException.

Allowing empty starts and/or stops essentially creates an "unconfigured" state represented by a null root object. When empty starts and empty stops are both disallowed, there is no "unconfigured" state: once started, getRoot() can be relied upon to always return a non-null, validated root object.

See AbstractConfiguredBean for a useful superclass that automatically handles starting and stopping based on the state of an associated PersistentObject.

Shared Roots

Each time setRoot() or getRoot() is invoked, a deep copy of the root object is made. This prevents external code from changing any node in the "official" object graph held by this instance, and allows invokers of getRoot() to modify to the returned object graph without affecting other invokers. However, there may be cases where this deep copy is an expensive operation in terms of time or memory. The getSharedRoot() method can be used in these situations. This method returns the same root object each time it is invoked (this shared root is itself a deep copy of the "official" root). Therefore, only the very first invocation pays the price of a copy. However, all invokers of getSharedRoot() must treat the object graph as read-only to avoid each other seeing unexpected changes.

Delegate Function

Instances must be configured with a PersistentObjectDelegate that knows how to validate the object graph and perform conversions to and from XML. See PersistentObjectDelegate and its implementations for details.

Schema Changes

Like any database, the XML schema may evolve over time. The PersistentObjectSchemaUpdater class provides a simple way to apply and manage schema updates using XSLT transforms.

Transaction Manager

If using Spring, consider using a PersistentObjectTransactionManager for transactional access to a PersistentObject.

See Also:
  • Field Details

    • log

      protected final Logger log
  • Constructor Details

    • PersistentObject

      public PersistentObject(PersistentObjectDelegate<T> delegate, File file, long writeDelay, long checkInterval)
      Constructor.

      The writeDelay is the maximum delay after an update operation before a write-back to the persistent file must be initiated.

      Parameters:
      delegate - delegate supplying required operations
      file - the file used to persist
      writeDelay - write delay in milliseconds, or zero for synchronous write-back
      checkInterval - check interval in milliseconds, or zero to disable persistent file checks
      Throws:
      IllegalArgumentException - if delegate or file is null
      IllegalArgumentException - if writeDelay or checkInterval is negative
    • PersistentObject

      public PersistentObject(PersistentObjectDelegate<T> delegate, File file)
      Simplified constructor configuring for synchronous write-back and no persistent file checks.

      Equivalent to:

      PersistentObject(delegate, file, 0L, 0L);
      Parameters:
      delegate - delegate supplying required operations
      file - the file used to persist
      Throws:
      IllegalArgumentException - if delegate or file is null
    • PersistentObject

      public PersistentObject()
      Default constructor. Caller must still configure the delegate and persistent file prior to start.
  • Method Details

    • getDelegate

      public PersistentObjectDelegate<T> getDelegate()
      Get the configured PersistentObjectDelegate.
      Returns:
      the delegate supplying required operations
    • setDelegate

      public void setDelegate(PersistentObjectDelegate<T> delegate)
      Parameters:
      delegate - delegate supplying required operations
      Throws:
      IllegalArgumentException - if delegate is null
      IllegalStateException - if this instance is started
    • getFile

      public File getFile()
      Get the persistent file containing the XML form of the persisted object.
      Returns:
      file used to persist the root object
    • setFile

      public void setFile(File file)
      Set the persistent file containing the XML form of the persisted object.
      Parameters:
      file - the file used to persist the root object
      Throws:
      IllegalArgumentException - if file is null
      IllegalStateException - if this instance is started
    • getWriteDelay

      public long getWriteDelay()
      Get the maximum delay after an update operation before a write-back to the persistent file must be initiated.
      Returns:
      write delay in milliseconds, or zero for synchronous write-back
    • setWriteDelay

      public void setWriteDelay(long writeDelay)
      Set the maximum delay after an update operation before a write-back to the persistent file must be initiated.
      Parameters:
      writeDelay - write delay in milliseconds, or zero for synchronous write-back
      Throws:
      IllegalArgumentException - if writeDelay is negative
      IllegalStateException - if this instance is started
    • getCheckInterval

      public long getCheckInterval()
      Get the delay time between periodic checks for changes in the underlying persistent file.
      Returns:
      check interval in milliseconds, or zero if periodic checks are disabled
    • setCheckInterval

      public void setCheckInterval(long checkInterval)
      Set the delay time between periodic checks for changes in the underlying persistent file.
      Parameters:
      checkInterval - check interval in milliseconds, or zero if periodic checks are disabled
      Throws:
      IllegalArgumentException - if writeDelay is negative
      IllegalStateException - if this instance is started
    • getVersion

      public long getVersion()
      Get the version of the current root.

      This returns a value which increases monotonically with each update. The version number is not persisted with the persistent file; each instance of this class keeps its own version count. The version is reset to zero when stop() is invoked.

      Returns:
      the current positive object version, or zero if no value has been loaded yet or this instance is not started
    • getNumBackups

      public int getNumBackups()
      Get the number of backup copies to preserve.

      Backup files have suffixes of the form .1, .2, etc., in reverse chronological order. Each time a new root object is written, the existing files are rotated.

      Back-ups are created via hard links and are only supported on UNIX systems.

      The default is zero, which disables backups.

      Returns:
      number of backing file backup copies
    • setNumBackups

      public void setNumBackups(int numBackups)
      Set the number of backup copies to preserve.
      Parameters:
      numBackups - number of backing file backup copies
      Throws:
      IllegalArgumentException - if numBackups is negative
      IllegalStateException - if no persistent file has been configured yet
      See Also:
    • isAllowEmptyStart

      public boolean isAllowEmptyStart()
      Determine whether this instance should allow an "empty start".

      The default for this property is false.

      Returns:
      if empty starts are allowed
    • setAllowEmptyStart

      public void setAllowEmptyStart(boolean allowEmptyStart)
      Configure whether an "empty start" is allowed.

      The default for this property is false.

      Parameters:
      allowEmptyStart - true to allow empty starts
      Throws:
      IllegalStateException - if this instance is started
    • isAllowEmptyStop

      public boolean isAllowEmptyStop()
      Determine whether this instance should allow an "empty stop".

      The default for this property is false.

      Returns:
      true if empty stops are allowed
    • setAllowEmptyStop

      public void setAllowEmptyStop(boolean allowEmptyStop)
      Configure whether an "empty stop" is allowed.

      The default for this property is false.

      Parameters:
      allowEmptyStop - true to allow empty stops
      Throws:
      IllegalStateException - if this instance is started
    • isStarted

      public boolean isStarted()
      Determine whether this instance is started.
      Returns:
      true if this instance currently started
    • start

      public void start()
      Start this instance. Does nothing if already started.
      Throws:
      PersistentObjectException - if an error occurs
    • stop

      public void stop()
      Stop this instance. Does nothing if already stopped.
      Throws:
      PersistentObjectException - if a delayed write back is pending and error occurs while performing the write
    • hasRoot

      public boolean hasRoot()
      Determine whether this instance has a non-null root object.
      Returns:
      true if this instance has a non-null root object, false if this instance is not started or is in an empty start or empty stop state
    • getRoot

      public T getRoot()
      Atomically read the root object.

      In the situation of an empty start or empty stop, this instance is "unconfigured" and null will be returned. This can only happen if setAllowEmptyStart(true) or setAllowEmptyStop(true) has been invoked.

      This returns a deep copy of the current root object; any subsequent modifications are not written back. Use getSharedRoot() instead to avoid the cost of the deep copy at the risk of seeing modifications caused by other invokers.

      Returns:
      the current root instance, or null during an empty start or empty stop
      Throws:
      IllegalStateException - if this instance is not started
      PersistentObjectException - if an error occurs
    • getSharedRoot

      public T getSharedRoot()
      Get a shared copy of the root object.

      This returns a copy of the root object, but it returns the same copy each time until the next change. This method is more efficient than getRoot(), but all callers must agree not to modify the returned object or any object in its graph of references.

      Returns:
      shared copy of the root instance, or null during an empty start or empty stop
      Throws:
      IllegalStateException - if this instance is not started
    • getRootSnapshot

      public PersistentObject<T>.Snapshot getRootSnapshot()
      Read the root object (as with getRoot()) and its version (as with getVersion()) in one atomic operation. This avoids the race condition inherent in trying to perform these operations separately.
      Returns:
      snapshot of the current root, or null during an empty start or empty stop
      Throws:
      IllegalStateException - if this instance is not started
      PersistentObjectException - if an error occurs
      See Also:
    • getSharedRootSnapshot

      public PersistentObject<T>.Snapshot getSharedRootSnapshot()
      Read the shared root object (as with getSharedRoot()) and its version (as with getVersion()) in one atomic operation. This avoids the race condition inherent in trying to perform these operations separately.
      Returns:
      snapshot of the current shared root, or null during an empty start or empty stop
      Throws:
      IllegalStateException - if this instance is not started
      PersistentObjectException - if an error occurs
      See Also:
    • setRoot

      public long setRoot(T newRoot, long expectedVersion, boolean alreadyValidated)
      Atomically update the root object.

      The given object is deep-copied, the copy replaces the current root, and the new version number is returned. If there is no write delay configured, the new version is written to the underlying file synchronously and a successful return from this method indicates the new root has been persisted. Otherwise, the write will occur at a later time in a separate thread.

      If expectedVersion is non-zero, then if the current version is not equal to it, a PersistentObjectVersionException exception is thrown. This mechanism can be used for optimistic locking.

      If empty stops are allowed, then newRoot may be null, in which case it replaces the current root and subsequent calls to getRoot() will return null. When a null newRoot is set, the persistent file is not modified.

      If the given root object is the same as the current root object, then no action is taken and the current (unchanged) version number is returned.

      After a successful change, any registered listeners are notified in a separate thread from the one that invoked this method.

      When using asynchronous write-back, it is possible that this method may succeed, even though writing out the new file fails. In that case, PersistentObjectDelegate.handleWritebackException() will be invoked (in a separate thread). Exceptions generated by synchronous write operations are simply wrapped in PersistentObjectException and rethrown.

      Parameters:
      newRoot - new persistent object
      expectedVersion - expected current version number, or zero to ignore the current version number
      alreadyValidated - true if newRoot has already been validated
      Returns:
      the new current version number (unchanged if newRoot is the same as the current root)
      Throws:
      IllegalArgumentException - if newRoot is null and empty stops are disallowed
      IllegalArgumentException - if expectedVersion is negative
      IllegalStateException - if this instance is not started
      PersistentObjectException - if an error occurs
      PersistentObjectVersionException - if expectedVersion is non-zero and not equal to the current version
      PersistentObjectValidationException - if the new root has validation errors
    • setRoot

      public long setRoot(T newRoot, long expectedVersion)
      Atomically update the root object.

      This method is equivalent to:

      setRoot(newRoot, expectedVersion, false);
      Parameters:
      newRoot - new persistent object
      expectedVersion - expected current version number, or zero to ignore the current version number
      Returns:
      the new current version number (unchanged if newRoot is the same as the current root)
    • setRoot

      public final long setRoot(T newRoot)
      Atomically update the root object.

      The is a convenience method, equivalent to:

      setRoot(newRoot, 0)

      This method cannot throw PersistentObjectVersionException.

      Parameters:
      newRoot - new persistent root object
      Returns:
      the new current version number (unchanged if newRoot is the same as the current root)
    • checkFile

      public void checkFile()
      Check the persistent file for an "out-of-band" update.

      If the persistent file has a newer timestamp than the timestamp of the most recently read or written version, then it will be read and applied to this instance.

      Throws:
      IllegalStateException - if this instance is not started
      PersistentObjectException - if an error occurs
    • addListener

      public void addListener(PersistentObjectListener<T> listener)
      Add a listener to be notified each time the object graph changes.

      Listeners are notified in a separate thread from the one that caused the root object to change.

      Parameters:
      listener - listener to add
      Throws:
      IllegalArgumentException - if listener is null
    • removeListener

      public void removeListener(PersistentObjectListener<T> listener)
      Remove a listener added via addListener().
      Parameters:
      listener - listener to remove
    • toString

      public String toString()
      Get a simple string description of this instance. This description appears in all log messages.
      Overrides:
      toString in class Object
    • read

      protected T read()
      Read the persistent file. Does not validate it.
      Returns:
      root object decoded from backing file
      Throws:
      PersistentObjectException - if the file does not exist or cannot be read
      PersistentObjectException - if an error occurs
    • write

      protected final void write(T obj)
      Write the persistent file and rotate any backups.

      A temporary file is created in the same directory and then renamed to provide for an atomic update (on supporting operating systems).

      Parameters:
      obj - root object to write
      Throws:
      IllegalArgumentException - if obj is null
      PersistentObjectException - if an error occurs
    • createResult

      protected Result createResult(OutputStream output, File systemId)
      Create a Result targeting the given OutputStream.

      The implementation in PersistentObject creates and returns a StreamResult.

      Parameters:
      output - XML output stream
      systemId - system ID
      Returns:
      XML result output
    • notifyListeners

      protected void notifyListeners(long newVersion, T oldRoot, T newRoot)
      Notify listeners of a change in value.
      Parameters:
      newVersion - the version number associated with the new root
      oldRoot - previous root object
      newRoot - new root object
    • read

      public static <T> T read(PersistentObjectDelegate<T> delegate, Source source, boolean validate)
      Read in a persistent object using the given delegate.

      This is a convenience method that can be used for a one-time deserialization from an XML Source without having to go through the whole PersistentObject lifecycle.

      Type Parameters:
      T - root object type
      Parameters:
      delegate - delegate supplying required operations
      source - source for serialized root object
      validate - whether to also validate the root object
      Returns:
      deserialized root object, never null
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectValidationException - if validate is true and the deserialized root has validation errors
      PersistentObjectException - if an error occurs
    • validate

      public void validate(T root)
      Validate a root object.

      Deletgates to PersistentObjectDelegate.validate(T) to perform the actual validation.

      Parameters:
      root - root object to validate
      Throws:
      IllegalArgumentException - if root is null
      PersistentObjectValidationException - if the root has validation errors
    • read

      public static <T> T read(PersistentObjectDelegate<T> delegate, File file, boolean validate)
      Read in a persistent object from the given File using the given delegate.

      This is a wrapper around read(PersistentObjectDelegate, Source, boolean) that handles opening and closing the given File.

      Type Parameters:
      T - root object type
      Parameters:
      delegate - delegate supplying required operations
      file - file to read from
      validate - whether to also validate the root object
      Returns:
      deserialized root object, never null
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectValidationException - if validate is true and the deserialized root has validation errors
      PersistentObjectException - if file cannot be read
      PersistentObjectException - if an error occurs
    • read

      public static <T> T read(PersistentObjectDelegate<T> delegate, InputStream input, boolean validate)
      Read in a persistent object from the given InputStream using the given delegate.

      This is a wrapper around read(PersistentObjectDelegate, Source, boolean).

      Type Parameters:
      T - root object type
      Parameters:
      delegate - delegate supplying required operations
      input - input to read from
      validate - whether to also validate the root object
      Returns:
      deserialized root object, never null
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectValidationException - if validate is true and the deserialized root has validation errors
      PersistentObjectException - if an I/O error occurs
      PersistentObjectException - if an error occurs
    • write

      public static <T> void write(T root, PersistentObjectDelegate<T> delegate, Result result)
      Write a persistent object using the given delegate.

      This is a convenience method that can be used for one-time serialization to an XML Result without having to go through the whole PersistentObject lifecycle.

      Type Parameters:
      T - root object type
      Parameters:
      root - root object to serialize
      delegate - delegate supplying required operations
      result - destination
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectException - if an error occurs
    • write

      public static <T> void write(T root, PersistentObjectDelegate<T> delegate, File file)
      Write a persistent object using the given delegate.

      This is a wrapper around write(Object, PersistentObjectDelegate, Result) that handles opening and closing the given File.

      Type Parameters:
      T - root object type
      Parameters:
      root - root object to serialize
      delegate - delegate supplying required operations
      file - destination file
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectException - if an error occurs
    • write

      public static <T> void write(T root, PersistentObjectDelegate<T> delegate, OutputStream output)
      Write a persistent object using the given delegate.

      This is a wrapper around write(Object, PersistentObjectDelegate, Result).

      Type Parameters:
      T - root object type
      Parameters:
      root - root object to serialize
      delegate - delegate supplying required operations
      output - XML destination
      Throws:
      IllegalArgumentException - if any parameter is null
      PersistentObjectException - if an error occurs