/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr.cache.change; import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.value.BinaryKey; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.Path.Segment; import org.modeshape.jcr.value.Property; /** * A thread-safe {@link Changes} implementation that records the changes and makes them available for iteration. */ @ThreadSafe public class RecordingChanges implements Changes, ChangeSet { private static final long serialVersionUID = 1L; /** * A map used to make sure that Name instances are reused, i.e. the same name instance is used for the same type. The main * purpose of this is to reduce the serialized footprint of the change set. */ private static final transient ConcurrentHashMap<Name, Name> NAME_INSTANCES_MAP = new ConcurrentHashMap<Name, Name>(); private final String processKey; private final String repositoryKey; private final String workspaceName; private final String journalId; private final String sessionId; private final Queue<Change> events = new ConcurrentLinkedQueue<Change>(); private final String uuid = UUID.randomUUID().toString(); private Set<NodeKey> nodeKeys = Collections.emptySet(); private Map<String, String> userData = Collections.emptyMap(); private Set<BinaryKey> unusedBinaries = Collections.newSetFromMap(new ConcurrentHashMap<BinaryKey, Boolean>()); private Set<BinaryKey> usedBinaries = Collections.newSetFromMap(new ConcurrentHashMap<BinaryKey, Boolean>()); private String userId; private DateTime timestamp; /** * Creates a new change set. * @param sessionId the ID of the session in which the change set was created; may not be null; * @param processKey the UUID of the process which created the change set; may not be null * @param repositoryKey the key of the repository for which the changes set is created; may not be null. * @param workspaceName the name of the workspace in which the changes occurred; may be null. * @param journalId the ID of the journal where this change set will be saved; may be null */ public RecordingChanges( String sessionId, String processKey, String repositoryKey, String workspaceName, String journalId ) { this.sessionId = sessionId; this.processKey = processKey; this.repositoryKey = repositoryKey; this.workspaceName = workspaceName; this.journalId = journalId; assert this.sessionId != null; assert this.processKey != null; assert this.repositoryKey != null; } @Override public void workspaceAdded( String workspaceName ) { events.add(new WorkspaceAdded(workspaceName)); } @Override public void workspaceRemoved( String workspaceName ) { events.add(new WorkspaceRemoved(workspaceName)); } @Override public void repositoryMetadataChanged() { events.add(new RepositoryMetadataChanged()); } @Override public void nodeCreated( NodeKey key, NodeKey parentKey, Path path, Name primaryType, Set<Name> mixinTypes, Map<Name, Property> properties ) { events.add(new NodeAdded(key, parentKey, path, filterName(primaryType), filterNameSet(mixinTypes), properties)); } @Override public void nodeRemoved( NodeKey key, NodeKey parentKey, Path path, Name primaryType, Set<Name> mixinTypes, Name parentPrimaryType, Set<Name> parentMixinTypes ) { events.add(new NodeRemoved(key, parentKey, path, filterName(primaryType), filterNameSet(mixinTypes), parentPrimaryType, parentMixinTypes)); } @Override public void nodeRenamed( NodeKey key, Path newPath, Segment oldName, Name primaryType, Set<Name> mixinTypes ) { events.add(new NodeRenamed(key, newPath, oldName, filterName(primaryType), filterNameSet(mixinTypes))); } @Override public void nodeMoved( NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey newParent, NodeKey oldParent, Path newPath, Path oldPath ) { events.add(new NodeMoved(key, filterName(primaryType), filterNameSet(mixinTypes), newParent, oldParent, newPath, oldPath )); } @Override public void nodeReordered( NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey parent, Path newPath, Path oldPath, Path reorderedBeforePath ) { events.add(new NodeReordered(key, filterName(primaryType), filterNameSet(mixinTypes), parent, newPath, oldPath, reorderedBeforePath)); } @Override public void nodeReordered (NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey parent, Path newPath, Path oldPath, Path reorderedBeforePath, Map<NodeKey, Map<Path, Path>> snsPathChangesByNodeKey) { events.add(new NodeReordered(key, filterName(primaryType), filterNameSet(mixinTypes), parent, newPath, oldPath, reorderedBeforePath, snsPathChangesByNodeKey)); } @Override public void nodeChanged( NodeKey key, Path path, Name primaryType, Set<Name> mixinTypes ) { events.add(new NodeChanged(key, path, filterName(primaryType), filterNameSet(mixinTypes))); } @Override public void nodeSequenced( NodeKey sequencedNodeKey, Path sequencedNodePath, Name sequencedNodePrimaryType, Set<Name> sequencedNodeMixinTypes, NodeKey outputNodeKey, Path outputNodePath, String outputPath, String userId, String selectedPath, String sequencerName ) { events.add(new NodeSequenced(sequencedNodeKey, sequencedNodePath, filterName(sequencedNodePrimaryType), filterNameSet(sequencedNodeMixinTypes), outputNodeKey, outputNodePath, outputPath, userId, selectedPath, sequencerName)); } @Override public void nodeSequencingFailure( NodeKey sequencedNodeKey, Path sequencedNodePath, Name sequencedNodePrimaryType, Set<Name> sequencedNodeMixinTypes, String outputPath, String userId, String selectedPath, String sequencerName, Throwable cause ) { events.add(new NodeSequencingFailure(sequencedNodeKey, sequencedNodePath, filterName(sequencedNodePrimaryType), filterNameSet(sequencedNodeMixinTypes), outputPath, userId, selectedPath, sequencerName, cause)); } @Override public void propertyAdded( NodeKey key, Name nodePrimaryType, Set<Name> nodeMixinTypes, Path nodePath, Property property ) { events.add(new PropertyAdded(key, filterName(nodePrimaryType), filterNameSet(nodeMixinTypes), nodePath, property )); } @Override public void propertyRemoved( NodeKey key, Name nodePrimaryType, Set<Name> nodeMixinTypes, Path nodePath, Property property ) { events.add(new PropertyRemoved(key, filterName(nodePrimaryType), filterNameSet(nodeMixinTypes), nodePath, property )); } @Override public void propertyChanged( NodeKey key, Name nodePrimaryType, Set<Name> nodeMixinTypes, Path nodePath, Property newProperty, Property oldProperty ) { events.add(new PropertyChanged(key, filterName(nodePrimaryType), filterNameSet(nodeMixinTypes), nodePath, newProperty, oldProperty)); } @Override public void binaryValueNoLongerUsed( BinaryKey key ) { events.add(new BinaryValueUnused(key)); unusedBinaries.add(key); } @Override public void binaryValueUsed( BinaryKey key ) { events.add(new BinaryValueUsed(key)); usedBinaries.add(key); } @Override public int size() { return events.size(); } @Override public boolean isEmpty() { return events.isEmpty() && nodeKeys.isEmpty(); // not all changed nodes cause events (e.g., shared nodes) } /** * Returns an iterator over the elements in this queue in proper sequence. The returned iterator is a "weakly consistent" * iterator that will never throw {@link ConcurrentModificationException}, and guarantees to traverse elements as they existed * upon construction of the iterator, and may (but is not guaranteed to) reflect any modifications subsequent to construction. * * @return an iterator over the elements in this queue in proper sequence; never null * @see java.lang.Iterable#iterator() */ @Override public Iterator<Change> iterator() { return events.iterator(); } @Override public Set<NodeKey> changedNodes() { return nodeKeys; } @Override public Set<BinaryKey> unusedBinaries() { return unusedBinaries; } @Override public Set<BinaryKey> usedBinaries() { return usedBinaries; } @Override public boolean hasBinaryChanges() { return !usedBinaries.isEmpty() || !unusedBinaries.isEmpty(); } /** * Sets the list of node keys involved in this change set. * * @param keys a Set<NodeKey>; may not be null */ public void setChangedNodes( Set<NodeKey> keys ) { if (keys != null) { this.nodeKeys = Collections.unmodifiableSet(new HashSet<NodeKey>(keys)); } } /** * Marks this change set as frozen (aka. closed). This means it should not accept any more changes. * * @param userId the username from the session which originated the changes; may not be null * @param userData a Map which can contains arbitrary information; may be null * @param timestamp a {@link DateTime} at which the changes set was created. */ public void freeze( String userId, Map<String, String> userData, DateTime timestamp ) { this.userId = userId; if (userData != null) { this.userData = Collections.unmodifiableMap(userData); } this.timestamp = timestamp; } @Override public String getProcessKey() { return processKey; } @Override public String getRepositoryKey() { return repositoryKey; } @Override public String getWorkspaceName() { return workspaceName; } @Override public DateTime getTimestamp() { return timestamp; } @Override public Map<String, String> getUserData() { return userData; } @Override public String getUserId() { return userId; } @Override public String getSessionId() { return sessionId; } @Override public String getJournalId() { return journalId; } @Override public String getUUID() { return uuid; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Save by '").append(getUserId()).append("' at ").append(getTimestamp()).append(" with user data = ") .append(userData).append(" in repository with key '").append(repositoryKey).append("' and workspace '") .append(workspaceName); if (journalId != null) { sb.append(". Journal id=").append(journalId); } sb.append("'\n"); for (Change change : this) { sb.append(" ").append(change).append("\n"); } sb.append("changed ").append(nodeKeys.size()).append(" nodes:\n"); for (NodeKey key : nodeKeys) { sb.append(" ").append(key).append("\n"); } return sb.toString(); } private Name filterName( Name input ) { if (input == null) { return null; } Name name = NAME_INSTANCES_MAP.putIfAbsent(input, input); return name != null ? name : input; } private Set<Name> filterNameSet( Set<Name> input ) { Set<Name> result = new HashSet<Name>(input.size()); for (Name name : input) { result.add(filterName(name)); } return result; } }