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