| /* |
| * 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 java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import javax.jcr.RepositoryException; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Sets; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Root; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.commons.UUIDUtils; |
| import org.apache.jackrabbit.oak.namepath.NamePathMapper; |
| import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder; |
| import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; |
| import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate; |
| import org.apache.jackrabbit.oak.plugins.tree.factories.RootFactory; |
| import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory; |
| 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.apache.jackrabbit.util.ISO8601; |
| import org.apache.jackrabbit.util.Text; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static org.apache.jackrabbit.JcrConstants.JCR_BASEVERSION; |
| import static org.apache.jackrabbit.JcrConstants.JCR_CREATED; |
| import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PREDECESSORS; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_ROOTVERSION; |
| import static org.apache.jackrabbit.JcrConstants.JCR_SUCCESSORS; |
| import static org.apache.jackrabbit.JcrConstants.JCR_UUID; |
| import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONABLEUUID; |
| import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONHISTORY; |
| import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONLABELS; |
| import static org.apache.jackrabbit.JcrConstants.NT_VERSION; |
| import static org.apache.jackrabbit.JcrConstants.NT_VERSIONHISTORY; |
| import static org.apache.jackrabbit.JcrConstants.NT_VERSIONLABELS; |
| import static org.apache.jackrabbit.oak.plugins.version.Utils.uuidFromNode; |
| import static org.apache.jackrabbit.oak.spi.version.VersionConstants.JCR_COPIED_FROM; |
| import static org.apache.jackrabbit.oak.spi.version.VersionConstants.REP_VERSIONSTORAGE; |
| import static org.apache.jackrabbit.oak.spi.version.VersionConstants.VERSION_STORE_PATH; |
| |
| /** |
| * Extends the {@link ReadOnlyVersionManager} with methods to modify the |
| * version store. |
| */ |
| public class ReadWriteVersionManager extends ReadOnlyVersionManager { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(ReadWriteVersionManager.class); |
| |
| private final NodeBuilder versionStorageNode; |
| private final NodeBuilder workspaceRoot; |
| private final TypePredicate isVersion; |
| private ReadOnlyNodeTypeManager ntMgr; |
| |
| public ReadWriteVersionManager(NodeBuilder versionStorageNode, |
| NodeBuilder workspaceRoot) { |
| this.versionStorageNode = checkNotNull(versionStorageNode); |
| this.workspaceRoot = checkNotNull(workspaceRoot); |
| this.isVersion = new TypePredicate(workspaceRoot.getNodeState(), NT_VERSION); |
| } |
| |
| @NotNull |
| @Override |
| protected Tree getVersionStorage() { |
| return TreeFactory.createReadOnlyTree(versionStorageNode.getNodeState()); |
| } |
| |
| @NotNull |
| @Override |
| protected Root getWorkspaceRoot() { |
| return RootFactory.createReadOnlyRoot(workspaceRoot.getNodeState()); |
| } |
| |
| @NotNull |
| @Override |
| protected ReadOnlyNodeTypeManager getNodeTypeManager() { |
| if (ntMgr == null) { |
| ntMgr = ReadOnlyNodeTypeManager.getInstance( |
| getWorkspaceRoot(), NamePathMapper.DEFAULT); |
| } |
| return ntMgr; |
| } |
| |
| /** |
| * Gets or creates the version history for the given |
| * {@code versionable} node. |
| * |
| * @param versionable the versionable node. |
| * @param infoMap The additional information as provided by {@link org.apache.jackrabbit.oak.spi.commit.CommitInfo#getInfo()} |
| * @return the version history node. |
| * @throws IllegalArgumentException if the given node does not have a |
| * {@code jcr:uuid} property. |
| */ |
| @NotNull |
| public NodeBuilder getOrCreateVersionHistory(@NotNull NodeBuilder versionable, @NotNull Map<String, Object> infoMap) { |
| checkNotNull(versionable); |
| String vUUID = uuidFromNode(versionable); |
| String relPath = getVersionHistoryPath(vUUID); |
| NodeBuilder node = versionStorageNode; |
| for (Iterator<String> it = PathUtils.elements(relPath).iterator(); it.hasNext(); ) { |
| String name = it.next(); |
| node = node.child(name); |
| if (!node.hasProperty(JCR_PRIMARYTYPE)) { |
| String nt; |
| if (it.hasNext()) { |
| nt = REP_VERSIONSTORAGE; |
| } else { |
| // last path element denotes nt:versionHistory node |
| nt = NT_VERSIONHISTORY; |
| } |
| node.setProperty(JCR_PRIMARYTYPE, nt, Type.NAME); |
| } |
| } |
| Object copiedFrom = infoMap.get(JCR_COPIED_FROM); |
| if (copiedFrom != null) { |
| node.setProperty(JCR_COPIED_FROM, copiedFrom.toString(), Type.WEAKREFERENCE); |
| } |
| |
| // use jcr:rootVersion node to detect if we need to initialize the |
| // version history |
| if (!node.hasChildNode(JCR_ROOTVERSION)) { |
| // jcr:versionableUuid property |
| node.setProperty(JCR_VERSIONABLEUUID, vUUID, Type.STRING); |
| node.setProperty(JCR_UUID, |
| UUIDUtils.generateUUID(), Type.STRING); |
| |
| // jcr:versionLabels child node |
| NodeBuilder vLabels = node.child(JCR_VERSIONLABELS); |
| vLabels.setProperty(JCR_PRIMARYTYPE, NT_VERSIONLABELS, Type.NAME); |
| |
| // jcr:rootVersion child node |
| createRootVersion(node, versionable); |
| } else if (!versionable.hasProperty(JCR_VERSIONHISTORY)) { |
| // connect versionable node with existing history |
| connectHistory(node.getChildNode(JCR_ROOTVERSION), |
| uuidFromNode(node), versionable); |
| } |
| return node; |
| } |
| |
| void removeVersion(String versionRelPath) throws CommitFailedException { |
| String historyRelPath = PathUtils.getAncestorPath(versionRelPath, 1); |
| String versionName = Text.getName(versionRelPath); |
| |
| NodeBuilder vh = resolve(versionStorageNode, historyRelPath); |
| |
| if (JCR_ROOTVERSION.equals(versionName)) { |
| String msg = "Removal of root version not allowed."; |
| throw new CommitFailedException(CommitFailedException.VERSION, VersionExceptionCode.ROOT_VERSION_REMOVAL.ordinal(), msg); |
| } |
| |
| NodeBuilder versionNode = vh.getChildNode(versionName); |
| String versionId = versionNode.getProperty(JCR_UUID).getValue(Type.STRING); |
| // unregister from labels |
| for (String label : getVersionLabels(historyRelPath, versionId)) { |
| removeVersionLabel(historyRelPath, label); |
| } |
| // reconnected predecessors and successors of the version being removed |
| PropertyState successorIds = versionNode.getProperty(JCR_SUCCESSORS); |
| PropertyState predecessorIds = versionNode.getProperty(JCR_PREDECESSORS); |
| |
| for (String succId : successorIds.getValue(Type.REFERENCES)) { |
| NodeBuilder successor = getVersionById(vh, succId); |
| if (successor == null) { |
| LOG.info("removeVersion : successor not found with uuid: {}, historyRelPath: {}, versionNode: {}, versionHistory: {}", |
| succId, historyRelPath, asLoggableString(versionNode), asLoggableString(vh)); |
| continue; |
| } |
| PropertyBuilder<String> pb = PropertyBuilder.array(Type.REFERENCE); |
| pb.setName(JCR_PREDECESSORS); |
| PropertyState successorsPredecessors = successor.getProperty(JCR_PREDECESSORS); |
| if (successorsPredecessors == null) { |
| LOG.info("removeVersion : successor has no jcr:predecessors property, uuid: {}, historyRelPath: {}, versionNode: {}, successor: {}, versionHistory: {}", |
| succId, historyRelPath, asLoggableString(versionNode), asLoggableString(successor), asLoggableString(vh)); |
| } else { |
| pb.setValues(successorsPredecessors.getValue(Type.REFERENCES)); |
| } |
| |
| pb.removeValue(versionId); |
| pb.addValues(predecessorIds.getValue(Type.REFERENCES)); |
| |
| successor.setProperty(pb.getPropertyState()); |
| } |
| |
| for (String predId : predecessorIds.getValue(Type.REFERENCES)) { |
| NodeBuilder predecessor = getVersionById(vh, predId); |
| if (predecessor == null) { |
| LOG.info("removeVersion : predecessor not found with uuid: {}, historyRelPath: {}, versionNode: {}, versionHistory: {}", |
| predId, historyRelPath, asLoggableString(versionNode), asLoggableString(vh)); |
| continue; |
| } |
| PropertyBuilder<String> pb = PropertyBuilder.array(Type.REFERENCE); |
| pb.setName(JCR_SUCCESSORS); |
| PropertyState predecessorsSuccessors = predecessor.getProperty(JCR_SUCCESSORS); |
| if (predecessorsSuccessors == null) { |
| LOG.info("removeVersion : predecessor has no jcr:successors property, uuid: {}, historyRelPath: {}, versionNode: {}, predecessor: {}, versionHistory: {}", |
| predId, historyRelPath, asLoggableString(versionNode), asLoggableString(predecessor), asLoggableString(vh)); |
| } else { |
| pb.setValues(predecessorsSuccessors.getValue(Type.REFERENCES)); |
| } |
| |
| pb.removeValue(versionId); |
| pb.addValues(successorIds.getValue(Type.REFERENCES)); |
| |
| predecessor.setProperty(pb.getPropertyState()); |
| } |
| versionNode.remove(); |
| } |
| |
| /** small helper to log a node - useful for later debugging **/ |
| private static String asLoggableString(NodeBuilder nb) { |
| try { |
| final StringBuilder sb = new StringBuilder(); |
| boolean empty = true; |
| for (PropertyState p : nb.getProperties()) { |
| if (empty) { |
| empty = false; |
| } else { |
| sb.append(", "); |
| } |
| sb.append(p); |
| } |
| return "{ " + sb + " }"; |
| } catch (Exception e) { |
| return "{ exception: " + e + ", message: " + e.getMessage() + " }"; |
| } |
| } |
| |
| public void checkout(NodeBuilder versionable) { |
| versionable.setProperty(JCR_ISCHECKEDOUT, true, Type.BOOLEAN); |
| PropertyState baseVersion = versionable.getProperty(JCR_BASEVERSION); |
| List<String> predecessors = Collections.singletonList(baseVersion.getValue(Type.REFERENCE)); |
| versionable.setProperty(JCR_PREDECESSORS, predecessors, Type.REFERENCES); |
| } |
| |
| public void checkin(@NotNull NodeBuilder versionable) |
| throws CommitFailedException { |
| NodeBuilder history = getOrCreateVersionHistory(versionable, |
| Collections.<String, Object>emptyMap()); |
| createVersion(history, versionable); |
| } |
| |
| public void restore(@NotNull NodeBuilder versionable, |
| @NotNull String versionUUID, |
| @Nullable VersionSelector selector) |
| throws CommitFailedException { |
| String versionPath = getIdentifierManager().getPath(versionUUID); |
| NodeBuilder history = getOrCreateVersionHistory(versionable, |
| Collections.<String, Object>emptyMap()); |
| NodeBuilder version = null; |
| if (versionPath != null) { |
| String versionName = PathUtils.getName(versionPath); |
| if (history.hasChildNode(versionName)) { |
| version = history.getChildNode(versionName); |
| } |
| } |
| if (version == null) { |
| throw new CommitFailedException(CommitFailedException.VERSION, |
| VersionExceptionCode.NO_SUCH_VERSION.ordinal(), |
| "The VersionHistory with UUID: " + uuidFromNode(versionable) + |
| " does not have a Version with UUID: " + versionUUID); |
| } |
| VersionableState versionableState = VersionableState.forRestore( |
| version, history, versionable, this, ntMgr); |
| versionableState.restore(selector); |
| } |
| |
| /** |
| * Restores a version from the history identified by {@code historyIdentifier} |
| * using the given version {@code selector}. |
| * |
| * @param historyIdentifier identifier of the version history node. |
| * @param selector the version selector. |
| * @param versionable the versionable node where the version is restored to. |
| * @throws CommitFailedException if an error occurs while restoring. |
| */ |
| void restore(@NotNull String historyIdentifier, |
| @NotNull VersionSelector selector, |
| @NotNull NodeBuilder versionable) |
| throws CommitFailedException, RepositoryException { |
| String historyPath = getIdentifierManager().getPath(historyIdentifier); |
| String historyRelPath = PathUtils.relativize(VERSION_STORE_PATH, historyPath); |
| NodeBuilder history = resolve(versionStorageNode, historyRelPath); |
| checkState(history.exists(), "Version history does not exist: " + historyPath); |
| NodeBuilder version = selector.select(history); |
| if (version == null) { |
| throw new CommitFailedException(CommitFailedException.VERSION, |
| VersionExceptionCode.NO_VERSION_TO_RESTORE.ordinal(), |
| "VersionSelector did not select any version from " + |
| "history: " + historyPath); |
| } |
| // make sure versionable nodes has a jcr:uuid |
| // (required to identify its version history) |
| String versionableUUUID = history.getProperty( |
| JCR_VERSIONABLEUUID).getValue(Type.STRING); |
| versionable.setProperty(JCR_UUID, versionableUUUID, Type.STRING); |
| restore(versionable, uuidFromNode(version), selector); |
| } |
| |
| /** |
| * Removes a version label from the jcr:versionLabels node of the referenced |
| * version history. |
| * |
| * @param historyRelPath relative path from the jcr:versionStorage node to |
| * the version history node. |
| * @param label the version label. |
| * @throws CommitFailedException if there is no such version history or if |
| * there is no label with the given name. |
| */ |
| public void removeVersionLabel(@NotNull String historyRelPath, |
| @NotNull String label) |
| throws CommitFailedException { |
| NodeBuilder labels = getVersionLabelsFor(checkNotNull(historyRelPath)); |
| if (!labels.hasProperty(checkNotNull(label))) { |
| throw new CommitFailedException(CommitFailedException.VERSION, |
| VersionExceptionCode.NO_SUCH_VERSION_LABEL.ordinal(), |
| "Version label " + label + " does not exist on this version history"); |
| } |
| labels.removeProperty(label); |
| } |
| |
| /** |
| * Removes the version history if it's empty. |
| * |
| * @param versionable the versionable node. |
| */ |
| void removeEmptyHistory(@NotNull NodeState versionable) { |
| NodeBuilder history = getVersionHistory(versionable); |
| if (isEmptyHistory(history.getNodeState())) { |
| history.remove(); |
| } |
| } |
| |
| // TODO: more methods that modify versions |
| |
| //------------------------------< internal >-------------------------------- |
| |
| /** |
| * Resolves the {@code relPath} based on the given {@code node} |
| * and returns the resulting node, possibly non-existing. |
| * |
| * @param node the resolved node. |
| * @param relPath a relative path. |
| * @return the resolved node. |
| */ |
| @NotNull |
| private NodeBuilder resolve(NodeBuilder node, String relPath) { |
| checkArgument(!PathUtils.isAbsolute(relPath), "Not a relative path"); |
| for (String name : PathUtils.elements(relPath)) { |
| node = node.getChildNode(name); |
| } |
| return node; |
| } |
| |
| /** |
| * Creates the root version in the given version history. |
| * |
| * @param vHistory the version history node. |
| * @param versionable the versionable node. |
| */ |
| private void createRootVersion(@NotNull NodeBuilder vHistory, |
| @NotNull NodeBuilder versionable) { |
| String versionUUID = UUIDUtils.generateUUID(); |
| NodeBuilder version = vHistory.child(JCR_ROOTVERSION); |
| version.setProperty(JCR_UUID, versionUUID, Type.STRING); |
| version.setProperty(JCR_PRIMARYTYPE, NT_VERSION, Type.NAME); |
| version.setProperty(JCR_CREATED, ISO8601.format(Calendar.getInstance()), Type.DATE); |
| version.setProperty(JCR_PREDECESSORS, Collections.emptyList(), Type.REFERENCES); |
| version.setProperty(JCR_SUCCESSORS, Collections.emptyList(), Type.REFERENCES); |
| |
| // incomplete frozen node on root-version (don't call create on versionable-state) |
| VersionableState.fromVersion(version, vHistory, versionable, this, getNodeTypeManager()); |
| |
| // set jcr:isCheckedOut, jcr:versionHistory, jcr:baseVersion and |
| // jcr:predecessors on versionable node |
| versionable.setProperty(JCR_ISCHECKEDOUT, true, Type.BOOLEAN); |
| versionable.setProperty(JCR_VERSIONHISTORY, uuidFromNode(vHistory), Type.REFERENCE); |
| versionable.setProperty(JCR_BASEVERSION, versionUUID, Type.REFERENCE); |
| // set predecessors to base version for the root version |
| versionable.setProperty(JCR_PREDECESSORS, Collections.singletonList(versionUUID), Type.REFERENCES); |
| } |
| |
| /** |
| * Creates a version in the given version history. If the given version |
| * history does not yet have a version, then a root version is created and |
| * the versionable node is in a checked out state. Otherwise a version is |
| * created and the versionable node is set to checked in. |
| * |
| * @param vHistory the version history node. |
| * @param versionable the versionable node. |
| * @throws CommitFailedException if creating the version fails. E.g. because |
| * the versionable node contains a OPV item with ABORT. |
| */ |
| private void createVersion(@NotNull NodeBuilder vHistory, |
| @NotNull NodeBuilder versionable) |
| throws IllegalArgumentException, CommitFailedException { |
| if (!vHistory.hasChildNode(JCR_ROOTVERSION)) { |
| createRootVersion(vHistory, versionable); |
| return; |
| } |
| |
| checkState(versionable.hasProperty(JCR_PREDECESSORS)); |
| PropertyState state = versionable.getProperty(JCR_PREDECESSORS); |
| List<String> predecessors = ImmutableList.copyOf(state.getValue(Type.REFERENCES)); |
| NodeBuilder version = vHistory.child(calculateVersion(vHistory, versionable)); |
| |
| String versionUUID = UUIDUtils.generateUUID(); |
| version.setProperty(JCR_UUID, versionUUID, Type.STRING); |
| version.setProperty(JCR_PRIMARYTYPE, NT_VERSION, Type.NAME); |
| version.setProperty(JCR_CREATED, ISO8601.format(Calendar.getInstance()), Type.DATE); |
| version.setProperty(JCR_PREDECESSORS, predecessors, Type.REFERENCES); |
| version.setProperty(JCR_SUCCESSORS, Collections.<String>emptyList(), Type.REFERENCES); |
| |
| // update successors of versions identified by predecessors |
| for (String id : predecessors) { |
| String name = PathUtils.getName(getIdentifierManager().getPath(id)); |
| NodeBuilder predecessor = vHistory.getChildNode(name); |
| state = predecessor.getProperty(JCR_SUCCESSORS); |
| if (state == null) { |
| throw new IllegalStateException("Missing " + JCR_SUCCESSORS + |
| " property on " + predecessor); |
| } |
| Set<String> refs = Sets.newHashSet(state.getValue(Type.REFERENCES)); |
| refs.add(versionUUID); |
| predecessor.setProperty(JCR_SUCCESSORS, refs, Type.REFERENCES); |
| } |
| |
| // jcr:frozenNode of created version |
| VersionableState versionableState = VersionableState.fromVersion(version, vHistory, versionable, this, getNodeTypeManager()); |
| versionableState.create(); |
| |
| // set jcr:isCheckedOut, jcr:versionHistory, jcr:baseVersion and |
| // jcr:predecessors on versionable node |
| versionable.setProperty(JCR_ISCHECKEDOUT, false, Type.BOOLEAN); |
| versionable.setProperty(JCR_VERSIONHISTORY, uuidFromNode(vHistory), Type.REFERENCE); |
| versionable.setProperty(JCR_BASEVERSION, versionUUID, Type.REFERENCE); |
| // clear predecessors for check-in |
| versionable.setProperty(JCR_PREDECESSORS, Collections.emptyList(), Type.REFERENCES); |
| } |
| |
| /** |
| * Connects a versionable node with the root version of an existing version |
| * history. |
| * |
| * @param rootVersion the root version of a version history. |
| * @param vHistoryUUID the uuid of the version history node. |
| * @param versionable the versionable node. |
| */ |
| private void connectHistory(@NotNull NodeBuilder rootVersion, |
| @NotNull String vHistoryUUID, |
| @NotNull NodeBuilder versionable) { |
| String rootVersionUUID = uuidFromNode(rootVersion); |
| versionable.setProperty(JCR_ISCHECKEDOUT, true, Type.BOOLEAN); |
| versionable.setProperty(JCR_VERSIONHISTORY, vHistoryUUID, Type.REFERENCE); |
| versionable.setProperty(JCR_BASEVERSION, rootVersionUUID, Type.REFERENCE); |
| versionable.setProperty(JCR_PREDECESSORS, |
| Collections.singleton(rootVersionUUID), Type.REFERENCES); |
| } |
| |
| /** |
| * <i>Copied from Apache Jackrabbit Core</i> |
| * <p> |
| * Calculates the name of the new version that will be created by a |
| * checkin call. The name is determined as follows: |
| * <ul> |
| * <li> first the predecessor version with the shortest name is searched. |
| * <li> if that predecessor version is the root version, the new version gets |
| * the name "{number of successors}+1" + ".0" |
| * <li> if that predecessor version has no successor, the last digit of it's |
| * version number is incremented. |
| * <li> if that predecessor version has successors but the incremented name |
| * does not exist, that name is used. |
| * <li> otherwise a ".0" is added to the name until a non conflicting name |
| * is found. |
| * </ul> |
| * <p> |
| * Example Graph: |
| * <pre> |
| * jcr:rootVersion |
| * | | |
| * 1.0 2.0 |
| * | |
| * 1.1 |
| * | |
| * 1.2 ---\ ------\ |
| * | \ \ |
| * 1.3 1.2.0 1.2.0.0 |
| * | | |
| * 1.4 1.2.1 ----\ |
| * | | \ |
| * 1.5 1.2.2 1.2.1.0 |
| * | | | |
| * 1.6 | 1.2.1.1 |
| * |-----/ |
| * 1.7 |
| * </pre> |
| * |
| * @param history the version history |
| * @param versionable the node to checkin |
| * @return the new version name |
| * @throws IllegalStateException if mandatory version properties are missing. |
| */ |
| protected String calculateVersion(@NotNull NodeBuilder history, |
| @NotNull NodeBuilder versionable) |
| throws IllegalStateException { |
| |
| // 1. search a predecessor, suitable for generating the new name |
| PropertyState predecessors = versionable.getProperty(JCR_PREDECESSORS); |
| |
| if (predecessors == null || predecessors.count() == 0) { |
| String message; |
| if (predecessors == null) { |
| message = "Mandatory jcr:predecessors property missing on node " + uuidFromNode(versionable); |
| } else { |
| message = "Mandatory jcr:predecessors property is empty on node " + uuidFromNode(versionable); |
| } |
| throw new IllegalStateException(message); |
| } |
| |
| String best = null; |
| for (String id : predecessors.getValue(Type.REFERENCES)) { |
| String name = PathUtils.getName(getIdentifierManager().getPath(id)); |
| if (best == null || name.length() < best.length()) { |
| best = name; |
| } |
| } |
| |
| if (best == null) { |
| String message = "Could not find 'best' predecessor node for " + |
| uuidFromNode(versionable); |
| throw new IllegalStateException(message); |
| } |
| |
| // 2. generate version name (assume no namespaces in version names) |
| String versionName = best; |
| int pos = versionName.lastIndexOf('.'); |
| if (pos > 0) { |
| String newVersionName = versionName.substring(0, pos + 1) |
| + (Integer.parseInt(versionName.substring(pos + 1)) + 1); |
| while (history.hasChildNode(newVersionName)) { |
| versionName += ".0"; |
| newVersionName = versionName; |
| } |
| return newVersionName; |
| } else { |
| // best is root version |
| checkState(history.hasChildNode(JCR_ROOTVERSION)); |
| NodeBuilder v = history.getChildNode(JCR_ROOTVERSION); |
| return String.valueOf(v.getProperty(JCR_SUCCESSORS).count() + 1) + ".0"; |
| } |
| } |
| |
| /** |
| * Returns the jcr:versionLabels node of the version history referenced |
| * by the given path. |
| * |
| * @param historyRelPath relative path from the jcr:versionStorage node |
| * to the history node. |
| * @return the jcr:versionLabels node. |
| * @throws CommitFailedException if there is no version history at the |
| * given path. |
| */ |
| private NodeBuilder getVersionLabelsFor(String historyRelPath) |
| throws CommitFailedException { |
| NodeBuilder history = resolve(versionStorageNode, historyRelPath); |
| if (!history.exists()) { |
| throw new CommitFailedException(CommitFailedException.VERSION, |
| VersionExceptionCode.UNEXPECTED_REPOSITORY_EXCEPTION.ordinal(), |
| "Version history does not exist: " + PathUtils.concat( |
| VERSION_STORE_PATH, historyRelPath)); |
| } |
| return history.child(JCR_VERSIONLABELS); |
| } |
| |
| @NotNull |
| private Iterable<String> getVersionLabels(@NotNull String historyRelPath, @NotNull String versionId) throws CommitFailedException { |
| List<String> labels = new ArrayList<String>(); |
| NodeBuilder labelNode = getVersionLabelsFor(historyRelPath); |
| for (PropertyState ps : labelNode.getProperties()) { |
| if (Type.REFERENCE == ps.getType()) { |
| if (versionId.equals(ps.getValue(Type.REFERENCE))) { |
| labels.add(ps.getName()); |
| } |
| } |
| } |
| return labels; |
| } |
| |
| @Nullable |
| private NodeBuilder getVersionById(@NotNull NodeBuilder vhBuilder, @NotNull String versionId) { |
| for (String childName : vhBuilder.getChildNodeNames()) { |
| NodeBuilder nb = vhBuilder.getChildNode(childName); |
| PropertyState uuid = nb.getProperty(JCR_UUID); |
| if (uuid != null && versionId.equals(uuid.getValue(Type.STRING))) { |
| return nb; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the version history for the given |
| * {@code versionable} node. |
| * |
| * @param versionable the versionable node. |
| * @return the version history node. |
| * @throws IllegalArgumentException if the given node does not have a |
| * {@code jcr:uuid} property. |
| */ |
| @NotNull |
| private NodeBuilder getVersionHistory(@NotNull NodeState versionable) { |
| checkNotNull(versionable); |
| String vUUID = uuidFromNode(versionable); |
| String relPath = getVersionHistoryPath(vUUID); |
| NodeBuilder node = versionStorageNode; |
| for (Iterator<String> it = PathUtils.elements(relPath).iterator(); it.hasNext(); ) { |
| String name = it.next(); |
| node = node.getChildNode(name); |
| if (!node.exists()) { |
| throw new IllegalArgumentException("No version history for this node"); |
| } |
| } |
| return node; |
| } |
| |
| /** |
| * Checks whether the passed node history hasn't been modified since its |
| * creation. It means that: (1) there's just one version, called jcr:rootVersion |
| * and (2) there are no custom labels. |
| * |
| * @param versionHistory to test |
| * @return {@code true} if the version history hasn't been changed yet |
| */ |
| private boolean isEmptyHistory(NodeState versionHistory) { |
| for (ChildNodeEntry entry : versionHistory.getChildNodeEntries()) { |
| String name = entry.getName(); |
| NodeState node = entry.getNodeState(); |
| if (!JCR_ROOTVERSION.equals(name) && isVersion.test(node)) { |
| return false; // a checked-in version |
| } |
| } |
| NodeState labels = versionHistory.getChildNode(JCR_VERSIONLABELS); |
| for (PropertyState prop : labels.getProperties()) { |
| if (prop.getType() == Type.REFERENCE) { |
| return false; // custom label |
| } |
| } |
| return true; |
| } |
| |
| } |