/*
* 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.session.SessionContext;
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 implementation 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);
/**
* Component context of the current session
*/
protected final SessionContext context;
/**
* workspace session
*/
protected final SessionImpl session;
/**
* item state manager for all workspace operations
*/
protected final UpdatableItemStateManager stateMgr;
/**
* hierarchy manager that operates on the locale 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 VersioningLock rwLock = new VersioningLock();
/**
* the node id of the current activity
*/
protected NodeId currentActivity;
/**
* Creates a new version manager base for the given session
*
* @param context component context of the current session
* @param stateMgr the underlying state manager
* @param hierMgr local hierarchy manager
*/
protected VersionManagerImplBase(
SessionContext context, UpdatableItemStateManager stateMgr,
HierarchyManager hierMgr) {
this.context = context;
this.session = context.getSessionImpl();
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;
}
/**
* Recursively 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.debug(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.debug(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;
}
context.getItemValidator().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 {
context.getItemValidator().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 VersioningLock.WriteLock lock;
public WriteOperation(VersioningLock.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 VersioningLock.WriteLock acquireWriteLock() {
while (true) {
try {
return rwLock.acquireWriteLock();
} catch (InterruptedException e) {
// ignore
}
}
}
/**
* acquires the read lock on this version manager.
* @return returns the read lock
*/
protected VersioningLock.ReadLock acquireReadLock() {
while (true) {
try {
return rwLock.acquireReadLock();
} 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;
VersioningLock.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();
}
}
}
}