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