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