| /* |
| * 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(InternalVersionHistory.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(); |
| } |
| |
| /** |
| * Initialies 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 |
| if (rootVersion.getSuccessors().isEmpty()) { |
| for (Name versionName : nameCache.keySet()) { |
| 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 IllegalArgumentException("Failed to create version " + name + "."); |
| } |
| } |
| |
| /** |
| * 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 InternalError("Version does not have a jcr:frozenNode: " + child.getNodeId()); |
| } |
| } |
| return v; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public NodeId getId() { |
| return historyId; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| 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 = new NodeId(); |
| 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 = new NodeId(); |
| 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 = new NodeId(); |
| 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(); |
| 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(); |
| } |
| } |
| } |