A node in a content tree consists of child nodes and properties, each
of which evolves through different states during its lifecycle. This
interface represents a specific, immutable state of a node.
The state of a node consists of named properties and child nodes. Names
are non-empty strings that never contain the forward slash character, "/".
Implementations may place additional restrictions on possible name strings.
The properties and child nodes are unordered, and no two properties or
two child nodes may have the same name. An implementation may additionally
restrict a property and a child node from having the same name.
Depending on context, a NodeState instance can be interpreted as
representing the state of just that node, of the subtree starting at
that node, or of an entire tree in case it's a root node.
The crucial difference between this interface and the similarly named
class in Jackrabbit 2.x is that this interface represents a specific,
immutable state of a node, whereas the Jackrabbit 2.x class represented
the current state of a node.
Immutability and thread-safety
As mentioned above, all node and property states are always immutable.
Thus repeating a method call is always guaranteed to produce the same
result as before unless some internal error occurs (see below). This
immutability only applies to a specific state instance. Different states
of a node can obviously be different, and in some cases even different
instances of the same state may behave slightly differently. For example
due to performance optimization or other similar changes the iteration
order of properties or child nodes may be different for two instances
of the same state.
In addition to being immutable, a specific state instance is guaranteed to
be fully thread-safe. Possible caching or other internal changes need to
be properly synchronized so that any number of concurrent clients can
safely access a state instance.
Persistence and error-handling
A node state can be (and often is) backed by local files or network
resources. All IO operations or related concerns like caching should be
handled transparently below this interface. Potential IO problems and
recovery attempts like retrying a timed-out network access need to be
handled below this interface, and only hard errors should be thrown up
as
RuntimeException that higher level code
is not expected to be able to recover from.
Since this interface exposes no higher level constructs like locking,
node types or even path parsing, there's no way for content access to
fail because of such concerns. Such functionality and related checked
exceptions or other control flow constructs should be implemented on
a higher level above this interface. On the other hand read access
controls can be implemented below this interface, in which
case some content that would otherwise be accessible might not show
up through such an implementation.
Existence and iterability of node states
The
#getChildNode(String) method is special in that it
never returns a
null value, even if the named child
node does not exist. Instead a client should use the
#exists()method on the returned child state to check whether that node exists.
The purpose of this separation of concerns is to allow an implementation
to lazily load content only when it's actually read instead of just
traversed. It also simplifies client code by avoiding the need for many
null checks when traversing paths.
The iterability of a node is a related concept to the
above-mentioned existence. A node state is iterable if it
is included in the return values of the
#getChildNodeCount(long),
#getChildNodeNames() and
#getChildNodeEntries() methods.
An iterable node is guaranteed to exist, though not all existing nodes
are necessarily iterable.
Furthermore, a non-existing node is guaranteed to contain no properties
or iterable child nodes. It can, however contain non-iterable children.
Such scenarios are typically the result of access control restrictions.
Decoration and virtual content
Not all content exposed by this interface needs to be backed by actual
persisted data. An implementation may want to provide derived data,
like for example the aggregate size of the entire subtree as an
extra virtual property. A virtualization, sharding or caching layer
could provide a composite view over multiple underlying trees.
Or a an access control layer could decide to hide certain content
based on specific rules. All such features need to be implemented
according to the API contract of this interface. A separate higher level
interface needs to be used if an implementation can't for example
guarantee immutability of exposed content as discussed above.
Equality and hash codes
Two node states are considered equal if and only if their existence,
properties and iterable child nodes match, regardless of ordering. The
Object#equals(Object) method needs to be implemented so that it
complies with this definition. And while node states are not meant for
use as hash keys, the
Object#hashCode() method should still be
implemented according to this equality contract.