/*
* 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.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.jcr.PropertyType;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.version.VersionException;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a <code>InternalVersionHistory</code>
*/
class InternalVersionHistoryImpl extends InternalVersionItemImpl
implements InternalVersionHistory {
/**
* default logger
*/
private static Logger log = LoggerFactory.getLogger(InternalVersionHistoryImpl.class);
/**
* The last current time that was returned by {@link #getCurrentTime()}.
*/
private static final Calendar CURRENT_TIME = Calendar.getInstance();
/**
* the cache of the version labels
* key = version label (String)
* value = version name
*/
private Map<Name, Name> labelCache = new HashMap<Name, Name>();
/**
* the root version of this history
*/
private InternalVersion rootVersion;
/**
* the hashmap of all versions names
* key = version name
* value = version id (NodeId)
*/
private Map<Name, NodeId> nameCache = new LinkedHashMap<Name, NodeId>();
/**
* the hashmap of all versions
* key = version id (NodeId)
* value = version
*/
private Map<NodeId, InternalVersion> versionCache = new HashMap<NodeId, InternalVersion>();
/**
* Temporary version cache, used on a refresh.
*/
private Map<NodeId, InternalVersion> tempVersionCache = new HashMap<NodeId, InternalVersion>();
/**
* the node that holds the label nodes
*/
private NodeStateEx labelNode;
/**
* the id of this history
*/
private NodeId historyId;
/**
* the id of the versionable node
*/
private NodeId versionableId;
/**
* Creates a new VersionHistory object for the given node state.
* @param vMgr version manager
* @param node version history node state
* @throws RepositoryException if an error occurs
*/
public InternalVersionHistoryImpl(InternalVersionManagerBase vMgr, NodeStateEx node)
throws RepositoryException {
super(vMgr, node);
init();
fixLegacy();
}
/**
* Initializes the history and loads all internal caches
*
* @throws RepositoryException if an error occurs
*/
private synchronized void init() throws RepositoryException {
nameCache.clear();
versionCache.clear();
labelCache.clear();
// get id
historyId = node.getNodeId();
// get versionable id
versionableId = NodeId.valueOf(node.getPropertyValue(NameConstants.JCR_VERSIONABLEUUID).toString());
// get label node
labelNode = node.getNode(NameConstants.JCR_VERSIONLABELS, 1);
// init label cache
try {
PropertyState[] labels = labelNode.getProperties();
for (PropertyState pState : labels) {
if (pState.getType() == PropertyType.REFERENCE) {
Name labelName = pState.getName();
NodeId id = pState.getValues()[0].getNodeId();
if (node.getState().hasChildNodeEntry(id)) {
labelCache.put(labelName, node.getState().getChildNodeEntry(id).getName());
} else {
log.warn("Error while resolving label reference. Version missing: " + id);
}
}
}
} catch (ItemStateException e) {
throw new RepositoryException(e);
}
// get root version
rootVersion = createVersionInstance(NameConstants.JCR_ROOTVERSION);
// get version entries
for (ChildNodeEntry child : node.getState().getChildNodeEntries()) {
if (child.getName().equals(NameConstants.JCR_VERSIONLABELS)) {
continue;
}
nameCache.put(child.getName(), child.getId());
}
}
// fix legacy
private void fixLegacy() throws RepositoryException {
if (rootVersion.getSuccessors().isEmpty()) {
for (Name versionName : getVersionNames()) {
InternalVersionImpl v = createVersionInstance(versionName);
v.legacyResolveSuccessors();
}
}
}
/**
* Reload this object and all its dependent version objects.
* @throws RepositoryException if an error occurs
*/
synchronized void reload() throws RepositoryException {
tempVersionCache.putAll(versionCache);
init();
// invalidate all versions that are not referenced any more
for (Object o : tempVersionCache.values()) {
InternalVersionImpl v = (InternalVersionImpl) o;
v.invalidate();
}
tempVersionCache.clear();
}
/**
* Create a version instance.
* @param name name of the version
* @return the new internal version
* @throws IllegalArgumentException if the version does not exist
*/
synchronized InternalVersionImpl createVersionInstance(Name name) {
try {
NodeStateEx nodeStateEx = node.getNode(name, 1);
InternalVersionImpl v = createVersionInstance(nodeStateEx);
versionCache.put(v.getId(), v);
vMgr.versionCreated(v);
// add labels
for (Name labelName: labelCache.keySet()) {
Name versionName = labelCache.get(labelName);
if (v.getName().equals(versionName)) {
v.internalAddLabel(labelName);
}
}
return v;
} catch (RepositoryException e) {
throw new InconsistentVersioningState("Failed to create version " + name + " in VHR " + historyId + ".", historyId, null);
}
}
/**
* Create a version instance. May resurrect versions temporarily swapped
* out when refreshing this history.
* @param child child node state
* @return new version instance
*/
synchronized InternalVersionImpl createVersionInstance(NodeStateEx child) {
InternalVersionImpl v = (InternalVersionImpl) tempVersionCache.remove(child.getNodeId());
if (v != null) {
v.clear();
} else {
// check if baseline
try {
NodeStateEx frozen = child.getNode(NameConstants.JCR_FROZENNODE, 1);
Name frozenType = frozen.getPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE).getName();
if (NameConstants.NT_CONFIGURATION.equals(frozenType)) {
v = new InternalBaselineImpl(this, child, child.getName());
} else {
v = new InternalVersionImpl(this, child, child.getName());
}
} catch (RepositoryException e) {
throw new InconsistentVersioningState("Version does not have a jcr:frozenNode: " + child.getNodeId(), historyId, e);
}
}
return v;
}
/**
* {@inheritDoc}
*/
@Override
public NodeId getId() {
return historyId;
}
/**
* {@inheritDoc}
*/
@Override
public InternalVersionItem getParent() {
return null;
}
/**
* {@inheritDoc}
*/
public InternalVersion getRootVersion() {
return rootVersion;
}
/**
* {@inheritDoc}
*/
public synchronized InternalVersion getVersion(Name versionName)
throws VersionException {
NodeId versionId = nameCache.get(versionName);
if (versionId == null) {
throw new VersionException("Version " + versionName + " does not exist.");
}
InternalVersion v = versionCache.get(versionId);
if (v == null) {
v = createVersionInstance(versionName);
}
return v;
}
/**
* {@inheritDoc}
*/
public synchronized boolean hasVersion(Name versionName) {
return nameCache.containsKey(versionName);
}
/**
* {@inheritDoc}
*/
public InternalVersion getVersion(NodeId id) {
InternalVersion v = getCachedVersion(id);
// If the version was not found, our cache may not have been
// synchronized with updates from another cluster node. Reload the history
// to be sure we have the latest updates and try again.
if (v == null) {
try {
reload();
} catch (RepositoryException e) {
// We should add the checked exception to this method definition
// so we don't need to wrap it.
// Avoiding it for now to limit impact of this fix.
throw new RuntimeException(e);
}
v = getCachedVersion(id);
}
return v;
}
/**
* Returns the version from cache, or <code>null</code> if it is not
* present.
* @param id the id of the version
* @return the version or <code>null</code> if not cached.
*/
private synchronized InternalVersion getCachedVersion(NodeId id) {
InternalVersion v = versionCache.get(id);
if (v == null) {
for (Name versionName : nameCache.keySet()) {
if (nameCache.get(versionName).equals(id)) {
v = createVersionInstance(versionName);
break;
}
}
}
return v;
}
/**
* {@inheritDoc}
*/
public synchronized InternalVersion getVersionByLabel(Name label) {
Name versionName = labelCache.get(label);
if (versionName == null) {
return null;
}
NodeId id = nameCache.get(versionName);
InternalVersion v = versionCache.get(id);
if (v == null) {
v = createVersionInstance(versionName);
}
return v;
}
/**
* {@inheritDoc}
*/
public synchronized Name[] getVersionNames() {
return nameCache.keySet().toArray(new Name[nameCache.size()]);
}
/**
* {@inheritDoc}
*/
public synchronized int getNumVersions() {
return nameCache.size();
}
/**
* {@inheritDoc}
*/
public NodeId getVersionableId() {
return versionableId;
}
/**
* {@inheritDoc}
*/
public synchronized Name[] getVersionLabels() {
return labelCache.keySet().toArray(new Name[labelCache.size()]);
}
/**
* {@inheritDoc}
*/
public NodeId getVersionLabelsId() {
return labelNode.getNodeId();
}
/**
* Removes the indicated version from this VersionHistory. If the specified
* vesion does not exist, if it specifies the root version or if it is
* referenced by any node e.g. as base version, a VersionException is thrown.
* <p>
* all successors of the removed version become successors of the
* predecessors of the removed version and vice versa. then, the entire
* version node and all its subnodes are removed.
*
* @param versionName name of the version to remove
* @throws VersionException if removal is not possible
*/
synchronized void removeVersion(Name versionName) throws RepositoryException {
InternalVersionImpl v = (InternalVersionImpl) getVersion(versionName);
if (v.equals(rootVersion)) {
String msg = "Removal of " + versionName + " not allowed.";
log.debug(msg);
throw new VersionException(msg);
}
// check if any references (from outside the version storage) exist on this version
if (vMgr.hasItemReferences(v.getId())) {
throw new ReferentialIntegrityException("Unable to remove version. At least once referenced.");
}
// unregister from labels
Name[] labels = v.internalGetLabels();
for (Name label : labels) {
v.internalRemoveLabel(label);
labelNode.removeProperty(label);
}
// detach from the version graph
v.internalDetach();
// check if referenced by an activity
InternalActivityImpl activity = v.getActivity();
if (activity != null) {
activity.removeVersion(v);
}
// remove from persistence state
node.removeNode(v.getName());
// and remove from history
versionCache.remove(v.getId());
nameCache.remove(versionName);
vMgr.versionDestroyed(v);
// Check if this was the last version in addition to the root version
if (!vMgr.hasItemReferences(node.getNodeId())) {
log.debug("Current version history has no references");
NodeStateEx[] childNodes = node.getChildNodes();
// Check if there is only root version and version labels nodes
if (childNodes.length == 2) {
log.debug("Removing orphan version history as it contains only two children");
NodeStateEx parentNode = node.getParent();
// Remove version history node
parentNode.removeNode(node.getName());
// store changes for this node and his children
parentNode.store();
} else {
node.store();
}
} else {
log.debug("Current version history has at least one reference");
// store changes
node.store();
}
// now also remove from labelCache
for (Name label : labels) {
labelCache.remove(label);
}
}
/**
* Sets the version <code>label</code> to the given <code>version</code>.
* If the label is already assigned to another version, a VersionException is
* thrown unless <code>move</code> is <code>true</code>. If <code>version</code>
* is <code>null</code>, the label is removed from the respective version.
* In either case, the version the label was previously assigned to is returned,
* or <code>null</code> of the label was not moved.
*
* @param versionName the name of the version
* @param label the label to assign
* @param move flag what to do by collisions
* @return the version that was previously assigned by this label or <code>null</code>.
* @throws VersionException if the version does not exist or if the label is already defined.
*/
synchronized InternalVersion setVersionLabel(Name versionName, Name label, boolean move)
throws VersionException {
InternalVersion version =
(versionName != null) ? getVersion(versionName) : null;
if (versionName != null && version == null) {
throw new VersionException("Version " + versionName + " does not exist in this version history.");
}
Name prevName = labelCache.get(label);
InternalVersionImpl prev = null;
if (prevName == null) {
if (version == null) {
return null;
}
} else {
prev = (InternalVersionImpl) getVersion(prevName);
if (prev.equals(version)) {
return version;
} else if (!move) {
// already defined elsewhere, throw
throw new VersionException("Version label " + label + " already defined for version " + prev.getName());
}
}
// update persistence
try {
if (version == null) {
labelNode.removeProperty(label);
} else {
labelNode.setPropertyValue(
label, InternalValue.create(version.getId()));
}
labelNode.store();
} catch (RepositoryException e) {
throw new VersionException(e);
}
// update internal structures
if (prev != null) {
prev.internalRemoveLabel(label);
labelCache.remove(label);
}
if (version != null) {
labelCache.put(label, version.getName());
((InternalVersionImpl) version).internalAddLabel(label);
}
return prev;
}
/**
* Checks in a node. It creates a new version with the given name and freezes
* the state of the given node.
*
* @param name new version name
* @param src source node to version
* @param created optional created date
* @return the newly created version
* @throws RepositoryException if an error occurs
*/
synchronized InternalVersionImpl checkin(
Name name, NodeStateEx src, Calendar created)
throws RepositoryException {
// copy predecessors from src node
InternalValue[] predecessors;
if (src.hasProperty(NameConstants.JCR_PREDECESSORS)) {
predecessors = src.getPropertyValues(NameConstants.JCR_PREDECESSORS);
// check all predecessors
for (InternalValue pred: predecessors) {
NodeId predId = pred.getNodeId();
// check if version exist
if (!nameCache.containsValue(predId)) {
throw new RepositoryException(
"Invalid predecessor in source node: " + predId);
}
}
} else {
// with simple versioning, the node does not contain a predecessors
// property and we just use the 'head' version as predecessor
Iterator<NodeId> iter = nameCache.values().iterator();
NodeId last = null;
while (iter.hasNext()) {
last = iter.next();
}
if (last == null) {
// should never happen
last = rootVersion.getId();
}
predecessors = new InternalValue[]{InternalValue.create(last)};
}
NodeId versionId = vMgr.getNodeIdFactory().newNodeId();
NodeStateEx vNode = node.addNode(name, NameConstants.NT_VERSION, versionId, true);
// check for jcr:activity
if (src.hasProperty(NameConstants.JCR_ACTIVITY)) {
InternalValue act = src.getPropertyValue(NameConstants.JCR_ACTIVITY);
vNode.setPropertyValue(NameConstants.JCR_ACTIVITY, act);
}
// initialize 'created', 'predecessors' and 'successors'
if (created == null) {
created = getCurrentTime();
}
vNode.setPropertyValue(NameConstants.JCR_CREATED, InternalValue.create(created));
vNode.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, predecessors);
vNode.setPropertyValues(NameConstants.JCR_SUCCESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY);
// checkin source node
InternalFrozenNodeImpl.checkin(vNode, NameConstants.JCR_FROZENNODE, src);
// update version graph
boolean isConfiguration = src.getEffectiveNodeType().includesNodeType(NameConstants.NT_CONFIGURATION);
InternalVersionImpl version = isConfiguration
? new InternalBaselineImpl(this, vNode, name)
: new InternalVersionImpl(this, vNode, name);
version.internalAttach();
// and store
node.store();
vMgr.versionCreated(version);
// update cache
versionCache.put(version.getId(), version);
nameCache.put(version.getName(), version.getId());
return version;
}
/**
* Creates a new version history below the given parent node and with
* the given name.
*
* @param vMgr version manager
* @param parent parent node
* @param name history name
* @param nodeState node state
* @param copiedFrom the id of the base version
* @return new node state
* @throws RepositoryException if an error occurs
*/
static NodeStateEx create(
InternalVersionManagerBase vMgr, NodeStateEx parent, Name name,
NodeState nodeState, NodeId copiedFrom) throws RepositoryException {
// create history node
NodeId historyId = vMgr.getNodeIdFactory().newNodeId();
NodeStateEx pNode = parent.addNode(name, NameConstants.NT_VERSIONHISTORY, historyId, true);
// set the versionable uuid
String versionableUUID = nodeState.getNodeId().toString();
pNode.setPropertyValue(NameConstants.JCR_VERSIONABLEUUID, InternalValue.create(versionableUUID));
// create label node
pNode.addNode(NameConstants.JCR_VERSIONLABELS, NameConstants.NT_VERSIONLABELS, null, false);
// initialize the 'jcr:copiedFrom' property
if (copiedFrom != null) {
pNode.setPropertyValue(NameConstants.JCR_COPIEDFROM, InternalValue.create(copiedFrom, true));
}
// create root version
NodeId versionId = vMgr.getNodeIdFactory().newNodeId();
NodeStateEx vNode = pNode.addNode(NameConstants.JCR_ROOTVERSION, NameConstants.NT_VERSION, versionId, true);
// initialize 'created' and 'predecessors'
vNode.setPropertyValue(NameConstants.JCR_CREATED, InternalValue.create(getCurrentTime()));
vNode.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY);
vNode.setPropertyValues(NameConstants.JCR_SUCCESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY);
// add also an empty frozen node to the root version
NodeStateEx node = vNode.addNode(NameConstants.JCR_FROZENNODE, NameConstants.NT_FROZENNODE, null, true);
// initialize the internal properties
node.setPropertyValue(NameConstants.JCR_FROZENUUID, InternalValue.create(versionableUUID));
node.setPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE,
InternalValue.create(nodeState.getNodeTypeName()));
Set<Name> mixins = nodeState.getMixinTypeNames();
if (!mixins.isEmpty()) {
InternalValue[] ivalues = new InternalValue[mixins.size()];
Iterator<Name> iter = mixins.iterator();
for (int i = 0; i < mixins.size(); i++) {
ivalues[i] = InternalValue.create(iter.next());
}
node.setPropertyValues(NameConstants.JCR_FROZENMIXINTYPES, PropertyType.NAME, ivalues);
}
parent.store(false);
pNode.store(true);
return pNode;
}
/**
* Returns the current time as a calendar instance and makes sure that no
* two Calendar instances represent the exact same time. If this method is
* called quickly in succession each Calendar instance returned is at least
* one millisecond later than the previous one.
*
* @return the current time.
*/
static Calendar getCurrentTime() {
long time = System.currentTimeMillis();
synchronized (CURRENT_TIME) {
if (time > CURRENT_TIME.getTimeInMillis()) {
CURRENT_TIME.setTimeInMillis(time);
} else {
CURRENT_TIME.add(Calendar.MILLISECOND, 1);
}
return (Calendar) CURRENT_TIME.clone();
}
}
}