blob: d51e54cf7b0b05538fc5f567d851a2eec02c3546 [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.segment;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
import static org.apache.jackrabbit.oak.segment.Segment.RECORD_ID_BYTES;
import static org.apache.jackrabbit.oak.segment.CacheWeights.OBJECT_HEADER_SIZE;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.StringUtils;
import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The in-memory representation of a "hidden class" of a node; inspired by the
* Chrome V8 Javascript engine).
* <p>
* Templates are always read fully in-memory.
*/
public class Template {
static final short ZERO_CHILD_NODES_TYPE = 0;
static final short SINGLE_CHILD_NODE_TYPE = 1;
static final short MANY_CHILD_NODES_TYPE = 2;
static final String ZERO_CHILD_NODES = null;
static final String MANY_CHILD_NODES = "";
@NotNull
private final SegmentReader reader;
/**
* The {@code jcr:primaryType} property, if present as a single-valued
* {@code NAME} property. Otherwise {@code null}.
*/
@Nullable
private final PropertyState primaryType;
/**
* The {@code jcr:mixinTypes} property, if present as a multi-valued
* {@code NAME} property. Otherwise {@code null}.
*/
@Nullable
private final PropertyState mixinTypes;
/**
* Templates of all the properties of a node, excluding the
* above-mentioned {@code NAME}-valued type properties, if any.
*/
@NotNull
private final PropertyTemplate[] properties;
/**
* Name of the single child node, if the node contains just one child.
* Otherwise {@link #ZERO_CHILD_NODES} (i.e. {@code null}) if there are
* no children, or {@link #MANY_CHILD_NODES} if there are more than one.
*/
@Nullable
private final String childName;
Template(@NotNull SegmentReader reader,
@Nullable PropertyState primaryType,
@Nullable PropertyState mixinTypes,
@Nullable PropertyTemplate[] properties,
@Nullable String childName) {
this.reader = checkNotNull(reader);
this.primaryType = primaryType;
this.mixinTypes = mixinTypes;
if (properties != null) {
this.properties = properties;
Arrays.sort(this.properties);
} else {
this.properties = new PropertyTemplate[0];
}
this.childName = childName;
}
Template(@NotNull SegmentReader reader, @NotNull NodeState state) {
this.reader = checkNotNull(reader);
checkNotNull(state);
PropertyState primary = null;
PropertyState mixins = null;
List<PropertyTemplate> templates = Lists.newArrayList();
for (PropertyState property : state.getProperties()) {
String name = property.getName();
Type<?> type = property.getType();
if ("jcr:primaryType".equals(name) && type == Type.NAME) {
primary = property;
} else if ("jcr:mixinTypes".equals(name) && type == Type.NAMES) {
mixins = property;
} else {
templates.add(new PropertyTemplate(property));
}
}
this.primaryType = primary;
this.mixinTypes = mixins;
this.properties =
templates.toArray(new PropertyTemplate[templates.size()]);
Arrays.sort(properties);
long count = state.getChildNodeCount(2);
if (count == 0) {
childName = ZERO_CHILD_NODES;
} else if (count == 1) {
childName = state.getChildNodeNames().iterator().next();
checkState(childName != null && !childName.equals(MANY_CHILD_NODES));
} else {
childName = MANY_CHILD_NODES;
}
}
@Nullable
PropertyState getPrimaryType() {
return primaryType;
}
@Nullable
PropertyState getMixinTypes() {
return mixinTypes;
}
PropertyTemplate[] getPropertyTemplates() {
return properties;
}
/**
* Returns the template of the named property, or {@code null} if no such
* property exists. Use the {@link #getPrimaryType()} and
* {@link #getMixinTypes()} for accessing the JCR type properties, as
* they don't have templates.
*
* @param name property name
* @return property template, or {@code} null if not found
*/
PropertyTemplate getPropertyTemplate(String name) {
int hash = name.hashCode();
int index = 0;
while (index < properties.length
&& properties[index].getName().hashCode() < hash) {
index++;
}
while (index < properties.length
&& properties[index].getName().hashCode() == hash) {
if (name.equals(properties[index].getName())) {
return properties[index];
}
index++;
}
return null;
}
@Nullable
String getChildName() {
return childName;
}
SegmentPropertyState getProperty(RecordId recordId, int index) {
checkElementIndex(index, properties.length);
Segment segment = checkNotNull(recordId).getSegment();
int offset = 2 * RECORD_ID_BYTES;
if (childName != ZERO_CHILD_NODES) {
offset += RECORD_ID_BYTES;
}
RecordId lid = segment.readRecordId(recordId.getRecordNumber(), offset);
ListRecord props = new ListRecord(lid, properties.length);
RecordId rid = props.getEntry(index);
return reader.readProperty(rid, properties[index]);
}
MapRecord getChildNodeMap(RecordId recordId) {
checkState(childName != ZERO_CHILD_NODES);
Segment segment = recordId.getSegment();
RecordId childNodesId = segment.readRecordId(recordId.getRecordNumber(), 2 * RECORD_ID_BYTES);
return reader.readMap(childNodesId);
}
public NodeState getChildNode(String name, RecordId recordId) {
if (childName == ZERO_CHILD_NODES) {
return MISSING_NODE;
} else if (childName == MANY_CHILD_NODES) {
MapRecord map = getChildNodeMap(recordId);
MapEntry child = map.getEntry(name);
if (child != null) {
return child.getNodeState();
} else {
return MISSING_NODE;
}
} else if (name.equals(childName)) {
Segment segment = recordId.getSegment();
RecordId childNodeId = segment.readRecordId(recordId.getRecordNumber(), 2 * RECORD_ID_BYTES);
return reader.readNode(childNodeId);
} else {
return MISSING_NODE;
}
}
Iterable<? extends ChildNodeEntry> getChildNodeEntries(RecordId recordId) {
if (childName == ZERO_CHILD_NODES) {
return Collections.emptyList();
} else if (childName == MANY_CHILD_NODES) {
MapRecord map = getChildNodeMap(recordId);
return map.getEntries();
} else {
Segment segment = recordId.getSegment();
RecordId childNodeId = segment.readRecordId(recordId.getRecordNumber(), 2 * RECORD_ID_BYTES);
return Collections.singletonList(new MemoryChildNodeEntry(
childName, reader.readNode(childNodeId)));
}
}
public boolean compare(RecordId thisId, RecordId thatId) {
checkNotNull(thisId);
checkNotNull(thatId);
// Compare properties
for (int i = 0; i < properties.length; i++) {
PropertyState thisProperty = getProperty(thisId, i);
PropertyState thatProperty = getProperty(thatId, i);
if (!thisProperty.equals(thatProperty)) {
return false;
}
}
// Compare child nodes
if (childName == ZERO_CHILD_NODES) {
return true;
} else if (childName != MANY_CHILD_NODES) {
NodeState thisChild = getChildNode(childName, thisId);
NodeState thatChild = getChildNode(childName, thatId);
return thisChild.equals(thatChild);
} else {
// TODO: Leverage the HAMT data structure for the comparison
MapRecord thisMap = getChildNodeMap(thisId);
MapRecord thatMap = getChildNodeMap(thatId);
if (Record.fastEquals(thisMap, thatMap)) {
return true; // shortcut
} else if (thisMap.size() != thatMap.size()) {
return false; // shortcut
} else {
// TODO: can this be optimized?
for (MapEntry entry : thisMap.getEntries()) {
String name = entry.getName();
MapEntry thatEntry = thatMap.getEntry(name);
if (thatEntry == null) {
return false;
} else if (!entry.getNodeState().equals(thatEntry.getNodeState())) {
return false;
}
}
return true;
}
}
}
//------------------------------------------------------------< Object >--
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
} else if (object instanceof Template) {
Template that = (Template) object;
return Objects.equal(primaryType, that.primaryType)
&& Objects.equal(mixinTypes, that.mixinTypes)
&& Arrays.equals(properties, that.properties)
&& Objects.equal(childName, that.childName);
} else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hashCode(primaryType, mixinTypes,
Arrays.asList(properties), getTemplateType(), childName);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ ");
if (primaryType != null) {
builder.append(primaryType);
builder.append(", ");
}
if (mixinTypes != null) {
builder.append(mixinTypes);
builder.append(", ");
}
for (PropertyTemplate property : properties) {
builder.append(property);
builder.append(" = ?, ");
}
if (childName == ZERO_CHILD_NODES) {
builder.append("<no children>");
} else if (childName == MANY_CHILD_NODES) {
builder.append("<many children>");
} else {
builder.append(childName + " = <node>");
}
builder.append(" }");
return builder.toString();
}
short getTemplateType() {
if (childName == ZERO_CHILD_NODES) {
return ZERO_CHILD_NODES_TYPE;
} else if (childName == MANY_CHILD_NODES) {
return MANY_CHILD_NODES_TYPE;
} else {
return SINGLE_CHILD_NODE_TYPE;
}
}
public int estimateMemoryUsage() {
int size = OBJECT_HEADER_SIZE;
size += 48;
size += StringUtils.estimateMemoryUsage(childName);
size += estimateMemoryUsage(mixinTypes);
size += estimateMemoryUsage(primaryType);
for (PropertyTemplate property : properties) {
size += property.estimateMemoryUsage();
}
return size;
}
private static int estimateMemoryUsage(PropertyState propertyState) {
if (propertyState == null) {
return 0;
}
int size = OBJECT_HEADER_SIZE;
size += StringUtils.estimateMemoryUsage(propertyState.getName());
for (int k = 0; k < propertyState.count(); k++) {
String s = propertyState.getValue(STRING, k);
size += StringUtils.estimateMemoryUsage(s);
}
return size;
}
}