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