/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.modeshape.services;
import org.fcrepo.kernel.api.FedoraSession;
import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
import org.fcrepo.kernel.api.services.VersionService;
import org.fcrepo.kernel.modeshape.FedoraBinaryImpl;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.version.LabelExistsVersionException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionManager;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.VERSIONABLE;
import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession;
import static org.slf4j.LoggerFactory.getLogger;
/**
* This service exposes management of node versioning. Instead of invoking
* the JCR VersionManager methods, this provides a level of indirection that
* allows for special handling of features built on top of JCR such as user
* transactions.
* @author Mike Durbin
*/
@Component
public class VersionServiceImpl extends AbstractService implements VersionService {
private static final Logger LOGGER = getLogger(VersionService.class);
private static final Pattern invalidLabelPattern = Pattern.compile("[~#@*+%{}<>\\[\\]|\"^]");
private static final Pattern invalidLabelEndsWithANumberPattern = Pattern.compile("^(.*[\\s]+)?[\\d]*$");
@Override
public String createVersion(final FedoraSession session, final String absPath, final String label) {
final Session jcrSession = getJcrSession(session);
try {
final Node node = jcrSession.getNode(absPath);
if (!isVersioningEnabled(node)) {
enableVersioning(node);
}
return checkpoint(jcrSession, absPath, label);
} catch (final RepositoryException e) {
throw new RepositoryRuntimeException(e);
}
}
@Override
public void revertToVersion(final FedoraSession session, final String absPath, final String label) {
final Session jcrSession = getJcrSession(session);
final Workspace workspace = jcrSession.getWorkspace();
try {
final Version v = getVersionForLabel(workspace, absPath, label);
if (v == null) {
throw new PathNotFoundException("Unknown version \"" + label + "\"!");
}
final VersionManager versionManager = workspace.getVersionManager();
final Version preRevertVersion = versionManager.checkin(absPath);
try {
preRevertVersion.getContainingHistory().addVersionLabel(preRevertVersion.getName(),
getPreRevertVersionLabel(label, preRevertVersion.getContainingHistory()), false);
} catch (final LabelExistsVersionException e) {
// fall-back behavior is to leave an unlabeled version
}
versionManager.restore(v, true);
versionManager.checkout(absPath);
} catch (final RepositoryException e) {
throw new RepositoryRuntimeException(e);
}
}
/**
* When we revert to a version, we snapshot first so that the "revert" action can be undone,
* this method generates a label suitable for that snapshot version to make it clear why
* it shows up in user's version history.
* @param targetLabel
* @param history
* @return
* @throws RepositoryException
*/
private static String getPreRevertVersionLabel(final String targetLabel, final VersionHistory history)
throws RepositoryException {
final String baseLabel = "auto-snapshot-before-" + targetLabel;
for (int i = 0; i < Integer.MAX_VALUE; i ++) {
final String label = baseLabel + (i == 0 ? "" : "-" + i);
if (!history.hasVersionLabel(label)) {
return label;
}
}
return baseLabel;
}
@Override
public void removeVersion(final FedoraSession session, final String absPath, final String label) {
final Session jcrSession = getJcrSession(session);
final Workspace workspace = jcrSession.getWorkspace();
try {
final Version v = getVersionForLabel(workspace, absPath, label);
if (v == null) {
throw new PathNotFoundException("Unknown version \"" + label + "\"!");
} else if (workspace.getVersionManager().getBaseVersion(absPath).equals(v) ) {
throw new VersionException("Cannot remove most recent version snapshot.");
} else {
// remove labels
final VersionHistory history = v.getContainingHistory();
final String[] versionLabels = history.getVersionLabels(v);
for ( final String versionLabel : versionLabels ) {
LOGGER.debug("Removing label: {}", versionLabel);
history.removeVersionLabel( versionLabel );
}
history.removeVersion( v.getName() );
}
} catch (final RepositoryException e) {
throw new RepositoryRuntimeException(e);
}
}
private static Version getVersionForLabel(final Workspace workspace, final String absPath,
final String label) throws RepositoryException {
// first see if there's a version label
final VersionHistory history = workspace.getVersionManager().getVersionHistory(absPath);
if (history.hasVersionLabel(label)) {
return history.getVersionByLabel(label);
}
return null;
}
private static boolean isVersioningEnabled(final Node n) throws RepositoryException {
return n.isNodeType(VERSIONABLE) || (FedoraBinaryImpl.hasMixin(n) && isVersioningEnabled(n.getParent()));
}
private static void enableVersioning(final Node node) throws RepositoryException {
node.addMixin(VERSIONABLE);
if (FedoraBinaryImpl.hasMixin(node)) {
node.getParent().addMixin(VERSIONABLE);
}
node.getSession().save();
}
private static String checkpoint(final Session session, final String absPath, final String label)
throws RepositoryException {
if (!validLabel(label)) {
throw new VersionException("Invalid label: " + label);
}
LOGGER.trace("Setting version checkpoint for {}", absPath);
final Workspace workspace = session.getWorkspace();
final VersionManager versionManager = workspace.getVersionManager();
final VersionHistory versionHistory = versionManager.getVersionHistory(absPath);
if (versionHistory.hasVersionLabel(label)) {
throw new LabelExistsVersionException("The specified label \"" + label
+ "\" is already assigned to another version of this resource!");
}
final Version v = versionManager.checkpoint(absPath);
if (v == null) {
return null;
}
versionHistory.addVersionLabel(v.getName(), label, false);
return v.getFrozenNode().getIdentifier();
}
private static boolean validLabel(final String label) {
final Matcher matcher = invalidLabelPattern.matcher(label);
if (matcher.find()) {
return false;
}
return !invalidLabelEndsWithANumberPattern.matcher(label).find();
}
}