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 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);