| /* |
| * 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()); |
| } |
| } |
| } |