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

org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder Maven / Gradle / Ivy

There is a newer version: 1.62.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.oak.plugins.memory;

// WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! //
//                                                                         //
// This class has complex internals that have evolved in over a hundred    //
// commits. It is a central component in how Oak handles modifications to  //
// content trees. Please use 'svn blame', 'svn log' and the referenced     //
// Jira issues to understand the reason for some of the more complex parts //
// of this class. See also the MemoryNodeBuilderTest for existing tests.   //
//                                                                         //
// WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! //

import static com.google.common.base.Objects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.apache.jackrabbit.oak.spi.state.AbstractNodeState.checkValidName;

import java.io.IOException;
import java.io.InputStream;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import com.google.common.base.Objects;
import com.google.common.io.ByteStreams;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
import org.apache.jackrabbit.oak.spi.state.MoveDetector;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;

/**
 * In-memory node state builder.
 * 

* A {@code MemoryNodeBuilder} instance tracks uncommitted changes without * relying on weak references or requiring hard references on the entire * accessed subtree. It does this by relying on {@code MutableNodeState} * instances for tracking uncommitted changes and on {@code Head} * instances for tracking the connectedness of the builder. A builder keeps * a reference to the parent builder and knows its own name, which is used * to check for relevant changes in its parent builder and update its state * accordingly. *

* A builder is in one of three possible states, which is tracked within * its {@code Head} instance: *

*
unconnected
*
* A child builder with no content changes starts in this state. * Before each access the unconnected builder checks its parent for * relevant changes. *
*
connected
*
* Once a builder is first modified, it switches to the connected state * and records all modification in a shared {@code MutableNodeState} * instance. Before each access the connected builder checks whether its * parents base state has been reset and if so, resets its own base state * accordingly. *
*
root
*
* Same as the connected state but only the root of the builder hierarchy * can have this state. *
*
*/ public class MemoryNodeBuilder implements NodeBuilder { /** * Parent builder, or {@code null} for a root builder. */ private final MemoryNodeBuilder parent; /** * Name of this child node within the parent builder, * or {@code null} for a root builder. */ private final String name; /** * Root builder, or {@code this} for the root builder itself. */ private final MemoryNodeBuilder rootBuilder; /** * Internal revision counter for the base state of this builder. The counter * is incremented in the root builder whenever its base state is reset. * Each builder instance has its own copy of this revision counter for * quickly checking whether its base state needs updating. * @see #reset(org.apache.jackrabbit.oak.spi.state.NodeState) * @see #base() */ private long baseRevision; /** * The base state of this builder, possibly non-existent if this builder * represents a new node that didn't yet exist in the base content tree. */ @Nonnull private NodeState base; /** * Head of the root builder. */ private final RootHead rootHead; /** * Head of this builder. Always use {@link #head()} for accessing to * ensure the connected state is correctly updated. */ private Head head; /** * Creates a new in-memory child builder. * @param parent parent builder * @param name name of this node */ protected MemoryNodeBuilder(MemoryNodeBuilder parent, String name) { this.parent = parent; this.name = name; this.rootBuilder = parent.rootBuilder; this.base = parent.base().getChildNode(name); this.baseRevision = parent.baseRevision; this.rootHead = parent.rootHead; this.head = new UnconnectedHead(this, base); } /** * Creates a new in-memory node state builder rooted at * and based on the passed {@code base} state. * @param base base state of the new builder */ public MemoryNodeBuilder(@Nonnull NodeState base) { this.parent = null; this.name = null; this.rootBuilder = this; this.baseRevision = 0; this.base = checkNotNull(base); this.rootHead = new RootHead(this); this.head = rootHead; } /** * Update the head of this builder to reflect the actual connected state. * @return head of this builder */ private Head head() { return head.update(); } /** * @return {@code true} iff this is the root builder */ protected final boolean isRoot() { return this == rootBuilder; } /** * Update the base state of this builder by recursively retrieving it * from its parent builder. * @return base state of this builder */ @Nonnull private NodeState base() { if (rootBuilder.baseRevision != baseRevision) { base = parent.base().getChildNode(name); baseRevision = rootBuilder.baseRevision; } return base; } /** * Factory method for creating new child state builders. Subclasses may * override this method to control the behavior of child state builders. * @return new builder */ protected MemoryNodeBuilder createChildBuilder(String name) { return new MemoryNodeBuilder(this, name); } /** * Called whenever this node is modified, i.e. a property is * added, changed or removed, or a child node is added or removed. Changes * inside child nodes or the subtrees below are not reported. The default * implementation triggers an {@link #updated()} call on the root builder * (unless this is already the root builder), which subclasses can use * to capture aggregate update information across the whole tree. */ protected void updated() { if (this != rootBuilder) { rootBuilder.updated(); } } /** * Accessor for parent builder */ protected final MemoryNodeBuilder getParent() { return parent; } /** * Accessor for name */ protected final String getName() { return name; } /** * Throws away all changes in this builder and resets the base to the * given node state. * * @param newBase new base state */ public void reset(@Nonnull NodeState newBase) { checkState(parent == null); base = checkNotNull(newBase); baseRevision = rootHead.setState(newBase) + 1; } /** * Replaces the current state of this builder with the given node state. * The base state remains unchanged. * * @param newState new state */ protected void set(NodeState newState) { if (parent == null) { // updating the base revision forces all sub-builders to refresh baseRevision = rootHead.setState(newState); } else { parent.setChildNode(name, newState); } } //--------------------------------------------------------< NodeBuilder >--- @Override @Nonnull public NodeState getNodeState() { return head().getImmutableNodeState(); } @Override @Nonnull public NodeState getBaseState() { return base(); } @Override public boolean exists() { return head().getCurrentNodeState().exists(); } @Override public boolean isNew() { return exists() && !getBaseState().exists(); } @Override public boolean isNew(String name) { return hasProperty(name) && !getBaseState().hasProperty(name); } @Override public boolean isModified() { return head().isModified(); } @Override public boolean isReplaced() { return head().isReplaced(); } @Override public boolean isReplaced(String name) { return head().isReplaced(name); } @Override public long getChildNodeCount(long max) { return head().getCurrentNodeState().getChildNodeCount(max); } @Nonnull @Override public Iterable getChildNodeNames() { return head().getCurrentNodeState().getChildNodeNames(); } @Override public boolean hasChildNode(@Nonnull String name) { return head().getCurrentNodeState().hasChildNode(checkNotNull(name)); } @Nonnull @Override public NodeBuilder child(@Nonnull String name) { if (hasChildNode(name)) { return getChildNode(name); } else { return setChildNode(name); } } @Nonnull @Override public NodeBuilder getChildNode(@Nonnull String name) { checkValidName(name); return createChildBuilder(name); } @Nonnull @Override public NodeBuilder setChildNode(@Nonnull String name) { return setChildNode(name, EMPTY_NODE); } @Nonnull @Override public NodeBuilder setChildNode(@Nonnull String name, @Nonnull NodeState state) { checkState(exists(), "This builder does not exist: " + this.name); head().getMutableNodeState().setChildNode(name, checkNotNull(state)); MemoryNodeBuilder builder = createChildBuilder(name); updated(); return builder; } @Override public boolean remove() { if (!isRoot() && exists()) { head().getMutableNodeState(); // Make sure the removed node is connected parent.head().getMutableNodeState().removeChildNode(name); updated(); return true; } else { return false; } } /** * This implementation has the same semantics as adding this node * with name {@code newName} as a new child of {@code newParent} followed * by removing this node. As a consequence this implementation allows * moving this node into the subtree rooted here, the result of which * is the same as removing this node. *

* See also {@link NodeBuilder#moveTo(NodeBuilder, String) the general contract} * for {@code MoveTo}. * * @param newParent builder for the new parent. * @param newName name of this child at the new parent * @return {@code true} on success, {@code false} otherwise * @throws IllegalArgumentException if the given name string is empty * or contains the forward slash character */ @Override public boolean moveTo(@Nonnull NodeBuilder newParent, @Nonnull String newName) throws IllegalArgumentException { checkNotNull(newParent); checkValidName(newName); if (isRoot() || !exists() || newParent.hasChildNode(newName)) { return false; } else { if (newParent.exists()) { annotateSourcePath(); NodeState nodeState = getNodeState(); newParent.setChildNode(newName, nodeState); remove(); return true; } else { return false; } } } /** * Annotate this builder with its source path if this builder has not * been transiently added. The source path is written to a property with * the name {@link MoveDetector#SOURCE_PATH}. *

* The source path of a builder is its current path if its current * source path annotation is empty and none of its parents has a source * path annotation set. Otherwise it is the source path of the first parent * (or self) that has its source path annotation set appended with the relative * path from that parent to this builder. *

* This builder has been transiently added when there exists no * base node at its source path. */ protected final void annotateSourcePath() { String sourcePath = getSourcePath(); if (!isTransientlyAdded(sourcePath)) { setProperty(MoveDetector.SOURCE_PATH, sourcePath); } } private final String getSourcePath() { // Traverse up the hierarchy until we encounter the first builder // having a source path annotation or until we hit the root MemoryNodeBuilder builder = this; String sourcePath = getSourcePathAnnotation(builder); while (sourcePath == null && builder.parent != null) { builder = builder.parent; sourcePath = getSourcePathAnnotation(builder); } if (sourcePath == null) { // Neither self nor any parent has a source path annotation. The source // path is just the path of this builder return getPath(); } else { // The source path is the source path of the first parent having a source // path annotation with the relative path from this builder up to that // parent appended. return PathUtils.concat(sourcePath, PathUtils.relativize(builder.getPath(), getPath())); } } private static String getSourcePathAnnotation(MemoryNodeBuilder builder) { PropertyState base = builder.getBaseState().getProperty(MoveDetector.SOURCE_PATH); PropertyState head = builder.getNodeState().getProperty(MoveDetector.SOURCE_PATH); if (Objects.equal(base, head)) { // Both null: no source path annotation // Both non null but equals: source path annotation is from a previous commit return null; } else { return head.getValue(Type.STRING); } } private boolean isTransientlyAdded(String path) { NodeState node = rootBuilder.getBaseState(); for (String name : PathUtils.elements(path)) { node = node.getChildNode(name); } return !node.exists(); } @Override public long getPropertyCount() { return head().getCurrentNodeState().getPropertyCount(); } @Nonnull @Override public Iterable getProperties() { return head().getCurrentNodeState().getProperties(); } @Override public boolean hasProperty(String name) { return head().getCurrentNodeState().hasProperty(checkNotNull(name)); } @Override public PropertyState getProperty(String name) { return head.update().getCurrentNodeState().getProperty(checkNotNull(name)); } @Override public boolean getBoolean(@Nonnull String name) { return head().getCurrentNodeState().getBoolean(checkNotNull(name)); } @Override @CheckForNull public String getString(@Nonnull String name) { return head().getCurrentNodeState().getString(checkNotNull(name)); } @Override @CheckForNull public String getName(@Nonnull String name) { return head().getCurrentNodeState().getName(checkNotNull(name)); } @Override @Nonnull public Iterable getNames(@Nonnull String name) { return head().getCurrentNodeState().getNames(checkNotNull(name)); } @Nonnull @Override public NodeBuilder setProperty(@Nonnull PropertyState property) { checkState(exists(), "This builder does not exist: " + name); head().getMutableNodeState().setProperty(checkNotNull(property)); updated(); return this; } @Nonnull @Override public NodeBuilder setProperty(String name, @Nonnull T value) { setProperty(PropertyStates.createProperty(name, value)); return this; } @Nonnull @Override public NodeBuilder setProperty(String name, @Nonnull T value, Type type) { setProperty(PropertyStates.createProperty(name, value, type)); return this; } @Nonnull @Override public NodeBuilder removeProperty(String name) { checkState(exists(), "This builder does not exist: " + name); if (head().getMutableNodeState().removeProperty(checkNotNull(name))) { updated(); } return this; } @Override public Blob createBlob(InputStream stream) throws IOException { try { return new ArrayBasedBlob(ByteStreams.toByteArray(stream)); } finally { stream.close(); } } /** * @return path of this builder. */ public final String getPath() { return parent == null ? "/" : getPath(new StringBuilder()).toString(); } private StringBuilder getPath(StringBuilder parentPath) { return parent == null ? parentPath : parent.getPath(parentPath).append('/').append(name); } @Override public String toString() { return toStringHelper(this).add("path", getPath()).toString(); } //------------------------------------------------------------< Head >--- /** * Implementations of this interface represent the different states * associated builders can have: unconnected, connected, * and root. Its methods provide access to the node state being * built by this builder. */ private interface Head { /** * Returns the up-to-date head of the associated builder. In most * cases the returned value will be the current head instance, but * a different head can be returned if a state transition is needed. * The returned value is then used as the new current head of the * builder. * * @return up-to-date head of the associated builder */ Head update(); /** * Returns the current node state associated with this head. This state * is only stable across one method call and must not be passed outside * the {@code NodeBuilder} API boundary. * @return current head state. */ NodeState getCurrentNodeState(); /** * Connects the builder to which this head belongs and all its parents * and return the mutable node state associated with this head. This state * is only stable across one method call and must not be passed outside * the {@code NodeBuilder} API boundary. * @return current head state. */ MutableNodeState getMutableNodeState(); /** * Returns the current nodes state associated with this head. * @return current head state. */ NodeState getImmutableNodeState(); /** * Check whether the associated builder represents a modified node, which has * either modified properties or removed or added child nodes. * @return {@code true} for a modified node */ boolean isModified(); /** * Check whether the associated builder represents a node that * used to exist but was replaced with other content. * * @return {@code true} for a replaced node */ boolean isReplaced(); /** * Check whether the named property is replaced. * * @param name property name * @return {@code true} for a replaced property */ boolean isReplaced(String name); } private static class UnconnectedHead implements Head { private final MemoryNodeBuilder builder; private final RootHead rootHead; private long revision; private NodeState state; UnconnectedHead( MemoryNodeBuilder builder, NodeState state) { this.builder = builder; this.rootHead = builder.rootHead; this.revision = builder.baseRevision; this.state = state; } @Override public Head update() { long rootRevision = rootHead.revision; if (revision != rootRevision) { // root revision changed: recursively re-get state from parent NodeState parentState = builder.parent.head().getCurrentNodeState(); NodeState newState = parentState.getChildNode(builder.name); if (newState instanceof MutableNodeState) { // transition state to ConnectedHead builder.head = new ConnectedHead( builder, (MutableNodeState) newState); return builder.head; } else { // update to match the latest revision state = newState; revision = rootRevision; } } return this; } @Override public NodeState getCurrentNodeState() { return state; } @Override public MutableNodeState getMutableNodeState() { // switch to connected state recursively up to the parent MutableNodeState parentState = builder.parent.head().getMutableNodeState(); MutableNodeState state = parentState.getMutableChildNode(builder.name); // triggers a head state transition at next access return new ConnectedHead(builder, state).getMutableNodeState(); } @Override public NodeState getImmutableNodeState() { assert !(state instanceof MutableNodeState); return state; } @Override public boolean isModified() { return EqualsDiff.modified(builder.base(), state); } @Override public boolean isReplaced() { return false; } @Override public boolean isReplaced(String name) { return false; } @Override public String toString() { return toStringHelper(this).add("path", builder.getPath()).toString(); } } private static class ConnectedHead implements Head { private final MemoryNodeBuilder builder; protected long revision; protected MutableNodeState state; public ConnectedHead(MemoryNodeBuilder builder, MutableNodeState state) { this.builder = builder; this.revision = builder.rootBuilder.baseRevision; this.state = state; } @Override public Head update() { if (revision != builder.rootBuilder.baseRevision) { // the root builder's base state has been reset: transition back // to unconnected and connect again if necessary. // No need to pass base() instead of base as the subsequent // call to update will take care of updating to the latest state. builder.head = new UnconnectedHead(builder, builder.base); return builder.head.update(); } else { return this; } } @Override public NodeState getCurrentNodeState() { return state; } @Override public MutableNodeState getMutableNodeState() { // incrementing the root revision triggers unconnected // child state to re-get their state on next access builder.rootHead.revision++; return state; } @Override public NodeState getImmutableNodeState() { return state.snapshot(); } @Override public boolean isModified() { return state.isModified(builder.base()); } @Override public boolean isReplaced() { return state.isReplaced(builder.base()); } @Override public boolean isReplaced(String name) { return state.isReplaced(builder.base(), name); } @Override public String toString() { return toStringHelper(this).add("path", builder.getPath()).toString(); } } private static class RootHead extends ConnectedHead { public RootHead(MemoryNodeBuilder builder) { // Base of root is always up to date. No need to call base() super(builder, new MutableNodeState(builder.base)); } @Override public Head update() { return this; } public final long setState(NodeState state) { this.state = new MutableNodeState(state); // To be able to make a distinction between set() and reset(), we revision++; // increment the revision twice and return revision++; // return the intermediate value } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy