blob: bd4f20b0ab5adcb17205040dff5f1a52b9a3e2d1 [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.plugins.document;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.Maps;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A builder for a commit, translating modifications into {@link UpdateOp}s.
*/
class CommitBuilder {
/** A marker revision when the commit is initially built */
static final Revision PSEUDO_COMMIT_REVISION = new Revision(Long.MIN_VALUE, 0, 0);
private final DocumentNodeStore nodeStore;
private final Revision revision;
private final RevisionVector baseRevision;
private RevisionVector startRevisions = new RevisionVector();
private final Map<Path, UpdateOp> operations = new LinkedHashMap<>();
private final Set<Path> addedNodes = new HashSet<>();
private final Set<Path> removedNodes = new HashSet<>();
/** Set of all nodes which have binary properties. **/
private final Set<Path> nodesWithBinaries = new HashSet<>();
private final Map<Path, Path> bundledNodes = new HashMap<>();
/**
* Creates a new builder with a pseudo commit revision. Building the commit
* must be done by calling {@link #build(Revision)}.
*
* @param nodeStore the node store.
* @param baseRevision the base revision if available.
*/
CommitBuilder(@NotNull DocumentNodeStore nodeStore,
@Nullable RevisionVector baseRevision) {
this(nodeStore, PSEUDO_COMMIT_REVISION, baseRevision);
}
/**
* Creates a new builder with the given commit {@code revision}.
*
* @param nodeStore the node store.
* @param revision the commit revision.
* @param baseRevision the base revision of the commit or {@code null} if
* none is set.
*/
CommitBuilder(@NotNull DocumentNodeStore nodeStore,
@NotNull Revision revision,
@Nullable RevisionVector baseRevision) {
this.nodeStore = checkNotNull(nodeStore);
this.revision = checkNotNull(revision);
this.baseRevision = baseRevision;
}
/**
* @return the commit revision.
*/
@NotNull
Revision getRevision() {
return revision;
}
/**
* @return the base revision or {@code null} if none is set.
*/
@Nullable
RevisionVector getBaseRevision() {
return baseRevision;
}
/**
* Add a node to the commit with the given path.
*
* @param path the path of the node to add.
* @return {@code this} builder.
*/
@NotNull
CommitBuilder addNode(@NotNull Path path) {
addNode(new DocumentNodeState(nodeStore, path, new RevisionVector(revision)));
return this;
}
/**
* Add a the given node and its properties to the commit.
*
* @param node the node state to add.
* @return {@code this} builder.
* @throws DocumentStoreException if there's already a modification for
* a node at the given {@code path} in this commit builder.
*/
@NotNull
CommitBuilder addNode(@NotNull DocumentNodeState node)
throws DocumentStoreException {
checkNotNull(node);
Path path = node.getPath();
UpdateOp op = node.asOperation(revision);
if (operations.containsKey(path)) {
String msg = "Node already added: " + path;
throw new DocumentStoreException(msg);
}
if (isBranchCommit()) {
NodeDocument.setBranchCommit(op, revision);
}
operations.put(path, op);
addedNodes.add(path);
return this;
}
/**
* Instructs the commit builder that the bundling root of the node at
* {@code path} is at {@code bundlingRootPath}.
*
* @param path the path of a node.
* @param bundlingRootPath the bundling root for the node.
* @return {@code this} builder.
*/
@NotNull
CommitBuilder addBundledNode(@NotNull Path path,
@NotNull Path bundlingRootPath) {
checkNotNull(path);
checkNotNull(bundlingRootPath);
bundledNodes.put(path, bundlingRootPath);
return this;
}
/**
* Removes a node in this commit.
*
* @param path the path of the node to remove.
* @param state the node state representing the node to remove.
* @return {@code this} builder.
* @throws DocumentStoreException if there's already a modification for
* a node at the given {@code path} in this commit builder.
*/
@NotNull
CommitBuilder removeNode(@NotNull Path path,
@NotNull NodeState state)
throws DocumentStoreException {
checkNotNull(path);
checkNotNull(state);
if (operations.containsKey(path)) {
String msg = "Node already removed: " + path;
throw new DocumentStoreException(msg);
}
removedNodes.add(path);
UpdateOp op = getUpdateOperationForNode(path);
op.setDelete(true);
NodeDocument.setDeleted(op, revision, true);
for (PropertyState p : state.getProperties()) {
updateProperty(path, p.getName(), null);
}
return this;
}
/**
* Updates a property to a given value.
*
* @param path the path of the node.
* @param propertyName the name of the property.
* @param value the value of the property.
* @return {@code this} builder.
*/
@NotNull
CommitBuilder updateProperty(@NotNull Path path,
@NotNull String propertyName,
@Nullable String value) {
checkNotNull(path);
checkNotNull(propertyName);
UpdateOp op = getUpdateOperationForNode(path);
String key = Utils.escapePropertyName(propertyName);
op.setMapEntry(key, revision, value);
return this;
}
/**
* Instructs the commit builder that the node at the given {@code path} has
* a reference to a binary.
*
* @param path the path of the node.
* @return {@code this} builder.
*/
@NotNull
CommitBuilder markNodeHavingBinary(@NotNull Path path) {
checkNotNull(path);
nodesWithBinaries.add(path);
return this;
}
/**
* Sets the start revisions of known clusterIds on this commit builder.
*
* @param startRevisions the start revisions derived from the start time
* in the clusterNodes entries.
* @return {@code this} builder.
*/
@NotNull
CommitBuilder withStartRevisions(@NotNull RevisionVector startRevisions) {
this.startRevisions = checkNotNull(startRevisions);
return this;
}
/**
* Builds the commit with the modifications.
*
* @return {@code this} builder.
* @throws IllegalStateException if this builder was created without an
* explicit commit revision and {@link #build(Revision)} should have
* been called instead.
*/
@NotNull
Commit build() {
if (PSEUDO_COMMIT_REVISION.equals(revision)) {
String msg = "Cannot build a commit with a pseudo commit revision";
throw new IllegalStateException(msg);
}
return new Commit(nodeStore, revision, baseRevision, startRevisions,
operations, addedNodes, removedNodes, nodesWithBinaries,
bundledNodes);
}
/**
* Builds the commit with the modifications and the given commit revision.
*
* @param revision the commit revision.
* @return {@code this} builder.
*/
@NotNull
Commit build(@NotNull Revision revision) {
checkNotNull(revision);
Revision from = this.revision;
Map<Path, UpdateOp> operations = Maps.transformValues(
this.operations, op -> rewrite(op, from, revision));
return new Commit(nodeStore, revision, baseRevision, startRevisions,
operations, addedNodes, removedNodes, nodesWithBinaries,
bundledNodes);
}
/**
* Returns the number of operations currently recorded by this commit
* builder.
*
* @return the number of operations.
*/
int getNumOperations() {
return operations.size();
}
//-------------------------< internal >-------------------------------------
private UpdateOp getUpdateOperationForNode(Path path) {
UpdateOp op = operations.get(path);
if (op == null) {
op = createUpdateOp(path, revision, isBranchCommit());
operations.put(path, op);
}
return op;
}
private static UpdateOp createUpdateOp(Path path,
Revision revision,
boolean isBranch) {
String id = Utils.getIdFromPath(path);
UpdateOp op = new UpdateOp(id, false);
NodeDocument.setModified(op, revision);
if (isBranch) {
NodeDocument.setBranchCommit(op, revision);
}
return op;
}
/**
* @return {@code true} if this is a branch commit.
*/
private boolean isBranchCommit() {
return baseRevision != null && baseRevision.isBranch();
}
private static UpdateOp rewrite(UpdateOp up, Revision from, Revision to) {
Map<UpdateOp.Key, UpdateOp.Operation> changes = Maps.newHashMap();
for (Map.Entry<UpdateOp.Key, UpdateOp.Operation> entry : up.getChanges().entrySet()) {
UpdateOp.Key k = entry.getKey();
UpdateOp.Operation op = entry.getValue();
if (from.equals(k.getRevision())) {
k = new UpdateOp.Key(k.getName(), to);
} else if (NodeDocument.MODIFIED_IN_SECS.equals(k.getName())) {
op = new UpdateOp.Operation(op.type, NodeDocument.getModifiedInSecs(to.getTimestamp()));
}
changes.put(k, op);
}
return new UpdateOp(up.getId(), up.isNew(), up.isDelete(), changes, null);
}
}