| /* |
| * 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.RepositoryException; |
| import javax.jcr.UnsupportedRepositoryOperationException; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| 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.namepath.NamePathMapper; |
| import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; |
| import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager; |
| import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory; |
| import org.apache.jackrabbit.oak.spi.state.NodeState; |
| import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; |
| import org.apache.jackrabbit.oak.spi.version.VersionConstants; |
| 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.checkNotNull; |
| |
| /** |
| * {@code ReadOnlyVersionManager} provides implementations for read-only |
| * version operations modeled after the ones available in {@link javax.jcr.version.VersionManager}. |
| */ |
| public abstract class ReadOnlyVersionManager { |
| |
| private static final Logger log = LoggerFactory.getLogger(ReadOnlyVersionManager.class); |
| |
| /** |
| * @return the read-only {@link Tree} for the jcr:versionStorage node. The |
| * returned {@code Tree} instance must be up-to-date with the |
| * {@code Root} returned by {@link #getWorkspaceRoot()}. |
| */ |
| @NotNull |
| protected abstract Tree getVersionStorage(); |
| |
| /** |
| /** |
| * @return the {@code Root} of the workspace. |
| */ |
| @NotNull |
| protected abstract Root getWorkspaceRoot(); |
| |
| /** |
| * @return the node type manager of this repository. |
| */ |
| @NotNull |
| protected abstract ReadOnlyNodeTypeManager getNodeTypeManager(); |
| |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Return a new instance of {@code ReadOnlyVersionManager} that reads version |
| * information from the tree at {@link VersionConstants#VERSION_STORE_PATH}. |
| * |
| * @param root The root to read version information from. |
| * @param namePathMapper The {@code NamePathMapper} to use. |
| * @return a new instance of {@code ReadOnlyVersionManager}. |
| */ |
| @NotNull |
| public static ReadOnlyVersionManager getInstance(final Root root, |
| final NamePathMapper namePathMapper) { |
| return new ReadOnlyVersionManager() { |
| @NotNull |
| @Override |
| protected Tree getVersionStorage() { |
| return root.getTree(VersionConstants.VERSION_STORE_PATH); |
| } |
| |
| @NotNull |
| @Override |
| protected Root getWorkspaceRoot() { |
| return root; |
| } |
| |
| @NotNull |
| @Override |
| protected ReadOnlyNodeTypeManager getNodeTypeManager() { |
| return ReadOnlyNodeTypeManager.getInstance(root, namePathMapper); |
| } |
| }; |
| } |
| |
| /** |
| * Returns {@code true} if the tree is checked out; otherwise |
| * {@code false}. The root node is always considered checked out. |
| * |
| * @param tree the tree to check. |
| * @return whether the tree is checked out or not. |
| */ |
| public boolean isCheckedOut(@NotNull Tree tree) { |
| // NOTE: if the given tree does not exist it mean that it is a non-accessible ancestor |
| // in this case it may result in wrong isCheckedOut value. This should never |
| // be the case in a commit hook because it operates on non-access- |
| // controlled NodeStates. This means consistency is not at risk |
| // but it may mean oak-jcr sees a node as checked out even though |
| // it is in fact read-only because of a checked-in (but non-accessible) ancestor. |
| // if this turns out to be an issue see NodeImpl#getReadOnlyTree for an potential fix |
| PropertyState p = tree.getProperty(VersionConstants.JCR_ISCHECKEDOUT); |
| if (p != null) { |
| return p.getValue(Type.BOOLEAN); |
| } |
| if (tree.isRoot()) { |
| return true; |
| } else { |
| // otherwise return checkedOut status of parent |
| return isCheckedOut(tree.getParent()); |
| } |
| } |
| |
| /** |
| * Returns the tree representing the version history of the given |
| * versionable tree or {@code null} if none exists yet. |
| * |
| * @param versionable the versionable tree. |
| * @return the version history or {@code null} if none exists yet. |
| * @throws UnsupportedRepositoryOperationException |
| * if the versionable tree is not actually |
| * versionable. |
| * @throws RepositoryException if an error occurs while checking the node |
| * type of the tree. |
| */ |
| @Nullable |
| public Tree getVersionHistory(@NotNull Tree versionable) |
| throws UnsupportedRepositoryOperationException, |
| RepositoryException { |
| checkVersionable(versionable); |
| String uuid = versionable.getProperty(VersionConstants.JCR_UUID).getValue(Type.STRING); |
| return TreeUtil.getTree(getVersionStorage(), getVersionHistoryPath(uuid)); |
| } |
| |
| /** |
| * Returns the version tree with the given uuid. |
| * |
| * @param uuid the uuid of the version tree. |
| * @return the version tree or {@code null} if there is none. |
| */ |
| @Nullable |
| public Tree getVersion(@NotNull String uuid) { |
| return getIdentifierManager().getTree(uuid); |
| } |
| |
| /** |
| * Returns the path of the version history for the given {@code uuid}. |
| * The returned path is relative to the version storage tree as returned |
| * by {@link #getVersionStorage()}. |
| * |
| * @param uuid the uuid of the versionable node |
| * @return the relative path of the version history for the given uuid. |
| */ |
| @NotNull |
| public String getVersionHistoryPath(@NotNull String uuid) { |
| String relPath = ""; |
| for (int i = 0; i < 3; i++) { |
| String name = uuid.substring(i * 2, i * 2 + 2); |
| relPath = PathUtils.concat(relPath, name); |
| } |
| return PathUtils.concat(relPath, uuid); |
| } |
| |
| /** |
| * Returns the tree representing the base version of the given versionable |
| * tree or {@code null} if none exists yet. This is the case when a |
| * versionable node is created, but is not yet saved. |
| * |
| * @param versionable the versionable tree. |
| * @return the tree representing the base version or {@code null}. |
| * @throws UnsupportedRepositoryOperationException |
| * if the versionable tree is not actually |
| * versionable. |
| * @throws RepositoryException if an error occurs while checking the node |
| * type of the tree. |
| */ |
| @Nullable |
| public Tree getBaseVersion(@NotNull Tree versionable) |
| throws UnsupportedRepositoryOperationException, |
| RepositoryException { |
| checkVersionable(versionable); |
| PropertyState p = versionable.getProperty(VersionConstants.JCR_BASEVERSION); |
| if (p == null) { |
| // version history does not yet exist |
| return null; |
| } |
| return getIdentifierManager().getTree(p.getValue(Type.STRING)); |
| } |
| |
| /** |
| * Returns {@code true} if the specified tree has {@link VersionConstants#REP_VERSIONSTORAGE} |
| * defines as primary node type i.e. is part of the intermediate version storage |
| * structure that contains the version histories and the versions. |
| * |
| * @param tree The tree to be tested. |
| * @return {@code true} if the target node has {@link VersionConstants#REP_VERSIONSTORAGE} |
| * defines as primary node type; {@code false} otherwise. |
| */ |
| public static boolean isVersionStoreTree(@NotNull Tree tree) { |
| return VersionConstants.REP_VERSIONSTORAGE.equals(TreeUtil.getPrimaryTypeName(tree)); |
| } |
| |
| /** |
| * Tries to retrieve the tree corresponding to specified {@code versionTree} |
| * outside of the version storage based on versionable path information |
| * stored with the version history. The following cases are distinguished: |
| * |
| * <ul> |
| * <li>Version History: If the given tree is a version history the |
| * associated versionable tree in the specified workspace is being returned |
| * based on the information stored in the versionable path property. If |
| * no versionable path property is present {@code null} is returned.</li> |
| * <li>Version: Same as for version history.</li> |
| * <li>Version Labels: Same as for version history.</li> |
| * <li>Frozen Node: If the given tree forms part of a frozen node the |
| * path of the target node is computed from the versionable path and |
| * the relative path of the frozen node.</li> |
| * <li>Other Nodes: If the specified tree is not part of the tree structure |
| * defined by a version history, {@code null} will be returned.</li> |
| * </ul> |
| * |
| * Please note that this method will not verify if the tree at the versionable |
| * path or the computed subtree actually exists. This must be asserted by |
| * the caller before operating on the tree. |
| * |
| * @param versionTree The tree from within the version storage for which |
| * that versionable correspondent should be retrieved. |
| * @param workspaceName The name of the workspace for which the target should be retrieved. |
| * @return A existing or non-existing tree pointing to the location of the |
| * correspondent tree outside of the version storage or {@code null} if the |
| * versionable path property for the specified workspace is missing or if |
| * the given tree is not located within the tree structure defined by a version history. |
| * |
| * @see VersionConstants#MIX_REP_VERSIONABLE_PATHS |
| */ |
| @Nullable |
| public Tree getVersionable(@NotNull Tree versionTree, @NotNull String workspaceName) { |
| Root root = getWorkspaceRoot(); |
| String relPath = ""; |
| Tree t = versionTree; |
| while (t.exists() && !isVersionStoreTree(t) && !t.isRoot()) { |
| String ntName = TreeUtil.getPrimaryTypeName(t); |
| if (VersionConstants.NT_FROZENNODE.equals(ntName)) { |
| relPath = PathUtils.relativize(t.getPath(), versionTree.getPath()); |
| } else if (JcrConstants.NT_VERSIONHISTORY.equals(ntName)) { |
| PropertyState prop = t.getProperty(workspaceName); |
| if (prop != null) { |
| return root.getTree(PathUtils.concat(prop.getValue(Type.PATH), relPath)); |
| } else { |
| // version history is missing the versionable path property for the given workspace name |
| log.warn("Missing versionable path property for {} at {}", workspaceName, t.getPath()); |
| break; |
| } |
| } |
| t = t.getParent(); |
| } |
| |
| // intermediate node in the version storage that matches none of the special |
| // conditions checked above and cannot be resolve to a versionable tree. |
| return null; |
| } |
| |
| //----------------------------< internal >---------------------------------- |
| /** |
| * @return an identifier manager that is able to resolve identifiers of |
| * nodes in the version storage. |
| */ |
| protected IdentifierManager getIdentifierManager() { |
| // FIXME: may need to revise this, because getVersionStorageTree() |
| // is not the same Root as getWorkspaceRoot() |
| return new IdentifierManager(getWorkspaceRoot()); |
| } |
| |
| /** |
| * Checks if the given {@code tree} is versionable and throws a {@link |
| * UnsupportedRepositoryOperationException} if it is not. |
| * |
| * @param tree the tree to check. |
| * @return the passed tree. |
| * @throws UnsupportedRepositoryOperationException |
| * if the tree is not versionable. |
| * @throws RepositoryException if an error occurs while checking the node |
| * type of the tree. |
| */ |
| @NotNull |
| protected Tree checkVersionable(@NotNull Tree tree) |
| throws UnsupportedRepositoryOperationException, |
| RepositoryException { |
| if (!isVersionable(checkNotNull(tree))) { |
| throw new UnsupportedRepositoryOperationException("Node at " + |
| tree.getPath() + " is not versionable"); |
| } |
| return tree; |
| } |
| |
| /** |
| * Returns {@code true} if the given {@code tree} is of type |
| * {@code mix:versionable}; {@code false} otherwise. |
| * |
| * @param tree the tree to check. |
| * @return whether the {@code tree} is versionable. |
| */ |
| protected boolean isVersionable(@NotNull Tree tree) { |
| return getNodeTypeManager().isNodeType( |
| checkNotNull(tree), VersionConstants.MIX_VERSIONABLE); |
| } |
| |
| /** |
| * Returns {@code true} if the given {@code versionableCandidate} is of type |
| * {@code mix:versionable}; {@code false} otherwise. |
| * |
| * @param versionableCandidate node state to check. |
| * @return whether the {@code versionableCandidate} is versionable. |
| */ |
| boolean isVersionable(NodeState versionableCandidate) { |
| // this is not 100% correct, because t.getPath() will |
| // not return the correct path for node after, but is |
| // sufficient to check if it is versionable |
| return isVersionable(TreeFactory.createReadOnlyTree(versionableCandidate)); |
| } |
| } |