blob: 659f8371a7b0e75cf993ae34b3cd42d2848a0a20 [file] [log] [blame]
/*
* 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.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import com.google.common.collect.ImmutableMap;
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.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 Path 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;
private int memory;
DocumentNodeState(@NotNull DocumentNodeStore store,
@NotNull Path path,
@NotNull RevisionVector rootRevision) {
this(store, path, rootRevision, Collections.<PropertyState>emptyList(), false, null);
}
DocumentNodeState(@NotNull DocumentNodeStore store, @NotNull Path path,
@NotNull RevisionVector rootRevision,
Iterable<? extends PropertyState> properties,
boolean hasChildren,
@Nullable RevisionVector lastRevision) {
this(store, path, rootRevision, asMap(properties),
hasChildren, 0, lastRevision, false);
}
public DocumentNodeState(@NotNull DocumentNodeStore store,
@NotNull Path path,
@NotNull RevisionVector rootRevision,
@NotNull Map<String, PropertyState> properties,
boolean hasChildren,
int memory,
@Nullable RevisionVector lastRevision,
boolean fromExternalChange) {
this(store, path, lastRevision, rootRevision,
fromExternalChange, createBundlingContext(checkNotNull(properties), hasChildren), memory);
}
protected DocumentNodeState(@NotNull DocumentNodeStore store,
@NotNull Path path,
@Nullable RevisionVector lastRevision,
@NotNull RevisionVector rootRevision,
boolean fromExternalChange,
BundlingContext bundlingContext,
int memory) {
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();
this.memory = memory;
}
/**
* 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, memory);
}
}
/**
* @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, memory);
}
/**
* 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(path.isRoot());
checkState(getRootRevision().isBranch());
return new DocumentBranchRootNodeState(store, branch, path, rootRevision, lastRevision, bundlingContext, memory);
}
/**
* @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 Path 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 = "";
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 (getPath().isRoot()) {
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.isIdFromLongPath(id)) {
op.set(NodeDocument.PATH, path.toString());
}
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;
}
@Override
public int getMemory() {
long size = memory;
if (size == 0) {
size = 40 // shallow
+ (lastRevision != null ? lastRevision.getMemory() : 0)
+ rootRevision.getMemory()
+ path.getMemory();
// 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;
}
memory = (int) size;
}
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
}
/**
* Returns all properties, including bundled, as Json serialized value.
*
* @return all properties, including bundled.
*/
public Map<String, String> getAllBundledProperties() {
Map<String, String> allProps = new HashMap<>();
for (Map.Entry<String, PropertyState> e : bundlingContext.getAllProperties().entrySet()) {
allProps.put(e.getKey(), asString(e.getValue()));
}
return allProps;
}
//------------------------------< 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(new Path(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
* the empty {@code String}, 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(@NotNull 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 input.getPath().getName();
}
@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();
}
/**
* 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,
new Path(path, childNodeName),
lastRevision,
rootRevision,
fromExternalChange,
bundlingContext.childContext(child),
memory);
}
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;
}
}
}