diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/CopyOnReadIdentityMap.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/CopyOnReadIdentityMap.java
new file mode 100644
index 0000000..696948a
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/CopyOnReadIdentityMap.java
@@ -0,0 +1,185 @@
+/*
+ * 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.multiplex;
+
+import com.google.common.base.Function;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This map wraps around the passed argument and caches all the returned values.
+ * It is meant to be wrapped around the result of
+ * {@link com.google.common.collect.Maps#transformValues(Map, Function)} method
+ * or its variant. This allows to preserve the laziness of map transformation and
+ * at the same time to avoid re-calculating the same values.
+ *
+ * It's immutable and used IdentityHashMap for caching values.
+ *
+ * @param - the type of keys maintained by this map
+ * @param - the type of mapped values
+ */
+class CopyOnReadIdentityMap implements Map {
+
+ private final Map map;
+
+ private Integer cachedSize;
+
+ private Boolean cachedIsEmpty;
+
+ private Map cachedValues;
+
+ private boolean allValuesCached;
+
+ public CopyOnReadIdentityMap(Map wrappedMap) {
+ this.map = wrappedMap;
+ }
+
+ @Override
+ public int size() {
+ if (allValuesCached) {
+ return cachedValues.size();
+ }
+ if (cachedSize == null) {
+ cachedSize = map.size();
+ }
+ return cachedSize;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ if (allValuesCached) {
+ return cachedValues.isEmpty();
+ }
+ if (cachedIsEmpty == null) {
+ if (cachedSize == null) {
+ cachedIsEmpty = map.isEmpty();
+ } else {
+ cachedIsEmpty = cachedSize > 0;
+ }
+ }
+ return cachedIsEmpty;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ if (allValuesCached) {
+ return cachedValues.containsKey(key);
+ }
+ if (cachedValues != null && cachedValues.containsKey(key)) {
+ return true;
+ } else {
+ return map.containsKey(key);
+ }
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ if (allValuesCached) {
+ return cachedValues.containsValue(value);
+ }
+ for (K k : keySet()) {
+ V v = get(k);
+ if (value == null && v == null) {
+ return true;
+ } else if (value != null && value.equals(v)) {
+ return true;
+ }
+ }
+ if (cachedValues == null) {
+ cachedValues = Collections.emptyMap();
+ }
+ allValuesCached = true;
+ return false;
+ }
+
+ @Override
+ public V get(Object key) {
+ if (allValuesCached) {
+ return cachedValues.get(key);
+ }
+ if (cachedValues != null && cachedValues.containsKey(key)) {
+ return cachedValues.get(key);
+ } else if (map.containsKey(key)) {
+ initCachedValues();
+ V v = map.get(key);
+ cachedValues.put((K) key, v);
+ return v;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public V put(K key, V value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public V remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set keySet() {
+ return map.keySet();
+ }
+
+ @Override
+ public Collection values() {
+ readAll();
+ return cachedValues.values();
+ }
+
+ @Override
+ public Set> entrySet() {
+ readAll();
+ return cachedValues.entrySet();
+ }
+
+ private void readAll() {
+ if (allValuesCached) {
+ return;
+ }
+ initCachedValues();
+ for (Entry e : map.entrySet()) {
+ if (!cachedValues.containsKey(e.getKey())) {
+ cachedValues.put(e.getKey(), e.getValue());
+ }
+ }
+ allValuesCached = true;
+ }
+
+ private void initCachedValues() {
+ if (cachedValues == null) {
+ cachedValues = new IdentityHashMap<>(map.size());
+ }
+ }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeBuilder.java
index 1e76a99..7e11b70 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeBuilder.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeBuilder.java
@@ -18,6 +18,7 @@ package org.apache.jackrabbit.oak.plugins.multiplex;
import com.google.common.base.Function;
import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
@@ -29,17 +30,16 @@ import org.apache.jackrabbit.oak.spi.state.NodeState;
import java.io.IOException;
import java.io.InputStream;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.collect.ImmutableMap.copyOf;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
-import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.transformValues;
import static java.lang.Long.MAX_VALUE;
import static java.util.Collections.singleton;
@@ -54,7 +54,7 @@ class MultiplexingNodeBuilder implements NodeBuilder {
private final MultiplexingContext ctx;
- private final Map nodeBuilders;
+ private Map nodeBuilders;
private final MountedNodeStore owningStore;
@@ -62,6 +62,8 @@ class MultiplexingNodeBuilder implements NodeBuilder {
private final MultiplexingNodeBuilder rootBuilder;
+ private MultiplexingNodeState baseState;
+
MultiplexingNodeBuilder(String path, Map nodeBuilders, MultiplexingContext ctx) {
this(path, nodeBuilders, ctx, null);
}
@@ -70,7 +72,7 @@ class MultiplexingNodeBuilder implements NodeBuilder {
checkArgument(nodeBuilders.size() == ctx.getStoresCount(), "Got %s builders but the context manages %s stores", nodeBuilders.size(), ctx.getStoresCount());
this.path = path;
this.ctx = ctx;
- this.nodeBuilders = newHashMap(nodeBuilders);
+ this.nodeBuilders = new CopyOnReadIdentityMap<>(nodeBuilders);
this.owningStore = ctx.getOwningStore(path);
this.parent = parent;
if (parent == null) {
@@ -86,16 +88,19 @@ class MultiplexingNodeBuilder implements NodeBuilder {
@Override
public NodeState getNodeState() {
- return new MultiplexingNodeState(path, buildersToNodeStates(nodeBuilders), ctx);
+ return new MultiplexingNodeState(path, new IdentityHashMap<>(buildersToNodeStates(nodeBuilders)), ctx);
}
@Override
public NodeState getBaseState() {
- return new MultiplexingNodeState(path, buildersToBaseStates(nodeBuilders), ctx);
+ if (baseState == null) {
+ baseState = new MultiplexingNodeState(path, buildersToBaseStates(nodeBuilders), ctx);
+ }
+ return baseState;
}
private static Map buildersToNodeStates(Map builders) {
- return copyOf(transformValues(builders, new Function() {
+ return transformValues(builders, new Function() {
@Override
public NodeState apply(NodeBuilder input) {
if (input.exists()) {
@@ -104,16 +109,16 @@ class MultiplexingNodeBuilder implements NodeBuilder {
return MISSING_NODE;
}
}
- }));
+ });
}
private static Map buildersToBaseStates(Map builders) {
- return copyOf(transformValues(builders, new Function() {
+ return transformValues(builders, new Function() {
@Override
public NodeState apply(NodeBuilder input) {
return input.getBaseState();
}
- }));
+ });
}
// node or property-related methods ; directly delegate to wrapped builder
@@ -247,7 +252,7 @@ class MultiplexingNodeBuilder implements NodeBuilder {
@Override
public boolean hasChildNode(String name) {
- String childPath = PathUtils.concat(path, name);
+ String childPath = simpleConcat(path, name);
MountedNodeStore mountedStore = ctx.getOwningStore(childPath);
return nodeBuilders.get(mountedStore).hasChildNode(name);
}
@@ -266,20 +271,21 @@ class MultiplexingNodeBuilder implements NodeBuilder {
for (String element : PathUtils.elements(path)) {
builder = builder.child(element);
}
+ if (nodeBuilders instanceof CopyOnReadIdentityMap) {
+ nodeBuilders = new IdentityHashMap<>(nodeBuilders);
+ }
nodeBuilders.put(mountedNodeStore, builder);
}
@Override
- public NodeBuilder getChildNode(String name) {
- String childPath = PathUtils.concat(path, name);
- MountedNodeStore mountedStore = ctx.getOwningStore(childPath);
- if (!nodeBuilders.get(mountedStore).hasChildNode(name)) {
- return MISSING_NODE.builder();
- }
- Map newNodeBuilders = newHashMap();
- for (MountedNodeStore mns : ctx.getAllMountedNodeStores()) {
- newNodeBuilders.put(mns, nodeBuilders.get(mns).getChildNode(name));
- }
+ public NodeBuilder getChildNode(final String name) {
+ String childPath = simpleConcat(path, name);
+ Map newNodeBuilders = Maps.transformValues(nodeBuilders, new Function() {
+ @Override
+ public NodeBuilder apply(NodeBuilder input) {
+ return input.getChildNode(name);
+ }
+ });
return new MultiplexingNodeBuilder(childPath, newNodeBuilders, ctx, this);
}
@@ -289,23 +295,25 @@ class MultiplexingNodeBuilder implements NodeBuilder {
}
@Override
- public NodeBuilder setChildNode(String name, NodeState nodeState) {
+ public NodeBuilder setChildNode(final String name, NodeState nodeState) {
checkState(exists(), "This builder does not exist: " + PathUtils.getName(path));
-
- String childPath = PathUtils.concat(path, name);
- MountedNodeStore childStore = ctx.getOwningStore(childPath);
+ String childPath = simpleConcat(path, name);
+ final MountedNodeStore childStore = ctx.getOwningStore(childPath);
if (childStore != owningStore && !nodeBuilders.get(childStore).exists()) {
createAncestors(childStore);
}
- NodeBuilder childBuilder = nodeBuilders.get(childStore).setChildNode(name, nodeState);
+ final NodeBuilder childBuilder = nodeBuilders.get(childStore).setChildNode(name, nodeState);
- Map newNodeBuilders = newHashMap();
- newNodeBuilders.put(childStore, childBuilder);
- for (MountedNodeStore mns : ctx.getAllMountedNodeStores()) {
- if (!newNodeBuilders.containsKey(mns)) {
- newNodeBuilders.put(mns, nodeBuilders.get(mns).getChildNode(name));
+ Map newNodeBuilders = Maps.transformEntries(nodeBuilders, new Maps.EntryTransformer() {
+ @Override
+ public NodeBuilder transformEntry(MountedNodeStore key, NodeBuilder value) {
+ if (key == childStore) {
+ return childBuilder;
+ } else {
+ return value.getChildNode(name);
+ }
}
- }
+ });
return new MultiplexingNodeBuilder(childPath, newNodeBuilders, ctx, this);
}
@@ -392,7 +400,29 @@ class MultiplexingNodeBuilder implements NodeBuilder {
return !node.exists();
}
- private String getPath() {
+ String getPath() {
return path;
}
+
+ /**
+ * This simplified version of {@link PathUtils#concat(String, String)} method
+ * assumes that the parentPath is valid and not null, while the second argument
+ * is just a name (not a subpath).
+ *
+ * @param parentPath the parent path
+ * @param name name to concatenate
+ * @return the parentPath concatenated with name
+ */
+ static String simpleConcat(String parentPath, String name) {
+ checkValidName(name);
+ if (PathUtils.denotesRoot(parentPath)) {
+ return parentPath + name;
+ } else {
+ return new StringBuilder(parentPath.length() + name.length() + 1)
+ .append(parentPath)
+ .append('/')
+ .append(name)
+ .toString();
+ }
+ }
}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeState.java
index b74564e..5cd2f71 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeState.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeState.java
@@ -19,6 +19,7 @@
package org.apache.jackrabbit.oak.plugins.multiplex;
import com.google.common.base.Function;
+import com.google.common.collect.Maps;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
@@ -33,15 +34,13 @@ import java.util.Map;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.compose;
-import static com.google.common.collect.ImmutableMap.copyOf;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
-import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.transformValues;
import static java.lang.Long.MAX_VALUE;
import static java.util.Collections.singleton;
-import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
+import static org.apache.jackrabbit.oak.plugins.multiplex.MultiplexingNodeBuilder.simpleConcat;
import static org.apache.jackrabbit.oak.spi.state.ChildNodeEntry.GET_NAME;
class MultiplexingNodeState extends AbstractNodeState {
@@ -73,7 +72,7 @@ class MultiplexingNodeState extends AbstractNodeState {
checkArgument(nodeStates.size() == ctx.getStoresCount(), "Got %s node states but the context manages %s stores", nodeStates.size(), ctx.getStoresCount());
this.path = path;
this.ctx = ctx;
- this.nodeStates = copyOf(nodeStates);
+ this.nodeStates = new CopyOnReadIdentityMap<>(nodeStates);
this.owningStore = ctx.getOwningStore(path);
}
@@ -106,23 +105,20 @@ class MultiplexingNodeState extends AbstractNodeState {
// child node operations
@Override
public boolean hasChildNode(String name) {
- String childPath = PathUtils.concat(path, name);
+ String childPath = simpleConcat(path, name);
MountedNodeStore mountedStore = ctx.getOwningStore(childPath);
return nodeStates.get(mountedStore).hasChildNode(name);
}
@Override
- public NodeState getChildNode(String name) {
- String childPath = PathUtils.concat(path, name);
- MountedNodeStore mountedStore = ctx.getOwningStore(childPath);
- if (!nodeStates.get(mountedStore).hasChildNode(name)) {
- return MISSING_NODE;
- }
-
- Map newNodeStates = newHashMap();
- for (MountedNodeStore mns : ctx.getAllMountedNodeStores()) {
- newNodeStates.put(mns, nodeStates.get(mns).getChildNode(name));
- }
+ public NodeState getChildNode(final String name) {
+ String childPath = simpleConcat(path, name);
+ Map newNodeStates = Maps.transformValues(nodeStates, new Function() {
+ @Override
+ public NodeState apply(NodeState input) {
+ return input.getChildNode(name);
+ }
+ });
return new MultiplexingNodeState(childPath, newNodeStates, ctx);
}
@@ -201,12 +197,12 @@ class MultiplexingNodeState extends AbstractNodeState {
// write operations
@Override
public NodeBuilder builder() {
- Map nodeBuilders = copyOf(transformValues(nodeStates, new Function() {
+ Map nodeBuilders = transformValues(nodeStates, new Function() {
@Override
public NodeBuilder apply(NodeState input) {
return input.builder();
}
- }));
+ });
return new MultiplexingNodeBuilder(path, nodeBuilders, ctx);
}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeStore.java
index 9916125..7f08577 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/multiplex/MultiplexingNodeStore.java
@@ -20,6 +20,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.oak.api.Blob;
import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.spi.commit.CommitHook;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
@@ -99,6 +100,9 @@ public class MultiplexingNodeStore implements NodeStore, Observable {
public NodeState merge(NodeBuilder builder, CommitHook commitHook, CommitInfo info) throws CommitFailedException {
checkArgument(builder instanceof MultiplexingNodeBuilder);
MultiplexingNodeBuilder nodeBuilder = (MultiplexingNodeBuilder) builder;
+ if (!PathUtils.denotesRoot(nodeBuilder.getPath())) {
+ throw new IllegalArgumentException();
+ }
// run commit hooks and apply the changes to the builder instance
NodeState processed = commitHook.processCommit(getRoot(), rebase(nodeBuilder), info);