| /* |
| * 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.document; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.TreeTraverser; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.cache.CacheValue; |
| import org.apache.jackrabbit.oak.commons.json.JsopBuilder; |
| import org.apache.jackrabbit.oak.commons.json.JsopReader; |
| import org.apache.jackrabbit.oak.commons.json.JsopTokenizer; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.commons.json.JsopWriter; |
| import org.apache.jackrabbit.oak.json.JsonSerializer; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlorUtils; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor; |
| import org.apache.jackrabbit.oak.plugins.document.bundlor.Matcher; |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; |
| import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; |
| import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry; |
| import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; |
| import org.apache.jackrabbit.oak.spi.state.NodeBuilder; |
| import org.apache.jackrabbit.oak.spi.state.NodeState; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Iterators; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static org.apache.jackrabbit.oak.commons.PathUtils.concat; |
| import static org.apache.jackrabbit.oak.commons.StringUtils.estimateMemoryUsage; |
| |
| /** |
| * A {@link NodeState} implementation for the {@link DocumentNodeStore}. |
| */ |
| public class DocumentNodeState extends AbstractDocumentNodeState implements CacheValue { |
| |
| private static final Logger log = LoggerFactory.getLogger(DocumentNodeState.class); |
| |
| public static final Children NO_CHILDREN = new Children(); |
| |
| /** |
| * The number of child nodes to fetch initially. |
| */ |
| static final int INITIAL_FETCH_SIZE = 100; |
| |
| /** |
| * The maximum number of child nodes to fetch in one call. (1600). |
| */ |
| static final int MAX_FETCH_SIZE = INITIAL_FETCH_SIZE << 4; |
| |
| private final String path; |
| private final RevisionVector lastRevision; |
| private final RevisionVector rootRevision; |
| private final boolean fromExternalChange; |
| private final Map<String, PropertyState> properties; |
| private final boolean hasChildren; |
| |
| private final DocumentNodeStore store; |
| private final BundlingContext bundlingContext; |
| |
| private AbstractDocumentNodeState cachedSecondaryState; |
| |
| DocumentNodeState(@NotNull DocumentNodeStore store, |
| @NotNull String path, |
| @NotNull RevisionVector rootRevision) { |
| this(store, path, rootRevision, Collections.<PropertyState>emptyList(), false, null); |
| } |
| |
| DocumentNodeState(@NotNull DocumentNodeStore store, @NotNull String path, |
| @NotNull RevisionVector rootRevision, |
| Iterable<? extends PropertyState> properties, |
| boolean hasChildren, |
| @Nullable RevisionVector lastRevision) { |
| this(store, path, rootRevision, asMap(properties), |
| hasChildren, lastRevision, false); |
| } |
| |
| private DocumentNodeState(@NotNull DocumentNodeStore store, |
| @NotNull String path, |
| @NotNull RevisionVector rootRevision, |
| @NotNull Map<String, PropertyState> properties, |
| boolean hasChildren, |
| @Nullable RevisionVector lastRevision, |
| boolean fromExternalChange) { |
| this(store, path, lastRevision, rootRevision, |
| fromExternalChange, createBundlingContext(checkNotNull(properties), hasChildren)); |
| } |
| |
| protected DocumentNodeState(@NotNull DocumentNodeStore store, |
| @NotNull String path, |
| @Nullable RevisionVector lastRevision, |
| @Nullable RevisionVector rootRevision, |
| boolean fromExternalChange, |
| BundlingContext bundlingContext) { |
| this.store = checkNotNull(store); |
| this.path = checkNotNull(path); |
| this.rootRevision = checkNotNull(rootRevision); |
| this.lastRevision = lastRevision; |
| this.fromExternalChange = fromExternalChange; |
| this.properties = bundlingContext.getProperties(); |
| this.bundlingContext = bundlingContext; |
| this.hasChildren = bundlingContext.hasChildren(); |
| } |
| |
| /** |
| * Creates a copy of this {@code DocumentNodeState} with the |
| * {@link #rootRevision} set to the given {@code root} revision. This method |
| * returns {@code this} instance if the given {@code root} revision is |
| * the same as the one in this instance and the {@link #fromExternalChange} |
| * flags are equal. |
| * |
| * @param root the root revision for the copy of this node state. |
| * @param externalChange if the {@link #fromExternalChange} flag must be |
| * set on the returned node state. |
| * @return a copy of this node state with the given root revision and |
| * external change flag. |
| */ |
| @Override |
| public DocumentNodeState withRootRevision(@NotNull RevisionVector root, |
| boolean externalChange) { |
| if (rootRevision.equals(root) && fromExternalChange == externalChange) { |
| return this; |
| } else { |
| return new DocumentNodeState(store, path, lastRevision, root, externalChange, bundlingContext); |
| } |
| } |
| |
| /** |
| * @return a copy of this {@code DocumentNodeState} with the |
| * {@link #fromExternalChange} flag set to {@code true}. |
| */ |
| @NotNull |
| public DocumentNodeState fromExternalChange() { |
| return new DocumentNodeState(store, path, lastRevision, rootRevision, true, bundlingContext); |
| } |
| |
| /** |
| * Returns this state as a branch root state connected to the given |
| * {@code branch}. |
| * |
| * @param branch the branch instance. |
| * @return a {@link DocumentBranchRootNodeState} connected to the given |
| * {@code branch}. |
| * @throws IllegalStateException if this is not a root node state or does |
| * not represent a branch state. |
| */ |
| @NotNull |
| DocumentNodeState asBranchRootState(@NotNull DocumentNodeStoreBranch branch) { |
| checkState(PathUtils.denotesRoot(path)); |
| checkState(getRootRevision().isBranch()); |
| return new DocumentBranchRootNodeState(store, branch, path, rootRevision, lastRevision, bundlingContext); |
| } |
| |
| /** |
| * @return {@code true} if this node state was created as a result of an |
| * external change; {@code false} otherwise. |
| */ |
| @Override |
| public boolean isFromExternalChange() { |
| return fromExternalChange; |
| } |
| |
| //--------------------------< AbstractDocumentNodeState >----------------------------------- |
| |
| /** |
| * Returns the root revision for this node state. This is the root revision |
| * passed from the parent node state. This revision therefore reflects the |
| * revision of the root node state where the traversal down the tree |
| * started. |
| * |
| * @return the revision of the root node state. |
| */ |
| @NotNull |
| public RevisionVector getRootRevision() { |
| return rootRevision; |
| } |
| |
| @Override |
| public String getPath() { |
| return path; |
| } |
| |
| @Override |
| public RevisionVector getLastRevision() { |
| return lastRevision; |
| } |
| |
| //--------------------------< NodeState >----------------------------------- |
| |
| |
| @Override |
| public boolean exists() { |
| return true; |
| } |
| |
| @Override |
| public PropertyState getProperty(@NotNull String name) { |
| return properties.get(name); |
| } |
| |
| @Override |
| public boolean hasProperty(@NotNull String name) { |
| return properties.containsKey(name); |
| } |
| |
| @NotNull |
| @Override |
| public Iterable<? extends PropertyState> getProperties() { |
| //Filter out the meta properties related to bundling from |
| //generic listing of props |
| if (bundlingContext.isBundled()){ |
| return Iterables.filter(properties.values(), BundlorUtils.NOT_BUNDLOR_PROPS); |
| } |
| return properties.values(); |
| } |
| |
| @Override |
| public boolean hasChildNode(@NotNull String name) { |
| if (!hasChildren || !isValidName(name)) { |
| return false; |
| } else { |
| return getChildNodeDoc(name) != null; |
| } |
| } |
| |
| @NotNull |
| @Override |
| public NodeState getChildNode(@NotNull String name) { |
| if (!hasChildren) { |
| checkValidName(name); |
| return EmptyNodeState.MISSING_NODE; |
| } |
| AbstractDocumentNodeState child = getChildNodeDoc(name); |
| if (child == null) { |
| checkValidName(name); |
| return EmptyNodeState.MISSING_NODE; |
| } else { |
| return child.withRootRevision(rootRevision, fromExternalChange); |
| } |
| } |
| |
| @Override |
| public long getChildNodeCount(long max) { |
| if (!hasChildren) { |
| return 0; |
| } |
| |
| int bundledChildCount = bundlingContext.getBundledChildNodeNames().size(); |
| if (bundlingContext.hasOnlyBundledChildren()){ |
| return bundledChildCount; |
| } |
| |
| String name = null; |
| long count = 0; |
| int fetchSize = INITIAL_FETCH_SIZE; |
| long remaining = Math.max(max, 1); // fetch at least once |
| Children c = NO_CHILDREN; |
| while (remaining > 0) { |
| c = store.getChildren(this, name, fetchSize); |
| count += c.children.size(); |
| remaining -= c.children.size(); |
| if (!c.hasMore) { |
| break; |
| } |
| name = c.children.get(c.children.size() - 1); |
| fetchSize = Math.min(fetchSize << 1, MAX_FETCH_SIZE); |
| } |
| if (!c.hasMore) { |
| // we know the exact value |
| return count + bundledChildCount; |
| } else { |
| // there are more than max |
| return Long.MAX_VALUE; |
| } |
| } |
| |
| @Override |
| public long getPropertyCount() { |
| if (bundlingContext.isBundled()){ |
| return Iterables.size(getProperties()); |
| } |
| return properties.size(); |
| } |
| |
| @NotNull |
| @Override |
| public Iterable<? extends ChildNodeEntry> getChildNodeEntries() { |
| if (!hasChildren) { |
| return Collections.emptyList(); |
| } |
| |
| AbstractDocumentNodeState secondaryState = getSecondaryNodeState(); |
| if (secondaryState != null){ |
| return secondaryState.getChildNodeEntries(); |
| } |
| |
| return new Iterable<ChildNodeEntry>() { |
| @Override |
| public Iterator<ChildNodeEntry> iterator() { |
| if (bundlingContext.isBundled()) { |
| //If all the children are bundled |
| if (bundlingContext.hasOnlyBundledChildren()){ |
| return getBundledChildren(); |
| } |
| return Iterators.concat(getBundledChildren(), new ChildNodeEntryIterator()); |
| } |
| |
| return new ChildNodeEntryIterator(); |
| } |
| }; |
| } |
| |
| @NotNull |
| @Override |
| public NodeBuilder builder() { |
| if ("/".equals(getPath())) { |
| if (getRootRevision().isBranch()) { |
| throw new IllegalStateException("Cannot create builder from branched DocumentNodeState"); |
| } else { |
| return new DocumentRootBuilder(this, store, store.createBranch(this)); |
| } |
| } else { |
| return new MemoryNodeBuilder(this); |
| } |
| } |
| |
| public Set<String> getBundledChildNodeNames(){ |
| return bundlingContext.getBundledChildNodeNames(); |
| } |
| |
| public boolean hasOnlyBundledChildren(){ |
| if (bundlingContext.isBundled()){ |
| return bundlingContext.hasOnlyBundledChildren(); |
| } |
| return false; |
| } |
| |
| String getPropertyAsString(String propertyName) { |
| return asString(properties.get(propertyName)); |
| } |
| |
| private String asString(PropertyState prop) { |
| if (prop == null) { |
| return null; |
| } else if (prop instanceof DocumentPropertyState) { |
| return ((DocumentPropertyState) prop).getValue(); |
| } |
| JsopBuilder builder = new JsopBuilder(); |
| new JsonSerializer(builder, store.getBlobSerializer()).serialize(prop); |
| return builder.toString(); |
| } |
| |
| Set<String> getPropertyNames() { |
| return properties.keySet(); |
| } |
| |
| @Override |
| public boolean hasNoChildren() { |
| return !hasChildren; |
| } |
| |
| @Override |
| protected NodeStateDiffer getNodeStateDiffer() { |
| return store; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder buff = new StringBuilder(); |
| buff.append("{ path: '").append(path).append("', "); |
| buff.append("rootRevision: '").append(rootRevision).append("', "); |
| buff.append("lastRevision: '").append(lastRevision).append("', "); |
| buff.append("properties: '").append(properties.values()).append("' }"); |
| return buff.toString(); |
| } |
| |
| /** |
| * Create an add operation for this node at the given revision. |
| * |
| * @param revision the revision this node is created. |
| */ |
| UpdateOp asOperation(@NotNull Revision revision) { |
| String id = Utils.getIdFromPath(path); |
| UpdateOp op = new UpdateOp(id, true); |
| if (Utils.isLongPath(path)) { |
| op.set(NodeDocument.PATH, path); |
| } |
| NodeDocument.setModified(op, revision); |
| NodeDocument.setDeleted(op, revision, false); |
| for (String p : properties.keySet()) { |
| String key = Utils.escapePropertyName(p); |
| op.setMapEntry(key, revision, getPropertyAsString(p)); |
| } |
| return op; |
| } |
| |
| String getId() { |
| return path + "@" + lastRevision; |
| } |
| |
| @Override |
| public int getMemory() { |
| long size = 40 // shallow |
| + (lastRevision != null ? lastRevision.getMemory() : 0) |
| + rootRevision.getMemory() |
| + estimateMemoryUsage(path); |
| // rough approximation for properties |
| for (Map.Entry<String, PropertyState> entry : bundlingContext.getAllProperties().entrySet()) { |
| // name |
| size += estimateMemoryUsage(entry.getKey()); |
| PropertyState propState = entry.getValue(); |
| if (propState.getType() != Type.BINARY |
| && propState.getType() != Type.BINARIES) { |
| for (int i = 0; i < propState.count(); i++) { |
| // size() returns length of string |
| // shallow memory: |
| // - 8 bytes per reference in values list |
| // - 48 bytes per string |
| // double usage per property because of parsed PropertyState |
| size += (56 + propState.size(i) * 2) * 2; |
| } |
| } else { |
| // calculate size based on blobId value |
| // referencing the binary in the blob store |
| // double the size because the parsed PropertyState |
| // will have a similarly sized blobId as well |
| size += (long)estimateMemoryUsage(asString(entry.getValue())) * 2; |
| } |
| } |
| if (size > Integer.MAX_VALUE) { |
| log.debug("Estimated memory footprint larger than Integer.MAX_VALUE: {}.", size); |
| size = Integer.MAX_VALUE; |
| } |
| return (int) size; |
| } |
| |
| public Iterable<DocumentNodeState> getAllBundledNodesStates() { |
| return new TreeTraverser<DocumentNodeState>(){ |
| @Override |
| public Iterable<DocumentNodeState> children(DocumentNodeState root) { |
| return Iterables.transform(() -> root.getBundledChildren(), ce -> (DocumentNodeState)ce.getNodeState()); |
| } |
| }.preOrderTraversal(this) |
| .filter(dns -> !dns.getPath().equals(this.getPath()) ); //Exclude this |
| } |
| |
| //------------------------------< internal >-------------------------------- |
| |
| @Nullable |
| private AbstractDocumentNodeState getChildNodeDoc(String childNodeName){ |
| AbstractDocumentNodeState secondaryState = getSecondaryNodeState(); |
| if (secondaryState != null){ |
| NodeState result = secondaryState.getChildNode(childNodeName); |
| //If given child node exist then cast it and return |
| //else return null |
| if (result.exists()){ |
| return (AbstractDocumentNodeState) result; |
| } |
| return null; |
| } |
| |
| Matcher child = bundlingContext.matcher.next(childNodeName); |
| if (child.isMatch()){ |
| if (bundlingContext.hasChildNode(child.getMatchedPath())){ |
| return createBundledState(childNodeName, child); |
| } else { |
| return null; |
| } |
| } else if (bundlingContext.hasOnlyBundledChildren()) { |
| return null; |
| } |
| |
| return store.getNode(concat(getPath(), childNodeName), lastRevision); |
| } |
| |
| @Nullable |
| private AbstractDocumentNodeState getSecondaryNodeState(){ |
| if (cachedSecondaryState == null){ |
| cachedSecondaryState = store.getSecondaryNodeState(getPath(), rootRevision, lastRevision); |
| } |
| return cachedSecondaryState; |
| } |
| |
| |
| /** |
| * Returns up to {@code limit} child node entries, starting after the given |
| * {@code name}. |
| * |
| * @param name the name of the lower bound child node entry (exclusive) or |
| * {@code null}, if the method should start with the first known |
| * child node. |
| * @param limit the maximum number of child node entries to return. |
| * @return the child node entries. |
| */ |
| @NotNull |
| private Iterable<ChildNodeEntry> getChildNodeEntries(@Nullable String name, |
| int limit) { |
| Iterable<? extends AbstractDocumentNodeState> children = store.getChildNodes(this, name, limit); |
| return Iterables.transform(children, new Function<AbstractDocumentNodeState, ChildNodeEntry>() { |
| @Override |
| public ChildNodeEntry apply(final AbstractDocumentNodeState input) { |
| return new AbstractChildNodeEntry() { |
| @NotNull |
| @Override |
| public String getName() { |
| return PathUtils.getName(input.getPath()); |
| } |
| |
| @NotNull |
| @Override |
| public NodeState getNodeState() { |
| return input; |
| } |
| }; |
| } |
| }); |
| } |
| |
| private static Map<String, PropertyState> asMap(Iterable<? extends PropertyState> props){ |
| ImmutableMap.Builder<String, PropertyState> builder = ImmutableMap.builder(); |
| for (PropertyState ps : props){ |
| builder.put(ps.getName(), ps); |
| } |
| return builder.build(); |
| } |
| |
| public String asString() { |
| JsopWriter json = new JsopBuilder(); |
| json.key("path").value(path); |
| json.key("rev").value(rootRevision.toString()); |
| if (lastRevision != null) { |
| json.key("lastRev").value(lastRevision.toString()); |
| } |
| if (hasChildren) { |
| json.key("hasChildren").value(true); |
| } |
| if (properties.size() > 0) { |
| json.key("prop").object(); |
| for (Map.Entry<String, PropertyState> e : bundlingContext.getAllProperties().entrySet()) { |
| json.key(e.getKey()).value(asString(e.getValue())); |
| } |
| json.endObject(); |
| } |
| return json.toString(); |
| } |
| |
| public static DocumentNodeState fromString(DocumentNodeStore store, String s) { |
| JsopTokenizer json = new JsopTokenizer(s); |
| String path = null; |
| RevisionVector rootRev = null; |
| RevisionVector lastRev = null; |
| boolean hasChildren = false; |
| HashMap<String, String> map = new HashMap<String, String>(); |
| while (true) { |
| String k = json.readString(); |
| json.read(':'); |
| if ("path".equals(k)) { |
| path = json.readString(); |
| } else if ("rev".equals(k)) { |
| rootRev = RevisionVector.fromString(json.readString()); |
| } else if ("lastRev".equals(k)) { |
| lastRev = RevisionVector.fromString(json.readString()); |
| } else if ("hasChildren".equals(k)) { |
| hasChildren = json.read() == JsopReader.TRUE; |
| } else if ("prop".equals(k)) { |
| json.read('{'); |
| while (true) { |
| if (json.matches('}')) { |
| break; |
| } |
| k = json.readString(); |
| json.read(':'); |
| String v = json.readString(); |
| map.put(k, v); |
| json.matches(','); |
| } |
| } |
| if (json.matches(JsopReader.END)) { |
| break; |
| } |
| json.read(','); |
| } |
| List<PropertyState> props = Lists.newArrayListWithCapacity(map.size()); |
| for (Entry<String, String> e : map.entrySet()) { |
| String value = e.getValue(); |
| if (value != null) { |
| props.add(store.createPropertyState(e.getKey(), value)); |
| } |
| } |
| return new DocumentNodeState(store, path, rootRev, props, hasChildren, lastRev); |
| } |
| |
| /** |
| * A list of children for a node. |
| */ |
| public static class Children implements CacheValue { |
| |
| /** |
| * Ascending sorted list of names of child nodes. |
| */ |
| final ArrayList<String> children = new ArrayList<String>(); |
| long cachedMemory; |
| boolean hasMore; |
| |
| @Override |
| public int getMemory() { |
| if (cachedMemory == 0) { |
| long size = 48; |
| if (!children.isEmpty()) { |
| size = 114; |
| for (String c : children) { |
| size += (long)estimateMemoryUsage(c) + 8; |
| } |
| } |
| cachedMemory = size; |
| } |
| if (cachedMemory > Integer.MAX_VALUE) { |
| log.debug("Estimated memory footprint larger than Integer.MAX_VALUE: {}.", cachedMemory); |
| return Integer.MAX_VALUE; |
| } else { |
| return (int)cachedMemory; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return children.toString(); |
| } |
| |
| public String asString() { |
| JsopWriter json = new JsopBuilder(); |
| if (hasMore) { |
| json.key("hasMore").value(true); |
| } |
| if (children.size() > 0) { |
| json.key("children").array(); |
| for (String c : children) { |
| json.value(c); |
| } |
| json.endArray(); |
| } |
| return json.toString(); |
| } |
| |
| public static Children fromString(String s) { |
| JsopTokenizer json = new JsopTokenizer(s); |
| Children children = new Children(); |
| while (true) { |
| if (json.matches(JsopReader.END)) { |
| break; |
| } |
| String k = json.readString(); |
| json.read(':'); |
| if ("hasMore".equals(k)) { |
| children.hasMore = json.read() == JsopReader.TRUE; |
| } else if ("children".equals(k)) { |
| json.read('['); |
| while (true) { |
| if (json.matches(']')) { |
| break; |
| } |
| String value = json.readString(); |
| children.children.add(value); |
| json.matches(','); |
| } |
| } |
| if (json.matches(JsopReader.END)) { |
| break; |
| } |
| json.read(','); |
| } |
| return children; |
| } |
| |
| } |
| |
| private class ChildNodeEntryIterator implements Iterator<ChildNodeEntry> { |
| |
| private String previousName; |
| private Iterator<ChildNodeEntry> current; |
| private int fetchSize = INITIAL_FETCH_SIZE; |
| private int currentRemaining = fetchSize; |
| |
| ChildNodeEntryIterator() { |
| fetchMore(); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| while (true) { |
| if (current == null) { |
| return false; |
| } else if (current.hasNext()) { |
| return true; |
| } else if (currentRemaining > 0) { |
| // current returned less than fetchSize |
| return false; |
| } |
| fetchMore(); |
| } |
| } |
| |
| @Override |
| public ChildNodeEntry next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| ChildNodeEntry entry = current.next(); |
| previousName = entry.getName(); |
| currentRemaining--; |
| return entry; |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| private void fetchMore() { |
| Iterator<ChildNodeEntry> entries = getChildNodeEntries( |
| previousName, fetchSize).iterator(); |
| currentRemaining = fetchSize; |
| fetchSize = Math.min(fetchSize * 2, MAX_FETCH_SIZE); |
| if (entries.hasNext()) { |
| current = entries; |
| } else { |
| current = null; |
| } |
| } |
| } |
| |
| //~----------------------------------------------< Bundling > |
| |
| private AbstractDocumentNodeState createBundledState(String childNodeName, Matcher child) { |
| return new DocumentNodeState( |
| store, |
| concat(path, childNodeName), |
| lastRevision, |
| rootRevision, |
| fromExternalChange, |
| bundlingContext.childContext(child)); |
| } |
| |
| private Iterator<ChildNodeEntry> getBundledChildren(){ |
| return Iterators.transform(bundlingContext.getBundledChildNodeNames().iterator(), |
| new Function<String, ChildNodeEntry>() { |
| @Override |
| public ChildNodeEntry apply(final String childNodeName) { |
| return new AbstractChildNodeEntry() { |
| @NotNull |
| @Override |
| public String getName() { |
| return childNodeName; |
| } |
| |
| @NotNull |
| @Override |
| public NodeState getNodeState() { |
| return createBundledState(childNodeName, bundlingContext.matcher.next(childNodeName)); |
| } |
| }; |
| } |
| }); |
| } |
| |
| private static BundlingContext createBundlingContext(Map<String, PropertyState> properties, |
| boolean hasNonBundledChildren) { |
| PropertyState bundlorConfig = properties.get(DocumentBundlor.META_PROP_PATTERN); |
| Matcher matcher = Matcher.NON_MATCHING; |
| boolean hasBundledChildren = false; |
| if (bundlorConfig != null){ |
| matcher = DocumentBundlor.from(bundlorConfig).createMatcher(); |
| hasBundledChildren = hasBundledProperty(properties, matcher, DocumentBundlor.META_PROP_BUNDLED_CHILD); |
| } |
| return new BundlingContext(matcher, properties, hasBundledChildren, hasNonBundledChildren); |
| } |
| |
| private static boolean hasBundledProperty(Map<String, PropertyState> props, Matcher matcher, String propName){ |
| String key = concat(matcher.getMatchedPath(), propName); |
| return props.containsKey(key); |
| } |
| |
| protected static class BundlingContext { |
| final Matcher matcher; |
| final Map<String, PropertyState> rootProperties; |
| final boolean hasBundledChildren; |
| final boolean hasNonBundledChildren; |
| |
| public BundlingContext(Matcher matcher, Map<String, PropertyState> rootProperties, |
| boolean hasBundledChildren, boolean hasNonBundledChildren) { |
| this.matcher = matcher; |
| this.rootProperties = ImmutableMap.copyOf(rootProperties); |
| this.hasBundledChildren = hasBundledChildren; |
| this.hasNonBundledChildren = hasNonBundledChildren; |
| } |
| |
| public BundlingContext childContext(Matcher childMatcher){ |
| return new BundlingContext(childMatcher, rootProperties, |
| hasBundledChildren(childMatcher), hasNonBundledChildren(childMatcher)); |
| } |
| |
| public Map<String, PropertyState> getProperties(){ |
| if (matcher.isMatch()){ |
| return BundlorUtils.getMatchingProperties(rootProperties, matcher); |
| } |
| return rootProperties; |
| } |
| |
| public boolean isBundled(){ |
| return matcher.isMatch(); |
| } |
| |
| public Map<String, PropertyState> getAllProperties(){ |
| return rootProperties; |
| } |
| |
| public boolean hasChildNode(String relativePath){ |
| String key = concat(relativePath, DocumentBundlor.META_PROP_BUNDLING_PATH); |
| return rootProperties.containsKey(key); |
| } |
| |
| public boolean hasChildren(){ |
| return hasNonBundledChildren || hasBundledChildren; |
| } |
| |
| public boolean hasOnlyBundledChildren(){ |
| return !hasNonBundledChildren; |
| } |
| |
| public Set<String> getBundledChildNodeNames(){ |
| if (isBundled()) { |
| return BundlorUtils.getChildNodeNames(rootProperties.keySet(), matcher); |
| } |
| return Collections.emptySet(); |
| } |
| |
| private boolean hasBundledChildren(Matcher matcher){ |
| if (isBundled()){ |
| return hasBundledProperty(rootProperties, matcher, DocumentBundlor.META_PROP_BUNDLED_CHILD); |
| } |
| return false; |
| } |
| |
| private boolean hasNonBundledChildren(Matcher matcher){ |
| if (isBundled()){ |
| return hasBundledProperty(rootProperties, matcher, DocumentBundlor.META_PROP_NON_BUNDLED_CHILD); |
| } |
| return false; |
| } |
| |
| } |
| } |