blob: 72084e4324eb32dbe858e99306ba5b3d00746e28 [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.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.Set;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.commons.PathUtils;
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.apache.jackrabbit.oak.spi.state.NodeState;
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 java.util.Collections.singletonList;
import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
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 Revision revision;
private final HashMap<String, UpdateOp> operations = new LinkedHashMap<String, UpdateOp>();
private final Set<Revision> collisions = new LinkedHashSet<Revision>();
private Branch b;
private boolean rollbackFailed;
/**
* 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 HashSet<String> modifiedNodes = new HashSet<String>();
private HashSet<String> addedNodes = new HashSet<String>();
private HashSet<String> removedNodes = new HashSet<String>();
/** Set of all nodes which have binary properties. **/
private HashSet<String> nodesWithBinaries = Sets.newHashSet();
private HashMap<String, String> bundledNodes = Maps.newHashMap();
/**
* 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.
*/
Commit(@NotNull DocumentNodeStore nodeStore,
@NotNull Revision revision,
@Nullable RevisionVector baseRevision) {
this.nodeStore = checkNotNull(nodeStore);
this.revision = checkNotNull(revision);
this.baseRevision = baseRevision;
}
UpdateOp getUpdateOperationForNode(String path) {
UpdateOp op = operations.get(path);
if (op == null) {
op = createUpdateOp(path, revision, getBranch() != null);
operations.put(path, op);
}
return op;
}
static UpdateOp createUpdateOp(String 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<String> getModifiedPaths() {
return modifiedNodes;
}
void updateProperty(String path, String propertyName, String value) {
UpdateOp op = getUpdateOperationForNode(path);
String key = Utils.escapePropertyName(propertyName);
op.setMapEntry(key, revision, value);
}
void addBundledNode(String path, String bundlingRootPath) {
bundledNodes.put(path, bundlingRootPath);
}
void markNodeHavingBinary(String path) {
this.nodesWithBinaries.add(path);
}
void addNode(DocumentNodeState n) {
String path = n.getPath();
if (operations.containsKey(path)) {
String msg = "Node already added: " + path;
LOG.error(msg);
throw new DocumentStoreException(msg);
}
UpdateOp op = n.asOperation(revision);
if (getBranch() != null) {
NodeDocument.setBranchCommit(op, revision);
}
operations.put(path, op);
addedNodes.add(path);
}
boolean isEmpty() {
return operations.isEmpty();
}
/**
* @return {@code true} if this commit did not succeed and the rollback
* was unable to revert all changes; otherwise {@code false}.
*/
boolean rollbackFailed() {
return rollbackFailed;
}
/**
* 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) {
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();
applyToDocumentStore(baseRevision);
}
}
/**
* Update the binary status in the update op.
*/
private void updateBinaryStatus() {
DocumentStore store = this.nodeStore.getDocumentStore();
for (String 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 {
applyToDocumentStore(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 applyToDocumentStore(RevisionVector baseBranchRevision)
throws ConflictException, DocumentStoreException {
// initially set the rollbackFailed flag to true
// the flag will be set to false at the end of the method
// when the commit succeeds
rollbackFailed = true;
// 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();
String commitRootPath = null;
if (baseBranchRevision != null) {
// branch commits always use root node as commit root
commitRootPath = "/";
}
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 (String p : operations.keySet()) {
markChanged(p);
if (commitRootPath == null) {
commitRootPath = p;
} else {
while (!PathUtils.isAncestor(commitRootPath, p)) {
commitRootPath = PathUtils.getParentPath(commitRootPath);
if (denotesRoot(commitRootPath)) {
break;
}
}
}
}
for (String 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 = PathUtils.getDepth(commitRootPath);
// 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 {
List<NodeDocument> oldDocs = store.createOrUpdate(NODES, changedNodes);
checkConflicts(oldDocs, changedNodes);
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: " + commitRoot;
NodeDocument commitRootDoc = store.find(NODES, commitRoot.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 {
try {
rollback(opLog, commitRoot);
rollbackFailed = false;
} catch (Throwable ex) {
// catch any exception caused by the rollback, log it
// and throw the original exception
LOG.warn("Rollback failed", ex);
}
if (e instanceof ConflictException) {
throw e;
} else {
throw DocumentStoreException.convert(e);
}
}
} finally {
if (success) {
rollbackFailed = false;
}
}
}
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<String> processedParents = Sets.newHashSet();
for (String path : addedNodes) {
if (denotesRoot(path)) {
continue;
}
String parentPath = PathUtils.getParentPath(path);
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);
}
}
private void rollback(List<UpdateOp> changed,
UpdateOp commitRoot) {
DocumentStore store = nodeStore.getDocumentStore();
for (UpdateOp op : changed) {
UpdateOp reverse = op.getReverseOperation();
if (op.isNew()) {
NodeDocument.setDeletedOnce(reverse);
}
store.findAndUpdate(NODES, reverse);
}
removeCollisionMarker(commitRoot.getId());
}
/**
* 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;
if (before != null) {
RevisionVector base = baseRevision;
if (nodeStore.isDisableBranches()) {
base = base.asTrunkRevision();
}
newestRev = before.getNewestRevision(
nodeStore, base, revision, getBranch(), 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";
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() + " was already added 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);
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 += ", before\n" + 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;
}
/**
* 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 (String path : modifiedNodes) {
UpdateOp op = operations.get(path);
// track _lastRev only when path is not for a bundled node state
if ((op == null || !hasContentChanges(op) || denotesRoot(path))
&& !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<String, ArrayList<String>> nodesWithChangedChildren = new HashMap<String, ArrayList<String>>();
for (String p : modifiedNodes) {
if (denotesRoot(p)) {
continue;
}
String parent = PathUtils.getParentPath(p);
ArrayList<String> list = nodesWithChangedChildren.get(parent);
if (list == null) {
list = new ArrayList<String>();
nodesWithChangedChildren.put(parent, list);
}
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<String> added = new ArrayList<String>();
List<String> removed = new ArrayList<String>();
List<String> changed = new ArrayList<String>();
for (String path : modifiedNodes) {
added.clear();
removed.clear();
changed.clear();
ArrayList<String> changes = nodesWithChangedChildren.get(path);
if (changes != null) {
for (String 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();
}
/**
* 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(String path,
List<String> added,
List<String> removed,
List<String> changed,
DiffCache.Entry cacheEntry) {
// update diff cache
JsopWriter w = new JsopStream();
for (String p : added) {
w.tag('+').key(PathUtils.getName(p)).object().endObject();
}
for (String p : removed) {
w.tag('-').value(PathUtils.getName(p));
}
for (String p : changed) {
w.tag('^').key(PathUtils.getName(p)).object().endObject();
}
cacheEntry.append(path, w.toString());
}
private void markChanged(String path) {
if (!denotesRoot(path) && !PathUtils.isAbsolute(path)) {
throw new IllegalArgumentException("path: " + path);
}
while (true) {
if (!modifiedNodes.add(path)) {
break;
}
if (denotesRoot(path)) {
break;
}
path = PathUtils.getParentPath(path);
}
}
public void removeNode(String path, NodeState state) {
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);
}
}
private boolean isBundled(String 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();
}
}