Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP <+>/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements. See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.jackrabbit.oak.plugins.memory;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\nimport javax.annotation.Nonnull;\n\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.collect.Maps;\n\nimport org.apache.jackrabbit.oak.api.PropertyState;\nimport org.apache.jackrabbit.oak.api.Type;\nimport org.apache.jackrabbit.oak.spi.state.AbstractNodeState;\nimport org.apache.jackrabbit.oak.spi.state.NodeBuilder;\nimport org.apache.jackrabbit.oak.spi.state.NodeState;\nimport org.apache.jackrabbit.oak.spi.state.NodeStateDiff;\n\nimport static com.google.common.base.Preconditions.checkNotNull;\nimport static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.with;\nimport static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.withNodes;\nimport static org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState.withProperties;\n\n/**\n * In-memory node state builder.\n *

\n * TODO: The following description is somewhat out of date\n *

\n * The following two builder states are used\n * to properly track uncommitted chances without relying on weak references\n * or requiring hard references on the entire accessed subtree:\n *

\n *
unmodified
\n *
\n * A child builder with no content changes starts in this state.\n * It keeps a reference to the parent builder and knows it's name for\n * use when connecting. Before each access the unconnected builder\n * checks the parent for relevant changes to connect to. As long as\n * there are no such changes, the builder remains unconnected and\n * uses the immutable base state to respond to any content accesses.\n *
\n *
connected
\n *
\n * Once a child node is first modified, it switches it's internal\n * state from the immutable base state to a mutable one and records\n * a hard reference to that state in the mutable parent state. After\n * that the parent reference is cleared and no more state checks are\n * made. Any other builder instances that refer to the same child node\n * will update their internal states to point to that same mutable\n * state instance and thus become connected at next access.\n * A root state builder is always connected.\n *
\n *
\n */\npublic class MemoryNodeBuilder implements NodeBuilder {\n\n private static final NodeState NULL_STATE = new MemoryNodeState(\n ImmutableMap.of(),\n ImmutableMap.of());\n\n /**\n * Parent builder, or {@code null} for a root builder.\n */\n private final MemoryNodeBuilder parent;\n\n /**\n * Name of this child node within the parent builder,\n * or {@code null} for a root builder.\n */\n private final String name;\n\n /**\n * Root builder, or {@code this} for the root itself.\n */\n private final MemoryNodeBuilder root;\n\n /**\n * Internal revision counter that is incremented in the root builder\n * whenever anything changes in the tree below. Each builder instance\n * has its own copy of the revision counter, for quickly checking whether\n * any state changes are needed.\n */\n private long revision;\n\n /**\n * The base state of this builder, or {@code null} if this builder\n * represents a new node that didn't yet exist in the base content tree.\n */\n private NodeState baseState;\n\n /**\n * The shared mutable state of connected builder instances, or\n * {@code null} until this builder has been connected.\n */\n private MutableNodeState writeState;\n\n /**\n * Creates a new in-memory node state builder.\n *\n * @param parent parent node state builder\n * @param name name of this node\n */\n protected MemoryNodeBuilder(MemoryNodeBuilder parent, String name) {\n this.parent = checkNotNull(parent);\n this.name = checkNotNull(name);\n\n this.root = parent.root;\n this.revision = -1;\n\n this.baseState = null;\n this.writeState = null;\n }\n\n /**\n * Creates a new in-memory node state builder.\n *\n * @param base base state of the new builder\n */\n public MemoryNodeBuilder(@Nonnull NodeState base) {\n this.parent = null;\n this.name = null;\n\n this.root = this;\n this.revision = 0;\n\n this.baseState = checkNotNull(base);\n this.writeState = new MutableNodeState(baseState);\n }\n\n private NodeState read() {\n if (revision != root.revision) {\n assert(parent != null); // root never gets here\n parent.read();\n\n // The builder could have been reset, need to re-get base state\n if (parent.baseState != null) {\n baseState = parent.baseState.getChildNode(name);\n } else {\n baseState = null;\n }\n\n // ... same for the write state\n if (parent.writeState != null) {\n writeState = parent.writeState.nodes.get(name);\n if (writeState == null\n && parent.writeState.nodes.containsKey(name)) {\n throw new IllegalStateException(\n \"This node has been removed\");\n }\n } else {\n writeState = null;\n }\n\n revision = root.revision;\n }\n if (writeState != null) {\n return writeState;\n } else if (baseState != null) {\n return baseState;\n } else {\n return NULL_STATE;\n }\n }\n\n private MutableNodeState write() {\n return write(root.revision + 1);\n }\n\n private MutableNodeState write(long newRevision) {\n if (writeState == null || revision != root.revision) {\n assert(parent != null); // root never gets here\n parent.write(newRevision);\n\n // The builder could have been reset, need to re-get base state\n if (parent.baseState != null) {\n baseState = parent.baseState.getChildNode(name);\n } else {\n baseState = null;\n }\n\n assert parent.writeState != null; // we just called parent.write()\n writeState = parent.writeState.nodes.get(name);\n if (writeState == null) {\n if (parent.writeState.nodes.containsKey(name)) {\n throw new IllegalStateException(\n \"This node has been removed\");\n } else {\n // need to make this node writable\n NodeState base = baseState;\n if (base == null) {\n base = NULL_STATE;\n }\n writeState = new MutableNodeState(base);\n parent.writeState.nodes.put(name, writeState);\n }\n }\n } else if (parent != null) {\n // make sure that all revision numbers up to the root gets updated\n parent.write(newRevision);\n }\n revision = newRevision;\n return writeState;\n }\n\n /**\n * Factory method for creating new child state builders. Subclasses may\n * override this method to control the behavior of child state builders.\n *\n * @return new builder\n */\n protected MemoryNodeBuilder createChildBuilder(String name) {\n return new MemoryNodeBuilder(this, name);\n }\n\n /**\n * Called whenever this node is modified, i.e. a property is\n * added, changed or removed, or a child node is added or removed. Changes\n * inside child nodes or the subtrees below are not reported. The default\n * implementation does nothing, but subclasses may override this method\n * to better track changes.\n */\n protected void updated() {\n // do nothing\n }\n\n protected void compareAgainstBaseState(NodeStateDiff diff) {\n NodeState state = read();\n if (writeState != null) {\n writeState.compareAgainstBaseState(state, diff);\n }\n }\n\n //--------------------------------------------------------< NodeBuilder >---\n\n @Override\n public boolean isNew() {\n return this != root && parent.isNew(name);\n }\n\n private boolean isNew(String name) {\n return (getBaseState() == null || !getBaseState().hasChildNode(name)) && hasChildNode(name);\n }\n\n @Override\n public boolean isRemoved() {\n return this != root && (parent.isRemoved() || parent.isRemoved(name));\n }\n\n private boolean isRemoved(String name) {\n return getBaseState() != null && getBaseState().hasChildNode(name) && !hasChildNode(name);\n }\n\n @Override\n public boolean isModified() {\n if (writeState == null) {\n return false;\n }\n else {\n NodeState baseState = getBaseState();\n for (Entry n : writeState.nodes.entrySet()) {\n if (n.getValue() == null) {\n return true;\n }\n if (baseState == null || !baseState.hasChildNode(n.getKey())) {\n return true;\n }\n }\n for (Entry p : writeState.properties.entrySet()) {\n PropertyState pState = p.getValue();\n if (pState == null) {\n return true;\n }\n if (baseState == null || !pState.equals(baseState.getProperty(p.getKey()))) {\n return true;\n }\n }\n return false;\n }\n }\n\n @Override\n public NodeState getNodeState() {\n read();\n if (writeState != null) {\n return writeState.snapshot();\n } else {\n // FIXME this assertion might fail when getNodeState() is called on a removed node.\n assert baseState != null; // guaranteed by read()\n return baseState;\n }\n }\n\n @Override\n public NodeState getBaseState() {\n return baseState;\n }\n\n @Override\n public void reset(NodeState newBase) {\n if (this == root) {\n baseState = checkNotNull(newBase);\n writeState = new MutableNodeState(baseState);\n revision++;\n } else {\n throw new IllegalStateException(\"Cannot reset a non-root builder\");\n }\n }\n\n @Override\n public long getChildNodeCount() {\n return read().getChildNodeCount();\n }\n\n @Override\n public boolean hasChildNode(String name) {\n return read().hasChildNode(name);\n }\n\n @Override\n public Iterable getChildNodeNames() {\n return read().getChildNodeNames();\n }\n\n @Override @Nonnull\n public NodeBuilder setNode(String name, NodeState state) {\n write();\n\n MutableNodeState childState = writeState.nodes.get(name);\n if (childState == null) {\n writeState.nodes.remove(name);\n childState = createChildBuilder(name).write();\n }\n childState.reset(state);\n\n updated();\n return this;\n }\n\n @Override @Nonnull\n public NodeBuilder removeNode(String name) {\n MutableNodeState mstate = write();\n\n if (mstate.base.getChildNode(name) != null) {\n mstate.nodes.put(name, null);\n } else {\n mstate.nodes.remove(name);\n }\n\n updated();\n return this;\n }\n\n @Override\n public long getPropertyCount() {\n return read().getPropertyCount();\n }\n\n @Override\n public Iterable getProperties() {\n return read().getProperties();\n }\n\n\n @Override\n public PropertyState getProperty(String name) {\n return read().getProperty(name);\n }\n\n @Override @Nonnull\n public NodeBuilder removeProperty(String name) {\n MutableNodeState mstate = write();\n\n if (mstate.base.getProperty(name) != null) {\n mstate.properties.put(name, null);\n } else {\n mstate.properties.remove(name);\n }\n\n updated();\n return this;\n }\n\n @Override\n public NodeBuilder setProperty(PropertyState property) {\n MutableNodeState mstate = write();\n mstate.properties.put(property.getName(), property);\n updated();\n return this;\n }\n\n @Override\n public NodeBuilder setProperty(String name, T value) {\n setProperty(PropertyStates.createProperty(name, value));\n return this;\n }\n\n @Override\n public NodeBuilder setProperty(String name, T value, Type type) {\n setProperty(PropertyStates.createProperty(name, value, type));\n return this;\n }\n\n @Override\n public NodeBuilder child(String name) {\n read(); // shortcut when dealing with a read-only child node\n if (baseState != null\n && baseState.hasChildNode(name)\n && (writeState == null || !writeState.nodes.containsKey(name))) {\n return createChildBuilder(name);\n }\n\n // no read-only child node found, switch to write mode\n write();\n assert writeState != null; // guaranteed by write()\n\n NodeState childBase = null;\n if (baseState != null) {\n childBase = baseState.getChildNode(name);\n }\n\n if (writeState.nodes.get(name) == null) {\n if (writeState.nodes.containsKey(name)) {\n // The child node was removed earlier and we're creating\n // a new child with the same name. Use the null state to\n // prevent the previous child state from re-surfacing.\n childBase = null;\n }\n writeState.nodes.put(name, new MutableNodeState(childBase));\n }\n\n MemoryNodeBuilder builder = createChildBuilder(name);\n builder.write();\n return builder;\n }\n\n /**\n * The mutable state being built. Instances of this class\n * are never passed beyond the containing {@code MemoryNodeBuilder},\n * so it's not a problem that we intentionally break the immutability\n * assumption of the {@link NodeState} interface.\n */\n private class MutableNodeState extends AbstractNodeState {\n\n /**\n * The immutable base state.\n */\n private NodeState base;\n\n /**\n * Set of added, modified or removed ({@code null} value)\n * property states.\n */\n private final Map properties =\n Maps.newHashMap();\n\n /**\n * Set of added, modified or removed ({@code null} value)\n * child nodes.\n */\n private final Map nodes =\n Maps.newHashMap();\n\n public MutableNodeState(NodeState base) {\n if (base != null) {\n this.base = base;\n } else {\n this.base = MemoryNodeBuilder.NULL_STATE;\n }\n }\n\n public NodeState snapshot() {\n Map nodes = Maps.newHashMap();\n for (Map.Entry entry : this.nodes.entrySet()) {\n String name = entry.getKey();\n MutableNodeState node = entry.getValue();\n NodeState before = base.getChildNode(name);\n if (node == null) {\n if (before != null) {\n nodes.put(name, null);\n }\n } else {\n NodeState after = node.snapshot();\n if (after != before) {\n nodes.put(name, after);\n }\n }\n }\n return with(base, Maps.newHashMap(this.properties), nodes);\n }\n\n void reset(NodeState newBase) {\n base = newBase;\n properties.clear();\n\n Iterator> iterator =\n nodes.entrySet().iterator();\n while (iterator.hasNext()) {\n Map.Entry entry = iterator.next();\n MutableNodeState cstate = entry.getValue();\n NodeState cbase = newBase.getChildNode(entry.getKey());\n if (cbase == null || cstate == null) {\n iterator.remove();\n } else {\n cstate.reset(cbase);\n }\n }\n }\n\n //-----------------------------------------------------< NodeState >--\n\n @Override\n public long getPropertyCount() {\n return withProperties(base, properties).getPropertyCount();\n }\n\n @Override\n public PropertyState getProperty(String name) {\n return withProperties(base, properties).getProperty(name);\n }\n\n @Override @Nonnull\n public Iterable getProperties() {\n Map copy = Maps.newHashMap(properties);\n return withProperties(base, copy).getProperties();\n }\n\n @Override\n public long getChildNodeCount() {\n return withNodes(base, nodes).getChildNodeCount();\n }\n\n @Override\n public boolean hasChildNode(String name) {\n return withNodes(base, nodes).hasChildNode(name);\n }\n\n @Override\n public NodeState getChildNode(String name) {\n return withNodes(base, nodes).getChildNode(name); // mutable\n }\n\n @Override @Nonnull\n public Iterable getChildNodeNames() {\n Map copy = Maps.newHashMap(nodes);\n return withNodes(base, copy).getChildNodeNames();\n }\n\n @Override\n public void compareAgainstBaseState(NodeState base, NodeStateDiff diff) {\n with(this.base, properties, nodes).compareAgainstBaseState(base, diff);\n }\n\n @Override @Nonnull\n public NodeBuilder builder() {\n throw new UnsupportedOperationException();\n }\n\n }\n\n}\n =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java (revision 425c7bee1232f5ef49af801c4902386096ff2503) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilder.java (revision ) @@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; - import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; @@ -268,11 +267,12 @@ @Override public boolean isModified() { + NodeState baseState = getBaseState(); if (writeState == null) { return false; } else { - NodeState baseState = getBaseState(); + Map nodes = writeState.nodes; for (Entry n : writeState.nodes.entrySet()) { if (n.getValue() == null) { return true; @@ -308,6 +308,7 @@ @Override public NodeState getBaseState() { + read(); return baseState; } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP <+>/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements. See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.jackrabbit.oak.plugins.memory;\n\nimport java.util.Collections;\n\nimport com.google.common.collect.ImmutableMap;\nimport org.apache.jackrabbit.oak.api.PropertyState;\nimport org.apache.jackrabbit.oak.spi.state.NodeBuilder;\nimport org.apache.jackrabbit.oak.spi.state.NodeState;\nimport org.junit.Ignore;\nimport org.junit.Test;\n\nimport static junit.framework.Assert.assertEquals;\nimport static junit.framework.Assert.assertFalse;\nimport static junit.framework.Assert.assertNotNull;\nimport static junit.framework.Assert.assertNull;\nimport static junit.framework.Assert.assertTrue;\nimport static junit.framework.Assert.fail;\nimport static org.apache.jackrabbit.oak.api.Type.STRING;\n\npublic class MemoryNodeBuilderTest {\n\n private static final NodeState BASE = new MemoryNodeState(\n ImmutableMap.of(\n \"a\", LongPropertyState.createLongProperty(\"a\", 1L),\n \"b\", LongPropertyState.createLongProperty(\"b\", 2L),\n \"c\", LongPropertyState.createLongProperty(\"c\", 3L)),\n ImmutableMap.of(\n \"x\", new MemoryNodeState(\n Collections.emptyMap(),\n Collections.singletonMap(\"q\", MemoryNodeState.EMPTY_NODE)),\n \"y\", MemoryNodeState.EMPTY_NODE,\n \"z\", MemoryNodeState.EMPTY_NODE));\n\n @Test\n public void testConnectOnAddProperty() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder childA = root.child(\"x\");\n NodeBuilder childB = root.child(\"x\");\n\n assertNull(childA.getProperty(\"test\"));\n childB.setProperty(\"test\", \"foo\");\n assertNotNull(childA.getProperty(\"test\"));\n }\n\n @Test\n public void testConnectOnUpdateProperty() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder childA = root.child(\"x\");\n NodeBuilder childB = root.child(\"x\");\n\n childB.setProperty(\"test\", \"foo\");\n\n childA.setProperty(\"test\", \"bar\");\n assertEquals(\"bar\", childA.getProperty(\"test\").getValue(STRING));\n assertEquals(\"bar\", childB.getProperty(\"test\").getValue(STRING));\n }\n\n @Test\n public void testConnectOnRemoveProperty() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder childA = root.child(\"x\");\n NodeBuilder childB = root.child(\"x\");\n\n childB.setProperty(\"test\", \"foo\");\n\n childA.removeProperty(\"test\");\n assertNull(childA.getProperty(\"test\"));\n assertNull(childB.getProperty(\"test\"));\n\n childA.setProperty(\"test\", \"bar\");\n assertEquals(\"bar\", childA.getProperty(\"test\").getValue(STRING));\n assertEquals(\"bar\", childB.getProperty(\"test\").getValue(STRING));\n }\n\n @Test\n public void testConnectOnAddNode() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder childA = root.child(\"x\");\n NodeBuilder childB = root.child(\"x\");\n\n assertFalse(childA.hasChildNode(\"test\"));\n assertFalse(childB.hasChildNode(\"test\"));\n\n childB.child(\"test\");\n assertTrue(childA.hasChildNode(\"test\"));\n assertTrue(childB.hasChildNode(\"test\"));\n }\n\n @Test\n public void testConnectOnRemoveNode() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder child = root.child(\"x\");\n\n root.removeNode(\"x\");\n try {\n child.getChildNodeCount();\n fail();\n } catch (IllegalStateException e) {\n // expected\n }\n\n root.child(\"x\");\n assertEquals(0, child.getChildNodeCount()); // reconnect!\n }\n\n @Test\n @Ignore(\"OAK-447\") // FIXME OAK-447\n public void testAddRemovedNodeAgain() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n\n root.removeNode(\"x\");\n NodeBuilder x = root.child(\"x\");\n\n x.child(\"q\");\n assertTrue(x.hasChildNode(\"q\"));\n }\n\n @Test\n public void testReset() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder child = root.child(\"x\");\n child.child(\"new\");\n\n assertTrue(child.hasChildNode(\"new\"));\n assertTrue(root.child(\"x\").hasChildNode(\"new\"));\n\n root.reset(BASE);\n assertFalse(child.hasChildNode(\"new\"));\n assertFalse(root.child(\"x\").hasChildNode(\"new\"));\n }\n\n @Test\n public void testReset2() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder x = root.child(\"x\");\n NodeBuilder y = x.child(\"y\");\n\n root.reset(BASE);\n assertTrue(root.hasChildNode(\"x\"));\n assertFalse(x.hasChildNode(\"y\"));\n }\n\n @Test\n @Ignore(\"OAK-448\") // FIXME: OAK-448\n public void testUnmodifiedEqualsBase() {\n NodeBuilder root = new MemoryNodeBuilder(BASE);\n NodeBuilder x = root.child(\"x\");\n assertEquals(x.getBaseState(), x.getNodeState());\n }\n\n}\n =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java (revision 425c7bee1232f5ef49af801c4902386096ff2503) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/memory/MemoryNodeBuilderTest.java (revision ) @@ -157,7 +157,6 @@ } @Test - @Ignore("OAK-448") // FIXME: OAK-448 public void testUnmodifiedEqualsBase() { NodeBuilder root = new MemoryNodeBuilder(BASE); NodeBuilder x = root.child("x");