blob: 032c044c21dfbcf2bd432508170a8099934f40df [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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.oak.api.Type.BOOLEAN;
import static org.apache.jackrabbit.oak.api.Type.LONG;
import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
import static org.apache.jackrabbit.oak.spi.state.AbstractNodeState.checkValidName;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;
import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
import org.apache.jackrabbit.oak.stats.MeterStats;
import org.apache.jackrabbit.oak.stats.NoopStats;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A record of type "NODE". This class can read a node record from a segment. It
* currently doesn't cache data (but the template is fully loaded).
*/
public class SegmentNodeState extends Record implements NodeState {
@NotNull
private final SegmentReader reader;
@Nullable
private final BlobStore blobStore;
@NotNull
private final Supplier<SegmentWriter> writer;
private final MeterStats readStats;
private volatile RecordId templateId = null;
private volatile Template template = null;
SegmentNodeState(
@NotNull SegmentReader reader,
@NotNull Supplier<SegmentWriter> writer,
@Nullable BlobStore blobStore,
@NotNull RecordId id,
MeterStats readStats
) {
super(id);
this.reader = checkNotNull(reader);
this.writer = checkNotNull(memoize(writer));
this.blobStore = blobStore;
this.readStats = readStats;
}
public SegmentNodeState(
@NotNull SegmentReader reader,
@NotNull SegmentWriter writer,
@Nullable BlobStore blobStore,
@NotNull RecordId id
) {
this(reader, Suppliers.ofInstance(writer), blobStore, id, NoopStats.INSTANCE);
}
public SegmentNodeState(
@NotNull SegmentReader reader,
@NotNull SegmentWriter writer,
@Nullable BlobStore blobStore,
@NotNull RecordId id,
MeterStats readStats
) {
this(reader, Suppliers.ofInstance(writer), blobStore, id, readStats);
}
RecordId getTemplateId() {
if (templateId == null) {
// no problem if updated concurrently,
// as each concurrent thread will just get the same value
templateId = getSegment().readRecordId(getRecordNumber(), 0, 1);
}
return templateId;
}
Template getTemplate() {
if (template == null) {
// no problem if updated concurrently,
// as each concurrent thread will just get the same value
template = reader.readTemplate(getTemplateId());
}
return template;
}
MapRecord getChildNodeMap() {
Segment segment = getSegment();
return reader.readMap(segment.readRecordId(getRecordNumber(), 0, 2));
}
@NotNull
static String getStableId(@NotNull ByteBuffer stableId) {
ByteBuffer buffer = stableId.duplicate();
long msb = buffer.getLong();
long lsb = buffer.getLong();
int offset = buffer.getInt();
return new UUID(msb, lsb) + ":" + offset;
}
/**
* Returns the stable id of this node. In contrast to the node's record id
* (which is technically the node's address) the stable id doesn't change
* after an online gc cycle. It might though change after an offline gc cycle.
*
* @return stable id
*/
public String getStableId() {
return getStableId(getStableIdBytes());
}
/**
* Returns the stable ID of this node, non parsed. In contrast to the node's
* record id (which is technically the node's address) the stable id doesn't
* change after an online gc cycle. It might though change after an offline
* gc cycle.
*
* @return the stable ID of this node.
*/
public ByteBuffer getStableIdBytes() {
// The first record id of this node points to the stable id.
RecordId id = getSegment().readRecordId(getRecordNumber());
if (id.equals(getRecordId())) {
// If that id is equal to the record id of this node then the stable
// id is the string representation of the record id of this node.
// See RecordWriters.NodeStateWriter.writeRecordContent()
return id.getBytes();
} else {
// Otherwise that id points to the serialised (msb, lsb, offset)
// stable id.
return id.getSegment().readBytes(id.getRecordNumber(), 0, RecordId.SERIALIZED_RECORD_ID_BYTES);
}
}
@Override
public boolean exists() {
return true;
}
@Override
public long getPropertyCount() {
readStats.mark();
Template template = getTemplate();
long count = template.getPropertyTemplates().length;
if (template.getPrimaryType() != null) {
count++;
}
if (template.getMixinTypes() != null) {
count++;
}
return count;
}
@Override
public boolean hasProperty(@NotNull String name) {
readStats.mark();
checkNotNull(name);
Template template = getTemplate();
switch (name) {
case JCR_PRIMARYTYPE:
return template.getPrimaryType() != null;
case JCR_MIXINTYPES:
return template.getMixinTypes() != null;
default:
return template.getPropertyTemplate(name) != null;
}
}
@Override @Nullable
public PropertyState getProperty(@NotNull String name) {
readStats.mark();
checkNotNull(name);
Template template = getTemplate();
PropertyState property = null;
if (JCR_PRIMARYTYPE.equals(name)) {
property = template.getPrimaryType();
} else if (JCR_MIXINTYPES.equals(name)) {
property = template.getMixinTypes();
}
if (property != null) {
return property;
}
PropertyTemplate propertyTemplate =
template.getPropertyTemplate(name);
if (propertyTemplate != null) {
Segment segment = getSegment();
RecordId id = getRecordId(segment, template, propertyTemplate);
return reader.readProperty(id, propertyTemplate);
} else {
return null;
}
}
private RecordId getRecordId(Segment segment, Template template,
PropertyTemplate propertyTemplate) {
int ids = 2;
if (template.getChildName() != Template.ZERO_CHILD_NODES) {
ids++;
}
RecordId rid = segment.readRecordId(getRecordNumber(), 0, ids);
ListRecord pIds = new ListRecord(rid, template.getPropertyTemplates().length);
return pIds.getEntry(propertyTemplate.getIndex());
}
@Override @NotNull
public Iterable<PropertyState> getProperties() {
readStats.mark();
Template template = getTemplate();
PropertyTemplate[] propertyTemplates = template.getPropertyTemplates();
List<PropertyState> list =
newArrayListWithCapacity(propertyTemplates.length + 2);
PropertyState primaryType = template.getPrimaryType();
if (primaryType != null) {
list.add(primaryType);
}
PropertyState mixinTypes = template.getMixinTypes();
if (mixinTypes != null) {
list.add(mixinTypes);
}
Segment segment = getSegment();
int ids = 2;
if (template.getChildName() != Template.ZERO_CHILD_NODES) {
ids++;
}
if (propertyTemplates.length > 0) {
ListRecord pIds = new ListRecord(segment.readRecordId(getRecordNumber(), 0, ids), propertyTemplates.length);
for (int i = 0; i < propertyTemplates.length; i++) {
RecordId propertyId = pIds.getEntry(i);
list.add(reader.readProperty(propertyId, propertyTemplates[i]));
}
}
return list;
}
@Override
public boolean getBoolean(@NotNull String name) {
readStats.mark();
return Boolean.TRUE.toString().equals(getValueAsString(name, BOOLEAN));
}
@Override
public long getLong(String name) {
readStats.mark();
String value = getValueAsString(name, LONG);
if (value != null) {
return Long.parseLong(value);
} else {
return 0;
}
}
@Override @Nullable
public String getString(String name) {
readStats.mark();
return getValueAsString(name, STRING);
}
@Override @NotNull
public Iterable<String> getStrings(@NotNull String name) {
readStats.mark();
return getValuesAsStrings(name, STRINGS);
}
@Override @Nullable
public String getName(@NotNull String name) {
readStats.mark();
return getValueAsString(name, NAME);
}
@Override @NotNull
public Iterable<String> getNames(@NotNull String name) {
readStats.mark();
return getValuesAsStrings(name, NAMES);
}
/**
* Optimized value access method. Returns the string value of a property
* of a given non-array type. Returns {@code null} if the named property
* does not exist, or is of a different type than given.
*
* @param name property name
* @param type property type
* @return string value of the property, or {@code null}
*/
@Nullable
private String getValueAsString(String name, Type<?> type) {
checkArgument(!type.isArray());
Template template = getTemplate();
if (JCR_PRIMARYTYPE.equals(name)) {
PropertyState primary = template.getPrimaryType();
if (primary != null) {
if (type == NAME) {
return primary.getValue(NAME);
} else {
return null;
}
}
} else if (JCR_MIXINTYPES.equals(name)
&& template.getMixinTypes() != null) {
return null;
}
PropertyTemplate propertyTemplate =
template.getPropertyTemplate(name);
if (propertyTemplate == null
|| propertyTemplate.getType() != type) {
return null;
}
Segment segment = getSegment();
RecordId id = getRecordId(segment, template, propertyTemplate);
return reader.readString(id);
}
/**
* Optimized value access method. Returns the string values of a property
* of a given array type. Returns an empty iterable if the named property
* does not exist, or is of a different type than given.
*
* @param name property name
* @param type property type
* @return string values of the property, or an empty iterable
*/
@NotNull
private Iterable<String> getValuesAsStrings(String name, Type<?> type) {
checkArgument(type.isArray());
Template template = getTemplate();
if (JCR_MIXINTYPES.equals(name)) {
PropertyState mixin = template.getMixinTypes();
if (type == NAMES && mixin != null) {
return mixin.getValue(NAMES);
} else if (type == NAMES || mixin != null) {
return emptyList();
}
} else if (JCR_PRIMARYTYPE.equals(name)
&& template.getPrimaryType() != null) {
return emptyList();
}
PropertyTemplate propertyTemplate =
template.getPropertyTemplate(name);
if (propertyTemplate == null
|| propertyTemplate.getType() != type) {
return emptyList();
}
Segment segment = getSegment();
RecordId id = getRecordId(segment, template, propertyTemplate);
segment = id.getSegment();
int size = segment.readInt(id.getRecordNumber());
if (size == 0) {
return emptyList();
}
id = segment.readRecordId(id.getRecordNumber(), 4);
if (size == 1) {
return singletonList(reader.readString(id));
}
List<String> values = newArrayListWithCapacity(size);
ListRecord list = new ListRecord(id, size);
for (RecordId value : list.getEntries()) {
values.add(reader.readString(value));
}
return values;
}
@Override
public long getChildNodeCount(long max) {
readStats.mark();
String childName = getTemplate().getChildName();
if (childName == Template.ZERO_CHILD_NODES) {
return 0;
} else if (childName == Template.MANY_CHILD_NODES) {
return getChildNodeMap().size();
} else {
return 1;
}
}
@Override
public boolean hasChildNode(@NotNull String name) {
readStats.mark();
String childName = getTemplate().getChildName();
if (childName == Template.ZERO_CHILD_NODES) {
return false;
} else if (childName == Template.MANY_CHILD_NODES) {
return getChildNodeMap().getEntry(name) != null;
} else {
return childName.equals(name);
}
}
@Override @NotNull
public NodeState getChildNode(@NotNull String name) {
readStats.mark();
String childName = getTemplate().getChildName();
if (childName == Template.MANY_CHILD_NODES) {
MapEntry child = getChildNodeMap().getEntry(name);
if (child != null) {
return child.getNodeState();
}
} else if (childName != Template.ZERO_CHILD_NODES
&& childName.equals(name)) {
RecordId childNodeId = getSegment().readRecordId(getRecordNumber(), 0, 2);
return reader.readNode(childNodeId);
}
checkValidName(name);
return MISSING_NODE;
}
@Override @NotNull
public Iterable<String> getChildNodeNames() {
readStats.mark();
String childName = getTemplate().getChildName();
if (childName == Template.ZERO_CHILD_NODES) {
return Collections.emptyList();
} else if (childName == Template.MANY_CHILD_NODES) {
return getChildNodeMap().getKeys();
} else {
return Collections.singletonList(childName);
}
}
@Override @NotNull
public Iterable<? extends ChildNodeEntry> getChildNodeEntries() {
readStats.mark();
String childName = getTemplate().getChildName();
if (childName == Template.ZERO_CHILD_NODES) {
return Collections.emptyList();
} else if (childName == Template.MANY_CHILD_NODES) {
return getChildNodeMap().getEntries();
} else {
RecordId childNodeId = getSegment().readRecordId(getRecordNumber(), 0, 2);
return Collections.singletonList(new MemoryChildNodeEntry(
childName, reader.readNode(childNodeId)));
}
}
@Override @NotNull
public SegmentNodeBuilder builder() {
return new SegmentNodeBuilder(this, blobStore, reader, writer.get(), readStats);
}
@Override
public boolean compareAgainstBaseState(NodeState base, NodeStateDiff diff) {
readStats.mark();
if (this == base || fastEquals(this, base)) {
return true; // no changes
} else if (base == EMPTY_NODE || !base.exists()) { // special case
return EmptyNodeState.compareAgainstEmptyState(this, diff);
} else if (!(base instanceof SegmentNodeState)) { // fallback
return AbstractNodeState.compareAgainstBaseState(this, base, diff);
}
SegmentNodeState that = (SegmentNodeState) base;
Template beforeTemplate = that.getTemplate();
RecordId beforeId = that.getRecordId();
Template afterTemplate = getTemplate();
RecordId afterId = getRecordId();
// Compare type properties
if (!compareProperties(
beforeTemplate.getPrimaryType(), afterTemplate.getPrimaryType(),
diff)) {
return false;
}
if (!compareProperties(
beforeTemplate.getMixinTypes(), afterTemplate.getMixinTypes(),
diff)) {
return false;
}
// Compare other properties, leveraging the ordering
int beforeIndex = 0;
int afterIndex = 0;
PropertyTemplate[] beforeProperties =
beforeTemplate.getPropertyTemplates();
PropertyTemplate[] afterProperties =
afterTemplate.getPropertyTemplates();
while (beforeIndex < beforeProperties.length
&& afterIndex < afterProperties.length) {
int d = Integer.valueOf(afterProperties[afterIndex].hashCode())
.compareTo(beforeProperties[beforeIndex].hashCode());
if (d == 0) {
d = afterProperties[afterIndex].getName().compareTo(
beforeProperties[beforeIndex].getName());
}
PropertyState beforeProperty = null;
PropertyState afterProperty = null;
if (d < 0) {
afterProperty =
afterTemplate.getProperty(afterId, afterIndex++);
} else if (d > 0) {
beforeProperty =
beforeTemplate.getProperty(beforeId, beforeIndex++);
} else {
afterProperty =
afterTemplate.getProperty(afterId, afterIndex++);
beforeProperty =
beforeTemplate.getProperty(beforeId, beforeIndex++);
}
if (!compareProperties(beforeProperty, afterProperty, diff)) {
return false;
}
}
while (afterIndex < afterProperties.length) {
if (!diff.propertyAdded(
afterTemplate.getProperty(afterId, afterIndex++))) {
return false;
}
}
while (beforeIndex < beforeProperties.length) {
PropertyState beforeProperty =
beforeTemplate.getProperty(beforeId, beforeIndex++);
if (!diff.propertyDeleted(beforeProperty)) {
return false;
}
}
String beforeChildName = beforeTemplate.getChildName();
String afterChildName = afterTemplate.getChildName();
if (afterChildName == Template.ZERO_CHILD_NODES) {
if (beforeChildName != Template.ZERO_CHILD_NODES) {
for (ChildNodeEntry entry
: beforeTemplate.getChildNodeEntries(beforeId)) {
if (!diff.childNodeDeleted(
entry.getName(), entry.getNodeState())) {
return false;
}
}
}
} else if (afterChildName != Template.MANY_CHILD_NODES) {
NodeState afterNode =
afterTemplate.getChildNode(afterChildName, afterId);
NodeState beforeNode =
beforeTemplate.getChildNode(afterChildName, beforeId);
if (!beforeNode.exists()) {
if (!diff.childNodeAdded(afterChildName, afterNode)) {
return false;
}
} else if (!fastEquals(afterNode, beforeNode)) {
if (!diff.childNodeChanged(
afterChildName, beforeNode, afterNode)) {
return false;
}
}
if (beforeChildName == Template.MANY_CHILD_NODES
|| (beforeChildName != Template.ZERO_CHILD_NODES
&& !beforeNode.exists())) {
for (ChildNodeEntry entry
: beforeTemplate.getChildNodeEntries(beforeId)) {
if (!afterChildName.equals(entry.getName())) {
if (!diff.childNodeDeleted(
entry.getName(), entry.getNodeState())) {
return false;
}
}
}
}
} else if (beforeChildName == Template.ZERO_CHILD_NODES) {
for (ChildNodeEntry entry
: afterTemplate.getChildNodeEntries(afterId)) {
if (!diff.childNodeAdded(
entry.getName(), entry.getNodeState())) {
return false;
}
}
} else if (beforeChildName != Template.MANY_CHILD_NODES) {
boolean beforeChildRemoved = true;
NodeState beforeChild =
beforeTemplate.getChildNode(beforeChildName, beforeId);
for (ChildNodeEntry entry
: afterTemplate.getChildNodeEntries(afterId)) {
String childName = entry.getName();
NodeState afterChild = entry.getNodeState();
if (beforeChildName.equals(childName)) {
beforeChildRemoved = false;
if (!fastEquals(afterChild, beforeChild)
&& !diff.childNodeChanged(
childName, beforeChild, afterChild)) {
return false;
}
} else if (!diff.childNodeAdded(childName, afterChild)) {
return false;
}
}
if (beforeChildRemoved) {
if (!diff.childNodeDeleted(beforeChildName, beforeChild)) {
return false;
}
}
} else {
MapRecord afterMap = afterTemplate.getChildNodeMap(afterId);
MapRecord beforeMap = beforeTemplate.getChildNodeMap(beforeId);
return afterMap.compare(beforeMap, diff);
}
return true;
}
private static boolean compareProperties(
PropertyState before, PropertyState after, NodeStateDiff diff) {
if (before == null) {
return after == null || diff.propertyAdded(after);
} else if (after == null) {
return diff.propertyDeleted(before);
} else {
return before.equals(after) || diff.propertyChanged(before, after);
}
}
//------------------------------------------------------------< Object >--
/**
* Indicates whether two {@link NodeState} instances are equal to each
* other. A return value of {@code true} clearly means that the instances
* are equal, while a return value of {@code false} doesn't necessarily mean
* the instances are not equal. These "false negatives" are an
* implementation detail and callers cannot rely on them being stable.
*
* @param a
* the first {@link NodeState} instance
* @param b
* the second {@link NodeState} instance
* @return {@code true}, if these two instances are equal.
*/
public static boolean fastEquals(NodeState a, NodeState b) {
if (Record.fastEquals(a, b)) {
return true;
}
if (a instanceof SegmentNodeState && b instanceof SegmentNodeState
&& ((SegmentNodeState) a).getStableId().equals(((SegmentNodeState) b).getStableId())) {
return true;
}
return false;
}
@Override
public int hashCode() {
return getStableId().hashCode();
}
@Override
public boolean equals(Object object) {
if (object instanceof SegmentNodeState) {
SegmentNodeState that = (SegmentNodeState) object;
if (fastEquals(this, that)) {
return true;
} else {
Template template = getTemplate();
return template.equals(that.getTemplate())
&& template.compare(getRecordId(), that.getRecordId());
}
} else {
return object instanceof NodeState
&& AbstractNodeState.equals(this, (NodeState) object);
}
}
@Override
public String toString() {
return AbstractNodeState.toString(this);
}
}