org.apache.jackrabbit.jcr2spi.state.TransientItemStateManager Maven / Gradle / Ivy
/*
* 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.jcr2spi.state;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemExistsException;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import org.apache.commons.collections.iterators.IteratorChain;
import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry;
import org.apache.jackrabbit.jcr2spi.operation.Operation;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.QValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TransientItemStateManager
adds support for transient changes on
* {@link ItemState}s and also provides methods to create new item states.
* While all other modifications can be invoked on the item state instances itself,
* creating a new node state is done using
* {@link #createNewNodeState(Name, String, Name, QNodeDefinition, NodeState)}
* and
* {@link #createNewPropertyState(Name, NodeState, QPropertyDefinition, QValue[], int)}.
*/
public class TransientItemStateManager implements ItemStateCreationListener {
/**
* Logger instance for this class.
*/
private static final Logger log = LoggerFactory.getLogger(TransientItemStateManager.class);
/**
* Added states
*/
private final Set addedStates = new LinkedHashSet();
/**
* Modified states
*/
private final Set modifiedStates = new LinkedHashSet();
/**
* Removed states
*/
private final Set removedStates = new LinkedHashSet();
/**
* Stale states
*/
private final Set staleStates = new LinkedHashSet();
/**
* Set of operations
*/
private final Set operations = new LinkedHashSet();
/**
*
*/
TransientItemStateManager() {
}
/**
* @return the operations that have been recorded until now.
*/
Iterator getOperations() {
return operations.iterator();
}
/**
* Add the given operation to the list of operations to be recorded within
* this TransientItemStateManager.
*
* @param operation
*/
void addOperation(Operation operation) {
operations.add(operation);
}
/**
* @return true
if this transient ISM has pending changes.
*/
boolean hasPendingChanges() {
return !operations.isEmpty();
}
/**
* Create the change log for the tree starting at target
. This
* includes a check if the ChangeLog to be created is totally 'self-contained'
* and independent; items within the scope of this update operation (i.e.
* below the target) must not have dependencies outside of this tree (e.g.
* moving a node requires that the target node including both old and new
* parents are saved).
*
* @param target
* @param throwOnStale Throws InvalidItemStateException if either the given
* ItemState
or any of its descendants is stale and the flag is true.
* @return
* @throws InvalidItemStateException if a stale ItemState
is
* encountered while traversing the state hierarchy. The changeLog
* might have been populated with some transient item states. A client should
* therefore not reuse the changeLog
if such an exception is thrown.
* @throws RepositoryException if state
is a new item state.
*/
ChangeLog getChangeLog(ItemState target, boolean throwOnStale) throws InvalidItemStateException, ConstraintViolationException, RepositoryException {
// fail-fast test: check status of this item's state
if (target.getStatus() == Status.NEW) {
String msg = "Cannot save/revert an item with status NEW (" +target+ ").";
log.debug(msg);
throw new RepositoryException(msg);
}
if (throwOnStale && Status.isStale(target.getStatus())) {
String msg = "Attempt to save/revert an item, that has been externally modified (" +target+ ").";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
Set ops = new LinkedHashSet();
Set affectedStates = new LinkedHashSet();
HierarchyEntry he = target.getHierarchyEntry();
if (he.getParent() == null) {
// the root entry -> the complete change log can be used for
// simplicity. collecting ops, states can be omitted.
if (throwOnStale && !staleStates.isEmpty()) {
String msg = "Cannot save changes: States has been modified externally.";
log.debug(msg);
throw new InvalidItemStateException(msg);
} else {
affectedStates.addAll(staleStates);
}
ops.addAll(operations);
affectedStates.addAll(addedStates);
affectedStates.addAll(modifiedStates);
affectedStates.addAll(removedStates);
} else {
// not root entry:
// - check if there is a stale state in the scope (save only)
if (throwOnStale) {
for (ItemState state : staleStates) {
if (containedInTree(target, state)) {
String msg = "Cannot save changes: States has been modified externally.";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
}
}
// - collect all affected states within the scope of save/undo
Iterator[] its = new Iterator[] {
addedStates.iterator(),
removedStates.iterator(),
modifiedStates.iterator()
};
IteratorChain chain = new IteratorChain(its);
if (!throwOnStale) {
chain.addIterator(staleStates.iterator());
}
while (chain.hasNext()) {
ItemState state = (ItemState) chain.next();
if (containedInTree(target, state)) {
affectedStates.add(state);
}
}
// - collect the set of operations and
// check if the affected states listed by the operations are all
// listed in the modified,removed or added states collected by this
// changelog.
for (Operation op : operations) {
Collection opStates = op.getAffectedItemStates();
for (ItemState state : opStates) {
if (affectedStates.contains(state)) {
// operation needs to be included
if (!affectedStates.containsAll(opStates)) {
// incomplete changelog: need to save a parent as well
String msg = "ChangeLog is not self contained.";
throw new ConstraintViolationException(msg);
}
// no violation: add operation an stop iteration over
// all affected states present in the operation.
ops.add(op);
break;
}
}
}
}
ChangeLog cl = new ChangeLog(target, ops, affectedStates);
return cl;
}
/**
* Creates a new transient {@link NodeState} that does not overlay any other
* {@link NodeState}.
*
* @param nodeName the name of the NodeState
to create.
* @param uniqueID the uniqueID of the NodeState
to create or
* null
if the created NodeState
* cannot be identified by a unique ID.
* @param nodeTypeName name of the node type of the new node state.
* @param definition The definition for the new node state.
* @param parent the parent of the new node state.
* @return a new transient {@link NodeState}.
*/
NodeState createNewNodeState(Name nodeName, String uniqueID, Name nodeTypeName,
QNodeDefinition definition, NodeState parent)
throws RepositoryException {
NodeEntry ne = ((NodeEntry) parent.getHierarchyEntry()).addNewNodeEntry(nodeName, uniqueID, nodeTypeName, definition);
try {
parent.markModified();
} catch (RepositoryException e) {
ne.remove();
throw e;
}
return ne.getNodeState();
}
/**
* Creates a new transient property state for a given parent
* node state.
*
* @param propName the name of the property state to create.
* @param parent the node state where to the new property is added.
* @param definition
* @return the created property state.
* @throws ItemExistsException if parent
already has a property
* with the given name.
* @throws ConstraintViolationException
* @throws RepositoryException
*/
PropertyState createNewPropertyState(Name propName, NodeState parent,
QPropertyDefinition definition,
QValue[] values, int propertyType)
throws ItemExistsException, ConstraintViolationException, RepositoryException {
// NOTE: callers must make sure, the property type is not 'undefined'
NodeEntry nodeEntry = (NodeEntry) parent.getHierarchyEntry();
PropertyEntry pe = nodeEntry.addNewPropertyEntry(propName, definition, values, propertyType);
try {
parent.markModified();
} catch (RepositoryException e) {
pe.remove();
throw e;
}
return pe.getPropertyState();
}
/**
* Disposes this transient item state manager. Clears all references to
* transiently modified item states.
*/
void dispose() {
addedStates.clear();
modifiedStates.clear();
removedStates.clear();
staleStates.clear();
// also clear all operations
operations.clear();
}
/**
* Remove the states and operations listed in the changeLog from internal
* list of modifications.
*
* @param subChangeLog
*/
void dispose(ChangeLog subChangeLog) {
Set affectedStates = subChangeLog.getAffectedStates();
addedStates.removeAll(affectedStates);
modifiedStates.removeAll(affectedStates);
removedStates.removeAll(affectedStates);
staleStates.removeAll(affectedStates);
operations.removeAll(subChangeLog.getOperations());
}
/**
* A state has been removed. If the state is not a new state
* (not in the collection of added ones), then remove
* it from the modified states collection and add it to the
* removed states collection.
*
* @param state state that has been removed
*/
private void removed(ItemState state) {
if (!addedStates.remove(state)) {
modifiedStates.remove(state);
}
removedStates.add(state);
}
/**
*
* @param parent
* @param state
* @return
*/
private static boolean containedInTree(ItemState parent, ItemState state) {
HierarchyEntry he = state.getHierarchyEntry();
HierarchyEntry pHe = parent.getHierarchyEntry();
// short cuts first
if (he == pHe || he.getParent() == pHe) {
return true;
}
if (!parent.isNode() || he == pHe.getParent()) {
return false;
}
// none of the simple cases: walk up hierarchy
HierarchyEntry pe = he.getParent();
while (pe != null) {
if (pe == pHe) {
return true;
}
pe = pe.getParent();
}
// state isn't descendant of 'parent'
return false;
}
//-----------------------------------------< ItemStateLifeCycleListener >---
/**
* Depending on status of the given state adapt change log.
* E.g. a revert on states will reset the status from 'existing modified' to
* 'existing'. A state which changes from 'existing' to 'existing modified'
* will go into the modified set of the change log, etc.
*
* @see ItemStateLifeCycleListener#statusChanged(ItemState, int)
*/
public void statusChanged(ItemState state, int previousStatus) {
/*
Update the collections of states that were transiently modified.
NOTE: cleanup of operations is omitted here. this is expected to
occur upon {@link ChangeLog#save()} and {@link ChangeLog#undo()}.
External modifications in contrast that clash with transient modifications
render the corresponding states stale.
*/
switch (state.getStatus()) {
case (Status.EXISTING):
switch (previousStatus) {
case Status.EXISTING_MODIFIED:
// was modified and got persisted or reverted
modifiedStates.remove(state);
break;
case Status.EXISTING_REMOVED:
// was transiently removed and is now reverted
removedStates.remove(state);
break;
case Status.STALE_MODIFIED:
// was modified and stale and is now reverted
staleStates.remove(state);
break;
case Status.NEW:
// was new and has been saved now
addedStates.remove(state);
break;
//default:
// INVALIDATED, MODIFIED ignore. no effect to transient modifications.
// any other status change is invalid -> see Status#isValidStatusChange(int, int
}
break;
case Status.EXISTING_MODIFIED:
// transition from EXISTING to EXISTING_MODIFIED
modifiedStates.add(state);
break;
case (Status.EXISTING_REMOVED):
// transition from EXISTING or EXISTING_MODIFIED to EXISTING_REMOVED
removed(state);
break;
case (Status.REMOVED):
switch (previousStatus) {
case Status.EXISTING_REMOVED:
// was transiently removed and removal was persisted.
// -> ignore
break;
case Status.NEW:
// a new entry was removed again: remember as removed
// in order to keep the operations and the affected
// states in sync
removed(state);
break;
}
// in any case: stop listening to status changes
state.removeListener(this);
break;
case Status.STALE_DESTROYED:
case Status.STALE_MODIFIED:
/**
state is stale due to external modification -> move it to
the collection of stale item states.
validation omitted for only 'existing_modified' states can
become stale see {@link Status#isValidStatusChange(int, int)}
*/
modifiedStates.remove(state);
staleStates.add(state);
break;
case Status.MODIFIED:
case Status.INVALIDATED:
// MODIFIED, INVALIDATED: ignore.
log.debug("Item " + state.getName() + " changed status from " + Status.getName(previousStatus) + " to " + Status.getName(state.getStatus()) + ".");
break;
default:
log.error("ItemState "+ state.getName() + " has invalid status: " + state.getStatus());
}
}
//-----------------------------------------< ItemStateCreationListener >---
/**
* @see ItemStateCreationListener#created(ItemState)
*/
public void created(ItemState state) {
// new state has been created
if (state.getStatus() == Status.NEW) {
addedStates.add(state);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy