Map<String, String> userData = context.getData();
final boolean acquireLock = false; // we already pre-locked all of the existing documents that we'll edit ...
DateTime timestamp = context.getValueFactories().getDateFactory().create();
String workspaceName = persistedCache.getWorkspaceName();
String repositoryKey = persistedCache.getRepositoryKey();
RecordingChanges changes = new RecordingChanges(context.getId(), context.getProcessId(), repositoryKey, workspaceName,
sessionContext().journalId());
// Get the documentStore ...
DocumentStore documentStore = persistedCache.documentStore();
DocumentTranslator translator = persistedCache.translator();
PathCache sessionPaths = new PathCache(this);
PathCache workspacePaths = new PathCache(persistedCache);
Set<NodeKey> removedNodes = null;
Set<BinaryKey> unusedBinaryKeys = new HashSet<>();
Set<BinaryKey> usedBinaryKeys = new HashSet<>();
Set<NodeKey> renamedExternalNodes = new HashSet<>();
for (NodeKey key : changedNodesInOrder) {
SessionNode node = changedNodes.get(key);
String keyStr = key.toString();
boolean isExternal = !node.getKey().getSourceKey().equalsIgnoreCase(workspaceCache().getRootKey().getSourceKey());
if (node == REMOVED) {
// We need to read some information from the node before we remove it ...
CachedNode persisted = persistedCache.getNode(key);
if (persisted != null) {
// This was a persistent node, so we have to generate an event and deal with the remove ...
if (removedNodes == null) {
removedNodes = new HashSet<>();
}
Name primaryType = persisted.getPrimaryType(this);
Set<Name> mixinTypes = persisted.getMixinTypes(this);
Path path = workspacePaths.getPath(persisted);
boolean queryable = persisted.isQueryable(this);
changes.nodeRemoved(key, persisted.getParentKey(persistedCache), path, primaryType, mixinTypes, queryable);
removedNodes.add(key);
// if there were any referrer changes for the removed nodes, we need to process them
ReferrerChanges referrerChanges = referrerChangesForRemovedNodes.get(key);
if (referrerChanges != null) {
EditableDocument doc = documentStore.edit(keyStr, false, acquireLock);
if (doc != null) translator.changeReferrers(doc, referrerChanges);
}
// if the node had any binary properties, make sure we decrement the ref count of each
for (Iterator<Property> propertyIterator = persisted.getProperties(persistedCache); propertyIterator.hasNext();) {
Property property = propertyIterator.next();
if (property.isBinary()) {
Object value = property.isMultiple() ? Arrays.asList(property.getValuesAsArray()) : property.getFirstValue();
translator.decrementBinaryReferenceCount(value, unusedBinaryKeys, null);
}
}
// Note 1: Do not actually remove the document from the documentStore yet; see below (note 2)
}
// Otherwise, the removed node was created in the session (but not ever persisted),
// so we don't have to do anything ...
} else {
// Get the primary and mixin type names; even though we're passing in the session, the two properties
// should be there and shouldn't require a looking in the cache...
Name primaryType = node.getPrimaryType(this);
Set<Name> mixinTypes = node.getMixinTypes(this);
boolean queryable = node.isQueryable(this);
CachedNode persisted = null;
Path newPath = sessionPaths.getPath(node);
NodeKey newParent = node.newParent();
EditableDocument doc = null;
ChangedAdditionalParents additionalParents = node.additionalParents();
if (node.isNew()) {
doc = Schematic.newDocument();
translator.setKey(doc, key);
translator.setParents(doc, newParent, null, additionalParents);
// Create an event ...
changes.nodeCreated(key, newParent, newPath, primaryType, mixinTypes, node.changedProperties(), queryable);
} else {
doc = documentStore.edit(keyStr, true, acquireLock);
if (doc == null) {
if (isExternal && renamedExternalNodes.contains(key)) {
// this is a renamed external node which has been processed in the parent, so we can skip it
continue;
}
// Could not find the entry in the documentStore, which means it was deleted by someone else
// just moments before we got our transaction to save ...
throw new DocumentNotFoundException(keyStr);
}
if (newParent != null) {
persisted = persistedCache.getNode(key);
// The node has moved (either within the same parent or to another parent) ...
Path oldPath = workspacePaths.getPath(persisted);
NodeKey oldParentKey = persisted.getParentKey(persistedCache);
if (!oldParentKey.equals(newParent) || (additionalParents != null && !additionalParents.isEmpty())) {
translator.setParents(doc, node.newParent(), oldParentKey, additionalParents);
}
// We only want to fire the event if the node we're working with is in the same workspace as the current
// workspace. The node will be in a different workspace when it is linked or un-linked
// (e.g. shareable node or jcr:system).
String workspaceKey = node.getKey().getWorkspaceKey();
boolean isSameWorkspace = persistedCache.getWorkspaceKey().equalsIgnoreCase(workspaceKey);
if (isSameWorkspace) {
changes.nodeMoved(key, primaryType, mixinTypes, newParent, oldParentKey, newPath, oldPath, queryable);
}
} else if (additionalParents != null) {
// The node in another workspace has been linked to this workspace ...
translator.setParents(doc, null, null, additionalParents);
}
// Deal with mixin changes here (since for new nodes they're put into the properties) ...
MixinChanges mixinChanges = node.mixinChanges(false);
if (mixinChanges != null && !mixinChanges.isEmpty()) {
Property oldProperty = translator.getProperty(doc, JcrLexicon.MIXIN_TYPES);
translator.addPropertyValues(doc, JcrLexicon.MIXIN_TYPES, true, mixinChanges.getAdded(),
unusedBinaryKeys, usedBinaryKeys);
translator.removePropertyValues(doc, JcrLexicon.MIXIN_TYPES, mixinChanges.getRemoved(), unusedBinaryKeys,
usedBinaryKeys);
// the property was changed ...
Property newProperty = translator.getProperty(doc, JcrLexicon.MIXIN_TYPES);
if (oldProperty == null) {
changes.propertyAdded(key, primaryType, mixinTypes, newPath, newProperty, queryable);
} else if (newProperty == null) {
changes.propertyRemoved(key, primaryType, mixinTypes, newPath, oldProperty, queryable);
} else {
changes.propertyChanged(key, primaryType, mixinTypes, newPath, newProperty, oldProperty, queryable);
}
}
}
LockChange lockChange = node.getLockChange();
if (lockChange != null) {
switch (lockChange) {
case LOCK_FOR_SESSION:
case LOCK_FOR_NON_SESSION:
// check is another session has already locked the document
if (translator.isLocked(doc)) {
throw new LockFailureException(key);
}
break;
case UNLOCK:
break;
}
}
// As we go through the removed and changed properties, we want to keep track of whether there are any
// effective modifications to the persisted properties.
boolean hasPropertyChanges = false;
// Save the removed properties ...
Set<Name> removedProperties = node.removedProperties();
if (!removedProperties.isEmpty()) {
assert !node.isNew();
if (persisted == null) {
persisted = persistedCache.getNode(key);
}
for (Name name : removedProperties) {
Property oldProperty = translator.removeProperty(doc, name, unusedBinaryKeys, usedBinaryKeys);
if (oldProperty != null) {
// the property was removed ...
changes.propertyRemoved(key, primaryType, mixinTypes, newPath, oldProperty, queryable);
// and we know that there are modifications to the properties ...
hasPropertyChanges = true;
}
}
}
// Save the changes to the properties
if (!node.changedProperties().isEmpty()) {
if (!node.isNew() && persisted == null) {
persisted = persistedCache.getNode(key);
}
for (Map.Entry<Name, Property> propEntry : node.changedProperties().entrySet()) {
Name name = propEntry.getKey();
Property prop = propEntry.getValue();
// Get the old property ...
Property oldProperty = persisted != null ? persisted.getProperty(name, persistedCache) : null;
translator.setProperty(doc, prop, unusedBinaryKeys, usedBinaryKeys);
if (oldProperty == null) {
// the property was created ...
changes.propertyAdded(key, primaryType, mixinTypes, newPath, prop, queryable);
// and we know that there are modifications to the properties ...
hasPropertyChanges = true;
} else if (hasPropertyChanges || !oldProperty.equals(prop)) {
// The 'hasPropertyChanges ||' in the above condition is what gives us the "slight optimization"
// mentioned in the longer comment above. This is noticeably more efficient (since the
// '!oldProperty.equals(prop)' has to be called for only some of the changes) and does result
// in correct indexing behavior, but the compromise is that some no-op property changes will
// result in a PROPERTY_CHANGE event. To remove all potential no-op PROPERTY CHANGE events,
// simply remove the 'hasPropertyChanges||' in the above condition.
// See MODE-1856 for details.
// the property was changed and is actually different than the persisted property ...
changes.propertyChanged(key, primaryType, mixinTypes, newPath, prop, oldProperty, queryable);
hasPropertyChanges = true;
}
}
}
// Save the change to the child references. Note that we only need to generate events for renames;
// moves (to the same or another parent), removes, and inserts are all recorded as changes in the
// child node, and events are generated handled when we process
// the child node.
ChangedChildren changedChildren = node.changedChildren();
MutableChildReferences appended = node.appended(false);
if ((changedChildren == null || changedChildren.isEmpty()) && (appended != null && !appended.isEmpty())) {
// Just appended children ...
translator.changeChildren(doc, changedChildren, appended);
} else if (changedChildren != null && !changedChildren.isEmpty()) {
if (!changedChildren.getRemovals().isEmpty()) {
// This node is not being removed (or added), but it has removals, and we have to calculate the paths
// of the removed nodes before we actually change the child references of this node.
for (NodeKey removed : changedChildren.getRemovals()) {
CachedNode persistent = persistedCache.getNode(removed);
if (persistent != null) {
if (appended != null && appended.hasChild(persistent.getKey())) {
// the same node has been both removed and appended => reordered at the end
ChildReference appendedChildRef = node.getChildReferences(this).getChild(persistent.getKey());
newPath = pathFactory().create(sessionPaths.getPath(node), appendedChildRef.getSegment());
Path oldPath = workspacePaths.getPath(persistent);
changes.nodeReordered(persistent.getKey(), primaryType, mixinTypes, node.getKey(), newPath,
oldPath, null, queryable);
}
}
}
}
// Now change the children ...
translator.changeChildren(doc, changedChildren, appended);
// Generate events for renames, as this is only captured in the parent node ...
Map<NodeKey, Name> newNames = changedChildren.getNewNames();
if (!newNames.isEmpty()) {
for (Map.Entry<NodeKey, Name> renameEntry : newNames.entrySet()) {
NodeKey renamedKey = renameEntry.getKey();
CachedNode oldRenamedNode = persistedCache.getNode(renamedKey);
if (oldRenamedNode == null) {
// The node was created in this session, so we can ignore this ...
continue;
}
Path renamedFromPath = workspacePaths.getPath(oldRenamedNode);
Path renamedToPath = pathFactory().create(renamedFromPath.getParent(), renameEntry.getValue());
changes.nodeRenamed(renamedKey, renamedToPath, renamedFromPath.getLastSegment(), primaryType,
mixinTypes, queryable);
if (isExternal) {
renamedExternalNodes.add(renamedKey);
}
}
}
// generate reordering events for nodes which have not been reordered to the end
Map<NodeKey, SessionNode.Insertions> insertionsByBeforeKey = changedChildren.getInsertionsByBeforeKey();
for (SessionNode.Insertions insertion : insertionsByBeforeKey.values()) {
for (ChildReference insertedRef : insertion.inserted()) {
CachedNode insertedNodePersistent = persistedCache.getNode(insertedRef);
CachedNode insertedNode = getNode(insertedRef.getKey());
Path nodeNewPath = sessionPaths.getPath(insertedNode);
if (insertedNodePersistent != null) {
Path nodeOldPath = workspacePaths.getPath(insertedNodePersistent);
Path insertedBeforePath = null;
CachedNode insertedBeforeNode = persistedCache.getNode(insertion.insertedBefore());
if (insertedBeforeNode != null) {
insertedBeforePath = workspacePaths.getPath(insertedBeforeNode);
boolean isSnsReordering = nodeOldPath.getLastSegment().getName()
.equals(insertedBeforePath.getLastSegment().getName());
if (isSnsReordering) {
nodeNewPath = insertedBeforePath;
}
}
changes.nodeReordered(insertedRef.getKey(), insertedNode.getPrimaryType(this),
insertedNode.getMixinTypes(this), node.getKey(), nodeNewPath, nodeOldPath,
insertedBeforePath, queryable);
} else {
// if the node is new and reordered at the same time (most likely due to either a version restore
// or explicit reordering of transient nodes) there is no "old path"
CachedNode insertedBeforeNode = getNode(insertion.insertedBefore().getKey());
Path insertedBeforePath = sessionPaths.getPath(insertedBeforeNode);
changes.nodeReordered(insertedRef.getKey(), insertedNode.getPrimaryType(this),
insertedNode.getMixinTypes(this), node.getKey(), nodeNewPath, null,
insertedBeforePath, queryable);
}
}
}
}
ReferrerChanges referrerChanges = node.getReferrerChanges();
boolean nodeChanged = false;
if (referrerChanges != null && !referrerChanges.isEmpty()) {
translator.changeReferrers(doc, referrerChanges);
changes.nodeChanged(key, newPath, primaryType, mixinTypes, queryable);
nodeChanged = true;
}
// write the federated segments
for (Map.Entry<String, String> federatedSegment : node.getAddedFederatedSegments().entrySet()) {
String externalNodeKey = federatedSegment.getKey();
String childName = federatedSegment.getValue();
translator.addFederatedSegment(doc, externalNodeKey, childName);
if (!nodeChanged) {
changes.nodeChanged(key, newPath, primaryType, mixinTypes, queryable);
nodeChanged = true;
}
}
Set<String> removedFederatedSegments = node.getRemovedFederatedSegments();
if (!removedFederatedSegments.isEmpty()) {
translator.removeFederatedSegments(doc, node.getRemovedFederatedSegments());
if (!nodeChanged) {
changes.nodeChanged(key, newPath, primaryType, mixinTypes, queryable);
nodeChanged = true;
}
}
// write additional node "metadata", meaning various flags which have internal meaning
if (!queryable) {
// we are only interested if the node is not queryable, as by default all nodes are queryable.
translator.setQueryable(doc, false);
}
if (node.isNew()) {
// We need to create the schematic entry for the new node ...
if (documentStore.storeDocument(keyStr, doc) != null) {
if (replacedNodes != null && replacedNodes.contains(key)) {
// Then a node is being removed and recreated with the same key ...
documentStore.localStore().put(keyStr, doc);
} else if (removedNodes != null && removedNodes.contains(key)) {
// Then a node is being removed and recreated with the same key ...
documentStore.localStore().put(keyStr, doc);
removedNodes.remove(key);
} else {
// We couldn't create the entry because one already existed ...
throw new DocumentAlreadyExistsException(keyStr);
}
}
} else {
boolean externalNodeChanged = isExternal
&& (hasPropertyChanges || node.hasNonPropertyChanges() || node.changedChildren()
.renameCount() > 0);
// writable connectors *may* change their data in-place, so the update operation needs to be called only
// after the index changes have finished.
if (externalNodeChanged) {
// in the case of external nodes, only if there are changes should the update be called
documentStore.updateDocument(keyStr, doc, node);
}
}
// The above code doesn't properly generate events for newly linked or unlinked nodes (e.g., shareable nodes
// in JCR), because NODE_ADDED or NODE_REMOVED events are generated based upon the creation or removal of the
// child nodes, whereas linking and unlinking nodes don't result in creation/removal of nodes. Instead,
// the linked/unlinked node is modified with the addition/removal of additional parents.
//
// NOTE that this happens somewhat rarely (as linked/shared nodes are used far less frequently) ...
//
if (additionalParents != null) {
// Generate NODE_ADDED events for each of the newly-added parents ...
for (NodeKey parentKey : additionalParents.getAdditions()) {
// Find the mutable parent node (if it exists) ...
SessionNode parent = this.changedNodes.get(parentKey);
if (parent != null) {
// Then the parent was changed in this session, so find the one-and-only child reference ...
ChildReference ref = parent.getChildReferences(this).getChild(key);
Path parentPath = sessionPaths.getPath(parent);
Path childPath = pathFactory().create(parentPath, ref.getSegment());
changes.nodeCreated(key, parentKey, childPath, primaryType, mixinTypes, null, queryable);
}
}
// Generate NODE_REMOVED events for each of the newly-removed parents ...
for (NodeKey parentKey : additionalParents.getRemovals()) {
// We need to read some information from the parent node before it was changed ...
CachedNode persistedParent = persistedCache.getNode(parentKey);
if (persistedParent != null) {
// Find the path to the removed child ...
ChildReference ref = persistedParent.getChildReferences(this).getChild(key);
if (ref != null) {
Path parentPath = workspacePaths.getPath(persistedParent);
Path childPath = pathFactory().create(parentPath, ref.getSegment());
changes.nodeRemoved(key, parentKey, childPath, primaryType, mixinTypes, queryable);
}
}
}
}
}
}
if (removedNodes != null) {
assert !removedNodes.isEmpty();
// we need to collect the referrers at the end only, so that other potential changes in references have been computed
Set<NodeKey> referrers = new HashSet<NodeKey>();
for (NodeKey removedKey : removedNodes) {
// we need the current document from the documentStore, because this differs from what's persisted
SchematicEntry entry = documentStore.get(removedKey.toString());
if (entry != null) {
// The entry hasn't yet been removed by another (concurrent) session ...
Document doc = documentStore.get(removedKey.toString()).getContent();
referrers.addAll(translator.getReferrers(doc, ReferenceType.STRONG));
}
}
// check referential integrity ...
referrers.removeAll(removedNodes);
if (!referrers.isEmpty()) {
throw new ReferentialIntegrityException(removedNodes, referrers);
}
// Now remove all of the nodes from the documentStore.
// Note 2: we do this last because the children are removed from their parent before the removal is handled above
// (see Node 1), meaning getting the path and other information for removed nodes never would work properly.
for (NodeKey removedKey : removedNodes) {
documentStore.remove(removedKey.toString());
}
}
if (!unusedBinaryKeys.isEmpty()) {
// There are some binary values that are no longer referenced ...
for (BinaryKey key : unusedBinaryKeys) {
changes.binaryValueNoLongerUsed(key);
}
}
if (!usedBinaryKeys.isEmpty()) {
// There are some binary values which need to be marked as used ...
for (BinaryKey key : usedBinaryKeys) {
changes.binaryValueUsed(key);
}
}
changes.setChangedNodes(changedNodes.keySet()); // don't need to make a copy
changes.freeze(userId, userData, timestamp);
return changes;
}