Class PersistentObject<T>
- Type Parameters:
T
- type of the root object
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 timesetRoot()
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
.
-
Nested Class Summary
Modifier and TypeClassDescriptionclass
Holds a "snapshot" of aPersistentObject
root object along with the version number corresponding to the snapshot. -
Field Summary
-
Constructor Summary
ConstructorDescriptionDefault constructor.PersistentObject
(PersistentObjectDelegate<T> delegate, File file) Simplified constructor configuring for synchronous write-back and no persistent file checks.PersistentObject
(PersistentObjectDelegate<T> delegate, File file, long writeDelay, long checkInterval) Constructor. -
Method Summary
Modifier and TypeMethodDescriptionvoid
addListener
(PersistentObjectListener<T> listener) Add a listener to be notified each time the object graph changes.void
Check the persistent file for an "out-of-band" update.protected Result
createResult
(OutputStream output, File systemId) Create aResult
targeting the givenOutputStream
.long
Get the delay time between periodic checks for changes in the underlying persistent file.Get the configuredPersistentObjectDelegate
.getFile()
Get the persistent file containing the XML form of the persisted object.int
Get the number of backup copies to preserve.getRoot()
Atomically read the root object.Read the root object (as withgetRoot()
) and its version (as withgetVersion()
) in one atomic operation.Get a shared copy of the root object.Read the shared root object (as withgetSharedRoot()
) and its version (as withgetVersion()
) in one atomic operation.long
Get the version of the current root.long
Get the maximum delay after an update operation before a write-back to the persistent file must be initiated.boolean
hasRoot()
Determine whether this instance has a non-null root object.boolean
Determine whether this instance should allow an "empty start".boolean
Determine whether this instance should allow an "empty stop".boolean
Determine whether this instance is started.protected void
notifyListeners
(long newVersion, T oldRoot, T newRoot) Notify listeners of a change in value.protected T
read()
Read the persistent file.static <T> T
read
(PersistentObjectDelegate<T> delegate, File file, boolean validate) Read in a persistent object from the givenFile
using the given delegate.static <T> T
read
(PersistentObjectDelegate<T> delegate, InputStream input, boolean validate) Read in a persistent object from the givenInputStream
using the given delegate.static <T> T
read
(PersistentObjectDelegate<T> delegate, Source source, boolean validate) Read in a persistent object using the given delegate.void
removeListener
(PersistentObjectListener<T> listener) Remove a listener added viaaddListener()
.void
setAllowEmptyStart
(boolean allowEmptyStart) Configure whether an "empty start" is allowed.void
setAllowEmptyStop
(boolean allowEmptyStop) Configure whether an "empty stop" is allowed.void
setCheckInterval
(long checkInterval) Set the delay time between periodic checks for changes in the underlying persistent file.void
setDelegate
(PersistentObjectDelegate<T> delegate) Configure thePersistentObjectDelegate
.void
Set the persistent file containing the XML form of the persisted object.void
setNumBackups
(int numBackups) Set the number of backup copies to preserve.final long
Atomically update the root object.long
Atomically update the root object.long
Atomically update the root object.void
setWriteDelay
(long writeDelay) Set the maximum delay after an update operation before a write-back to the persistent file must be initiated.void
start()
Start this instance.void
stop()
Stop this instance.toString()
Get a simple string description of this instance.void
Validate a root object.protected final void
Write the persistent file and rotate any backups.static <T> void
write
(T root, PersistentObjectDelegate<T> delegate, File file) Write a persistent object using the given delegate.static <T> void
write
(T root, PersistentObjectDelegate<T> delegate, OutputStream output) Write a persistent object using the given delegate.static <T> void
write
(T root, PersistentObjectDelegate<T> delegate, Result result) Write a persistent object using the given delegate.
-
Field Details
-
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 operationsfile
- the file used to persistwriteDelay
- write delay in milliseconds, or zero for synchronous write-backcheckInterval
- check interval in milliseconds, or zero to disable persistent file checks- Throws:
IllegalArgumentException
- ifdelegate
orfile
is nullIllegalArgumentException
- ifwriteDelay
orcheckInterval
is negative
-
PersistentObject
Simplified constructor configuring for synchronous write-back and no persistent file checks.Equivalent to:
PersistentObject(delegate, file, 0L, 0L);
- Parameters:
delegate
- delegate supplying required operationsfile
- the file used to persist- Throws:
IllegalArgumentException
- ifdelegate
orfile
is null
-
PersistentObject
public PersistentObject()Default constructor. Caller must still configure the delegate and persistent file prior to start.
-
-
Method Details
-
getDelegate
Get the configuredPersistentObjectDelegate
.- Returns:
- the delegate supplying required operations
-
setDelegate
Configure thePersistentObjectDelegate
.- Parameters:
delegate
- delegate supplying required operations- Throws:
IllegalArgumentException
- ifdelegate
is nullIllegalStateException
- if this instance is started
-
getFile
Get the persistent file containing the XML form of the persisted object.- Returns:
- file used to persist the root object
-
setFile
Set the persistent file containing the XML form of the persisted object.- Parameters:
file
- the file used to persist the root object- Throws:
IllegalArgumentException
- iffile
is nullIllegalStateException
- 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
- ifwriteDelay
is negativeIllegalStateException
- 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
- ifwriteDelay
is negativeIllegalStateException
- 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
- ifnumBackups
is negativeIllegalStateException
- 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
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)
orsetAllowEmptyStop(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 startedPersistentObjectException
- if an error occurs
-
getRootSnapshot
Read the root object (as withgetRoot()
) and its version (as withgetVersion()
) 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 startedPersistentObjectException
- if an error occurs- See Also:
-
setRoot
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, aPersistentObjectVersionException
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 togetRoot()
will return null. When a nullnewRoot
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 inPersistentObjectException
and rethrown.- Parameters:
newRoot
- new persistent objectexpectedVersion
- expected current version number, or zero to ignore the current version numberalreadyValidated
- true ifnewRoot
has already been validated- Returns:
- the new current version number (unchanged if
newRoot
is the same as the current root) - Throws:
IllegalArgumentException
- ifnewRoot
is null and empty stops are disallowedIllegalArgumentException
- ifexpectedVersion
is negativeIllegalStateException
- if this instance is not startedPersistentObjectException
- if an error occursPersistentObjectVersionException
- ifexpectedVersion
is non-zero and not equal to the current versionPersistentObjectValidationException
- if the new root has validation errors
-
setRoot
Atomically update the root object.This method is equivalent to:
setRoot(newRoot, expectedVersion, false);
- Parameters:
newRoot
- new persistent objectexpectedVersion
- 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
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 startedPersistentObjectException
- if an error occurs
-
addListener
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
- iflistener
is null
-
removeListener
Remove a listener added viaaddListener()
.- Parameters:
listener
- listener to remove
-
toString
Get a simple string description of this instance. This description appears in all log messages. -
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 readPersistentObjectException
- if an error occurs
-
write
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
- ifobj
is nullPersistentObjectException
- if an error occurs
-
createResult
Create aResult
targeting the givenOutputStream
.The implementation in
PersistentObject
creates and returns aStreamResult
.- Parameters:
output
- XML output streamsystemId
- system ID- Returns:
- XML result output
-
notifyListeners
Notify listeners of a change in value.- Parameters:
newVersion
- the version number associated with the new rootoldRoot
- previous root objectnewRoot
- new root object
-
read
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 wholePersistentObject
lifecycle.- Type Parameters:
T
- root object type- Parameters:
delegate
- delegate supplying required operationssource
- source for serialized root objectvalidate
- whether to also validate the root object- Returns:
- deserialized root object, never null
- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectValidationException
- ifvalidate
is true and the deserialized root has validation errorsPersistentObjectException
- if an error occurs
-
validate
Validate a root object.Deletgates to
PersistentObjectDelegate.validate(T)
to perform the actual validation.- Parameters:
root
- root object to validate- Throws:
IllegalArgumentException
- ifroot
is nullPersistentObjectValidationException
- if the root has validation errors
-
read
Read in a persistent object from the givenFile
using the given delegate.This is a wrapper around
read(PersistentObjectDelegate, Source, boolean)
that handles opening and closing the givenFile
.- Type Parameters:
T
- root object type- Parameters:
delegate
- delegate supplying required operationsfile
- file to read fromvalidate
- whether to also validate the root object- Returns:
- deserialized root object, never null
- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectValidationException
- ifvalidate
is true and the deserialized root has validation errorsPersistentObjectException
- iffile
cannot be readPersistentObjectException
- if an error occurs
-
read
Read in a persistent object from the givenInputStream
using the given delegate.This is a wrapper around
read(PersistentObjectDelegate, Source, boolean)
.- Type Parameters:
T
- root object type- Parameters:
delegate
- delegate supplying required operationsinput
- input to read fromvalidate
- whether to also validate the root object- Returns:
- deserialized root object, never null
- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectValidationException
- ifvalidate
is true and the deserialized root has validation errorsPersistentObjectException
- if an I/O error occursPersistentObjectException
- if an error occurs
-
write
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 wholePersistentObject
lifecycle.- Type Parameters:
T
- root object type- Parameters:
root
- root object to serializedelegate
- delegate supplying required operationsresult
- destination- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectException
- if an error occurs
-
write
Write a persistent object using the given delegate.This is a wrapper around
write(Object, PersistentObjectDelegate, Result)
that handles opening and closing the givenFile
.- Type Parameters:
T
- root object type- Parameters:
root
- root object to serializedelegate
- delegate supplying required operationsfile
- destination file- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectException
- if an error occurs
-
write
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 serializedelegate
- delegate supplying required operationsoutput
- XML destination- Throws:
IllegalArgumentException
- if any parameter is nullPersistentObjectException
- if an error occurs
-