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}