| /* |
| * 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.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Sets; |
| import org.apache.jackrabbit.oak.commons.json.JsopStream; |
| import org.apache.jackrabbit.oak.commons.json.JsopWriter; |
| import org.apache.jackrabbit.oak.plugins.document.util.Utils; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static com.google.common.base.Objects.equal; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.collect.Iterables.filter; |
| import static com.google.common.collect.Iterables.transform; |
| import static com.google.common.collect.Lists.partition; |
| import static java.util.Collections.singletonList; |
| import static org.apache.jackrabbit.oak.plugins.document.Collection.JOURNAL; |
| import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES; |
| import static org.apache.jackrabbit.oak.plugins.document.Document.MOD_COUNT; |
| import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.COLLISIONS; |
| import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.SPLIT_CANDIDATE_THRESHOLD; |
| |
| /** |
| * A higher level object representing a commit. |
| */ |
| public class Commit { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(Commit.class); |
| |
| protected final DocumentNodeStore nodeStore; |
| private final RevisionVector baseRevision; |
| private final RevisionVector startRevisions; |
| private final Revision revision; |
| private final HashMap<Path, UpdateOp> operations = new LinkedHashMap<>(); |
| private final Set<Revision> collisions = new LinkedHashSet<Revision>(); |
| private Branch b; |
| private Rollback rollback = Rollback.NONE; |
| |
| /** |
| * List of all node paths which have been modified in this commit. In addition to the nodes |
| * which are actually changed it also contains there parent node paths |
| */ |
| private final HashSet<Path> modifiedNodes = new HashSet<>(); |
| |
| private final HashSet<Path> addedNodes = new HashSet<>(); |
| private final HashSet<Path> removedNodes = new HashSet<>(); |
| |
| /** Set of all nodes which have binary properties. **/ |
| private final HashSet<Path> nodesWithBinaries = new HashSet<>(); |
| private final HashMap<Path, Path> bundledNodes = new HashMap<>(); |
| |
| /** |
| * Create a new Commit. |
| * |
| * @param nodeStore the node store. |
| * @param revision the revision for this commit. |
| * @param baseRevision the base revision for this commit or {@code null} if |
| * there is none. |
| * @param startRevisions the revisions for each cluster node corresponding |
| * to the start time of the cluster nodes. |
| */ |
| Commit(@NotNull DocumentNodeStore nodeStore, |
| @NotNull Revision revision, |
| @Nullable RevisionVector baseRevision, |
| @NotNull RevisionVector startRevisions) { |
| this.nodeStore = checkNotNull(nodeStore); |
| this.revision = checkNotNull(revision); |
| this.baseRevision = baseRevision; |
| this.startRevisions = startRevisions; |
| } |
| |
| Commit(@NotNull DocumentNodeStore nodeStore, |
| @NotNull Revision revision, |
| @Nullable RevisionVector baseRevision, |
| @NotNull RevisionVector startRevisions, |
| @NotNull Map<Path, UpdateOp> operations, |
| @NotNull Set<Path> addedNodes, |
| @NotNull Set<Path> removedNodes, |
| @NotNull Set<Path> nodesWithBinaries, |
| @NotNull Map<Path, Path> bundledNodes) { |
| this(nodeStore, revision, baseRevision, startRevisions); |
| this.operations.putAll(operations); |
| this.addedNodes.addAll(addedNodes); |
| this.removedNodes.addAll(removedNodes); |
| this.nodesWithBinaries.addAll(nodesWithBinaries); |
| this.bundledNodes.putAll(bundledNodes); |
| } |
| |
| UpdateOp getUpdateOperationForNode(Path path) { |
| UpdateOp op = operations.get(path); |
| if (op == null) { |
| op = createUpdateOp(path, revision, isBranchCommit()); |
| operations.put(path, op); |
| } |
| return op; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * The revision for this new commit. That is, the changes within this commit |
| * will be visible with this revision. |
| * |
| * @return the revision for this new commit. |
| */ |
| @NotNull |
| Revision getRevision() { |
| return revision; |
| } |
| |
| /** |
| * Returns the base revision for this commit. That is, the revision passed |
| * to {@link DocumentNodeStore#newCommit}. The base revision may be |
| * <code>null</code>, e.g. for the initial commit of the root node, when |
| * there is no base revision. |
| * |
| * @return the base revision of this commit or <code>null</code>. |
| */ |
| @Nullable |
| RevisionVector getBaseRevision() { |
| return baseRevision; |
| } |
| |
| /** |
| * @return all modified paths, including ancestors without explicit |
| * modifications. |
| */ |
| @NotNull |
| Iterable<Path> getModifiedPaths() { |
| return modifiedNodes; |
| } |
| |
| boolean isEmpty() { |
| return operations.isEmpty(); |
| } |
| |
| /** |
| * Performs a rollback of this commit if necessary. |
| * |
| * @return {@code false} if a rollback was necessary and the rollback did |
| * not complete successfully, {@code true} otherwise. |
| */ |
| boolean rollback() { |
| boolean success = false; |
| try { |
| rollback.perform(this.nodeStore.getDocumentStore()); |
| success = true; |
| } catch (Throwable t) { |
| // catch any exception caused by the rollback and log it |
| LOG.warn("Rollback failed", t); |
| } |
| return success; |
| } |
| |
| /** |
| * Applies this commit to the store. |
| * |
| * @throws ConflictException if the commit failed because of a conflict. |
| * @throws DocumentStoreException if the commit cannot be applied. |
| */ |
| void apply() throws ConflictException, DocumentStoreException { |
| boolean success = false; |
| RevisionVector baseRev = getBaseRevision(); |
| boolean isBranch = baseRev != null && baseRev.isBranch(); |
| Revision rev = getRevision(); |
| if (isBranch && !nodeStore.isDisableBranches()) { |
| try { |
| // prepare commit |
| prepare(baseRev); |
| success = true; |
| } finally { |
| if (!success) { |
| rollback(); |
| Branch branch = getBranch(); |
| if (branch != null) { |
| branch.removeCommit(rev.asBranchRevision()); |
| if (!branch.hasCommits()) { |
| nodeStore.getBranches().remove(branch); |
| } |
| } |
| } |
| } |
| } else { |
| applyInternal(); |
| } |
| } |
| |
| /** |
| * Apply the changes to the document store and the cache. |
| */ |
| private void applyInternal() |
| throws ConflictException, DocumentStoreException { |
| if (!operations.isEmpty()) { |
| updateParentChildStatus(); |
| updateBinaryStatus(); |
| applyToDocumentStore(); |
| } |
| } |
| |
| private void prepare(RevisionVector baseRevision) |
| throws ConflictException, DocumentStoreException { |
| if (!operations.isEmpty()) { |
| updateParentChildStatus(); |
| updateBinaryStatus(); |
| applyToDocumentStoreWithTiming(baseRevision); |
| } |
| } |
| |
| /** |
| * Update the binary status in the update op. |
| */ |
| private void updateBinaryStatus() { |
| DocumentStore store = this.nodeStore.getDocumentStore(); |
| |
| for (Path path : this.nodesWithBinaries) { |
| NodeDocument nd = store.getIfCached(Collection.NODES, Utils.getIdFromPath(path)); |
| if ((nd == null) || !nd.hasBinary()) { |
| UpdateOp updateParentOp = getUpdateOperationForNode(path); |
| NodeDocument.setHasBinary(updateParentOp); |
| } |
| } |
| } |
| |
| /** |
| * Apply the changes to the document store. |
| */ |
| void applyToDocumentStore() throws ConflictException, DocumentStoreException { |
| applyToDocumentStoreWithTiming(null); |
| } |
| |
| /** |
| * Apply the changes to the document store. |
| * |
| * @param baseBranchRevision the base revision of this commit. Currently only |
| * used for branch commits. |
| * @throws ConflictException if a conflict is detected with another commit. |
| * @throws DocumentStoreException if an error occurs while writing to the |
| * underlying store. |
| */ |
| private void applyToDocumentStoreWithTiming(RevisionVector baseBranchRevision) |
| throws ConflictException, DocumentStoreException { |
| long start = System.nanoTime(); |
| try { |
| applyToDocumentStore(baseBranchRevision); |
| } finally { |
| nodeStore.getStatsCollector().doneChangesApplied( |
| TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - start)); |
| } |
| } |
| |
| /** |
| * Apply the changes to the document store. |
| * |
| * @param baseBranchRevision the base revision of this commit. Currently only |
| * used for branch commits. |
| * @throws ConflictException if a conflict is detected with another commit. |
| * @throws DocumentStoreException if an error occurs while writing to the |
| * underlying store. |
| */ |
| private void applyToDocumentStore(RevisionVector baseBranchRevision) |
| throws ConflictException, DocumentStoreException { |
| // initially set the rollback to always fail until we have changes |
| // in an oplog list and a commit root. |
| rollback = Rollback.FAILED; |
| |
| // the value in _revisions.<revision> property of the commit root node |
| // regular commits use "c", which makes the commit visible to |
| // other readers. branch commits use the base revision to indicate |
| // the visibility of the commit |
| String commitValue = baseBranchRevision != null ? baseBranchRevision.getBranchRevision().toString() : "c"; |
| DocumentStore store = nodeStore.getDocumentStore(); |
| Path commitRootPath = null; |
| if (baseBranchRevision != null) { |
| // branch commits always use root node as commit root |
| commitRootPath = Path.ROOT; |
| } |
| ArrayList<UpdateOp> changedNodes = new ArrayList<UpdateOp>(); |
| // operations are added to this list before they are executed, |
| // so that all operations can be rolled back if there is a conflict |
| ArrayList<UpdateOp> opLog = new ArrayList<UpdateOp>(); |
| |
| // Compute the commit root |
| for (Path p : operations.keySet()) { |
| markChanged(p); |
| if (commitRootPath == null) { |
| commitRootPath = p; |
| } else { |
| while (!commitRootPath.isAncestorOf(p)) { |
| Path parent = commitRootPath.getParent(); |
| if (parent == null) { |
| break; |
| } |
| commitRootPath = parent; |
| } |
| } |
| } |
| |
| // adjust commit root when it falls on a bundled node |
| commitRootPath = bundledNodes.getOrDefault(commitRootPath, commitRootPath); |
| |
| rollback = new Rollback(revision, opLog, |
| Utils.getIdFromPath(commitRootPath), |
| nodeStore.getCreateOrUpdateBatchSize()); |
| |
| for (Path p : bundledNodes.keySet()){ |
| markChanged(p); |
| } |
| |
| // push branch changes to journal |
| if (baseBranchRevision != null) { |
| // store as external change |
| JournalEntry doc = JOURNAL.newDocument(store); |
| doc.modified(modifiedNodes); |
| Revision r = revision.asBranchRevision(); |
| store.create(JOURNAL, singletonList(doc.asUpdateOp(r))); |
| } |
| |
| int commitRootDepth = commitRootPath.getDepth(); |
| // check if there are real changes on the commit root |
| boolean commitRootHasChanges = operations.containsKey(commitRootPath); |
| for (UpdateOp op : operations.values()) { |
| NodeDocument.setCommitRoot(op, revision, commitRootDepth); |
| changedNodes.add(op); |
| } |
| // create a "root of the commit" if there is none |
| UpdateOp commitRoot = getUpdateOperationForNode(commitRootPath); |
| |
| boolean success = false; |
| try { |
| opLog.addAll(changedNodes); |
| |
| if (conditionalCommit(changedNodes, commitValue)) { |
| success = true; |
| } else { |
| int batchSize = nodeStore.getCreateOrUpdateBatchSize(); |
| for (List<UpdateOp> updates : partition(changedNodes, batchSize)) { |
| List<NodeDocument> oldDocs = store.createOrUpdate(NODES, updates); |
| checkConflicts(oldDocs, updates); |
| checkSplitCandidate(oldDocs); |
| } |
| |
| // finally write the commit root (the commit root might be written |
| // twice, first to check if there was a conflict, and only then to |
| // commit the revision, with the revision property set) |
| NodeDocument.setRevision(commitRoot, revision, commitValue); |
| if (commitRootHasChanges) { |
| // remove previously added commit root |
| NodeDocument.removeCommitRoot(commitRoot, revision); |
| } |
| opLog.add(commitRoot); |
| if (baseBranchRevision == null) { |
| // create a clone of the commitRoot in order |
| // to set isNew to false. If we get here the |
| // commitRoot document already exists and |
| // only needs an update |
| UpdateOp commit = commitRoot.copy(); |
| commit.setNew(false); |
| // only set revision on commit root when there is |
| // no collision for this commit revision |
| commit.containsMapEntry(COLLISIONS, revision, false); |
| NodeDocument before = nodeStore.updateCommitRoot(commit, revision); |
| if (before == null) { |
| String msg = "Conflicting concurrent change. " + |
| "Update operation failed: " + commit; |
| NodeDocument commitRootDoc = store.find(NODES, commit.getId()); |
| if (commitRootDoc == null) { |
| throw new DocumentStoreException(msg); |
| } else { |
| throw new ConflictException(msg, |
| commitRootDoc.getConflictsFor( |
| Collections.singleton(revision))); |
| } |
| } else { |
| success = true; |
| // if we get here the commit was successful and |
| // the commit revision is set on the commitRoot |
| // document for this commit. |
| // now check for conflicts/collisions by other commits. |
| // use original commitRoot operation with |
| // correct isNew flag. |
| checkConflicts(commitRoot, before); |
| checkSplitCandidate(before); |
| } |
| } else { |
| // this is a branch commit, do not fail on collisions now |
| // trying to merge the branch will fail later |
| createOrUpdateNode(store, commitRoot); |
| } |
| } |
| } catch (Exception e) { |
| // OAK-3084 do not roll back if already committed |
| if (success) { |
| LOG.error("Exception occurred after commit. Rollback will be suppressed.", e); |
| } else { |
| if (e instanceof ConflictException) { |
| throw e; |
| } else { |
| throw DocumentStoreException.convert(e); |
| } |
| } |
| } finally { |
| if (success) { |
| rollback = Rollback.NONE; |
| } |
| } |
| } |
| |
| private boolean conditionalCommit(List<UpdateOp> changedNodes, |
| String commitValue) |
| throws DocumentStoreException { |
| // conditional commit is only possible when not on a branch |
| // and commit root is on the same document as the changes |
| if (!Utils.isCommitted(commitValue) || changedNodes.size() != 1) { |
| return false; |
| } |
| UpdateOp op = changedNodes.get(0); |
| DocumentStore store = nodeStore.getDocumentStore(); |
| NodeDocument doc = store.getIfCached(NODES, op.getId()); |
| if (doc == null || doc.getModCount() == null) { |
| // document not in cache or store does not maintain modCount |
| return false; |
| } |
| try { |
| checkConflicts(op, doc); |
| } catch (ConflictException e) { |
| // remove collision marker again |
| removeCollisionMarker(op.getId()); |
| return false; |
| } |
| // if we get here, update based on current doc does not conflict |
| // create a new commit update operation, setting the revisions |
| // commit entry together with the other changes |
| UpdateOp commit = op.copy(); |
| NodeDocument.unsetCommitRoot(commit, revision); |
| NodeDocument.setRevision(commit, revision, commitValue); |
| // make the update conditional on the modCount |
| commit.equals(MOD_COUNT, doc.getModCount()); |
| NodeDocument before = nodeStore.updateCommitRoot(commit, revision); |
| if (before != null) { |
| checkSplitCandidate(before); |
| } |
| return before != null; |
| } |
| |
| private void removeCollisionMarker(String id) { |
| UpdateOp removeCollision = new UpdateOp(id, false); |
| NodeDocument.removeCollision(removeCollision, revision); |
| nodeStore.getDocumentStore().findAndUpdate(NODES, removeCollision); |
| } |
| |
| private void updateParentChildStatus() { |
| final Set<Path> processedParents = Sets.newHashSet(); |
| for (Path path : addedNodes) { |
| Path parentPath = path.getParent(); |
| if (parentPath == null) { |
| continue; |
| } |
| |
| if (processedParents.contains(parentPath)) { |
| continue; |
| } |
| |
| //Ignore setting children path for bundled nodes |
| if (isBundled(parentPath)){ |
| continue; |
| } |
| |
| processedParents.add(parentPath); |
| UpdateOp op = getUpdateOperationForNode(parentPath); |
| NodeDocument.setChildrenFlag(op, true); |
| } |
| } |
| |
| /** |
| * Try to create or update the node. If there was a conflict, this method |
| * throws a {@link ConflictException}, even though the change is still applied. |
| * |
| * @param store the store |
| * @param op the operation |
| * @throws ConflictException if there was a conflict introduced by the |
| * given update operation. |
| */ |
| private void createOrUpdateNode(DocumentStore store, UpdateOp op) |
| throws ConflictException, DocumentStoreException { |
| NodeDocument doc = store.createOrUpdate(NODES, op); |
| checkConflicts(op, doc); |
| checkSplitCandidate(doc); |
| } |
| |
| private void checkSplitCandidate(Iterable<NodeDocument> docs) { |
| for (NodeDocument doc : docs) { |
| checkSplitCandidate(doc); |
| } |
| } |
| |
| private void checkSplitCandidate(@Nullable NodeDocument doc) { |
| if (doc == null) { |
| return; |
| } |
| if (doc.getMemory() > SPLIT_CANDIDATE_THRESHOLD || doc.hasBinary()) { |
| nodeStore.addSplitCandidate(doc.getId()); |
| } |
| } |
| |
| /** |
| * Checks if the update operation introduced any conflicts on the given |
| * document. The document shows the state right before the operation was |
| * applied. |
| * |
| * @param op the update operation. |
| * @param before how the document looked before the update was applied or |
| * {@code null} if it didn't exist before. |
| * @throws ConflictException if there was a conflict introduced by the |
| * given update operation. |
| */ |
| private void checkConflicts(@NotNull UpdateOp op, |
| @Nullable NodeDocument before) |
| throws ConflictException { |
| DocumentStore store = nodeStore.getDocumentStore(); |
| collisions.clear(); |
| if (baseRevision != null) { |
| Revision newestRev = null; |
| Branch branch = null; |
| if (before != null) { |
| RevisionVector base = baseRevision; |
| if (nodeStore.isDisableBranches()) { |
| base = base.asTrunkRevision(); |
| } |
| branch = getBranch(); |
| newestRev = before.getNewestRevision( |
| nodeStore, base, revision, branch, collisions); |
| } |
| String conflictMessage = null; |
| Set<Revision> conflictRevisions = Sets.newHashSet(); |
| if (newestRev == null) { |
| if ((op.isDelete() || !op.isNew()) |
| && !allowConcurrentAddRemove(before, op)) { |
| conflictMessage = "The node " + |
| op.getId() + " does not exist or is already deleted " + |
| "at base revision " + baseRevision + ", branch: " + branch; |
| if (before != null && !before.getLocalDeleted().isEmpty()) { |
| conflictRevisions.add(before.getLocalDeleted().firstKey()); |
| } |
| } |
| } else { |
| conflictRevisions.add(newestRev); |
| if (op.isNew() && !allowConcurrentAddRemove(before, op)) { |
| conflictMessage = "The node " + |
| op.getId() + " already existed in revision\n" + |
| formatConflictRevision(newestRev); |
| } else if (baseRevision.isRevisionNewer(newestRev) |
| && (op.isDelete() || isConflicting(before, op))) { |
| conflictMessage = "The node " + |
| op.getId() + " was changed in revision\n" + |
| formatConflictRevision(newestRev) + |
| ", which was applied after the base revision\n" + |
| baseRevision; |
| } |
| } |
| if (conflictMessage == null && before != null) { |
| // the modification was successful |
| // -> check for collisions and conflict (concurrent updates |
| // on a node are possible if property updates do not overlap) |
| // TODO: unify above conflict detection and isConflicting() |
| boolean allowConflictingDeleteChange = allowConcurrentAddRemove(before, op); |
| for (Revision r : collisions) { |
| Collision c = new Collision(before, r, op, revision, nodeStore, startRevisions); |
| if (c.isConflicting() && !allowConflictingDeleteChange) { |
| // mark collisions on commit root |
| if (c.mark(store).equals(revision)) { |
| // our revision was marked |
| if (baseRevision.isBranch()) { |
| // this is a branch commit. do not fail immediately |
| // merging this branch will fail later. |
| } else { |
| // fail immediately |
| conflictMessage = "The node " + |
| op.getId() + " was changed in revision\n" + |
| formatConflictRevision(r) + |
| ", which was applied after the base revision\n" + |
| baseRevision; |
| conflictRevisions.add(r); |
| } |
| } |
| } |
| } |
| } |
| if (conflictMessage != null) { |
| conflictMessage += ", commit revision: " + revision; |
| if (LOG.isDebugEnabled()) { |
| LOG.debug(conflictMessage + "; document:\n" + |
| (before == null ? "" : before.format())); |
| } |
| throw new ConflictException(conflictMessage, conflictRevisions); |
| } |
| } |
| } |
| |
| private void checkConflicts(List<NodeDocument> oldDocs, |
| List<UpdateOp> updates) |
| throws ConflictException { |
| int i = 0; |
| List<ConflictException> exceptions = new ArrayList<ConflictException>(); |
| Set<Revision> revisions = new HashSet<Revision>(); |
| for (NodeDocument doc : oldDocs) { |
| UpdateOp op = updates.get(i++); |
| try { |
| checkConflicts(op, doc); |
| } catch (ConflictException e) { |
| exceptions.add(e); |
| Iterables.addAll(revisions, e.getConflictRevisions()); |
| } |
| } |
| if (!exceptions.isEmpty()) { |
| throw new ConflictException("Following exceptions occurred during the bulk update operations: " + exceptions, revisions); |
| } |
| } |
| |
| private String formatConflictRevision(Revision r) { |
| if (nodeStore.getHeadRevision().isRevisionNewer(r)) { |
| return r + " (not yet visible)"; |
| } else if (baseRevision != null |
| && !baseRevision.isRevisionNewer(r) |
| && !equal(baseRevision.getRevision(r.getClusterId()), r)) { |
| return r + " (older than base " + baseRevision + ")"; |
| } else { |
| return r.toString(); |
| } |
| } |
| |
| /** |
| * Checks whether the given <code>UpdateOp</code> conflicts with the |
| * existing content in <code>doc</code>. The check is done based on the |
| * {@link #baseRevision} of this commit. An <code>UpdateOp</code> conflicts |
| * when there were changes after {@link #baseRevision} on properties also |
| * contained in <code>UpdateOp</code>. |
| * |
| * @param doc the contents of the nodes before the update. |
| * @param op the update to perform. |
| * @return <code>true</code> if the update conflicts; <code>false</code> |
| * otherwise. |
| */ |
| private boolean isConflicting(@Nullable NodeDocument doc, |
| @NotNull UpdateOp op) { |
| if (baseRevision == null || doc == null) { |
| // no conflict is possible when there is no baseRevision |
| // or document did not exist before |
| return false; |
| } |
| return doc.isConflicting(op, baseRevision, revision, |
| nodeStore.getEnableConcurrentAddRemove()); |
| } |
| |
| /** |
| * Checks whether a concurrent add/remove operation is allowed with the |
| * given before document and update operation. This method will first check |
| * if the concurrent add/remove feature is enable and return {@code false} |
| * immediately if it is disabled. Only when enabled will this method check |
| * if there is a conflict based on the given document and update operation. |
| * See also {@link #isConflicting(NodeDocument, UpdateOp)}. |
| * |
| * @param before the contents of the document before the update. |
| * @param op the update to perform. |
| * @return {@code true} is a concurrent add/remove update is allowed; |
| * {@code false} otherwise. |
| */ |
| private boolean allowConcurrentAddRemove(@Nullable NodeDocument before, |
| @NotNull UpdateOp op) { |
| return nodeStore.getEnableConcurrentAddRemove() |
| && !isConflicting(before, op); |
| } |
| |
| /** |
| * @return the branch if this is a branch commit, otherwise {@code null}. |
| */ |
| @Nullable |
| private Branch getBranch() { |
| if (baseRevision == null || !baseRevision.isBranch()) { |
| return null; |
| } |
| if (b == null) { |
| b = nodeStore.getBranches().getBranch( |
| new RevisionVector(revision.asBranchRevision())); |
| } |
| return b; |
| } |
| |
| /** |
| * @return {@code true} if this is a branch commit. |
| */ |
| private boolean isBranchCommit() { |
| return baseRevision != null && baseRevision.isBranch(); |
| } |
| |
| /** |
| * Applies the lastRev updates to the {@link LastRevTracker} of the |
| * DocumentNodeStore. |
| * |
| * @param isBranchCommit whether this is a branch commit. |
| */ |
| void applyLastRevUpdates(boolean isBranchCommit) { |
| LastRevTracker tracker = nodeStore.createTracker(revision, isBranchCommit); |
| for (Path path : modifiedNodes) { |
| UpdateOp op = operations.get(path); |
| // track _lastRev only when path is not for a bundled node state |
| if ((op == null || !hasContentChanges(op) || path.isRoot()) |
| && !isBundled(path)) { |
| // track intermediate node and root |
| tracker.track(path); |
| } |
| } |
| } |
| |
| /** |
| * Apply the changes to the DocumentNodeStore (to update the cache). |
| * |
| * @param before the revision right before this commit. |
| * @param isBranchCommit whether this is a commit to a branch |
| */ |
| public void applyToCache(RevisionVector before, boolean isBranchCommit) { |
| HashMap<Path, ArrayList<Path>> nodesWithChangedChildren = new HashMap<>(); |
| for (Path p : modifiedNodes) { |
| if (p.isRoot()) { |
| continue; |
| } |
| Path parent = p.getParent(); |
| ArrayList<Path> list = nodesWithChangedChildren |
| .computeIfAbsent(parent, k -> new ArrayList<>()); |
| list.add(p); |
| } |
| // the commit revision with branch flag if this is a branch commit |
| Revision rev = isBranchCommit ? revision.asBranchRevision() : revision; |
| RevisionVector after = before.update(rev); |
| DiffCache.Entry cacheEntry = nodeStore.getDiffCache().newEntry(before, after, true); |
| List<Path> added = new ArrayList<>(); |
| List<Path> removed = new ArrayList<>(); |
| List<Path> changed = new ArrayList<>(); |
| for (Path path : modifiedNodes) { |
| added.clear(); |
| removed.clear(); |
| changed.clear(); |
| ArrayList<Path> changes = nodesWithChangedChildren.get(path); |
| if (changes != null) { |
| for (Path s : changes) { |
| if (addedNodes.contains(s)) { |
| added.add(s); |
| } else if (removedNodes.contains(s)) { |
| removed.add(s); |
| } else { |
| changed.add(s); |
| } |
| } |
| } |
| UpdateOp op = operations.get(path); |
| |
| // apply to cache only when path is not for a bundled node state |
| if (!isBundled(path)) { |
| boolean isNew = op != null && op.isNew(); |
| nodeStore.applyChanges(before, after, rev, path, isNew, |
| added, removed, changed); |
| } |
| addChangesToDiffCacheEntry(path, added, removed, changed, cacheEntry); |
| } |
| cacheEntry.done(); |
| } |
| |
| void markChanged(Path path) { |
| while (true) { |
| if (!modifiedNodes.add(path)) { |
| break; |
| } |
| path = path.getParent(); |
| if (path == null) { |
| break; |
| } |
| } |
| } |
| |
| |
| @NotNull |
| RevisionVector getStartRevisions() { |
| return startRevisions; |
| } |
| |
| /** |
| * Apply the changes of a node to the cache. |
| * |
| * @param path the path |
| * @param added the list of added child nodes |
| * @param removed the list of removed child nodes |
| * @param changed the list of changed child nodes |
| * @param cacheEntry the cache entry changes are added to |
| */ |
| private void addChangesToDiffCacheEntry(Path path, |
| List<Path> added, |
| List<Path> removed, |
| List<Path> changed, |
| DiffCache.Entry cacheEntry) { |
| // update diff cache |
| JsopWriter w = new JsopStream(); |
| for (Path p : added) { |
| w.tag('+').key(p.getName()).object().endObject(); |
| } |
| for (Path p : removed) { |
| w.tag('-').value(p.getName()); |
| } |
| for (Path p : changed) { |
| w.tag('^').key(p.getName()).object().endObject(); |
| } |
| cacheEntry.append(path, w.toString()); |
| } |
| |
| private boolean isBundled(Path path) { |
| return bundledNodes.containsKey(path); |
| } |
| |
| private static final Function<UpdateOp.Key, String> KEY_TO_NAME = |
| new Function<UpdateOp.Key, String>() { |
| @Override |
| public String apply(UpdateOp.Key input) { |
| return input.getName(); |
| } |
| }; |
| |
| private static boolean hasContentChanges(UpdateOp op) { |
| return filter(transform(op.getChanges().keySet(), |
| KEY_TO_NAME), Utils.PROPERTY_OR_DELETED).iterator().hasNext(); |
| } |
| } |