blob: 42bf67189dbcf36a2c8a524b1b92894836bf4f7d [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.version;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.version.OnParentVersionAction;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory;
import org.apache.jackrabbit.oak.plugins.tree.impl.TreeProviderService;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.Editor;
import org.apache.jackrabbit.oak.spi.lock.LockConstants;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.version.VersionConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static org.apache.jackrabbit.JcrConstants.MIX_VERSIONABLE;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.jackrabbit.JcrConstants.JCR_BASEVERSION;
import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
import static org.apache.jackrabbit.oak.spi.version.VersionConstants.RESTORE_PREFIX;
/**
* TODO document
*/
class VersionEditor implements Editor {
private final VersionEditor parent;
private final ReadWriteVersionManager vMgr;
private final NodeBuilder node;
private final String name;
private Boolean isVersionable = null;
private NodeState before;
private NodeState after;
private boolean isReadOnly;
private CommitInfo commitInfo;
public VersionEditor(@NotNull NodeBuilder versionStore,
@NotNull NodeBuilder workspaceRoot,
@NotNull CommitInfo commitInfo) {
this(null, new ReadWriteVersionManager(checkNotNull(versionStore),
checkNotNull(workspaceRoot)), workspaceRoot, "", commitInfo);
}
VersionEditor(@Nullable VersionEditor parent,
@NotNull ReadWriteVersionManager vMgr,
@NotNull NodeBuilder node,
@NotNull String name,
@NotNull CommitInfo commitInfo) {
this.parent = parent;
this.vMgr = checkNotNull(vMgr);
this.node = checkNotNull(node);
this.name = checkNotNull(name);
this.commitInfo = commitInfo;
}
@Override
public void enter(NodeState before, NodeState after)
throws CommitFailedException {
this.before = before;
this.after = after;
if (isVersionable()) {
vMgr.getOrCreateVersionHistory(node, commitInfo.getInfo());
}
// calculate isReadOnly state
if (after.exists() || isVersionable()) {
// deleted or versionable -> check if it was checked in
// a node cannot be modified if it was checked in
// unless it has a new identifier
isReadOnly = wasCheckedIn() && !hasNewIdentifier() && !isIgnoreOnOPV();
} else {
// otherwise inherit from parent
isReadOnly = parent != null && parent.isReadOnly && !isIgnoreOnOPV();
}
}
@Override
public void leave(NodeState before, NodeState after)
throws CommitFailedException {
}
@Override
public void propertyAdded(PropertyState after)
throws CommitFailedException {
if (after.getName().equals(JCR_BASEVERSION)
&& this.after.hasProperty(JcrConstants.JCR_VERSIONHISTORY)
&& !this.after.hasProperty(JCR_ISCHECKEDOUT)
&& !this.before.exists()) {
Tree tree = new TreeProviderService().createReadOnlyTree(this.node.getNodeState());
if (vMgr.getNodeTypeManager().isNodeType(
TreeUtil.getPrimaryTypeName(tree), TreeUtil.getMixinTypeNames(tree), MIX_VERSIONABLE)) {
// OAK-10462: the node has mix:versionable, but not the mandatory property jcr:isCheckedOut,
// so it has to be sentinel node for a restore operation.
// Unfortunately, there is no API available to detect that.
vMgr.restore(node, after.getValue(Type.REFERENCE), null);
return;
}
}
if (!isReadOnly || getOPV(after) == OnParentVersionAction.IGNORE) {
return;
}
// JCR allows to put a lock on a checked in node.
if (after.getName().equals(JcrConstants.JCR_LOCKOWNER)
|| after.getName().equals(JcrConstants.JCR_LOCKISDEEP)) {
return;
}
throwCheckedIn("Cannot add property " + after.getName()
+ " on checked in node");
}
@Override
public void propertyChanged(PropertyState before, PropertyState after)
throws CommitFailedException {
if (!isVersionable()) {
if (!isVersionProperty(after) && isReadOnly && getOPV(after) != OnParentVersionAction.IGNORE) {
throwCheckedIn("Cannot change property " + after.getName()
+ " on checked in node");
}
return;
}
String propName = after.getName();
if (propName.equals(JCR_ISCHECKEDOUT)) {
if (wasCheckedIn()) {
vMgr.checkout(node);
} else {
vMgr.checkin(node);
}
} else if (propName.equals(JCR_BASEVERSION)) {
String baseVersion = after.getValue(Type.REFERENCE);
if (baseVersion.startsWith(RESTORE_PREFIX)) {
baseVersion = baseVersion.substring(RESTORE_PREFIX.length());
node.setProperty(JCR_BASEVERSION, baseVersion, Type.REFERENCE);
}
vMgr.restore(node, baseVersion, null);
} else if (isVersionProperty(after)) {
throwProtected(after.getName());
} else if (isReadOnly && getOPV(after) != OnParentVersionAction.IGNORE) {
throwCheckedIn("Cannot change property " + after.getName()
+ " on checked in node");
}
}
@Override
public void propertyDeleted(PropertyState before)
throws CommitFailedException {
if (isReadOnly) {
if (!isVersionProperty(before) && !isLockProperty(before) && getOPV(before) != OnParentVersionAction.IGNORE) {
throwCheckedIn("Cannot delete property on checked in node");
}
}
}
@Override
public Editor childNodeAdded(String name, NodeState after) {
return childNodeChanged(name, MISSING_NODE, after);
}
@Override
public Editor childNodeChanged(String name, NodeState before,
NodeState after) {
return new VersionEditor(this, vMgr, node.child(name), name, commitInfo);
}
@Override
public Editor childNodeDeleted(String name, NodeState before) {
return new VersionEditor(this, vMgr, MISSING_NODE.builder(), name, commitInfo);
}
/**
* Returns {@code true} if the node of this VersionDiff is versionable;
* {@code false} otherwise.
*
* @return whether the node is versionable.
*/
private boolean isVersionable() {
if (isVersionable == null) {
isVersionable = vMgr.isVersionable(after);
}
return isVersionable;
}
private boolean isVersionProperty(PropertyState state) {
return VersionConstants.VERSION_PROPERTY_NAMES
.contains(state.getName());
}
private boolean isLockProperty(PropertyState state) {
return LockConstants.LOCK_PROPERTY_NAMES.contains(state.getName());
}
/**
* @return {@code true} if this node <b>was</b> checked in. That is,
* this method checks the before state for the jcr:isCheckedOut
* property.
*/
private boolean wasCheckedIn() {
PropertyState prop = before.getProperty(JCR_ISCHECKEDOUT);
if (prop != null) {
return !prop.getValue(Type.BOOLEAN);
}
// new node or not versionable, check parent
return parent != null && parent.wasCheckedIn();
}
private boolean hasNewIdentifier() {
String beforeId = buildBeforeIdentifier(new StringBuilder()).toString();
String afterId = buildAfterIdentifier(new StringBuilder()).toString();
return !beforeId.equals(afterId);
}
private StringBuilder buildBeforeIdentifier(StringBuilder identifier) {
String uuid = before.getString(JCR_UUID);
if (uuid != null) {
identifier.append(uuid);
} else if (parent != null) {
parent.buildBeforeIdentifier(identifier);
identifier.append("/").append(name);
}
return identifier;
}
private StringBuilder buildAfterIdentifier(StringBuilder identifier) {
String uuid = after.getString(JCR_UUID);
if (uuid != null) {
identifier.append(uuid);
} else if (parent != null) {
parent.buildAfterIdentifier(identifier);
identifier.append("/").append(name);
}
return identifier;
}
private static void throwCheckedIn(String msg)
throws CommitFailedException {
throw new CommitFailedException(CommitFailedException.VERSION,
VersionExceptionCode.NODE_CHECKED_IN.ordinal(), msg);
}
private static void throwProtected(String name)
throws CommitFailedException {
throw new CommitFailedException(CommitFailedException.CONSTRAINT, 100,
"Property is protected: " + name);
}
private boolean isIgnoreOnOPV() throws CommitFailedException {
if (this.parent != null) {
try {
NodeDefinition definition = this.vMgr.getNodeTypeManager().getDefinition(TreeFactory.createTree(parent.node), this.name);
return definition.getOnParentVersion() == OnParentVersionAction.IGNORE;
} catch (Exception e) {
throw new CommitFailedException(CommitFailedException.VERSION,
VersionExceptionCode.UNEXPECTED_REPOSITORY_EXCEPTION.ordinal(), e.getMessage());
}
}
return false;
}
private int getOPV(PropertyState property) throws CommitFailedException {
try {
return this.vMgr.getNodeTypeManager().getDefinition(TreeFactory.createReadOnlyTree(this.node.getNodeState()),
property, false).getOnParentVersion();
} catch (Exception e) {
throw new CommitFailedException(CommitFailedException.VERSION,
VersionExceptionCode.UNEXPECTED_REPOSITORY_EXCEPTION.ordinal(), e.getMessage());
}
}
}