/*
* 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.*;
import javax.jcr.ItemExistsException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.OnParentVersionAction;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionManager;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.ItemValidator;
import org.apache.jackrabbit.core.NodeTypeInstanceHandler;
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.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.UpdatableItemStateManager;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The JCR Version Manager implementation is split in several classes in order to
* group related methods together.
* <p>
* this class provides methods for the restore operations.
* <p>
* Implementation note: methods starting with "internal" are considered to be
* executed within a "write operations" block.
*/
abstract public class VersionManagerImplRestore extends VersionManagerImplBase {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(VersionManagerImplRestore.class);
/**
* Creates a new version manager for the given session
*
* @param context component context of the current session
* @param stateMgr the underlying state manager
* @param hierMgr local hierarchy manager
*/
protected VersionManagerImplRestore(
SessionContext context, UpdatableItemStateManager stateMgr,
HierarchyManager hierMgr) {
super(context, stateMgr, hierMgr);
}
/**
* @param state the state to restore
* @param v the version to restore
* @param removeExisting remove existing flag
* @throws RepositoryException if an error occurs
*
* @see javax.jcr.version.VersionManager#restore(String, Version, boolean)
*/
protected void restore(NodeStateEx state, InternalVersion v, boolean removeExisting)
throws RepositoryException {
checkVersionable(state);
// check if 'own' version
if (!v.getVersionHistory().equals(getVersionHistory(state))) {
String msg = "Unable to restore version. Not same version history.";
log.error(msg);
throw new VersionException(msg);
}
WriteOperation ops = startWriteOperation();
try {
internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting);
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
/**
* @param state the state to restore
* @param versionName the name of the version to restore
* @param removeExisting remove existing flag
* @throws RepositoryException if an error occurs
*
* @see VersionManager#restore(String, String, boolean)
*/
protected void restore(NodeStateEx state, Name versionName, boolean removeExisting)
throws RepositoryException {
checkVersionable(state);
InternalVersion v = getVersionHistory(state).getVersion(versionName);
DateVersionSelector gvs = new DateVersionSelector(v.getCreated());
WriteOperation ops = startWriteOperation();
try {
internalRestore(state, v, gvs, removeExisting);
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
/**
* @param state the state to restore
* @param versionLabel the name of the version to restore
* @param removeExisting remove existing flag
* @throws RepositoryException if an error occurs
*
* @see VersionManager#restoreByLabel(String, String, boolean)
*/
protected void restoreByLabel(NodeStateEx state, Name versionLabel, boolean removeExisting)
throws RepositoryException {
checkVersionable(state);
InternalVersion v = getVersionHistory(state).getVersionByLabel(versionLabel);
if (v == null) {
String msg = "No version for label " + versionLabel + " found.";
log.error(msg);
throw new VersionException(msg);
}
WriteOperation ops = startWriteOperation();
try {
internalRestore(state, v, new LabelVersionSelector(versionLabel), removeExisting);
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
/**
* Restores the <code>version</code> below the <code>parent</code> node
* using the indicated <code>name</code>
*
* @param parent parent node
* @param name desired name
* @param v version to restore
* @param removeExisting remove exiting flag
* @throws RepositoryException if an error occurs
*/
protected void restore(NodeStateEx parent, Name name, InternalVersion v,
boolean removeExisting)
throws RepositoryException {
// check if versionable node exists
InternalFrozenNode fn = v.getFrozenNode();
if (stateMgr.hasItemState(fn.getFrozenId())) {
if (removeExisting) {
NodeStateEx existing = parent.getNode(fn.getFrozenId());
checkVersionable(existing);
// move versionable node below this one using the given "name"
WriteOperation ops = startWriteOperation();
try {
NodeStateEx exParent = existing.getParent();
NodeStateEx state = parent.moveFrom(existing, name, false);
exParent.store();
parent.store();
// and restore it
internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting);
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
} else {
String msg = "Unable to restore version. Versionable node already exists.";
log.error(msg);
throw new ItemExistsException(msg);
}
} else {
WriteOperation ops = startWriteOperation();
try {
// create new node below parent
NodeStateEx state = parent.addNode(name, fn.getFrozenPrimaryType(), fn.getFrozenId());
state.setMixins(fn.getFrozenMixinTypes());
internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting);
parent.store();
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
}
/**
* @param versions Versions to restore
* @param removeExisting remove existing flag
* @throws RepositoryException if an error occurs
* @throws ItemStateException if an error occurs
*
* @see VersionManager#restore(Version[], boolean)
* @see VersionManager#restore(Version, boolean)
*/
protected void internalRestore(VersionSet versions, boolean removeExisting)
throws RepositoryException, ItemStateException {
// now restore all versions that have a node in the workspace
int numRestored = 0;
while (versions.versions().size() > 0) {
Set<InternalVersion> restored = null;
for (InternalVersion v : versions.versions().values()) {
NodeStateEx state = getNodeStateEx(v.getFrozenNode().getFrozenId());
if (state != null) {
// todo: check should operate on workspace states, too
int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_HOLD;
checkModify(state, options, Permission.NONE);
restored = internalRestore(state, v, versions, removeExisting);
// remove restored versions from set
for (InternalVersion r : restored) {
versions.versions().remove(r.getVersionHistory().getId());
}
numRestored += restored.size();
break;
}
}
if (restored == null) {
String msg = numRestored == 0
? "Unable to restore. At least one version needs existing versionable node in workspace."
: "Unable to restore. All versions with non existing versionable nodes need parent.";
log.error(msg);
throw new VersionException(msg);
}
}
}
/**
* Internal method to restore a version.
*
* @param state the state to restore
* @param version version to restore
* @param vsel the version selector that will select the correct version for
* OPV=Version child nodes.
* @param removeExisting remove existing flag
* @return set of restored versions
* @throws RepositoryException if an error occurs
* @throws ItemStateException if an error occurs
*/
protected Set<InternalVersion> internalRestore(NodeStateEx state,
InternalVersion version,
VersionSelector vsel,
boolean removeExisting)
throws RepositoryException, ItemStateException {
// fail if root version
if (version.isRootVersion()) {
String msg = "Restore of root version not allowed.";
log.error(msg);
throw new VersionException(msg);
}
boolean isFull = checkVersionable(state);
// check permission
Path path = hierMgr.getPath(state.getNodeId());
session.getAccessManager().checkPermission(path, Permission.VERSION_MNGMT);
// 1. The child node and properties of N will be changed, removed or
// added to, depending on their corresponding copies in V and their
// own OnParentVersion attributes (see 7.2.8, below, for details).
Set<InternalVersion> restored = new HashSet<InternalVersion>();
internalRestoreFrozen(state, version.getFrozenNode(), vsel, restored, removeExisting, false);
restored.add(version);
if (isFull) {
// 2. N's jcr:baseVersion property will be changed to point to V.
state.setPropertyValue(
NameConstants.JCR_BASEVERSION, InternalValue.create(version.getId()));
// 4. N's jcr:predecessor property is set to null
state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY);
// set version history
state.setPropertyValue(NameConstants.JCR_VERSIONHISTORY, InternalValue.create(version.getVersionHistory().getId()));
// also clear mergeFailed
state.removeProperty(NameConstants.JCR_MERGEFAILED);
} else {
// with simple versioning, the node is checked in automatically,
// thus not allowing any branches
vMgr.checkin(session, state, null);
}
// 3. N's jcr:isCheckedOut property is set to false.
state.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(false));
state.store();
// check if a baseline is restored
if (version instanceof InternalBaseline) {
// just restore all base versions
InternalBaseline baseline = (InternalBaseline) version;
internalRestore(baseline.getBaseVersions(), true);
// ensure that the restored root node has a jcr:configuration property
// since it might not have been recorded by the initial checkin of the
// configuration
NodeId configId = baseline.getConfigurationId();
NodeId rootId = baseline.getConfigurationRootId();
NodeStateEx rootNode = state.getNode(rootId);
rootNode.setPropertyValue(NameConstants.JCR_CONFIGURATION, InternalValue.create(configId));
rootNode.store();
}
return restored;
}
/**
* Restores the properties and child nodes from the frozen state.
*
* @param state state to restore
* @param freeze the frozen node
* @param vsel version selector
* @param restored set of restored versions
* @param removeExisting remove existing flag
* @param copy if <code>true</code> a pure copy is performed
* @throws RepositoryException if an error occurs
* @throws ItemStateException if an error occurs
*/
protected void internalRestoreFrozen(NodeStateEx state,
InternalFrozenNode freeze,
VersionSelector vsel,
Set<InternalVersion> restored,
boolean removeExisting,
boolean copy)
throws RepositoryException, ItemStateException {
// check uuid
if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE)) {
if (!state.getNodeId().equals(freeze.getFrozenId())) {
String msg = "Unable to restore version of " + safeGetJCRPath(state) + ". UUID changed.";
log.error(msg);
throw new ItemExistsException(msg);
}
}
// check primary type
if (!freeze.getFrozenPrimaryType().equals(state.getState().getNodeTypeName())) {
// todo: implement
String msg = "Unable to restore version of " + safeGetJCRPath(state) + ". PrimaryType change not supported yet.";
log.error(msg);
throw new UnsupportedRepositoryOperationException(msg);
}
// adjust mixins
state.setMixins(freeze.getFrozenMixinTypes());
// For each property P present on F (other than jcr:frozenPrimaryType,
// jcr:frozenMixinTypes and jcr:frozenUuid):
// - If P has an OPV of COPY or VERSION then F/P is copied to N/P,
// replacing any existing N/P.
// - F will never have a property with an OPV of IGNORE, INITIALIZE, COMPUTE
// or ABORT (see 15.2 Check-In: Creating a Version).
Set<Name> propNames = new HashSet<Name>();
PropertyState[] props = freeze.getFrozenProperties();
for (PropertyState prop : props) {
// don't restore jcr:activity
Name name = prop.getName();
if (!name.equals(NameConstants.JCR_ACTIVITY)) {
state.copyFrom(prop);
propNames.add(name);
}
}
// For each property P present on N but not on F:
// - If P has an OPV of COPY, VERSION or ABORT then N/P is removed. Note that
// while a node with a child item of OPV ABORT cannot be versioned, it is
// legal for a previously versioned node to have such a child item added to it
// and then for it to be restored to the state that it had before that item was
// added, as this step indicates.
// - If P has an OPV of IGNORE then no change is made to N/P.
// - If P has an OPV of INITIALIZE then, if N/P has a default value (either
// defined in the node type of N or implementation-defined) its value is
// changed to that default value. If N/P has no default value then it is left
// unchanged.
// - If P has an OPV of COMPUTE then the value of N/P may be changed
// according to an implementation-specific mechanism.
// remove properties that do not exist in the frozen representation
for (PropertyState prop: state.getProperties()) {
Name propName = prop.getName();
if (!propNames.contains(propName)) {
int opv = state.getDefinition(prop).getOnParentVersion();
if (opv == OnParentVersionAction.COPY
|| opv == OnParentVersionAction.VERSION
|| opv == OnParentVersionAction.ABORT) {
state.removeProperty(propName);
} else if (opv == OnParentVersionAction.INITIALIZE) {
InternalValue[] values = computeAutoValues(state, state.getDefinition(prop), true);
if (values != null) {
state.setPropertyValues(propName, prop.getType(), values, prop.isMultiValued());
}
} else if (opv == OnParentVersionAction.COMPUTE) {
InternalValue[] values = computeAutoValues(state, state.getDefinition(prop), false);
if (values != null) {
state.setPropertyValues(propName, prop.getType(), values, prop.isMultiValued());
}
}
}
}
// add 'auto-create' properties that do not exist yet
for (QPropertyDefinition def: state.getEffectiveNodeType().getAutoCreatePropDefs()) {
if (!state.hasProperty(def.getName())) {
InternalValue[] values = computeAutoValues(state, def, true);
if (values != null) {
state.setPropertyValues(def.getName(), def.getRequiredType(), values, def.isMultiple());
}
}
}
// For each child node C present on N but not on F:
// - If C has an OPV of COPY, VERSION or ABORT then N/C is removed.
// Note that while a node with a child item of OPV ABORT cannot be
// versioned, it is legal for a previously versioned node to have such
// a child item added to it and then for it to be restored to the state
// that it had before that item was added, as this step indicates.
// - If C has an OPV of IGNORE then no change is made to N/C.
// - If C has an OPV of INITIALIZE then N/C is re-initialized as if it
// were newly created, as defined in its node type.
// - If C has an OPV of COMPUTE then N/C may be re-initialized according
// to an implementation-specific mechanism.
LinkedList<ChildNodeEntry> toDelete = new LinkedList<ChildNodeEntry>();
for (ChildNodeEntry entry: state.getState().getChildNodeEntries()) {
if (!freeze.hasFrozenChildNode(entry.getName(), entry.getIndex())) {
NodeStateEx child = state.getNode(entry.getName(), entry.getIndex());
int opv = child.getDefinition().getOnParentVersion();
if (copy || opv == OnParentVersionAction.COPY
|| opv == OnParentVersionAction.VERSION
|| opv == OnParentVersionAction.ABORT) {
toDelete.addFirst(entry);
} else if (opv == OnParentVersionAction.INITIALIZE) {
log.warn("OPV.INITIALIZE not supported yet on restore of existing child nodes: " + safeGetJCRPath(child));
} else if (opv == OnParentVersionAction.COMPUTE) {
log.warn("OPV.COMPUTE not supported yet on restore of existing child nodes: " + safeGetJCRPath(child));
}
}
}
for (ChildNodeEntry entry: toDelete) {
state.removeNode(entry.getName(), entry.getIndex());
}
// need to sync with state manager
state.store();
// create a map that contains a int->NodeStateEx mapping for each child name
Map<Name, Map<Integer, NodeStateEx>> entryToNodeStateExMapping = new HashMap<Name, Map<Integer, NodeStateEx>>();
for (ChildNodeEntry entry : state.getState().getChildNodeEntries()) {
Map<Integer, NodeStateEx> id2stateMap = entryToNodeStateExMapping
.get(entry.getName());
if (id2stateMap == null) {
id2stateMap = new HashMap<Integer, NodeStateEx>();
}
id2stateMap.put(entry.getIndex(),
state.getNode(entry.getName(), entry.getIndex()));
entryToNodeStateExMapping.put(entry.getName(), id2stateMap);
}
// For each child node C present on F:
// - F will never have a child node with an OPV of IGNORE, INITIALIZE,
// COMPUTE or ABORT (see 15.2 Check-In: Creating a Version).
for (ChildNodeEntry entry : freeze.getFrozenChildNodes()) {
InternalFreeze child = freeze.getFrozenChildNode(entry.getName(), entry.getIndex());
NodeStateEx restoredChild = null;
if (child instanceof InternalFrozenNode) {
// - If C has an OPV of COPY or VERSION:
// - B is true, then F/C and its subgraph is copied to N/C, replacing
// any existing N/C and its subgraph and any node in the workspace
// with the same identifier as C or a node in the subgraph of C is
// removed.
// - B is false, then F/C and its subgraph is copied to N/C, replacing
// any existing N/C and its subgraph unless there exists a node in the
// workspace with the same identifier as C, or a node in the subgraph
// of C, in which case an ItemExistsException is thrown , all
// changes made by the restore are rolled back leaving N unchanged.
InternalFrozenNode f = (InternalFrozenNode) child;
// if node is present, remove it
Map<Integer, NodeStateEx> id2stateMap = entryToNodeStateExMapping
.get(entry.getName());
if (id2stateMap != null
&& id2stateMap.containsKey(entry.getIndex())) {
state.removeNode(id2stateMap.get(entry.getIndex()));
}
// check for existing
if (f.getFrozenId() != null) {
if (stateMgr.hasItemState(f.getFrozenId())) {
NodeStateEx existing = state.getNode(f.getFrozenId());
if (removeExisting) {
NodeStateEx parent = existing.getParent();
parent.removeNode(existing);
parent.store();
} else if (existing.getState().isShareable()) {
// if existing node is shareable, then clone it
restoredChild = state.moveFrom(existing, existing.getName(), true);
} else if (!existing.hasAncestor(state.getNodeId())){
String msg = "Unable to restore node, item already exists " +
"outside of restored tree: " + safeGetJCRPath(existing);
log.error(msg);
throw new ItemExistsException(msg);
}
}
}
if (restoredChild == null) {
restoredChild = state.addNode(f.getName(), f.getFrozenPrimaryType(), f.getFrozenId());
restoredChild.setMixins(f.getFrozenMixinTypes());
}
internalRestoreFrozen(restoredChild, f, vsel, restored, removeExisting, true);
} else if (child instanceof InternalFrozenVersionHistory) {
// Each child node C of N where C has an OPV of VERSION and C is
// mix:versionable, is represented in F not as a copy of N/C but as
// special node containing a reference to the version history of
// C. On restore, the following occurs:
// - If the workspace currently has an already existing node corresponding
// to C's version history and the removeExisting flag of the restore is
// set to true, then that instance of C becomes the child of the restored N.
// - If the workspace currently has an already existing node corresponding
// to C's version history and the removeExisting flag of the restore is
// set to false then an ItemExistsException is thrown.
// - If the workspace does not have an instance of C then one is restored from
// C's version history:
// - If the restore was initiated through a restoreByLabel where L is
// the specified label and there is a version of C with the label L then
// that version is restored.
// - If the version history of C does not contain a version with the label
// L or the restore was initiated by a method call that does not specify
// a label then the workspace in which the restore is being performed
// will determine which particular version of C will be restored. This
// determination depends on the configuration of the workspace and
// is outside the scope of this specification.
InternalFrozenVersionHistory fh = (InternalFrozenVersionHistory) child;
InternalVersionHistory vh = vMgr.getVersionHistory(fh.getVersionHistoryId());
// get desired version from version selector
InternalVersion v = vsel.select(vh);
Name oldVersion = null;
// check if representing versionable already exists somewhere
NodeId nodeId = vh.getVersionableId();
if (stateMgr.hasItemState(nodeId)) {
restoredChild = state.getNode(nodeId);
if (restoredChild.getParentId().equals(state.getNodeId())) {
// if same parent, ignore
} else if (removeExisting) {
NodeStateEx parent = restoredChild.getNode(restoredChild.getParentId());
state.moveFrom(restoredChild, fh.getName(), false);
parent.store();
// get old version name
oldVersion = getBaseVersion(restoredChild).getName();
} else {
// since we delete the OPV=Copy children beforehand, all
// found nodes must be outside of this tree
String msg = "Unable to restore node, item already exists " +
"outside of restored tree: " + safeGetJCRPath(restoredChild);
log.error(msg);
throw new ItemExistsException(msg);
}
}
// check existing version of item exists
if (restoredChild == null) {
if (v == null) {
// if version selector was unable to select version,
// choose the initial one
List<InternalVersion> vs = vh.getRootVersion().getSuccessors();
if (vs.isEmpty()) {
String msg = "Unable to select appropriate version for "
+ child.getName() + " using " + vsel;
log.error(msg);
throw new VersionException(msg);
}
v = vs.get(0);
}
InternalFrozenNode f = v.getFrozenNode();
restoredChild = state.addNode(fh.getName(), f.getFrozenPrimaryType(), f.getFrozenId());
restoredChild.setMixins(f.getFrozenMixinTypes());
} else {
if (v == null || oldVersion == null || v.getName().equals(oldVersion)) {
v = null;
}
}
if (v != null) {
try {
internalRestore(restoredChild, v, vsel, removeExisting);
} catch (RepositoryException e) {
log.error("Error while restoring node: " + e);
log.error(" child path: " + restoredChild);
log.error(" selected version: " + v.getName());
StringBuilder avail = new StringBuilder();
for (Name name: vh.getVersionNames()) {
avail.append(name);
avail.append(", ");
}
log.error(" available versions: " + avail);
log.error(" versionselector: " + vsel);
throw e;
}
// add this version to set
restored.add(v);
}
}
if (restoredChild != null && state.getEffectiveNodeType().hasOrderableChildNodes()) {
// In a repository that supports orderable child nodes, the relative
// ordering of the set of child nodes C that are copied from F is
// preserved.
// order at end
ArrayList<ChildNodeEntry> list = new ArrayList<ChildNodeEntry>(state.getState().getChildNodeEntries());
ChildNodeEntry toReorder = null;
boolean isLast = true;
for (ChildNodeEntry e: list) {
if (e.getId().equals(restoredChild.getNodeId())) {
toReorder = e;
} else if (toReorder != null) {
isLast = false;
}
}
if (toReorder != null && !isLast) {
list.remove(toReorder);
list.add(toReorder);
state.getState().setChildNodeEntries(list);
}
}
}
}
/**
* Computes the auto generated values and falls back to the default values
* specified in the property definition
* @param state parent state
* @param def property definition
* @param useDefaultValues if <code>true</code> the default values are respected
* @return the values or <code>null</code>
* @throws RepositoryException if the values cannot be computed.
*/
private InternalValue[] computeAutoValues(NodeStateEx state, QPropertyDefinition def,
boolean useDefaultValues)
throws RepositoryException {
// compute system generated values if necessary
InternalValue[] values =
new NodeTypeInstanceHandler(session.getUserID()).
computeSystemGeneratedPropertyValues(state.getState(), def);
if (values == null && useDefaultValues) {
values = InternalValue.create(def.getDefaultValues());
}
// avoid empty value array for single value property
if (values != null && values.length == 0 && !def.isMultiple()) {
values = null;
}
return values;
}
}