| /* |
| * 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.core.version; |
| |
| import java.util.Calendar; |
| import java.util.Set; |
| import java.util.HashSet; |
| |
| import javax.jcr.RepositoryException; |
| import javax.jcr.UnsupportedRepositoryOperationException; |
| import javax.jcr.NamespaceException; |
| import javax.jcr.Node; |
| import javax.jcr.PropertyType; |
| import javax.jcr.ItemNotFoundException; |
| import javax.jcr.version.Version; |
| |
| import org.apache.jackrabbit.core.HierarchyManager; |
| import org.apache.jackrabbit.core.SessionImpl; |
| import org.apache.jackrabbit.core.NodeImpl; |
| import org.apache.jackrabbit.core.value.InternalValue; |
| import org.apache.jackrabbit.core.id.NodeId; |
| import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; |
| import org.apache.jackrabbit.core.state.DefaultISMLocking; |
| import org.apache.jackrabbit.core.state.ISMLocking; |
| import org.apache.jackrabbit.core.state.ItemStateException; |
| import org.apache.jackrabbit.core.state.LocalItemStateManager; |
| import org.apache.jackrabbit.core.state.UpdatableItemStateManager; |
| import org.apache.jackrabbit.core.state.NodeState; |
| import org.apache.jackrabbit.spi.commons.name.NameConstants; |
| import org.apache.jackrabbit.spi.Path; |
| import org.slf4j.LoggerFactory; |
| import org.slf4j.Logger; |
| |
| /** |
| * The JCR Version Manager impementation is split in several classes in order to |
| * group related methods together. |
| * </p> |
| * this class provides basic routines for all operations and the methods related |
| * to checkin and checkout. |
| */ |
| abstract public class VersionManagerImplBase { |
| |
| /** |
| * default logger |
| */ |
| private static final Logger log = LoggerFactory.getLogger(VersionManagerImplBase.class); |
| |
| /** |
| * workspace session |
| */ |
| protected final SessionImpl session; |
| |
| /** |
| * item state manager for all workspace operations |
| */ |
| protected final UpdatableItemStateManager stateMgr; |
| |
| /** |
| * hierarch manager that operates on the localte state manager |
| */ |
| protected final HierarchyManager hierMgr; |
| |
| /** |
| * node type registry |
| */ |
| protected final NodeTypeRegistry ntReg; |
| |
| /** |
| * the session version manager. |
| */ |
| protected final InternalVersionManager vMgr; |
| |
| /** |
| * the lock on this version manager |
| */ |
| private final DefaultISMLocking rwLock = new DefaultISMLocking(); |
| |
| /** |
| * the node id of the current activity |
| */ |
| protected NodeId currentActivity; |
| |
| /** |
| * Creates a new version manager base for the given session |
| * @param session workspace sesion |
| * @param stateMgr the underlying state manager |
| * @param hierMgr local hierarchy manager |
| */ |
| protected VersionManagerImplBase(SessionImpl session, |
| UpdatableItemStateManager stateMgr, |
| HierarchyManager hierMgr) { |
| this.session = session; |
| this.stateMgr = stateMgr; |
| this.hierMgr = hierMgr; |
| this.ntReg = session.getNodeTypeManager().getNodeTypeRegistry(); |
| this.vMgr = session.getInternalVersionManager(); |
| } |
| |
| /** |
| * Performs a checkin or checkout operation. if <code>checkin</code> is |
| * <code>true</code> the node is checked in. if <code>checkout</code> is |
| * <code>true</code> the node is checked out. if both flags are <code>true</code> |
| * the checkin is performed prior to the checkout and the operation is |
| * equivalent to a checkpoint operation. |
| * |
| * @param state node state |
| * @param checkin if <code>true</code> the node is checked in. |
| * @param checkout if <code>true</code> the node is checked out. |
| * @param created create time of the new version (if any), |
| * or <code>null</code> for the current time |
| * @return the node id of the base version or <code>null</code> for a pure |
| * checkout. |
| * @throws RepositoryException if an error occurs |
| */ |
| protected NodeId checkoutCheckin( |
| NodeStateEx state, |
| boolean checkin, boolean checkout, Calendar created) |
| throws RepositoryException { |
| assert(checkin || checkout); |
| |
| // check if versionable |
| boolean isFull = checkVersionable(state); |
| |
| // check flags |
| if (isCheckedOut(state)) { |
| if (checkout && !checkin) { |
| // pure checkout |
| String msg = safeGetJCRPath(state) + ": Node is already checked-out. ignoring."; |
| log.debug(msg); |
| return null; |
| } |
| } else { |
| if (!checkout) { |
| // pure checkin |
| String msg = safeGetJCRPath(state) + ": Node is already checked-in. ignoring."; |
| log.debug(msg); |
| if (isFull) { |
| return getBaseVersionId(state); |
| } else { |
| // get base version from version history |
| return vMgr.getHeadVersionOfNode(state.getNodeId()).getId(); |
| } |
| } |
| checkin = false; |
| } |
| |
| NodeId baseId = isFull && checkout |
| ? vMgr.canCheckout(state, currentActivity) |
| : null; |
| |
| // perform operation |
| WriteOperation ops = startWriteOperation(); |
| try { |
| // the 2 cases could be consolidated but is clearer this way |
| if (checkin) { |
| // check for configuration |
| if (state.getEffectiveNodeType().includesNodeType(NameConstants.NT_CONFIGURATION)) { |
| // collect the base versions and the the rep:versions property of the configuration |
| Set<NodeId> baseVersions = collectBaseVersions(state); |
| InternalValue[] vs = new InternalValue[baseVersions.size()]; |
| int i=0; |
| for (NodeId id: baseVersions) { |
| vs[i++] = InternalValue.create(id); |
| } |
| state.setPropertyValues(NameConstants.REP_VERSIONS, PropertyType.REFERENCE, vs); |
| state.store(); |
| } |
| InternalVersion v = vMgr.checkin(session, state, created); |
| baseId = v.getId(); |
| if (isFull) { |
| state.setPropertyValue( |
| NameConstants.JCR_BASEVERSION, |
| InternalValue.create(baseId)); |
| state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); |
| state.removeProperty(NameConstants.JCR_ACTIVITY); |
| } |
| } |
| if (checkout) { |
| if (isFull) { |
| state.setPropertyValues( |
| NameConstants.JCR_PREDECESSORS, |
| PropertyType.REFERENCE, |
| new InternalValue[]{InternalValue.create(baseId)} |
| ); |
| if (currentActivity != null) { |
| state.setPropertyValue( |
| NameConstants.JCR_ACTIVITY, |
| InternalValue.create(currentActivity) |
| ); |
| } |
| } |
| } |
| state.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(checkout)); |
| state.store(); |
| ops.save(); |
| return baseId; |
| } catch (ItemStateException e) { |
| throw new RepositoryException(e); |
| } finally { |
| ops.close(); |
| } |
| } |
| |
| /** |
| * Collects the base versions for the workspace configuration referenced by |
| * the given config node. |
| * @param config the config |
| * @return the id of the new base version |
| * @throws RepositoryException if an error occurs |
| */ |
| private Set<NodeId> collectBaseVersions(NodeStateEx config) throws RepositoryException { |
| NodeId rootId = config.getPropertyValue(NameConstants.JCR_ROOT).getNodeId(); |
| NodeStateEx root = getNodeStateEx(rootId); |
| if (root == null) { |
| String msg = "Configuration root node for " + safeGetJCRPath(config) + " not found."; |
| log.error(msg); |
| throw new ItemNotFoundException(msg); |
| } |
| Set<NodeId> baseVersions = new HashSet<NodeId>(); |
| collectBaseVersions(root, baseVersions); |
| return baseVersions; |
| } |
| |
| /** |
| * Recursivly collects all base versions of this configuration tree. |
| * @param root node to traverse |
| * @param baseVersions set of base versions to fill |
| * @throws RepositoryException if an error occurs |
| */ |
| private void collectBaseVersions(NodeStateEx root, Set<NodeId> baseVersions) |
| throws RepositoryException { |
| if (!baseVersions.isEmpty()) { |
| // base version of configuration root already recorded |
| if (root.hasProperty(NameConstants.JCR_CONFIGURATION) |
| && root.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { |
| // don't traverse into child nodes that have a jcr:configuration |
| // property as they belong to a different configuration. |
| return; |
| } |
| } |
| InternalVersion baseVersion = getBaseVersion(root); |
| if (baseVersion.isRootVersion()) { |
| String msg = "Unable to checkin configuration as it has unversioned child node: " + safeGetJCRPath(root); |
| log.error(msg); |
| throw new UnsupportedRepositoryOperationException(msg); |
| } |
| baseVersions.add(baseVersion.getId()); |
| |
| for (NodeStateEx child: root.getChildNodes()) { |
| collectBaseVersions(child, baseVersions); |
| } |
| } |
| |
| /** |
| * Checks if the underlying node is versionable, i.e. has 'mix:versionable' or a |
| * 'mix:simpleVersionable'. |
| * @param state node state |
| * @return <code>true</code> if this node is full versionable, i.e. is |
| * of nodetype mix:versionable |
| * @throws UnsupportedRepositoryOperationException if this node is not versionable at all |
| */ |
| protected boolean checkVersionable(NodeStateEx state) |
| throws UnsupportedRepositoryOperationException, RepositoryException { |
| if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { |
| return true; |
| } else if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) { |
| return false; |
| } else { |
| String msg = "Unable to perform a versioning operation on a " + |
| "non versionable node: " + safeGetJCRPath(state); |
| log.error(msg); |
| throw new UnsupportedRepositoryOperationException(msg); |
| } |
| } |
| |
| /** |
| * Returns the JCR path for the given node state without throwing an exception. |
| * @param state node state |
| * @return a JCR path string |
| */ |
| protected String safeGetJCRPath(NodeStateEx state) { |
| Path path; |
| try { |
| path = hierMgr.getPath(state.getNodeId()); |
| } catch (RepositoryException e) { |
| log.warn("unable to calculate path for {}", state.getNodeId()); |
| return state.getNodeId().toString(); |
| } |
| try { |
| return session.getJCRPath(path); |
| } catch (NamespaceException e) { |
| log.warn("unable to calculate path for {}", path); |
| return path.toString(); |
| } |
| } |
| |
| /** |
| * Determines the checked-out status of the given node state. |
| * <p/> |
| * A node is considered <i>checked-out</i> if it is versionable and |
| * checked-out, or is non-versionable but its nearest versionable ancestor |
| * is checked-out, or is non-versionable and there are no versionable |
| * ancestors. |
| * |
| * @param state node state |
| * @return a boolean |
| * @see javax.jcr.version.VersionManager#isCheckedOut(String) |
| * @see Node#isCheckedOut() |
| * @throws RepositoryException if an error occurs |
| */ |
| protected boolean isCheckedOut(NodeStateEx state) throws RepositoryException { |
| return state.getPropertyValue(NameConstants.JCR_ISCHECKEDOUT).getBoolean(); |
| } |
| |
| /** |
| * Returns the node id of the base version, retrieved from the node state |
| * @param state node state |
| * @return the node id of the base version or <code>null</code> if not defined |
| */ |
| protected NodeId getBaseVersionId(NodeStateEx state) { |
| InternalValue value = state.getPropertyValue(NameConstants.JCR_BASEVERSION); |
| return value == null ? null : value.getNodeId(); |
| } |
| |
| /** |
| * Returns the internal version history for the underlying node. |
| * @param state node state |
| * @return internal version history |
| * @throws RepositoryException if an error occurs |
| */ |
| protected InternalVersionHistory getVersionHistory(NodeStateEx state) |
| throws RepositoryException { |
| boolean isFull = checkVersionable(state); |
| if (isFull) { |
| NodeId id = state.getPropertyValue(NameConstants.JCR_VERSIONHISTORY).getNodeId(); |
| return vMgr.getVersionHistory(id); |
| } else { |
| return vMgr.getVersionHistoryOfNode(state.getNodeId()); |
| } |
| } |
| |
| /** |
| * helper class that returns the internal version for a JCR one. |
| * @param v the jcr version |
| * @return the internal version |
| * @throws RepositoryException if an error occurs |
| */ |
| protected InternalVersion getVersion(Version v) throws RepositoryException { |
| if (v == null) { |
| return null; |
| } else { |
| return vMgr.getVersion(((VersionImpl) v).getNodeId()); |
| } |
| } |
| |
| /** |
| * Returns the internal base version for the underlying node. |
| * @param state node state |
| * @return internal base version |
| * @throws RepositoryException if an error occurs |
| */ |
| protected InternalVersion getBaseVersion(NodeStateEx state) throws RepositoryException { |
| boolean isFull = checkVersionable(state); |
| if (isFull) { |
| NodeId id = getBaseVersionId(state); |
| return vMgr.getVersion(id); |
| } else { |
| // note, that the method currently only works for linear version |
| // graphs (i.e. simple versioning) |
| return vMgr.getHeadVersionOfNode(state.getNodeId()); |
| } |
| } |
| |
| /** |
| * returns the node state for the given node id |
| * @param nodeId the node id |
| * @throws RepositoryException if an error occurs |
| * @return the node state or null if not found |
| */ |
| protected NodeStateEx getNodeStateEx(NodeId nodeId) throws RepositoryException { |
| if (!stateMgr.hasItemState(nodeId)) { |
| return null; |
| } |
| try { |
| return new NodeStateEx( |
| stateMgr, |
| ntReg, |
| (NodeState) stateMgr.getItemState(nodeId), |
| null); |
| } catch (ItemStateException e) { |
| throw new RepositoryException(e); |
| } |
| } |
| |
| /** |
| * Checks modify and permissions |
| * @param state state to check |
| * @param options options to check |
| * @param permissions permissions to check |
| * @throws RepositoryException if an error occurs |
| */ |
| protected void checkModify(NodeStateEx state, int options, int permissions) |
| throws RepositoryException { |
| NodeImpl node; |
| try { |
| node = session.getNodeById(state.getNodeId()); |
| } catch (RepositoryException e) { |
| // ignore |
| return; |
| } |
| session.getValidator().checkModify(node, options, permissions); |
| } |
| |
| /** |
| * Checks modify and permissions |
| * @param node node to check |
| * @param options options to check |
| * @param permissions permissions to check |
| * @throws RepositoryException if an error occurs |
| */ |
| protected void checkModify(NodeImpl node, int options, int permissions) |
| throws RepositoryException { |
| session.getValidator().checkModify(node, options, permissions); |
| } |
| |
| /** |
| * Helper for managing write operations. |
| */ |
| public class WriteOperation { |
| |
| /** |
| * Flag for successful completion of the write operation. |
| */ |
| private boolean success = false; |
| |
| private final ISMLocking.WriteLock lock; |
| |
| public WriteOperation(ISMLocking.WriteLock lock) { |
| this.lock = lock; |
| } |
| |
| /** |
| * Saves the pending operations in the {@link LocalItemStateManager}. |
| * |
| * @throws ItemStateException if the pending state is invalid |
| * @throws RepositoryException if the pending state could not be persisted |
| */ |
| public void save() throws ItemStateException, RepositoryException { |
| stateMgr.update(); |
| success = true; |
| } |
| |
| /** |
| * Closes the write operation. The pending operations are cancelled |
| * if they could not be properly saved. Finally the write lock is |
| * released. |
| */ |
| public void close() { |
| try { |
| if (!success) { |
| // update operation failed, cancel all modifications |
| stateMgr.cancel(); |
| } |
| } finally { |
| lock.release(); |
| } |
| } |
| } |
| |
| /** |
| * Acquires the write lock on this version manager. |
| * @return returns the write lock |
| */ |
| protected ISMLocking.WriteLock acquireWriteLock() { |
| while (true) { |
| try { |
| return rwLock.acquireWriteLock(null); |
| } catch (InterruptedException e) { |
| // ignore |
| } |
| } |
| } |
| |
| /** |
| * acquires the read lock on this version manager. |
| * @return returns the read lock |
| */ |
| protected ISMLocking.ReadLock acquireReadLock() { |
| while (true) { |
| try { |
| return rwLock.acquireReadLock(null); |
| } catch (InterruptedException e) { |
| // ignore |
| } |
| } |
| } |
| |
| |
| /** |
| * Starts a write operation by acquiring the write lock and setting the |
| * item state manager to the "edit" state. If something goes wrong, the |
| * write lock is released and an exception is thrown. |
| * <p> |
| * The pattern for using this method and the returned helper instance is: |
| * <pre> |
| * WriteOperation operation = startWriteOperation(); |
| * try { |
| * ... |
| * operation.save(); // if everything is OK |
| * ... |
| * } catch (...) { |
| * ... |
| * } finally { |
| * operation.close(); |
| * } |
| * </pre> |
| * |
| * @return write operation helper |
| * @throws RepositoryException if the write operation could not be started |
| */ |
| public WriteOperation startWriteOperation() throws RepositoryException { |
| boolean success = false; |
| ISMLocking.WriteLock lock = acquireWriteLock(); |
| try { |
| stateMgr.edit(); |
| success = true; |
| return new WriteOperation(lock); |
| } catch (IllegalStateException e) { |
| String msg = "Unable to start edit operation."; |
| throw new RepositoryException(msg, e); |
| } finally { |
| if (!success) { |
| lock.release(); |
| } |
| } |
| } |
| |
| } |