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