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.modeshape.services; 017 018import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 019import org.fcrepo.kernel.api.services.VersionService; 020import org.fcrepo.kernel.modeshape.FedoraBinaryImpl; 021import org.slf4j.Logger; 022import org.springframework.stereotype.Component; 023 024import javax.jcr.Node; 025import javax.jcr.PathNotFoundException; 026import javax.jcr.RepositoryException; 027import javax.jcr.Session; 028import javax.jcr.Workspace; 029import javax.jcr.version.LabelExistsVersionException; 030import javax.jcr.version.Version; 031import javax.jcr.version.VersionException; 032import javax.jcr.version.VersionHistory; 033import javax.jcr.version.VersionManager; 034 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.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, final String absPath, final String label) { 058 try { 059 final Node node = session.getNode(absPath); 060 if (!isVersioningEnabled(node)) { 061 enableVersioning(node); 062 } 063 return checkpoint(session, absPath, label); 064 } catch (final RepositoryException e) { 065 throw new RepositoryRuntimeException(e); 066 } 067 } 068 069 @Override 070 public void revertToVersion(final Session session, final String absPath, final String label) { 071 final Workspace workspace = session.getWorkspace(); 072 try { 073 final Version v = getVersionForLabel(workspace, absPath, label); 074 if (v == null) { 075 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 076 } 077 final VersionManager versionManager = workspace.getVersionManager(); 078 final Version preRevertVersion = versionManager.checkin(absPath); 079 080 try { 081 preRevertVersion.getContainingHistory().addVersionLabel(preRevertVersion.getName(), 082 getPreRevertVersionLabel(label, preRevertVersion.getContainingHistory()), false); 083 } catch (final LabelExistsVersionException e) { 084 // fall-back behavior is to leave an unlabeled version 085 } 086 versionManager.restore(v, true); 087 versionManager.checkout(absPath); 088 } catch (final RepositoryException e) { 089 throw new RepositoryRuntimeException(e); 090 } 091 } 092 093 /** 094 * When we revert to a version, we snapshot first so that the "revert" action can be undone, 095 * this method generates a label suitable for that snapshot version to make it clear why 096 * it shows up in user's version history. 097 * @param targetLabel 098 * @param history 099 * @return 100 * @throws RepositoryException 101 */ 102 private static String getPreRevertVersionLabel(final String targetLabel, final VersionHistory history) 103 throws RepositoryException { 104 final String baseLabel = "auto-snapshot-before-" + targetLabel; 105 for (int i = 0; i < Integer.MAX_VALUE; i ++) { 106 final String label = baseLabel + (i == 0 ? "" : "-" + i); 107 if (!history.hasVersionLabel(label)) { 108 return label; 109 } 110 } 111 return baseLabel; 112 } 113 114 @Override 115 public void removeVersion(final Session session, final String absPath, final String label) { 116 final Workspace workspace = session.getWorkspace(); 117 try { 118 final Version v = getVersionForLabel(workspace, absPath, label); 119 if (v == null) { 120 throw new PathNotFoundException("Unknown version \"" + label + "\"!"); 121 } else if (workspace.getVersionManager().getBaseVersion(absPath).equals(v) ) { 122 throw new VersionException("Cannot remove most recent version snapshot."); 123 } else { 124 // remove labels 125 final VersionHistory history = v.getContainingHistory(); 126 final String[] versionLabels = history.getVersionLabels(v); 127 for ( final String versionLabel : versionLabels ) { 128 LOGGER.debug("Removing label: {}", versionLabel); 129 history.removeVersionLabel( versionLabel ); 130 } 131 history.removeVersion( v.getName() ); 132 } 133 } catch (final RepositoryException e) { 134 throw new RepositoryRuntimeException(e); 135 } 136 } 137 138 139 private static Version getVersionForLabel(final Workspace workspace, final String absPath, 140 final String label) throws RepositoryException { 141 // first see if there's a version label 142 final VersionHistory history = workspace.getVersionManager().getVersionHistory(absPath); 143 144 if (history.hasVersionLabel(label)) { 145 return history.getVersionByLabel(label); 146 } 147 return null; 148 } 149 150 private static boolean isVersioningEnabled(final Node n) throws RepositoryException { 151 return n.isNodeType(VERSIONABLE) || (FedoraBinaryImpl.hasMixin(n) && isVersioningEnabled(n.getParent())); 152 } 153 154 private static void enableVersioning(final Node node) throws RepositoryException { 155 node.addMixin(VERSIONABLE); 156 157 if (FedoraBinaryImpl.hasMixin(node)) { 158 node.getParent().addMixin(VERSIONABLE); 159 } 160 node.getSession().save(); 161 } 162 163 private static String checkpoint(final Session session, final String absPath, final String label) 164 throws RepositoryException { 165 if (!validLabel(label)) { 166 throw new VersionException("Invalid label: " + label); 167 } 168 169 LOGGER.trace("Setting version checkpoint for {}", absPath); 170 final Workspace workspace = session.getWorkspace(); 171 final VersionManager versionManager = workspace.getVersionManager(); 172 final VersionHistory versionHistory = versionManager.getVersionHistory(absPath); 173 if (versionHistory.hasVersionLabel(label)) { 174 throw new LabelExistsVersionException("The specified label \"" + label 175 + "\" is already assigned to another version of this resource!"); 176 } 177 final Version v = versionManager.checkpoint(absPath); 178 if (v == null) { 179 return null; 180 } 181 versionHistory.addVersionLabel(v.getName(), label, false); 182 return v.getFrozenNode().getIdentifier(); 183 } 184 185 private static boolean validLabel(final String label) { 186 final Matcher matcher = invalidLabelPattern.matcher(label); 187 return !matcher.find(); 188 } 189 190}