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 java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import static org.fcrepo.kernel.FedoraJcrTypes.VERSIONABLE; 039import static org.slf4j.LoggerFactory.getLogger; 040 041/** 042 * This service exposes management of node versioning. Instead of invoking 043 * the JCR VersionManager methods, this provides a level of indirection that 044 * allows for special handling of features built on top of JCR such as user 045 * transactions. 046 * @author Mike Durbin 047 */ 048 049@Component 050public class VersionServiceImpl extends AbstractService implements VersionService { 051 052 private static final Logger LOGGER = getLogger(VersionService.class); 053 054 private static final Pattern invalidLabelPattern = Pattern.compile("[~#@*+%{}<>\\[\\]|\"^]"); 055 056 @Override 057 public String createVersion(final Session session, 058 final String absPath, final String label) throws RepositoryException { 059 final Node node = session.getNode(absPath); 060 if (!isVersioningEnabled(node)) { 061 enableVersioning(node); 062 } 063 return checkpoint(session, absPath, label); 064 } 065 066 @Override 067 public void revertToVersion(final Session session, final String absPath, 068 final String label) throws RepositoryException { 069 final Workspace workspace = session.getWorkspace(); 070 final Version v = getVersionForLabel(workspace, absPath, label); 071 if (v == null) { 072 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 073 } 074 final VersionManager versionManager = workspace.getVersionManager(); 075 final Version preRevertVersion = versionManager.checkin(absPath); 076 077 try { 078 preRevertVersion.getContainingHistory().addVersionLabel(preRevertVersion.getName(), 079 getPreRevertVersionLabel(label, preRevertVersion.getContainingHistory()), false); 080 } catch (final LabelExistsVersionException e) { 081 // fall-back behavior is to leave an unlabeled version 082 } 083 versionManager.restore(v, true); 084 versionManager.checkout(absPath); 085 } 086 087 /** 088 * When we revert to a version, we snapshot first so that the "revert" action can be undone, 089 * this method generates a label suitable for that snapshot version to make it clear why 090 * it shows up in user's version history. 091 * @param targetLabel 092 * @param history 093 * @return 094 * @throws RepositoryException 095 */ 096 private static String getPreRevertVersionLabel(final String targetLabel, final VersionHistory history) 097 throws RepositoryException { 098 final String baseLabel = "auto-snapshot-before-" + targetLabel; 099 for (int i = 0; i < Integer.MAX_VALUE; i ++) { 100 final String label = baseLabel + (i == 0 ? "" : "-" + i); 101 if (!history.hasVersionLabel(label)) { 102 return label; 103 } 104 } 105 return baseLabel; 106 } 107 108 @Override 109 public void removeVersion(final Session session, final String absPath, 110 final String label) throws RepositoryException { 111 final Workspace workspace = session.getWorkspace(); 112 final Version v = getVersionForLabel(workspace, absPath, label); 113 114 if (v == null) { 115 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 116 } else if (workspace.getVersionManager().getBaseVersion(absPath).equals(v) ) { 117 throw new VersionException("Cannot remove most recent version snapshot."); 118 } else { 119 // remove labels 120 final VersionHistory history = v.getContainingHistory(); 121 final String[] versionLabels = history.getVersionLabels(v); 122 for ( final String versionLabel : versionLabels ) { 123 LOGGER.debug("Removing label: {}", versionLabel); 124 history.removeVersionLabel( versionLabel ); 125 } 126 history.removeVersion( v.getName() ); 127 } 128 } 129 130 131 private static Version getVersionForLabel(final Workspace workspace, final String absPath, 132 final String label) throws RepositoryException { 133 // first see if there's a version label 134 final VersionHistory history = workspace.getVersionManager().getVersionHistory(absPath); 135 136 if (history.hasVersionLabel(label)) { 137 return history.getVersionByLabel(label); 138 } 139 // there was no version with the given JCR Version Label, check to see if 140 // there's a version whose UUID is equal to the label 141 final VersionIterator versionIt = history.getAllVersions(); 142 if (versionIt == null) { 143 return null; 144 } 145 while (versionIt.hasNext()) { 146 final Version v = versionIt.nextVersion(); 147 if (v.getFrozenNode().getIdentifier().equals(label)) { 148 return v; 149 } 150 } 151 return null; 152 } 153 154 private static boolean isVersioningEnabled(final Node n) throws RepositoryException { 155 return n.isNodeType(VERSIONABLE) || (FedoraBinaryImpl.hasMixin(n) && isVersioningEnabled(n.getParent())); 156 } 157 158 private static void enableVersioning(final Node node) throws RepositoryException { 159 node.addMixin(VERSIONABLE); 160 161 if (FedoraBinaryImpl.hasMixin(node)) { 162 node.getParent().addMixin(VERSIONABLE); 163 } 164 node.getSession().save(); 165 } 166 167 private static String checkpoint(final Session session, final String absPath, final String label) 168 throws RepositoryException { 169 if (!validLabel(label)) { 170 throw new VersionException("Invalid label: " + label); 171 } 172 173 LOGGER.trace("Setting version checkpoint for {}", absPath); 174 final Workspace workspace = session.getWorkspace(); 175 final VersionManager versionManager = workspace.getVersionManager(); 176 final VersionHistory versionHistory = versionManager.getVersionHistory(absPath); 177 if (versionHistory.hasVersionLabel(label)) { 178 throw new LabelExistsVersionException("The specified label \"" + label 179 + "\" is already assigned to another version of this resource!"); 180 } 181 final Version v = versionManager.checkpoint(absPath); 182 if (v == null) { 183 return null; 184 } 185 versionHistory.addVersionLabel(v.getName(), label, false); 186 return v.getFrozenNode().getIdentifier(); 187 } 188 189 private static boolean validLabel(final String label) { 190 final Matcher matcher = invalidLabelPattern.matcher(label); 191 return !matcher.find(); 192 } 193 194}