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

org.dellroad.stuff.pobj.PersistentObject Maven / Gradle / Ivy


/*
 * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.stuff.pobj;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.validation.ConstraintViolation;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.dellroad.stuff.io.AtomicUpdateFileOutputStream;
import org.dellroad.stuff.io.FileStreamRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Main class for Simple XML Persistence Objects (POBJ).
 *
 * 

Overview

* *

* A {@link 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 {@link File#renameTo * 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 {@code config.xml} * file is a typical use case; beans whose behavior is determined by the configured information can subclass * {@link AbstractConfiguredBean} and have their lifecycles managed automatically. * *

* Because each change involves copying of the entire graph, an efficient graph copy operation is desirable. * The {@linkplain AbstractDelegate#copy default method} is to serialize and then deserialize the object graph * to/from XML in memory. See {@link org.dellroad.stuff.java.GraphCloneable} for a much more efficient approach. * *

Validation

* *

* Validation is peformed in Java (not in XML) and defined by the provided {@link PersistentObjectDelegate}. This delegate * guarantees that only a valid root Java object can be read or written. Setting an invalid Java root object via * {@link #setRoot 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 * {@link #setRoot 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 {@link FileStreamRepository}). Set the {@link #getNumBackups numBackups} property to enable. * *

"Out-of-band" Writes

* *

* When a non-zero {@linkplain #getCheckInterval 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 * {@link PersistentObject#setRoot 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 * {@linkplain #getCheckInterval check interval} milliseconds has passed. * *

* A special case of this is effected when {@link PersistentObject#setRoot 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 {@linkplain #start 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 {@link #getRoot} will return null. This situation will correct itself as soon as the object graph is written via * {@link #setRoot setRoot()} or the persistent file appears (effecting an "out-of-band" update). At that time, {@linkplain * #addListener listeners} will be notified for the first time, with {@link PersistentObjectEvent#getOldRoot} returning null. * *

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

* Similarly, "empty stops" are allowed when the {@link #isAllowEmptyStop allowEmptyStop} property is set to {@code true} * (by default it is {@code false}). An "empty stop" occurs when a null value is passed to {@link #setRoot setRoot()} * (note, an invalid XML file appearing on disk does not cause an empty start). * Subsequent invocations of {@link #getRoot} will return {@code null}. The persistent file is not * modified when null is passed to {@link #setRoot}. When empty stops are disallowed, then invoking * {@link #setRoot} with a {@code null} object will result in an {@link 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, {@link #getRoot} * can be relied upon to always return a non-null, validated root object. * *

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

Shared Roots

* * Each time {@link #setRoot setRoot()} or {@link #getRoot 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 {@link #getRoot 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 {@link #getSharedRoot 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 {@link #getSharedRoot getSharedRoot()} * must treat the object graph as read-only to avoid each other seeing unexpected changes. * *

Delegate Function

* *

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

Schema Changes

* *

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

Transaction Manager

* *

* If using Spring, consider using a {@link PersistentObjectTransactionManager} for transactional access to a * {@link PersistentObject}. * * @param type of the root object * @see PersistentObjectDelegate * @see PersistentObjectSchemaUpdater * @see PersistentObjectTransactionManager * @see AbstractConfiguredBean */ public class PersistentObject { private static final long EXECUTOR_SHUTDOWN_TIMEOUT = 1000; // 1 second protected final Logger log = LoggerFactory.getLogger(this.getClass()); private final HashSet> listeners = new HashSet>(); private PersistentObjectDelegate delegate; private FileStreamRepository streamRepository; private long writeDelay; private long checkInterval; private T root; // current root object (private) private T sharedRoot; // current root object (shared) private T writebackRoot; // pending persistent file writeback value private ScheduledExecutorService scheduledExecutor; // used for file checking and delayed write-back private ExecutorService notifyExecutor; // used to notify listeners private ScheduledFuture pendingWrite; // a pending delayed write-back private long version; // current root object version private long timestamp; // timestamp of persistent file when we last read it private boolean allowEmptyStart; private boolean allowEmptyStop; private boolean started; /** * Constructor. * *

* The {@code writeDelay} is the maximum delay after an update operation before a write-back to the persistent file * must be initiated. * * @param delegate delegate supplying required operations * @param file the file used to persist * @param writeDelay write delay in milliseconds, or zero for immediate write-back * @param checkInterval check interval in milliseconds, or zero to disable persistent file checks * @throws IllegalArgumentException if {@code delegate} or {@code file} is null * @throws IllegalArgumentException if {@code writeDelay} or {@code checkInterval} is negative */ public PersistentObject(PersistentObjectDelegate delegate, File file, long writeDelay, long checkInterval) { this.setDelegate(delegate); this.setFile(file); this.setWriteDelay(writeDelay); this.setCheckInterval(checkInterval); } /** * Simplified constructor configuring for immediate write-back and no persistent file checks. * *

* Equivalent to: *

* PersistentObject(delegate, file, 0L, 0L); *
* * @param delegate delegate supplying required operations * @param file the file used to persist * @throws IllegalArgumentException if {@code delegate} or {@code file} is null */ public PersistentObject(PersistentObjectDelegate delegate, File file) { this(delegate, file, 0, 0); } /** * Default constructor. Caller must still configure the delegate and persistent file prior to start. */ public PersistentObject() { } /** * Get the configured {@link PersistentObjectDelegate}. * * @return the delegate supplying required operations */ public synchronized PersistentObjectDelegate getDelegate() { return this.delegate; } /** * Configure the {@link PersistentObjectDelegate}. * * @param delegate delegate supplying required operations * @throws IllegalArgumentException if {@code delegate} is null * @throws IllegalStateException if this instance is started */ public synchronized void setDelegate(PersistentObjectDelegate delegate) { if (delegate == null) throw new IllegalArgumentException("null delegate"); if (this.isStarted()) throw new IllegalStateException("can't set the delegate while started"); this.delegate = delegate; } /** * Get the persistent file containing the XML form of the persisted object. * * @return file used to persist the root object */ public synchronized File getFile() { return this.streamRepository != null ? this.streamRepository.getFile() : null; } /** * Set the persistent file containing the XML form of the persisted object. * * @param file the file used to persist the root object * @throws IllegalArgumentException if {@code file} is null * @throws IllegalStateException if this instance is started */ public synchronized void setFile(File file) { if (file == null) throw new IllegalArgumentException("null file"); if (this.isStarted()) throw new IllegalStateException("can't set the persistent file while started"); this.streamRepository = new FileStreamRepository(file); } /** * Get the maximum delay after an update operation before a write-back to the persistent file * must be initiated. * * @return write delay in milliseconds, or zero for immediate write-back */ public synchronized long getWriteDelay() { return this.writeDelay; } /** * Set the maximum delay after an update operation before a write-back to the persistent file * must be initiated. * * @param writeDelay write delay in milliseconds, or zero for immediate write-back * @throws IllegalArgumentException if {@code writeDelay} is negative * @throws IllegalStateException if this instance is started */ public synchronized void setWriteDelay(long writeDelay) { if (writeDelay < 0) throw new IllegalArgumentException("negative writeDelay"); if (this.isStarted()) throw new IllegalStateException("can't set the write delay while started"); this.writeDelay = writeDelay; } /** * Get the delay time between periodic checks for changes in the underlying persistent file. * * @return check interval in milliseconds, or zero if periodic checks are disabled */ public synchronized long getCheckInterval() { return this.checkInterval; } /** * Set the delay time between periodic checks for changes in the underlying persistent file. * * @param checkInterval check interval in milliseconds, or zero if periodic checks are disabled * @throws IllegalArgumentException if {@code writeDelay} is negative * @throws IllegalStateException if this instance is started */ public synchronized void setCheckInterval(long checkInterval) { if (checkInterval < 0) throw new IllegalArgumentException("negative checkInterval"); if (this.isStarted()) throw new IllegalStateException("can't set the check interval while started"); this.checkInterval = checkInterval; } /** * 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 {@link #stop stop()} is invoked. * * @return the current positive object version, or zero if no value has been loaded yet or this instance is not started */ public synchronized long getVersion() { return this.version; } /** * 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. * * @return number of backing file backup copies */ public int getNumBackups() { return this.streamRepository != null ? this.streamRepository.getNumBackups() : 0; } /** * Set the number of backup copies to preserve. * * @param numBackups number of backing file backup copies * @throws IllegalArgumentException if {@code numBackups} is negative * @throws IllegalStateException if no persistent file has been configured yet * @see #getNumBackups */ public void setNumBackups(int numBackups) { if (this.streamRepository == null) throw new IllegalStateException("the persistent file must be configured prior to the number of backups"); this.streamRepository.setNumBackups(numBackups); } /** * Determine whether this instance should allow an "empty start". * *

* The default for this property is false. * * @return if empty starts are allowed */ public boolean isAllowEmptyStart() { return this.allowEmptyStart; } /** * Configure whether an "empty start" is allowed. * *

* The default for this property is false. * * @param allowEmptyStart true to allow empty starts * @throws IllegalStateException if this instance is started */ public synchronized void setAllowEmptyStart(boolean allowEmptyStart) { if (this.isStarted()) throw new IllegalStateException("can't change whether empty starts are allowed while started"); this.allowEmptyStart = allowEmptyStart; } /** * Determine whether this instance should allow an "empty stop". * *

* The default for this property is false. * * @return true if empty stops are allowed */ public boolean isAllowEmptyStop() { return this.allowEmptyStop; } /** * Configure whether an "empty stop" is allowed. * *

* The default for this property is false. * * @param allowEmptyStop true to allow empty stops * @throws IllegalStateException if this instance is started */ public synchronized void setAllowEmptyStop(boolean allowEmptyStop) { if (this.isStarted()) throw new IllegalStateException("can't set the check interval while started"); this.allowEmptyStop = allowEmptyStop; } /** * Determine whether this instance is started. * * @return true if this instance currently started */ public synchronized boolean isStarted() { return this.started; } /** * Start this instance. Does nothing if already started. * * @throws PersistentObjectException if an error occurs */ public synchronized void start() { // Already started? if (this.started) return; // Sanity check if (this.streamRepository == null) throw new PersistentObjectException("no file configured"); if (this.delegate == null) throw new PersistentObjectException("no delegate configured"); // Create executor services this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); this.notifyExecutor = Executors.newSingleThreadExecutor(); // Read file (if it exists) this.log.info(this + ": starting"); if (this.getFile().exists()) { // Apply file try { this.applyFile(this.getFile().lastModified(), true); } catch (PersistentObjectException e) { if (!this.isAllowEmptyStart()) throw e; this.log.warn("empty start: unable to load persistent file `" + this.getFile() + "': " + e); } } else { // Persistent file does not exist, so get default value from delegate final T defaultValue = this.delegate.getDefaultValue(); // If no default value, we have an empty start situation, otherwise apply the default value if (defaultValue == null) { if (!this.isAllowEmptyStart()) { throw new PersistentObjectException("persistent file `" + this.getFile() + "' does not exist," + " there is no default value, and empty starts are disallowed"); } this.log.info(this + ": empty start: persistent file `" + this.getFile() + "' does not exist" + " and there is no default value"); } else { this.log.info(this + ": persistent file `" + this.getFile() + "' does not exist, applying default value"); try { this.setRootInternal(defaultValue, 0, false, true, false); } catch (PersistentObjectException e) { // e.g., validation failure if (!this.isAllowEmptyStart()) throw e; this.log.warn("empty start: unable to apply default value: " + e); } } } // Start checking the file if (this.checkInterval > 0) { this.scheduledExecutor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { PersistentObject.this.checkFileTimeout(); } }, this.checkInterval, this.checkInterval, TimeUnit.MILLISECONDS); } // Done this.started = true; } /** * Stop this instance. Does nothing if already stopped. * * @throws PersistentObjectException if a delayed write back is pending and error occurs while performing the write */ public synchronized void stop() { // Already stopped? if (!this.started) return; // Perform any lingering pending save now if (this.cancelPendingWrite()) this.writeback(); // Stop executor services this.log.info(this + ": shutting down"); this.scheduledExecutor.shutdown(); this.notifyExecutor.shutdown(); this.awaitTermination(this.scheduledExecutor, "scheduledExecutor"); this.awaitTermination(this.notifyExecutor, "notifyExecutor"); // Reset this.scheduledExecutor = null; this.notifyExecutor = null; this.root = null; this.sharedRoot = null; this.writebackRoot = null; this.timestamp = 0; this.started = false; } /** * Determine whether this instance has a non-null root object. * * @return 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 */ public boolean hasRoot() { return this.root != null; } /** * 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 {@link #setAllowEmptyStart setAllowEmptyStart(true)} or {@link #setAllowEmptyStop * setAllowEmptyStop(true)} has been invoked. * *

* This returns a deep copy of the current root object; any subsequent modifications are not written back. * Use {@link #getSharedRoot} instead to avoid the cost of the deep copy at the risk of seeing modifications * caused by other invokers. * * @return the current root instance, or null during an empty start or empty stop * @throws IllegalStateException if this instance is not started * @throws PersistentObjectException if an error occurs */ public synchronized T getRoot() { // Sanity check if (!this.started) throw new IllegalStateException("not started"); // Copy root return this.root != null ? this.delegate.copy(this.root) : null; } /** * 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 {@link #getRoot}, but all callers must agree not to modify the returned object * or any object in its graph of references. * * @return shared copy of the root instance, or null during an empty start or empty stop * @throws IllegalStateException if this instance is not started */ public synchronized T getSharedRoot() { if (this.sharedRoot == null) this.sharedRoot = this.getRoot(); return this.sharedRoot; } /** * Read the root object (as with {@link #getRoot}) and its version (as with {@link #getVersion}) in one atomic operation. * This avoids the race condition inherent in trying to perform these operations separately. * * @return snapshot of the current root, or null during an empty start or empty stop * @throws IllegalStateException if this instance is not started * @throws PersistentObjectException if an error occurs * @see #getRoot */ public synchronized Snapshot getRootSnapshot() { T myRoot = this.getRoot(); if (myRoot == null) return null; return new Snapshot(myRoot, this.version); } /** * Read the shared root object (as with {@link #getSharedRoot}) and its version (as with {@link #getVersion}) * in one atomic operation. * This avoids the race condition inherent in trying to perform these operations separately. * * @return snapshot of the current shared root, or null during an empty start or empty stop * @throws IllegalStateException if this instance is not started * @throws PersistentObjectException if an error occurs * @see #getSharedRoot */ public synchronized Snapshot getSharedRootSnapshot() { T mySharedRoot = this.getSharedRoot(); if (mySharedRoot == null) return null; return new Snapshot(mySharedRoot, this.version); } /** * 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 immediately 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 {@code expectedVersion} is non-zero, then if the current version is not equal to it, * a {@link PersistentObjectVersionException} exception is thrown. This mechanism * can be used for optimistic locking. * *

* If empty stops are allowed, then {@code newRoot} may be null, in which case it replaces the * current root and subsequent calls to {@link #getRoot} will return null. When a null * {@code newRoot} is set, the persistent file is not modified. * *

* If the given root object is {@linkplain PersistentObjectDelegate#isSameGraph 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 {@linkplain PersistentObjectListener listeners} are notified in a * separate thread from the one that invoked this method. * * @param newRoot new persistent object * @param expectedVersion expected current version number, or zero to ignore the current version number * @return the new current version number (unchanged if {@code newRoot} is * {@linkplain PersistentObjectDelegate#isSameGraph the same as} the current root) * @throws IllegalArgumentException if {@code newRoot} is null and empty stops are disallowed * @throws IllegalArgumentException if {@code expectedVersion} is negative * @throws IllegalStateException if this instance is not started * @throws PersistentObjectException if an error occurs * @throws PersistentObjectVersionException if {@code expectedVersion} is non-zero and not equal to the current version * @throws PersistentObjectValidationException if the new root has validation errors */ public final synchronized long setRoot(T newRoot, long expectedVersion) { return this.setRootInternal(newRoot, expectedVersion, false, false, false); } synchronized long setRootInternal(T newRoot, long expectedVersion, boolean readingFile, boolean allowNotStarted, boolean alreadyValidated) { // Sanity check if (newRoot == null && !this.isAllowEmptyStop()) throw new IllegalArgumentException("newRoot is null but empty stops are not enabled"); if (!this.started && !allowNotStarted) throw new IllegalStateException("not started"); if (expectedVersion < 0) throw new IllegalStateException("negative expectedVersion"); // Check version number if (expectedVersion != 0 && this.version != expectedVersion) throw new PersistentObjectVersionException(this.version, expectedVersion); // Check for sameness if (this.root == null && newRoot == null) return this.version; if (this.root != null && newRoot != null && this.delegate.isSameGraph(this.root, newRoot)) return this.version; // Validate the new root if (newRoot != null && !alreadyValidated) { Set> violations = this.delegate.validate(newRoot); if (!violations.isEmpty()) throw new PersistentObjectValidationException(violations); } // Do the update final T oldRoot = this.root; this.root = newRoot != null ? this.delegate.copy(newRoot) : null; this.version++; this.sharedRoot = null; // Perform write-back, either now or later if (!readingFile && this.root != null) { this.writebackRoot = this.root; if (this.writeDelay == 0) this.writeback(); else if (this.pendingWrite == null) { this.pendingWrite = this.scheduledExecutor.schedule(new Runnable() { @Override public void run() { PersistentObject.this.writeTimeout(); } }, this.writeDelay, TimeUnit.MILLISECONDS); } } // Notify listeners this.notifyListeners(this.version, oldRoot, newRoot); // Done return this.version; } /** * Atomically update the root object. * *

* The is a convenience method, equivalent to: *

* {@link #setRoot(Object, long) setRoot}(newRoot, 0) *
* *

* This method cannot throw {@link PersistentObjectVersionException}. * * @param newRoot new persistent root object * @return the new current version number (unchanged if {@code newRoot} is * {@linkplain PersistentObjectDelegate#isSameGraph the same as} the current root) */ public final synchronized long setRoot(T newRoot) { return this.setRoot(newRoot, 0); } /** * 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 * @throws PersistentObjectException if an error occurs */ public synchronized void checkFile() { // Sanity check if (!this.started) throw new IllegalStateException("not started"); // Get file timestamp long fileTime = this.getFile().lastModified(); if (fileTime == 0) return; // Check whether file has newly appeared or just been updated if (this.timestamp != 0 && fileTime <= this.timestamp) return; // Read new file this.log.info(this + ": detected out-of-band update of persistent file `" + this.getFile() + "'"); this.applyFile(fileTime, false); } /** * 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. * * @param listener listener to add * @throws IllegalArgumentException if {@code listener} is null */ public void addListener(PersistentObjectListener listener) { if (listener == null) throw new IllegalArgumentException("null listener"); synchronized (this.listeners) { this.listeners.add(listener); } } /** * Remove a listener added via {@link #addListener addListener()}. * * @param listener listener to remove */ public void removeListener(PersistentObjectListener listener) { synchronized (this.listeners) { this.listeners.remove(listener); } } /** * Get a simple string description of this instance. This description appears in all log messages. */ @Override public String toString() { return this.getClass().getSimpleName() + "[" + this.getFile().getName() + "]"; } /** * Read the persistent file. Does not validate it. * * @return root object decoded from backing file * @throws PersistentObjectException if the file does not exist or cannot be read * @throws PersistentObjectException if an error occurs */ protected T read() { // Open file this.log.info(this + ": reading persistent file `" + this.getFile() + "'"); BufferedInputStream input; try { input = new BufferedInputStream(this.streamRepository.getInputStream()); } catch (IOException e) { throw new PersistentObjectException("error opening persistent file", e); } // Parse XML try { StreamSource source = new StreamSource(input); source.setSystemId(this.getFile()); return PersistentObject.read(this.delegate, source, false); } finally { try { input.close(); } catch (IOException e) { // ignore } } } /** * 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). * * @param obj root object to write * @throws IllegalArgumentException if {@code obj} is null * @throws PersistentObjectException if an error occurs */ protected final synchronized void write(T obj) { // Sanity check if (obj == null) throw new IllegalArgumentException("null obj"); // Open atomic update output and buffer it AtomicUpdateFileOutputStream updateOutput; try { updateOutput = this.streamRepository.getOutputStream(); } catch (IOException e) { throw new PersistentObjectException("error creating temporary file", e); } BufferedOutputStream output = new BufferedOutputStream(updateOutput); // Serialize to XML try { // Set up XML result Result result = this.createResult(output, updateOutput.getTempFile()); // Serialize root object PersistentObject.write(obj, this.delegate, result); // Commit output try { output.close(); } catch (IOException e) { throw new PersistentObjectException("error closing temporary file", e); } // Success output = null; } finally { if (output != null) updateOutput.cancel(); } // Update file timestamp this.timestamp = this.streamRepository.getFileTimestamp(); } /** * Create a {@link Result} targeting the given {@link OutputStream}. * *

* The implementation in {@link PersistentObject} creates and returns a {@link StreamResult}. * * @param output XML output stream * @param systemId system ID * @return XML result output */ protected Result createResult(OutputStream output, File systemId) { StreamResult result = new StreamResult(output); result.setSystemId(systemId); return result; } /** * Notify listeners of a change in value. * * @param newVersion the version number associated with the new root * @param oldRoot previous root object * @param newRoot new root object */ protected void notifyListeners(long newVersion, T oldRoot, T newRoot) { // Snapshot listeners final ArrayList> listenersCopy = new ArrayList>(); synchronized (this.listeners) { listenersCopy.addAll(this.listeners); } // Notify them final PersistentObjectEvent event = new PersistentObjectEvent(this, newVersion, oldRoot, newRoot); this.notifyExecutor.submit(new Runnable() { @Override public void run() { PersistentObject.this.doNotifyListeners(listenersCopy, event); } }); } // Read the persistent file and apply it private synchronized void applyFile(long newTimestamp, boolean allowNotStarted) { this.cancelPendingWrite(); this.timestamp = newTimestamp; // update timestamp even if update fails to avoid loops this.setRootInternal(this.read(), 0, true, allowNotStarted, false); } // Handle a write-back timeout private synchronized void writeTimeout() { // Check for cancel race if (this.pendingWrite == null) return; this.pendingWrite = null; // Write it try { this.writeback(); } catch (ThreadDeath t) { throw t; } catch (Throwable t) { this.delegate.handleWritebackException(this, t); } } // Handle a check file timeout private synchronized void checkFileTimeout() { // Handle race condition if (!this.started) return; // Check file try { this.checkFile(); } catch (ThreadDeath t) { throw t; } catch (Throwable t) { this.log.error(this + ": error attempting to apply out-of-band update", t); } } // Write back root to persistent file private synchronized void writeback() { T objectToWrite = this.writebackRoot; assert objectToWrite != null; this.writebackRoot = null; this.write(objectToWrite); } // Cancel a pending write and return true if there was one private synchronized boolean cancelPendingWrite() { if (this.pendingWrite == null) return false; this.pendingWrite.cancel(false); this.pendingWrite = null; return true; } // Notify listeners. This is invoked in a separate thread. private void doNotifyListeners(ArrayList> list, PersistentObjectEvent event) { for (PersistentObjectListener listener : list) { try { listener.handleEvent(event); } catch (ThreadDeath t) { throw t; } catch (Throwable t) { this.log.error(this + ": error notifying listener " + listener, t); } } } // Wait for an ExecutorService to completely shut down private void awaitTermination(ExecutorService executor, String name) { boolean shutdown = false; try { shutdown = executor.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { this.log.warn(this + ": interrupted while awaiting " + name + " termination"); Thread.currentThread().interrupt(); } if (!shutdown) this.log.warn(this + ": failed to completely shut down " + name); } // Convience reader & writer /** * 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 {@link Source} without having * to go through the whole {@link PersistentObject} lifecycle. * * @param root object type * @param delegate delegate supplying required operations * @param source source for serialized root object * @param validate whether to also validate the root object * @return deserialized root object, never null * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectValidationException if {@code validate} is true and the deserialized root has validation errors * @throws PersistentObjectException if an error occurs */ public static T read(PersistentObjectDelegate delegate, Source source, boolean validate) { // Sanity check if (delegate == null) throw new IllegalArgumentException("null delegate"); if (source == null) throw new IllegalArgumentException("null source"); // Parse XML T root; try { root = delegate.deserialize(source); } catch (IOException e) { throw new PersistentObjectException("error reading persistent object", e); } // Check result if (root == null) throw new PersistentObjectException("null object returned by delegate.deserialize()"); // Validate result if (validate) { final Set> violations = delegate.validate(root); if (!violations.isEmpty()) throw new PersistentObjectValidationException(violations); } // Done return root; } /** * Read in a persistent object from the given {@link File} using the given delegate. * *

* This is a wrapper around {@link #read(PersistentObjectDelegate, Source, boolean)} that handles * opening and closing the given {@link File}. * * @param root object type * @param delegate delegate supplying required operations * @param file file to read from * @param validate whether to also validate the root object * @return deserialized root object, never null * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectValidationException if {@code validate} is true and the deserialized root has validation errors * @throws PersistentObjectException if {@code file} cannot be read * @throws PersistentObjectException if an error occurs */ public static T read(PersistentObjectDelegate delegate, File file, boolean validate) { // Sanity check if (file == null) throw new IllegalArgumentException("null file"); // Open file BufferedInputStream input; try { input = new BufferedInputStream(new FileInputStream(file)); } catch (IOException e) { throw new PersistentObjectException("error opening persistent file", e); } // Parse XML try { StreamSource source = new StreamSource(input); source.setSystemId(file); return PersistentObject.read(delegate, source, validate); } finally { try { input.close(); } catch (IOException e) { // ignore } } } /** * Read in a persistent object from the given {@link InputStream} using the given delegate. * *

* This is a wrapper around {@link #read(PersistentObjectDelegate, Source, boolean)}. * * @param root object type * @param delegate delegate supplying required operations * @param input input to read from * @param validate whether to also validate the root object * @return deserialized root object, never null * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectValidationException if {@code validate} is true and the deserialized root has validation errors * @throws PersistentObjectException if an I/O error occurs * @throws PersistentObjectException if an error occurs */ public static T read(PersistentObjectDelegate delegate, InputStream input, boolean validate) { // Sanity check if (input == null) throw new IllegalArgumentException("null input"); // Proceed return PersistentObject.read(delegate, new StreamSource(new BufferedInputStream(input)), validate); } /** * Write a persistent object using the given delegate. * *

* This is a convenience method that can be used for one-time serialization to an XML {@link Result} without having * to go through the whole {@link PersistentObject} lifecycle. * * @param root object type * @param root root object to serialize * @param delegate delegate supplying required operations * @param result destination * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectException if an error occurs */ public static void write(T root, PersistentObjectDelegate delegate, Result result) { // Sanity check if (root == null) throw new IllegalArgumentException("null root"); if (delegate == null) throw new IllegalArgumentException("null delegate"); if (result == null) throw new IllegalArgumentException("null result"); // Serialize root object try { delegate.serialize(root, result); } catch (IOException e) { throw new PersistentObjectException("error writing persistent file", e); } } /** * Write a persistent object using the given delegate. * *

* This is a wrapper around {@link #write(Object, PersistentObjectDelegate, Result)} that handles * opening and closing the given {@link File}. * * @param root object type * @param root root object to serialize * @param delegate delegate supplying required operations * @param file destination file * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectException if an error occurs */ public static void write(T root, PersistentObjectDelegate delegate, File file) { // Sanity check if (root == null) throw new IllegalArgumentException("null root"); if (delegate == null) throw new IllegalArgumentException("null delegate"); if (file == null) throw new IllegalArgumentException("null file"); // Write to file try { final BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(file)); final StreamResult result = new StreamResult(output); result.setSystemId(file); PersistentObject.write(root, delegate, result); output.close(); } catch (IOException e) { throw new PersistentObjectException("error writing persistent file", e); } } /** * Write a persistent object using the given delegate. * *

* This is a wrapper around {@link #write(Object, PersistentObjectDelegate, Result)}. * * @param root object type * @param root root object to serialize * @param delegate delegate supplying required operations * @param output XML destination * @throws IllegalArgumentException if any parameter is null * @throws PersistentObjectException if an error occurs */ public static void write(T root, PersistentObjectDelegate delegate, OutputStream output) { // Sanity check if (root == null) throw new IllegalArgumentException("null root"); if (delegate == null) throw new IllegalArgumentException("null delegate"); if (output == null) throw new IllegalArgumentException("null output"); // Write PersistentObject.write(root, delegate, new StreamResult(new BufferedOutputStream(output))); } // Snapshot class /** * Holds a "snapshot" of a {@link PersistentObject} root object along with the version number * corresponding to the snapshot. * * @see PersistentObject#getRootSnapshot */ public class Snapshot { private final T root; private final long version; public Snapshot(T root, long version) { this.root = root; this.version = version; } /** * Get the persistent root associated with this instance. * * @return persistent object associated with this snapshot */ public T getRoot() { return this.root; } /** * Get the version number of the persistent root associated with this instance. * * @return version number associated with this snapshot */ public long getVersion() { return this.version; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy