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