blob: 5be69010323a147a49f2fd447540c719cbb1f48f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.jcr.version;
import javax.jcr.InvalidItemStateException;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.LabelExistsVersionException;
import javax.jcr.version.VersionException;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager;
import org.apache.jackrabbit.oak.plugins.version.ReadOnlyVersionManager;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.stats.Clock;
import org.apache.jackrabbit.util.ISO8601;
import org.jetbrains.annotations.NotNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.jackrabbit.JcrConstants.JCR_CREATED;
import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT;
import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONLABELS;
/**
* {@code ReadWriteVersionManager}...
*/
public class ReadWriteVersionManager extends ReadOnlyVersionManager {
private final SessionDelegate sessionDelegate;
private final VersionStorage versionStorage;
private final Clock clock = Clock.ACCURATE;
public ReadWriteVersionManager(@NotNull SessionDelegate sessionDelegate) {
this.sessionDelegate = sessionDelegate;
this.versionStorage = new VersionStorage(sessionDelegate.getRoot());
}
/**
* Called by the write methods to refresh the state of the possible
* session associated with this instance. The default implementation
* of this method does nothing, but a subclass can use this callback
* to keep a session in sync with the persisted version changes.
*
* @throws RepositoryException if the session could not be refreshed
*/
protected void refresh() throws RepositoryException {
sessionDelegate.refresh(true);
}
@Override
@NotNull
protected Tree getVersionStorage() {
return versionStorage.getTree();
}
@Override
@NotNull
protected Root getWorkspaceRoot() {
return sessionDelegate.getRoot();
}
@Override
@NotNull
protected ReadOnlyNodeTypeManager getNodeTypeManager() {
return ReadOnlyNodeTypeManager.getInstance(
sessionDelegate.getRoot(), NamePathMapper.DEFAULT);
}
/**
* Performs a checkin on a versionable tree and returns the tree that
* represents the created version.
*
* @param versionable the versionable node to check in.
* @return the created version.
* @throws InvalidItemStateException if the current root has pending
* changes.
* @throws UnsupportedRepositoryOperationException
* if the versionable tree isn't actually
* versionable.
* @throws RepositoryException if an error occurs while checking the
* node type of the tree.
*/
@NotNull
public Tree checkin(@NotNull Tree versionable)
throws RepositoryException, InvalidItemStateException,
UnsupportedRepositoryOperationException {
if (sessionDelegate.hasPendingChanges()) {
throw new InvalidItemStateException("Unable to perform checkin. " +
"Session has pending changes.");
}
if (!isVersionable(versionable)) {
throw new UnsupportedRepositoryOperationException(
versionable.getPath() + " is not versionable");
}
if (isCheckedOut(versionable)) {
Tree baseVersion = getExistingBaseVersion(versionable);
versionable.setProperty(JCR_ISCHECKEDOUT, Boolean.FALSE, Type.BOOLEAN);
PropertyState created = baseVersion.getProperty(JCR_CREATED);
long c = created == null ? 0 : ISO8601.parse(created.getValue(Type.DATE)).getTimeInMillis();
try {
long last = Math.max(c, clock.getTimeIncreasing());
// wait for clock to change so that the new version has a distinct
// timestamp from the last checkin performed by this VersionManager
// see https://issues.apache.org/jira/browse/OAK-7512
clock.waitUntil(last);
} catch (InterruptedException e) {
throw new RepositoryException(e);
}
try {
sessionDelegate.commit();
refresh();
} catch (CommitFailedException e) {
sessionDelegate.refresh(true);
throw e.asRepositoryException();
}
}
return getExistingBaseVersion(getWorkspaceRoot().getTree(versionable.getPath()));
}
/**
* Performs a checkout on a versionable tree.
*
* @param workspaceRoot a fresh workspace root without pending changes.
* @param versionablePath the absolute path to the versionable node to check out.
* @throws UnsupportedRepositoryOperationException
* if the versionable tree isn't actually
* versionable.
* @throws RepositoryException if an error occurs while checking the
* node type of the tree.
* @throws IllegalStateException if the workspaceRoot has pending changes.
* @throws IllegalArgumentException if the {@code versionablePath} is
* not absolute.
*/
public void checkout(@NotNull Root workspaceRoot,
@NotNull String versionablePath)
throws UnsupportedRepositoryOperationException,
InvalidItemStateException, RepositoryException {
checkState(!workspaceRoot.hasPendingChanges());
checkArgument(PathUtils.isAbsolute(versionablePath));
Tree versionable = workspaceRoot.getTree(versionablePath);
if (!isVersionable(versionable)) {
throw new UnsupportedRepositoryOperationException(
versionable.getPath() + " is not versionable");
}
if (!isCheckedOut(versionable)) {
versionable.setProperty(JCR_ISCHECKEDOUT,
Boolean.TRUE, Type.BOOLEAN);
try {
workspaceRoot.commit();
refresh();
} catch (CommitFailedException e) {
workspaceRoot.refresh();
throw e.asRepositoryException();
}
}
}
public void addVersionLabel(@NotNull VersionStorage versionStorage,
@NotNull String versionHistoryOakRelPath,
@NotNull String versionIdentifier,
@NotNull String oakVersionLabel,
boolean moveLabel) throws RepositoryException {
Tree versionHistory = TreeUtil.getTree(checkNotNull(versionStorage.getTree()),
checkNotNull(versionHistoryOakRelPath));
Tree labels = checkNotNull(versionHistory).getChild(JCR_VERSIONLABELS);
PropertyState existing = labels.getProperty(checkNotNull(oakVersionLabel));
if (existing != null) {
if (moveLabel) {
labels.removeProperty(existing.getName());
} else {
throw new LabelExistsVersionException("Version label '"
+ oakVersionLabel + "' already exists on this version history");
}
}
labels.setProperty(oakVersionLabel, versionIdentifier, Type.REFERENCE);
try {
sessionDelegate.commit(versionStorage.getRoot());
refresh();
} catch (CommitFailedException e) {
versionStorage.refresh();
throw e.asRepositoryException();
}
}
public void removeVersionLabel(@NotNull VersionStorage versionStorage,
@NotNull String versionHistoryOakRelPath,
@NotNull String oakVersionLabel)
throws RepositoryException {
Tree versionHistory = TreeUtil.getTree(checkNotNull(versionStorage.getTree()),
checkNotNull(versionHistoryOakRelPath));
Tree labels = checkNotNull(versionHistory).getChild(JCR_VERSIONLABELS);
if (!labels.hasProperty(oakVersionLabel)) {
throw new VersionException("Version label " + oakVersionLabel +
" does not exist on this version history");
}
labels.removeProperty(oakVersionLabel);
try {
sessionDelegate.commit(versionStorage.getRoot());
refresh();
} catch (CommitFailedException e) {
versionStorage.refresh();
throw e.asRepositoryException();
}
}
public void removeVersion(@NotNull VersionStorage versionStorage,
@NotNull String versionHistoryOakRelPath,
@NotNull String oakVersionName)
throws RepositoryException {
Tree versionHistory = TreeUtil.getTree(versionStorage.getTree(), versionHistoryOakRelPath);
if (versionHistory == null || !versionHistory.exists()) {
throw new VersionException("Version history " + versionHistoryOakRelPath + " does not exist on this version storage");
}
Tree version = versionHistory.getChild(oakVersionName);
if (!version.exists()) {
throw new VersionException("Version " + oakVersionName + " does not exist on this version history");
}
version.remove();
try {
sessionDelegate.commit(versionStorage.getRoot());
refresh();
} catch (CommitFailedException e) {
versionStorage.refresh();
throw e.asRepositoryException();
}
}
// TODO: more methods that modify versions
//------------------------------------------------------------< private >---
@NotNull
private Tree getExistingBaseVersion(@NotNull Tree versionableTree) throws RepositoryException {
Tree baseVersion = getBaseVersion(versionableTree);
if (baseVersion == null) {
throw new IllegalStateException("Base version does not exist for " + versionableTree.getPath());
}
return baseVersion;
}
}