001/** 002 * Copyright 2015 DuraSpace, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.fcrepo.kernel.impl.services; 017 018import org.fcrepo.kernel.impl.FedoraBinaryImpl; 019import org.fcrepo.kernel.services.VersionService; 020import org.slf4j.Logger; 021import org.springframework.stereotype.Component; 022 023import javax.jcr.Node; 024import javax.jcr.PathNotFoundException; 025import javax.jcr.RepositoryException; 026import javax.jcr.Session; 027import javax.jcr.Workspace; 028import javax.jcr.version.LabelExistsVersionException; 029import javax.jcr.version.Version; 030import javax.jcr.version.VersionException; 031import javax.jcr.version.VersionHistory; 032import javax.jcr.version.VersionIterator; 033import javax.jcr.version.VersionManager; 034 035import static org.fcrepo.kernel.FedoraJcrTypes.VERSIONABLE; 036import static org.slf4j.LoggerFactory.getLogger; 037 038/** 039 * This service exposes management of node versioning. Instead of invoking 040 * the JCR VersionManager methods, this provides a level of indirection that 041 * allows for special handling of features built on top of JCR such as user 042 * transactions. 043 * @author Mike Durbin 044 */ 045 046@Component 047public class VersionServiceImpl extends AbstractService implements VersionService { 048 049 private static final Logger LOGGER = getLogger(VersionService.class); 050 051 @Override 052 public String createVersion(final Session session, 053 final String absPath, final String label) throws RepositoryException { 054 final Node node = session.getNode(absPath); 055 if (!isVersioningEnabled(node)) { 056 enableVersioning(node); 057 } 058 return checkpoint(session, absPath, label); 059 } 060 061 @Override 062 public void revertToVersion(final Session session, final String absPath, 063 final String label) throws RepositoryException { 064 final Workspace workspace = session.getWorkspace(); 065 final Version v = getVersionForLabel(workspace, absPath, label); 066 if (v == null) { 067 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 068 } 069 final VersionManager versionManager = workspace.getVersionManager(); 070 final Version preRevertVersion = versionManager.checkin(absPath); 071 072 try { 073 preRevertVersion.getContainingHistory().addVersionLabel(preRevertVersion.getName(), 074 getPreRevertVersionLabel(label, preRevertVersion.getContainingHistory()), false); 075 } catch (final LabelExistsVersionException e) { 076 // fall-back behavior is to leave an unlabeled version 077 } 078 versionManager.restore(v, true); 079 versionManager.checkout(absPath); 080 } 081 082 /** 083 * When we revert to a version, we snapshot first so that the "revert" action can be undone, 084 * this method generates a label suitable for that snapshot version to make it clear why 085 * it shows up in user's version history. 086 * @param targetLabel 087 * @param history 088 * @return 089 * @throws RepositoryException 090 */ 091 private static String getPreRevertVersionLabel(final String targetLabel, final VersionHistory history) 092 throws RepositoryException { 093 final String baseLabel = "auto-snapshot-before-" + targetLabel; 094 for (int i = 0; i < Integer.MAX_VALUE; i ++) { 095 final String label = baseLabel + (i == 0 ? "" : "-" + i); 096 if (!history.hasVersionLabel(label)) { 097 return label; 098 } 099 } 100 return baseLabel; 101 } 102 103 @Override 104 public void removeVersion(final Session session, final String absPath, 105 final String label) throws RepositoryException { 106 final Workspace workspace = session.getWorkspace(); 107 final Version v = getVersionForLabel(workspace, absPath, label); 108 109 if (v == null) { 110 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 111 } else if (workspace.getVersionManager().getBaseVersion(absPath).equals(v) ) { 112 throw new VersionException("Cannot remove most recent version snapshot."); 113 } else { 114 // remove labels 115 final VersionHistory history = v.getContainingHistory(); 116 final String[] versionLabels = history.getVersionLabels(v); 117 for ( final String versionLabel : versionLabels ) { 118 LOGGER.debug("Removing label: {}", versionLabel); 119 history.removeVersionLabel( versionLabel ); 120 } 121 history.removeVersion( v.getName() ); 122 } 123 } 124 125 126 private static Version getVersionForLabel(final Workspace workspace, final String absPath, 127 final String label) throws RepositoryException { 128 // first see if there's a version label 129 final VersionHistory history = workspace.getVersionManager().getVersionHistory(absPath); 130 131 if (history.hasVersionLabel(label)) { 132 return history.getVersionByLabel(label); 133 } 134 // there was no version with the given JCR Version Label, check to see if 135 // there's a version whose UUID is equal to the label 136 final VersionIterator versionIt = history.getAllVersions(); 137 if (versionIt == null) { 138 return null; 139 } 140 while (versionIt.hasNext()) { 141 final Version v = versionIt.nextVersion(); 142 if (v.getFrozenNode().getIdentifier().equals(label)) { 143 return v; 144 } 145 } 146 return null; 147 } 148 149 private static boolean isVersioningEnabled(final Node n) throws RepositoryException { 150 return n.isNodeType(VERSIONABLE) || (FedoraBinaryImpl.hasMixin(n) && isVersioningEnabled(n.getParent())); 151 } 152 153 private static void enableVersioning(final Node node) throws RepositoryException { 154 node.addMixin(VERSIONABLE); 155 156 if (FedoraBinaryImpl.hasMixin(node)) { 157 node.getParent().addMixin(VERSIONABLE); 158 } 159 node.getSession().save(); 160 } 161 162 private static String checkpoint(final Session session, final String absPath, final String label) 163 throws RepositoryException { 164 LOGGER.trace("Setting version checkpoint for {}", absPath); 165 final Workspace workspace = session.getWorkspace(); 166 final VersionManager versionManager = workspace.getVersionManager(); 167 final VersionHistory versionHistory = versionManager.getVersionHistory(absPath); 168 if (versionHistory.hasVersionLabel(label)) { 169 throw new LabelExistsVersionException("The specified label \"" + label 170 + "\" is already assigned to another version of this resource!"); 171 } 172 final Version v = versionManager.checkpoint(absPath); 173 if (v == null) { 174 return null; 175 } 176 versionHistory.addVersionLabel(v.getName(), label, false); 177 return v.getFrozenNode().getIdentifier(); 178 } 179 180}