/*
* 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;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES_ON_NODE;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionManager;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.session.SessionOperation;
import org.apache.jackrabbit.core.session.SessionWriteOperation;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.UpdatableItemStateManager;
import org.apache.jackrabbit.core.version.InconsistentVersioningState;
import org.apache.jackrabbit.core.version.InternalActivity;
import org.apache.jackrabbit.core.version.InternalBaseline;
import org.apache.jackrabbit.core.version.InternalVersion;
import org.apache.jackrabbit.core.version.InternalVersionHistory;
import org.apache.jackrabbit.core.version.NodeStateEx;
import org.apache.jackrabbit.core.version.VersionImpl;
import org.apache.jackrabbit.core.version.VersionManagerImplConfig;
import org.apache.jackrabbit.core.version.VersionSet;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of the {@link javax.jcr.version.VersionManager}.
* <p/>
* This class implements the JCR Version Manager interface but most of the
* operations are performed in the super classes. this is only cosmetic to
* avoid huge source files.
*/
public class VersionManagerImpl extends VersionManagerImplConfig
implements VersionManager {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(VersionManagerImpl.class);
/**
* Creates a new version manager
*
* @param context component context of the current session
* @param stateMgr the underlying state manager
* @param hierMgr local hierarchy manager
*/
public VersionManagerImpl(
SessionContext context, UpdatableItemStateManager stateMgr,
HierarchyManager hierMgr) {
super(context, stateMgr, hierMgr);
}
private <T> T perform(SessionOperation<T> operation)
throws RepositoryException {
return context.getSessionState().perform(operation);
}
/** Wrapper around {@link #checkin(String, Calendar)}. */
public Version checkin(String absPath) throws RepositoryException {
return checkin(absPath, null);
}
/**
* Creates a new version of the node at the given path.
*
* @param absPath node path
* @param created create time of the new version,
* or <code>null</code> for the current time
* @return new version
* @throws RepositoryException if the version can not be created
*/
public Version checkin(final String absPath, final Calendar created)
throws RepositoryException {
return perform(new SessionWriteOperation<Version> () {
public Version perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE,
Permission.VERSION_MNGMT);
NodeId baseId = checkoutCheckin(state, true, false, created);
return (Version) session.getNodeById(baseId);
}
public String toString() {
return "versionManager.checkin(" + absPath + ", " + created + ")";
}
});
}
/**
* {@inheritDoc}
*/
public void checkout(final String absPath) throws RepositoryException {
perform(new SessionWriteOperation<NodeId> () {
public NodeId perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_LOCK | CHECK_HOLD,
Permission.VERSION_MNGMT);
return checkoutCheckin(state, false, true, null);
}
public String toString() {
return "versionManager.checkout(" + absPath + ")";
}
});
}
/**
* {@inheritDoc}
*/
public Version checkpoint(final String absPath) throws RepositoryException {
return perform(new SessionWriteOperation<Version> () {
public Version perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE,
Permission.VERSION_MNGMT);
NodeId baseId = checkoutCheckin(state, true, true, null);
return (Version) session.getNodeById(baseId);
}
public String toString() {
return "versionManager.checkpoint(" + absPath + ")";
}
});
}
/** Wrapper around {@link Node#isCheckedOut()}. */
public boolean isCheckedOut(String absPath) throws RepositoryException {
return session.getNode(absPath).isCheckedOut();
}
/**
* {@inheritDoc}
*/
public VersionHistory getVersionHistory(final String absPath)
throws RepositoryException {
return perform(new SessionOperation<VersionHistory> () {
public VersionHistory perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(absPath);
InternalVersionHistory vh = getVersionHistory(state);
if (vh == null) {
throw new InconsistentVersioningState("Couldn't get version history for node " + state.getNodeId());
}
return (VersionHistory) session.getNodeById(vh.getId());
}
public String toString() {
return "versionManager.getVersionHistory(" + absPath + ")";
}
});
}
/**
* {@inheritDoc}
*/
public Version getBaseVersion(final String absPath)
throws RepositoryException {
return perform(new SessionOperation<Version> () {
public Version perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(absPath);
InternalVersion v = getBaseVersion(state);
return (Version) session.getNodeById(v.getId());
}
public String toString() {
return "versionManager.getBaseVersion(" + absPath + ")";
}
});
}
/** Wrapper around {@link #restore(Version[], boolean)}. */
public void restore(Version version, boolean removeExisting)
throws RepositoryException {
restore(new Version[]{version}, removeExisting);
}
/**
* {@inheritDoc}
*/
public void restore(final Version[] versions, final boolean removeExisting)
throws RepositoryException {
perform(new SessionWriteOperation<Object> () {
public Object perform(SessionContext context)
throws RepositoryException {
// check for pending changes
if (session.hasPendingChanges()) {
throw new InvalidItemStateException(
"Unable to restore version. Session has pending changes.");
}
// add all versions to map of versions to restore
Map<NodeId, InternalVersion> toRestore =
new HashMap<NodeId, InternalVersion>();
for (Version version : versions) {
InternalVersion v =
vMgr.getVersion(((VersionImpl) version).getNodeId());
// check for collision
NodeId historyId = v.getVersionHistory().getId();
if (toRestore.containsKey(historyId)) {
throw new VersionException(
"Unable to restore. Two or more versions have same version history.");
}
toRestore.put(historyId, v);
}
WriteOperation ops = startWriteOperation();
try {
internalRestore(
new VersionSet(toRestore, true), removeExisting);
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
return this;
}
public String toString() {
return "versionManager.restore(versions, " + removeExisting + ")";
}
});
}
/**
* {@inheritDoc}
*/
public void restore(
final String absPath, final String versionName,
final boolean removeExisting) throws RepositoryException {
perform(new SessionWriteOperation<Object> () {
public Object perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
Permission.NONE);
restore(state, context.getQName(versionName), removeExisting);
return this;
}
public String toString() {
return "versionManager.restore("
+ absPath + ", " + versionName + ", "
+ removeExisting + ")";
}
});
}
/**
* {@inheritDoc}
*/
public void restore(
final String absPath, final Version version, final boolean removeExisting)
throws RepositoryException {
perform(new SessionWriteOperation<Object> () {
public Object perform(SessionContext context)
throws RepositoryException {
// first check if node exists
if (session.nodeExists(absPath)) {
throw new VersionException(
"VersionManager.restore(String, Version, boolean)"
+ " not allowed on existing nodes; use"
+ " VersionManager.restore(Version, boolean) instead: "
+ absPath);
} else {
// parent has to exist
Path path = context.getQPath(absPath);
Path parentPath = path.getAncestor(1);
Name name = path.getName();
NodeImpl parent = context.getItemManager().getNode(parentPath);
NodeStateEx state = getNodeState(
parent,
CHECK_PENDING_CHANGES | ItemValidator.CHECK_LOCK | CHECK_HOLD,
Permission.NONE);
// check if given version is a baseline
InternalVersion v = getVersion(version);
if (v instanceof InternalBaseline) {
restore(state, name, (InternalBaseline) v);
} else {
restore(state, name, v, removeExisting);
}
}
return this;
}
public String toString() {
return "versionManager.restore("
+ absPath + ", version, " + removeExisting + ")";
}
});
}
/**
* Same as {@link #restore(String, String, boolean)} but to ensure
* backward compatibility for Node.restore(Version, boolean).
*
* @param node the node to restore
* @param version the version to restore
* @param removeExisting the remove existing flag
* @throws RepositoryException if an error occurs
*/
protected void restore(NodeImpl node, Version version, boolean removeExisting)
throws RepositoryException {
NodeStateEx state = getNodeState(
node.getPath(),
CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
Permission.NONE);
InternalVersion v = getVersion(version);
restore(state, v, removeExisting);
}
/**
* {@inheritDoc}
*/
public void restoreByLabel(
final String absPath, final String versionLabel,
final boolean removeExisting) throws RepositoryException {
perform(new SessionWriteOperation<Object> () {
public Object perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
Permission.NONE);
restoreByLabel(
state, context.getQName(versionLabel), removeExisting);
return this;
}
public String toString() {
return "versionManager.restoreByLabel("
+ absPath + ", " + versionLabel + ", "
+ removeExisting + ")";
}
});
}
/**
* Does an update.
* @see javax.jcr.Node#update(String)
*
* @param node the node to update
* @param srcWorkspaceName the source workspace name
* @throws RepositoryException if an error occurs
*/
public void update(NodeImpl node, String srcWorkspaceName)
throws RepositoryException {
NodeStateEx state = getNodeState(node,
ItemValidator.CHECK_PENDING_CHANGES,
Permission.VERSION_MNGMT);
mergeOrUpdate(state, srcWorkspaceName, null, false, false);
}
/** Wrapper around {@link #merge(String, String, boolean, boolean)}. */
public NodeIterator merge(
String absPath, String srcWorkspace, boolean bestEffort)
throws RepositoryException {
return merge(absPath, srcWorkspace, bestEffort, false);
}
/**
* {@inheritDoc}
*/
public NodeIterator merge(
final String absPath, final String srcWorkspaceName,
final boolean bestEffort, final boolean isShallow)
throws RepositoryException {
return perform(new SessionWriteOperation<NodeIterator> () {
public NodeIterator perform(SessionContext context)
throws RepositoryException {
NodeStateEx state = getNodeState(
absPath,
CHECK_PENDING_CHANGES,
Permission.VERSION_MNGMT);
List<ItemId> failedIds = new LinkedList<ItemId>();
mergeOrUpdate(state, srcWorkspaceName, failedIds, bestEffort, isShallow);
return new LazyItemIterator(context, failedIds);
}
public String toString() {
return "versionManager.merge("
+ absPath + ", " + srcWorkspaceName + ", "
+ bestEffort + ", " + isShallow + ")";
}
});
}
/**
* Combines merge and update method
* @param state the state to merge or update
* @param srcWorkspaceName source workspace name
* @param failedIds list that will contain the failed ids.
* if <code>null</code> and update will be performed.
* @param bestEffort best effort flag
* @param isShallow is shallow flag
* @throws RepositoryException if an error occurs
*/
private void mergeOrUpdate(NodeStateEx state, String srcWorkspaceName,
List<ItemId> failedIds, boolean bestEffort,
boolean isShallow)
throws RepositoryException {
// if same workspace, ignore
if (!srcWorkspaceName.equals(session.getWorkspace().getName())) {
// check authorization for specified workspace
if (!session.getAccessManager().canAccess(srcWorkspaceName)) {
String msg = "not authorized to access " + srcWorkspaceName;
log.error(msg);
throw new AccessDeniedException(msg);
}
// get root node of src workspace
SessionImpl srcSession = null;
try {
// create session on other workspace for current subject
// (may throw NoSuchWorkspaceException and AccessDeniedException)
srcSession = ((RepositoryImpl) session.getRepository())
.createSession(session.getSubject(), srcWorkspaceName);
WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace();
NodeId rootNodeId = ((NodeImpl) srcSession.getRootNode()).getNodeId();
NodeStateEx srcRoot = new NodeStateEx(
srcWsp.getItemStateManager(),
ntReg,
rootNodeId);
merge(state, srcRoot, failedIds, bestEffort, isShallow);
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
if (srcSession != null) {
// we don't need the other session anymore, logout
srcSession.logout();
}
}
}
}
/**
* {@inheritDoc}
*/
public void doneMerge(String absPath, Version version)
throws RepositoryException {
NodeStateEx state = getNodeState(absPath,
ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
Permission.VERSION_MNGMT);
finishMerge(state, version, false);
}
/**
* {@inheritDoc}
*/
public void cancelMerge(String absPath, Version version)
throws RepositoryException {
NodeStateEx state = getNodeState(absPath,
ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
Permission.VERSION_MNGMT);
finishMerge(state, version, true);
}
/**
* {@inheritDoc}
*/
public Node createConfiguration(String absPath) throws RepositoryException {
if (session.nodeExists(absPath)) {
NodeStateEx state = getNodeState(absPath,
ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
Permission.VERSION_MNGMT);
// check versionable
if (!checkVersionable(state)) {
throw new UnsupportedRepositoryOperationException("Node not full versionable: " + absPath);
}
if (state.getPropertyValue(NameConstants.JCR_CONFIGURATION) != null) {
String msg = "Node is already a configuration root: " + absPath;
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
NodeId configId = createConfiguration(state);
return session.getNodeById(configId);
} else {
String msg = "Create configuration node must exist: " + absPath;
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
}
/**
* {@inheritDoc}
*/
public Node setActivity(Node activity) throws RepositoryException {
Node oldActivity = getActivity();
if (activity == null) {
currentActivity = null;
} else {
NodeImpl actNode = (NodeImpl) activity;
if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
currentActivity = actNode.getNodeId();
}
return oldActivity;
}
/**
* {@inheritDoc}
*/
public Node getActivity() throws RepositoryException {
if (currentActivity == null) {
return null;
} else {
return session.getNodeById(currentActivity);
}
}
/**
* {@inheritDoc}
*/
public Node createActivity(String title) throws RepositoryException {
NodeId id = vMgr.createActivity(session, title);
return session.getNodeById(id);
}
/**
* {@inheritDoc}
*/
public void removeActivity(Node node) throws RepositoryException {
NodeImpl actNode = (NodeImpl) node;
if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
NodeId actId = actNode.getNodeId();
vMgr.removeActivity(session, actId);
if (actId.equals(currentActivity)) {
currentActivity = null;
}
}
/**
* {@inheritDoc}
*/
public NodeIterator merge(Node activityNode) throws RepositoryException {
NodeImpl actNode = (NodeImpl) activityNode;
if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
InternalActivity activity = vMgr.getActivity(actNode.getNodeId());
if (activity == null) {
String msg = "Given activity not found in version storage.";
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
List<ItemId> failedIds = new ArrayList<ItemId>();
merge(activity, failedIds);
return new LazyItemIterator(context, failedIds);
}
/**
* returns the node state for the given path
* @param path path of the node
* @throws RepositoryException if an error occurs
* @return the node state
*/
private NodeStateEx getNodeState(String path) throws RepositoryException {
return getNodeState(path, 0, 0);
}
/**
* checks the permissions for the given path and returns the node state
* @param path path of the node
* @param options options to check
* @param permissions permissions to check
* @throws RepositoryException if an error occurs
* @return the node state
*/
private NodeStateEx getNodeState(String path, int options, int permissions)
throws RepositoryException {
return getNodeState((NodeImpl) session.getNode(path), options, permissions);
}
/**
* checks the permissions for the given path and returns the node state
* @param node the node
* @param options options to check
* @param permissions permissions to check
* @throws RepositoryException if an error occurs
* @return the node state
*/
private NodeStateEx getNodeState(NodeImpl node, int options, int permissions)
throws RepositoryException {
try {
if (options > 0 || permissions > 0) {
context.getItemValidator().checkModify(node, options, permissions);
}
return new NodeStateEx(
stateMgr,
ntReg,
(NodeState) stateMgr.getItemState(node.getNodeId()),
node.getQName());
} catch (ItemStateException e) {
throw new RepositoryException(e);
}
}
}