Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.version; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.model.ForumModel; import org.alfresco.repo.policy.PolicyScope; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.version.VersionRevertCallback.RevertAspectAction; import org.alfresco.repo.version.VersionRevertCallback.RevertAssocAction; import org.alfresco.repo.version.common.VersionHistoryImpl; import org.alfresco.repo.version.common.VersionImpl; import org.alfresco.repo.version.common.VersionUtil; import org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicy; import org.alfresco.repo.version.traitextender.VersionServiceExtension; import org.alfresco.repo.version.traitextender.VersionServiceTrait; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.repository.AspectMissingException; import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.version.ReservedVersionNameException; import org.alfresco.service.cmr.version.Version; import org.alfresco.service.cmr.version.VersionHistory; import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.cmr.version.VersionServiceException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.traitextender.AJProxyTrait; import org.alfresco.traitextender.Extend; import org.alfresco.traitextender.ExtendedTrait; import org.alfresco.traitextender.Extensible; import org.alfresco.traitextender.Trait; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.extensions.surf.util.ParameterCheck; /** * Version2 Service - implements version2Store (a lighter implementation of the lightWeightVersionStore) */ public class Version2ServiceImpl extends VersionServiceImpl implements VersionService, Version2Model, Extensible { private static Log logger = LogFactory.getLog(Version2ServiceImpl.class); private PermissionService permissionService; private ExtendedTrait<VersionServiceTrait> versionServiceTrait; public Version2ServiceImpl() { versionServiceTrait = new ExtendedTrait<VersionServiceTrait>( AJProxyTrait.create(this, VersionServiceTrait.class)); } public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } /** * Initialise method */ @Override public void initialise() { super.initialise(); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public StoreRef getVersionStoreReference() { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } return new StoreRef(StoreRef.PROTOCOL_WORKSPACE, Version2Model.STORE_ID); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public Version createVersion(NodeRef nodeRef, Map<String, Serializable> versionProperties) throws ReservedVersionNameException, AspectMissingException { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } long startTime = System.currentTimeMillis(); int versionNumber = 0; // deprecated (unused) // Create the version Version version = createVersion(nodeRef, versionProperties, versionNumber); if (logger.isDebugEnabled()) { logger.debug("created version (" + VersionUtil.convertNodeRef(version.getFrozenStateNodeRef()) + ") in " + (System.currentTimeMillis() - startTime) + " ms"); } return version; } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public Collection<Version> createVersion(Collection<NodeRef> nodeRefs, Map<String, Serializable> versionProperties) throws ReservedVersionNameException, AspectMissingException { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } /* * Note: we can't control the order of the list, so if we have children and parents in the list and the * parents get versioned before the children and the children are not already versioned then the parents * child references will be pointing to the node ref, rather than the version history. */ long startTime = System.currentTimeMillis(); Collection<Version> result = new ArrayList<Version>(nodeRefs.size()); int versionNumber = 0; // deprecated (unused) // Version each node in the list for (NodeRef nodeRef : nodeRefs) { result.add(createVersion(nodeRef, versionProperties, versionNumber)); } if (logger.isDebugEnabled()) { logger.debug("created version list (" + getVersionStoreReference() + ") in " + (System.currentTimeMillis() - startTime) + " ms (with " + nodeRefs.size() + " nodes)"); } return result; } @Override protected Version createVersion(NodeRef nodeRef, Map<String, Serializable> origVersionProperties, int versionNumber) throws ReservedVersionNameException { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } long startTime = System.currentTimeMillis(); // Copy the version properties (to prevent unexpected side effects to the caller) Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(); if (origVersionProperties != null) { versionProperties.putAll(origVersionProperties); } // We don't want either the adding of the Versionable aspect, or the setting // of the version label, to affect the auditable properties on the node that // is being versioned. // So, disable the auditable aspect on that node for now policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); // If the version aspect is not there then add it to the 'live' (versioned) node if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false) { // Add the versionable aspect to the node this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null); } // Call the policy behaviour invokeBeforeCreateVersion(nodeRef); // version "description" property is added as a standard version property (if not null) rather than a metadata version property String versionDescription = (String) versionProperties.get(Version.PROP_DESCRIPTION); versionProperties.remove(Version.PROP_DESCRIPTION); // don'tfreeze previous version label versionProperties.remove(ContentModel.PROP_VERSION_LABEL); // Check that the supplied additional version properties do not clash with the reserved ones VersionUtil.checkVersionPropertyNames(versionProperties.keySet()); // Check the repository for the version history for this node NodeRef versionHistoryRef = getVersionHistoryNodeRef(nodeRef); NodeRef currentVersionRef = null; Version currentVersion = null; if (versionHistoryRef == null) { // Create the version history versionHistoryRef = createVersionHistory(nodeRef); } else { // ALF-3962 fix // check for corrupted version histories that are marked with version label "0" checkForCorruptedVersions(versionHistoryRef, nodeRef); // Since we have an existing version history we should be able to lookup // the current version Pair<Boolean, Version> result = getCurrentVersionImpl(versionHistoryRef, nodeRef); boolean headVersion = false; if (result != null) { currentVersion = result.getSecond(); headVersion = result.getFirst(); } if (currentVersion == null) { throw new VersionServiceException(MSGID_ERR_NOT_FOUND); } // Need to check that we are not about to create branch since this is not currently supported if (!headVersion) { // belt-and-braces - remove extra check at some point // although child assocs should be in ascending time (hence version creation) order VersionHistory versionHistory = buildVersionHistory(versionHistoryRef, nodeRef); if (versionHistory.getSuccessors(currentVersion).size() == 0) { if (logger.isDebugEnabled()) { logger.debug("Belt-and-braces: current version does seem to be head version [" + versionHistoryRef + ", " + nodeRef + "]"); } } else { throw new VersionServiceException(MSGID_ERR_NO_BRANCHES); } } } // Create the node details QName classRef = this.nodeService.getType(nodeRef); PolicyScope nodeDetails = new PolicyScope(classRef); // Get the node details by calling the onVersionCreate policy behaviour invokeOnCreateVersion(nodeRef, versionProperties, nodeDetails); // Calculate the version label String versionLabel = invokeCalculateVersionLabel(classRef, currentVersion, versionNumber, versionProperties); // Extract Type Definition QName sourceTypeRef = nodeService.getType(nodeRef); long nodeDbId = (Long) this.nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_DBID); Set<QName> nodeAspects = this.nodeService.getAspects(nodeRef); // Create the new version node (child of the version history) NodeRef newVersionRef = createNewVersion( sourceTypeRef, versionHistoryRef, getStandardVersionProperties(nodeRef, nodeDbId, nodeAspects, versionNumber, versionLabel, versionDescription), versionProperties, versionNumber, nodeDetails); if (currentVersionRef == null) { // Set the new version to be the root version in the version history this.dbNodeService.createAssociation(versionHistoryRef, newVersionRef, Version2Model.ASSOC_ROOT_VERSION); } // Create the version data object Version version = getVersion(newVersionRef); // Disabling behavior to be able to create properties for a locked node, see ALF-16540 policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_LOCKABLE); try { // Set the new version label on the 'live' (versioned) node this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, version.getVersionLabel()); // Set the version type (MNT-14681 fix) this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_TYPE, version.getVersionType()); } finally { policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_LOCKABLE); } // Re-enable the auditable aspect (if we turned it off earlier) policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); // Invoke the policy behaviour invokeAfterCreateVersion(nodeRef, version); if (logger.isTraceEnabled()) { logger.trace("created version (" + getVersionStoreReference() + ") " + newVersionRef + " " + (System.currentTimeMillis() - startTime) + " ms"); } // Return the data object representing the newly created version return version; } /** * Creates a new version history node, applying the root version aspect is required * * @param nodeRef the node ref * @return the version history node reference */ protected NodeRef createVersionHistory(NodeRef nodeRef) { long start = System.currentTimeMillis(); HashMap<QName, Serializable> props = new HashMap<QName, Serializable>(); props.put(ContentModel.PROP_NAME, nodeRef.getId()); props.put(Version2Model.PROP_QNAME_VERSIONED_NODE_ID, nodeRef.getId()); // Create a new version history node ChildAssociationRef childAssocRef = this.dbNodeService.createNode(getRootNode(), Version2Model.CHILD_QNAME_VERSION_HISTORIES, QName.createQName(Version2Model.NAMESPACE_URI, nodeRef.getId()), Version2Model.TYPE_QNAME_VERSION_HISTORY, props); if (logger.isTraceEnabled()) { logger.trace("created version history nodeRef: " + childAssocRef.getChildRef() + " for " + nodeRef + " in " + (System.currentTimeMillis() - start) + " ms"); } return childAssocRef.getChildRef(); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public VersionHistory getVersionHistory(NodeRef nodeRef) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } VersionHistory versionHistory = null; // Get the version history regardless of whether the node is still 'live' or not NodeRef versionHistoryRef = getVersionHistoryNodeRef(nodeRef); if (versionHistoryRef != null) { versionHistory = buildVersionHistory(versionHistoryRef, nodeRef); } return versionHistory; } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public Version getCurrentVersion(NodeRef nodeRef) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } Version version = null; // get the current version, if the 'live' (versioned) node has the "versionable" aspect if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) { NodeRef versionHistoryRef = getVersionHistoryNodeRef(nodeRef); if (versionHistoryRef != null) { Pair<Boolean, Version> result = getCurrentVersionImpl(versionHistoryRef, nodeRef); if (result != null) { version = result.getSecond(); } } } return version; } /** * Get a map containing the standard list of version properties populated. * * @param nodeRef the node reference * @return the standard version properties */ protected Map<QName, Serializable> getStandardVersionProperties(NodeRef nodeRef, long nodeDbId, Set<QName> nodeAspects, int versionNumber, String versionLabel, String versionDescription) { Map<QName, Serializable> result = new HashMap<QName, Serializable>(10); // deprecated (unused) //result.put(Version2Model.PROP_QNAME_VERSION_NUMBER, versionNumber); // Set the version label result.put(Version2Model.PROP_QNAME_VERSION_LABEL, versionLabel); // Set the version description (can be null) result.put(Version2Model.PROP_QNAME_VERSION_DESCRIPTION, versionDescription); // Set the versionable node store id result.put(Version2Model.PROP_QNAME_FROZEN_NODE_REF, nodeRef); // Set the versionable node store id result.put(Version2Model.PROP_QNAME_FROZEN_NODE_DBID, nodeDbId); return result; } /** * Creates a new version node, setting the properties both calculated and specified. * * @param sourceTypeRef the reference to the node being versioned * @param versionHistoryRef version history node reference * @param standardVersionProperties version properties * @param versionProperties version properties * @param versionNumber the version number * @param nodeDetails PolicyScope * @return the version node reference */ protected NodeRef createNewVersion(QName sourceTypeRef, NodeRef versionHistoryRef, Map<QName, Serializable> standardVersionProperties, Map<String, Serializable> versionProperties, int versionNumber, PolicyScope nodeDetails) { ChildAssociationRef childAssocRef = null; // Disable auto-version behaviour this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE); this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_MULTILINGUAL_DOCUMENT); this.policyBehaviourFilter.disableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER); NodeRef versionNodeRef = null; try { // "copy" type and properties childAssocRef = this.dbNodeService.createNode(versionHistoryRef, Version2Model.CHILD_QNAME_VERSIONS, QName.createQName(Version2Model.NAMESPACE_URI, Version2Model.CHILD_VERSIONS + "-" + versionNumber), // TODO - testing - note: all children (of a versioned node) will have the same version number, maybe replace with a version sequence of some sort 001-...00n sourceTypeRef, nodeDetails.getProperties()); versionNodeRef = childAssocRef.getChildRef(); // NOTE: special ML case - see also MultilingualContentServiceImpl.makeMLContainer if (sourceTypeRef.equals(ContentModel.TYPE_MULTILINGUAL_CONTAINER)) { // Set the permissions to allow anything by anyone permissionService.setPermission(versionNodeRef, PermissionService.ALL_AUTHORITIES, PermissionService.ALL_PERMISSIONS, true); permissionService.setPermission(versionNodeRef, AuthenticationUtil.getGuestUserName(), PermissionService.ALL_PERMISSIONS, true); } // add aspect with the standard version properties to the 'version' node nodeService.addAspect(versionNodeRef, Version2Model.ASPECT_VERSION, standardVersionProperties); // store the meta data storeVersionMetaData(versionNodeRef, versionProperties); freezeChildAssociations(versionNodeRef, nodeDetails.getChildAssociations()); freezeAssociations(versionNodeRef, nodeDetails.getAssociations()); freezeAspects(nodeDetails, versionNodeRef, nodeDetails.getAspects()); } finally { // Enable auto-version behaviour this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE); this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_MULTILINGUAL_DOCUMENT); this.policyBehaviourFilter.enableBehaviour(ContentModel.TYPE_MULTILINGUAL_CONTAINER); } // If the auditable aspect is not there then add it to the 'version' node (after original aspects have been frozen) if (dbNodeService.hasAspect(versionNodeRef, ContentModel.ASPECT_AUDITABLE) == false) { dbNodeService.addAspect(versionNodeRef, ContentModel.ASPECT_AUDITABLE, null); } if (logger.isTraceEnabled()) { logger.trace("newVersion created (" + versionNumber + ") " + versionNodeRef); } // Return the created node reference return versionNodeRef; } /** * Store the version meta data * * @param versionNodeRef the version node reference * @param versionProperties the version properties */ private void storeVersionMetaData(NodeRef versionNodeRef, Map<String, Serializable> versionProperties) { // TODO - these are stored as "residual" properties (ie. without property type) - see also NodeBrowser // - need to review, eg. how can we store arbitrary map of metadata properties, that could be indexed/searched (if configured against versionStore) for (Map.Entry<String, Serializable> entry : versionProperties.entrySet()) { dbNodeService.setProperty(versionNodeRef, QName.createQName(Version2Model.NAMESPACE_URI, Version2Model.PROP_METADATA_PREFIX + entry.getKey()), entry.getValue()); } } /** * Freeze the aspects * * @param nodeDetails the node details * @param versionNodeRef the version node reference * @param aspects the set of aspects */ private void freezeAspects(PolicyScope nodeDetails, NodeRef versionNodeRef, Set<QName> aspects) { for (QName aspect : aspects) { if (logger.isTraceEnabled()) { logger.trace("freezeAspect: " + versionNodeRef + " " + aspect); } if (aspect.equals(ContentModel.ASPECT_AUDITABLE)) { // freeze auditable aspect properties (eg. created, creator, modifed, modifier, accessed) for (Map.Entry<QName, Serializable> entry : nodeDetails.getProperties(aspect).entrySet()) { if (entry.getKey().equals(ContentModel.PROP_CREATOR)) { dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_FROZEN_CREATOR, entry.getValue()); } else if (entry.getKey().equals(ContentModel.PROP_CREATED)) { dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_FROZEN_CREATED, entry.getValue()); } else if (entry.getKey().equals(ContentModel.PROP_MODIFIER)) { dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_FROZEN_MODIFIER, entry.getValue()); } else if (entry.getKey().equals(ContentModel.PROP_MODIFIED)) { dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_FROZEN_MODIFIED, entry.getValue()); } else if (entry.getKey().equals(ContentModel.PROP_ACCESSED)) { dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_FROZEN_ACCESSED, entry.getValue()); } else { throw new AlfrescoRuntimeException("Unexpected auditable property: " + entry.getKey()); } } } else { // Freeze the details of the aspect dbNodeService.addAspect(versionNodeRef, aspect, nodeDetails.getProperties(aspect)); } // ALF-9638: Freeze the aspect specific associations freezeChildAssociations(versionNodeRef, nodeDetails.getChildAssociations(aspect)); freezeAssociations(versionNodeRef, nodeDetails.getAssociations(aspect)); } } /** * Freeze child associations * * @param versionNodeRef the version node reference * @param childAssociations the child associations */ private void freezeChildAssociations(NodeRef versionNodeRef, List<ChildAssociationRef> childAssociations) { for (ChildAssociationRef childAssocRef : childAssociations) { HashMap<QName, Serializable> properties = new HashMap<QName, Serializable>(); NodeRef childRef = childAssocRef.getChildRef(); QName sourceTypeRef = nodeService.getType(childRef); // Set the reference property to point to the child node properties.put(ContentModel.PROP_REFERENCE, childRef); // Create child version reference dbNodeService.createNode(versionNodeRef, childAssocRef.getTypeQName(), childAssocRef.getQName(), sourceTypeRef, properties); } } /** * Freeze associations * * @param versionNodeRef the version node reference * @param associations the list of associations * * @since 3.3 (Ent) */ private void freezeAssociations(NodeRef versionNodeRef, List<AssociationRef> associations) { for (AssociationRef targetAssocRef : associations) { HashMap<QName, Serializable> properties = new HashMap<QName, Serializable>(); QName sourceTypeRef = nodeService.getType(targetAssocRef.getSourceRef()); NodeRef targetRef = targetAssocRef.getTargetRef(); // Set the reference property to point to the target node properties.put(ContentModel.PROP_REFERENCE, targetRef); properties.put(Version2Model.PROP_QNAME_ASSOC_DBID, targetAssocRef.getId()); // Create peer version reference dbNodeService.createNode(versionNodeRef, Version2Model.CHILD_QNAME_VERSIONED_ASSOCS, targetAssocRef.getTypeQName(), sourceTypeRef, properties); } } /** * Gets all versions in version history * * @param versionHistoryRef the version history nodeRef * @return list of all versions */ protected List<Version> getAllVersions(NodeRef versionHistoryRef) { List<ChildAssociationRef> versionAssocs = getVersionAssocs(versionHistoryRef, true); List<Version> versions = new ArrayList<Version>(versionAssocs.size()); for (ChildAssociationRef versionAssoc : versionAssocs) { versions.add(getVersion(versionAssoc.getChildRef())); } return versions; } private List<ChildAssociationRef> getVersionAssocs(NodeRef versionHistoryRef, boolean preLoad) { // note: resultant list is ordered by (a) explicit index and (b) association creation time return dbNodeService.getChildAssocs(versionHistoryRef, Version2Model.CHILD_QNAME_VERSIONS, RegexQNamePattern.MATCH_ALL, preLoad); } /** * Builds a version history object from the version history reference. * <p> * The node ref is passed to enable the version history to be scoped to the * appropriate branch in the version history. * * @param versionHistoryRef the node ref for the version history * @param nodeRef the node reference * @return a constructed version history object */ @Override protected VersionHistory buildVersionHistory(NodeRef versionHistoryRef, NodeRef nodeRef) { VersionHistory versionHistory = null; // List of versions with current one last and root one first. List<Version> versions = getAllVersions(versionHistoryRef); if (versionComparatorDesc != null) { Collections.sort(versions, Collections.reverseOrder(versionComparatorDesc)); } // Build the version history object boolean isRoot = true; Version preceeding = null; for (Version version : versions) { if (isRoot == true) { versionHistory = new VersionHistoryImpl(version, versionComparatorDesc); isRoot = false; } else { ((VersionHistoryImpl) versionHistory).addVersion(version, preceeding); } preceeding = version; } return versionHistory; } /** * Constructs the a version object to contain the version information from the version node ref. * * @param versionRef the version reference * @return object containing verison data */ @Override protected Version getVersion(NodeRef versionRef) { if (versionRef == null) { return null; } Map<String, Serializable> versionProperties = new HashMap<String, Serializable>(); // Get the standard node details and get the meta data Map<QName, Serializable> nodeProperties = this.dbNodeService.getProperties(versionRef); if (logger.isTraceEnabled()) { logger.trace("getVersion: " + versionRef + " nodeProperties=\n" + nodeProperties.keySet()); } // TODO consolidate with VersionUtil.convertFrozenToOriginalProps for (QName key : nodeProperties.keySet()) { Serializable value = nodeProperties.get(key); String keyName = key.getLocalName(); int idx = keyName.indexOf(Version2Model.PROP_METADATA_PREFIX); if (idx == 0) { // versioned metadata property - additional (optional) metadata, set during versioning versionProperties.put(keyName.substring(Version2Model.PROP_METADATA_PREFIX.length()), value); } else { if (key.equals(Version2Model.PROP_QNAME_VERSION_DESCRIPTION)) { versionProperties.put(Version.PROP_DESCRIPTION, (String) value); } else if (key.equals(Version2Model.PROP_QNAME_VERSION_LABEL)) { versionProperties.put(VersionBaseModel.PROP_VERSION_LABEL, (String) value); } else { if (keyName.equals(Version.PROP_DESCRIPTION) || keyName.equals(VersionBaseModel.PROP_VERSION_LABEL)) { // ignore reserved localname (including cm:description, cm:versionLabel) } else { // all other properties versionProperties.put(keyName, value); } } } } // Create and return the version object NodeRef newNodeRef = new NodeRef(new StoreRef(Version2Model.STORE_PROTOCOL, Version2Model.STORE_ID), versionRef.getId()); Version result = new VersionImpl(versionProperties, newNodeRef); if (logger.isTraceEnabled()) { logger.trace("getVersion: " + versionRef + " versionProperties=\n" + versionProperties.keySet()); } // done return result; } /** * Gets a reference to the version history node for a given 'real' node. * * @param nodeRef a node reference * @return a reference to the version history node, null of none */ @Override protected NodeRef getVersionHistoryNodeRef(NodeRef nodeRef) { // assume noderef is a 'live' node NodeRef vhNodeRef = this.dbNodeService.getChildByName(getRootNode(), Version2Model.CHILD_QNAME_VERSION_HISTORIES, nodeRef.getId()); // DEPRECATED: for backwards compatibility, in case of a version node (eg. via getCurrentVersion) can lookup 'live' node via UUID if (vhNodeRef == null) { if (nodeService.exists(nodeRef)) { vhNodeRef = this.dbNodeService.getChildByName(getRootNode(), Version2Model.CHILD_QNAME_VERSION_HISTORIES, (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_UUID)); } } return vhNodeRef; } /** * Gets current version of the passed node ref * * This uses the version label as a mechanism for looking up the version node. */ private Pair<Boolean, Version> getCurrentVersionImpl(NodeRef versionHistoryRef, NodeRef nodeRef) { Pair<Boolean, Version> result = null; String versionLabel = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); // note: resultant list is ordered by (a) explicit index and (b) association creation time List<ChildAssociationRef> versionAssocs = getVersionAssocs(versionHistoryRef, false); // Current version should be head version (since no branching) int cnt = versionAssocs.size(); for (int i = cnt; i > 0; i--) { ChildAssociationRef versionAssoc = versionAssocs.get(i - 1); String tempLabel = (String) this.dbNodeService.getProperty(versionAssoc.getChildRef(), Version2Model.PROP_QNAME_VERSION_LABEL); if (tempLabel != null && tempLabel.equals(versionLabel) == true) { boolean headVersion = (i == cnt); if (!headVersion) { throw new ConcurrencyFailureException( "Unexpected: current version does not appear to be 1st version in the list [" + versionHistoryRef + ", " + nodeRef + "]"); } result = new Pair<Boolean, Version>(headVersion, getVersion(versionAssoc.getChildRef())); break; } } return result; } /** * Check if versions are marked with invalid version label, if true > apply default serial version label (e.g. "1.0", "1.1") * * @param versionHistory a version histore node reference * @param nodeRef a node reference */ private void checkForCorruptedVersions(NodeRef versionHistory, NodeRef nodeRef) { // get the current version label in live store String versionLabel = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); if (versionLabel != null && versionLabel.equals("0")) { // need to correct version labels List<Version> versions = getAllVersions(versionHistory); // sort versions by node id Collections.sort(versions, new Comparator<Version>() { public int compare(Version v1, Version v2) { int result = v1.getFrozenModifiedDate().compareTo(v2.getFrozenModifiedDate()); if (result == 0) { Long dbid1 = (Long) nodeService.getProperty(v1.getFrozenStateNodeRef(), ContentModel.PROP_NODE_DBID); Long dbid2 = (Long) nodeService.getProperty(v2.getFrozenStateNodeRef(), ContentModel.PROP_NODE_DBID); if (dbid1 != null && dbid2 != null) { result = dbid1.compareTo(dbid2); } else { result = 0; if (logger.isWarnEnabled()) { logger.warn("node-dbid property is missing for versions: " + v1.toString() + " or " + v2.toString()); } } } return result; } }); SerialVersionLabelPolicy serialVersionLabelPolicy = new SerialVersionLabelPolicy(); QName classRef = this.nodeService.getType(nodeRef); Version preceedingVersion = null; for (Version version : versions) { // re-calculate version label versionLabel = serialVersionLabelPolicy.calculateVersionLabel(classRef, preceedingVersion, 0, version.getVersionProperties()); // update version with new version label NodeRef versionNodeRef = new NodeRef(StoreRef.PROTOCOL_WORKSPACE, version.getFrozenStateNodeRef().getStoreRef().getIdentifier(), version.getFrozenStateNodeRef().getId()); this.dbNodeService.setProperty(versionNodeRef, Version2Model.PROP_QNAME_VERSION_LABEL, versionLabel); version.getVersionProperties().put(VersionBaseModel.PROP_VERSION_LABEL, versionLabel); // remember preceding version preceedingVersion = version; } // update current version label in live store this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, versionLabel); } } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void revert(NodeRef nodeRef) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } revert(nodeRef, getCurrentVersion(nodeRef), true); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void revert(NodeRef nodeRef, boolean deep) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } revert(nodeRef, getCurrentVersion(nodeRef), deep); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void revert(NodeRef nodeRef, Version version) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } revert(nodeRef, version, true); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void revert(NodeRef nodeRef, Version version, boolean deep) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } if (logger.isDebugEnabled()) { logger.debug("revert nodeRef:" + nodeRef); } // Check the mandatory parameters ParameterCheck.mandatory("nodeRef", nodeRef); ParameterCheck.mandatory("version", version); // Cross check that the version provided relates to the node reference provided if (nodeRef.getId().equals( ((NodeRef) version.getVersionProperty(Version2Model.PROP_FROZEN_NODE_REF)).getId()) == false) { // Error since the version provided does not correspond to the node reference provided throw new VersionServiceException(MSGID_ERR_REVERT_MISMATCH); } // Turn off any auto-version policy behaviours this.policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); try { // The current (old) values Map<QName, Serializable> oldProps = this.nodeService.getProperties(nodeRef); Set<QName> oldAspectQNames = this.nodeService.getAspects(nodeRef); QName oldNodeTypeQName = nodeService.getType(nodeRef); // Store the current version label String currentVersionLabel = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL); // The frozen (which will become new) values // Get the node that represents the frozen state NodeRef versionNodeRef = version.getFrozenStateNodeRef(); boolean needToRestoreDiscussion = !this.nodeService.hasAspect(versionNodeRef, ForumModel.ASPECT_DISCUSSABLE) && this.nodeService.hasAspect(nodeRef, ForumModel.ASPECT_DISCUSSABLE); Map<QName, Serializable> forumProps = null; // always collect forum properties Map<QName, Serializable> currentVersionProp = this.nodeService.getProperties(nodeRef); forumProps = new HashMap<QName, Serializable>(); for (QName key : currentVersionProp.keySet()) { if (key.getNamespaceURI().equals(NamespaceService.FORUMS_MODEL_1_0_URI)) { forumProps.put(key, currentVersionProp.get(key)); } } Map<QName, Serializable> newProps = this.nodeService.getProperties(versionNodeRef); VersionUtil.convertFrozenToOriginalProps(newProps); Set<QName> newAspectQNames = this.nodeService.getAspects(versionNodeRef); QName newNodeTypeQName = nodeService.getType(versionNodeRef); // RevertDetails - given to policy behaviours VersionRevertDetailsImpl revertDetails = new VersionRevertDetailsImpl(); revertDetails.setNodeRef(nodeRef); revertDetails.setNodeType(newNodeTypeQName); // Do we want to maintain any existing property values? Collection<QName> propsToLeaveAlone = new ArrayList<QName>(); Collection<QName> assocsToLeaveAlone = new ArrayList<QName>(); // The VersionRevertCallback was added in r50122 on HEAD-QA branch in ACE-1001 // If it is required to preserve this callback when the type is changed, // this part should be reimplemented. // see MNT-14688 if (newNodeTypeQName.equals(oldNodeTypeQName)) { // The node did not change the type, check the associations TypeDefinition typeDef = dictionaryService.getType(oldNodeTypeQName); if (typeDef != null) { for (QName assocName : typeDef.getAssociations().keySet()) { if (getRevertAssocAction(oldNodeTypeQName, assocName, revertDetails) == RevertAssocAction.IGNORE) { assocsToLeaveAlone.add(assocName); } } } } for (QName aspect : oldAspectQNames) { AspectDefinition aspectDef = dictionaryService.getAspect(aspect); if (aspectDef != null) { if (getRevertAspectAction(aspect, revertDetails) == RevertAspectAction.IGNORE) { propsToLeaveAlone.addAll(aspectDef.getProperties().keySet()); } for (QName assocName : aspectDef.getAssociations().keySet()) { if (getRevertAssocAction(aspect, assocName, revertDetails) == RevertAssocAction.IGNORE) { assocsToLeaveAlone.addAll(aspectDef.getAssociations().keySet()); } } } } for (QName prop : propsToLeaveAlone) { if (oldProps.containsKey(prop)) { newProps.put(prop, oldProps.get(prop)); } } this.nodeService.setProperties(nodeRef, newProps); //Restore forum properties this.nodeService.addProperties(nodeRef, forumProps); // Restore the type this.nodeService.setType(nodeRef, newNodeTypeQName); Set<QName> aspectsToRemove = new HashSet<QName>(oldAspectQNames); aspectsToRemove.removeAll(newAspectQNames); Set<QName> aspectsToAdd = new HashSet<QName>(newAspectQNames); aspectsToAdd.removeAll(oldAspectQNames); // add aspects that are not on the current node for (QName aspect : aspectsToAdd) { if (getRevertAspectAction(aspect, revertDetails) != RevertAspectAction.IGNORE) { this.nodeService.addAspect(nodeRef, aspect, null); } } // Don't remove forum aspects if (needToRestoreDiscussion) { Set<QName> ignoredAspects = new HashSet<QName>(); for (QName aspectForCheck : aspectsToRemove) { if (aspectForCheck.getNamespaceURI().equals(NamespaceService.FORUMS_MODEL_1_0_URI)) { ignoredAspects.add(aspectForCheck); } } aspectsToRemove.removeAll(ignoredAspects); } // remove aspects that are not on the frozen node for (QName aspect : aspectsToRemove) { if (getRevertAspectAction(aspect, revertDetails) != RevertAspectAction.IGNORE) { this.nodeService.removeAspect(nodeRef, aspect); } } // Re-add the versionable aspect to the reverted node if (this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false) { this.nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null); } // Re-set the version label property (since it should not be modified from the original) this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, currentVersionLabel); // Add/remove the child nodes List<ChildAssociationRef> children = new ArrayList<ChildAssociationRef>( this.nodeService.getChildAssocs(nodeRef)); List<ChildAssociationRef> versionedChildren = this.nodeService.getChildAssocs(versionNodeRef); for (ChildAssociationRef versionedChild : versionedChildren) { if (children.contains(versionedChild) == false) { NodeRef childRef = null; ChildAssociationRef assocToKeep = null; if (this.nodeService.exists(versionedChild.getChildRef()) == true) { // The node was a primary child of the parent, but that is no longer the case. Despite this // the node still exits so this means it has been moved. // The best thing to do in this situation will be to re-add the node as a child, but it will not // be a primary child String childRefName = (String) this.nodeService.getProperty(versionedChild.getChildRef(), ContentModel.PROP_NAME); childRef = this.nodeService.getChildByName(nodeRef, versionedChild.getTypeQName(), childRefName); // we can faced with association that allow duplicate names if (childRef == null) { List<ChildAssociationRef> allAssocs = nodeService.getParentAssocs( versionedChild.getChildRef(), versionedChild.getTypeQName(), RegexQNamePattern.MATCH_ALL); for (ChildAssociationRef assocToCheck : allAssocs) { if (children.contains(assocToCheck)) { childRef = assocToCheck.getChildRef(); assocToKeep = assocToCheck; break; } } } if (childRef == null) { childRef = this.nodeService.addChild(nodeRef, versionedChild.getChildRef(), versionedChild.getTypeQName(), versionedChild.getQName()).getChildRef(); } } else { if (versionedChild.isPrimary() == true) { // Only try to restore missing children if we are doing a deep revert // Look and see if we have a version history for the child node if (deep == true && getVersionHistoryNodeRef(versionedChild.getChildRef()) != null) { // We're going to try and restore the missing child node and recreate the assoc childRef = restore(versionedChild.getChildRef(), nodeRef, versionedChild.getTypeQName(), versionedChild.getQName()); } // else the deleted child did not have a version history so we can't restore the child // and so we can't revert the association } // else // Since this was never a primary assoc and the child has been deleted we won't recreate // the missing node as it was never owned by the node and we wouldn't know where to put it. } if (childRef != null) { if (assocToKeep != null) { children.remove(assocToKeep); } else { children.remove(nodeService.getPrimaryParent(childRef)); } } } else { children.remove(versionedChild); } } // Don't remove forum children if (needToRestoreDiscussion) { List<ChildAssociationRef> ignoredChildren = new ArrayList<ChildAssociationRef>(); for (ChildAssociationRef childForCheck : children) { if (childForCheck.getTypeQName().getNamespaceURI() .equals(NamespaceService.FORUMS_MODEL_1_0_URI)) { ignoredChildren.add(childForCheck); } } children.removeAll(ignoredChildren); } for (ChildAssociationRef ref : children) { if (!assocsToLeaveAlone.contains(ref.getTypeQName())) { this.nodeService.removeChild(nodeRef, ref.getChildRef()); } } // Add/remove the target associations for (AssociationRef assocRef : this.nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL)) { if (!assocsToLeaveAlone.contains(assocRef.getTypeQName())) { this.nodeService.removeAssociation(assocRef.getSourceRef(), assocRef.getTargetRef(), assocRef.getTypeQName()); } } for (AssociationRef versionedAssoc : this.nodeService.getTargetAssocs(versionNodeRef, RegexQNamePattern.MATCH_ALL)) { if (!assocsToLeaveAlone.contains(versionedAssoc.getTypeQName())) { if (this.nodeService.exists(versionedAssoc.getTargetRef()) == true) { this.nodeService.createAssociation(nodeRef, versionedAssoc.getTargetRef(), versionedAssoc.getTypeQName()); } } // else // Since the target of the assoc no longer exists we can't recreate the assoc } } finally { // Turn auto-version policies back on this.policyBehaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); } invokeAfterVersionRevert(nodeRef, version); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public NodeRef restore(NodeRef nodeRef, NodeRef parentNodeRef, QName assocTypeQName, QName assocQName) { return restore(nodeRef, parentNodeRef, assocTypeQName, assocQName, true); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public NodeRef restore(NodeRef nodeRef, NodeRef parentNodeRef, QName assocTypeQName, QName assocQName, boolean deep) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } NodeRef restoredNodeRef = null; // Check that the node does not exist if (this.nodeService.exists(nodeRef) == true) { // Error since you can not restore a node that already exists throw new VersionServiceException(MSGID_ERR_RESTORE_EXISTS, new Object[] { nodeRef.toString() }); } // Try and get the version details that we want to restore to Version version = getHeadVersion(nodeRef); if (version == null) { // Error since there is no version information available to restore the node from throw new VersionServiceException(MSGID_ERR_RESTORE_NO_VERSION, new Object[] { nodeRef.toString() }); } // Set the uuid of the new node Map<QName, Serializable> props = new HashMap<QName, Serializable>(1); props.put(ContentModel.PROP_NODE_UUID, ((NodeRef) version.getVersionProperty(Version2Model.PROP_FROZEN_NODE_REF)).getId()); props.put(ContentModel.PROP_VERSION_LABEL, version.getVersionLabel()); // Get the type of the frozen node QName type = (QName) dbNodeService.getType(VersionUtil.convertNodeRef(version.getFrozenStateNodeRef())); // Disable auto-version behaviour this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE); try { // Create the restored node restoredNodeRef = this.nodeService.createNode(parentNodeRef, assocTypeQName, assocQName, type, props) .getChildRef(); } finally { // Enable auto-version behaviour this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE); } // Now we need to revert the newly restored node revert(restoredNodeRef, version, deep); return restoredNodeRef; } /** * Get the head version given a node reference * * @param nodeRef the node reference * @return the 'head' version */ private Version getHeadVersion(NodeRef nodeRef) { NodeRef versionHistoryNodeRef = getVersionHistoryNodeRef(nodeRef); Version headVersion = null; if (versionHistoryNodeRef != null) { VersionHistory versionHistory = buildVersionHistory(versionHistoryNodeRef, nodeRef); if (versionHistory != null) { headVersion = versionHistory.getHeadVersion(); } } return headVersion; } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void deleteVersionHistory(NodeRef nodeRef) throws AspectMissingException { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } // Get the version history node for the node is question and delete it NodeRef versionHistoryNodeRef = getVersionHistoryNodeRef(nodeRef); if (versionHistoryNodeRef != null) { try { // Disable auto-version behaviour this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE); // Delete the version history node this.dbNodeService.deleteNode(versionHistoryNodeRef); if (this.nodeService.exists(nodeRef) == true && this.nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == true) { // Reset the version label property on the versionable node this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, null); } } finally { this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE); } } } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public void deleteVersion(NodeRef nodeRef, Version version) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } // Check the mandatory parameters ParameterCheck.mandatory("nodeRef", nodeRef); ParameterCheck.mandatory("version", version); Version currentVersion = getCurrentVersion(nodeRef); // Delete the version node this.dbNodeService.deleteNode(VersionUtil.convertNodeRef(version.getFrozenStateNodeRef())); if (currentVersion.getVersionLabel().equals(version.getVersionLabel())) { Version headVersion = getHeadVersion(nodeRef); if (headVersion != null) { // Reset the version label property on the versionable node to new head version // Disable the VersionableAspect for this change though, we don't want // to have this create a new version for the property change! policyBehaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); this.nodeService.setProperty(nodeRef, ContentModel.PROP_VERSION_LABEL, headVersion.getVersionLabel()); } else { deleteVersionHistory(nodeRef); } } } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public boolean isAVersion(NodeRef nodeRef) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } NodeRef realNodeRef = nodeRef; if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL)) { realNodeRef = VersionUtil.convertNodeRef(nodeRef); } return this.dbNodeService.hasAspect(realNodeRef, Version2Model.ASPECT_VERSION); } @Override @Extend(extensionAPI = VersionServiceExtension.class, traitAPI = VersionServiceTrait.class) public boolean isVersioned(NodeRef nodeRef) { if (logger.isDebugEnabled()) { logger.debug("Run as user " + AuthenticationUtil.getRunAsUser()); logger.debug("Fully authenticated " + AuthenticationUtil.getFullyAuthenticatedUser()); } NodeRef realNodeRef = nodeRef; if (nodeRef.getStoreRef().getProtocol().equals(VersionBaseModel.STORE_PROTOCOL)) { realNodeRef = VersionUtil.convertNodeRef(nodeRef); } return this.dbNodeService.hasAspect(realNodeRef, ContentModel.ASPECT_VERSIONABLE); } @Override public <T extends Trait> ExtendedTrait<T> getTrait(Class<? extends T> traitAPI) { return (ExtendedTrait<T>) versionServiceTrait; } }