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