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}