/*
* 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.core.state;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import javax.jcr.PropertyType;
import javax.jcr.ReferentialIntegrityException;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.observation.EventStateCollectionFactory;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider;
import org.apache.jackrabbit.data.core.InternalXAResource;
import org.apache.jackrabbit.data.core.TransactionContext;
import org.apache.jackrabbit.data.core.TransactionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extension to <code>LocalItemStateManager</code> that remembers changes on
* multiple save() requests and commits them only when an associated transaction
* is itself committed.
*/
public class XAItemStateManager extends LocalItemStateManager implements InternalXAResource {
/**
* The logger instance.
*/
private static Logger log = LoggerFactory.getLogger(XAItemStateManager.class);
/**
* Default change log attribute name.
*/
private static final String DEFAULT_ATTRIBUTE_NAME = "ChangeLog";
/**
* This map holds the ChangeLog on a per thread basis while this state
* manager is in one of the {@link #prepare}, {@link #commit}, {@link
* #rollback} methods.
*/
private final Map commitLogs = Collections.synchronizedMap(new IdentityHashMap());
/**
* Current instance-local change log.
*/
private transient ChangeLog txLog;
/**
* Current update operation.
*/
private transient SharedItemStateManager.Update update;
/**
* Change log attribute name.
*/
private final String attributeName;
/**
* Optional virtual item state provider.
*/
private VirtualItemStateProvider virtualProvider;
/**
* Creates a new instance of this class with a custom attribute name.
*
* @param sharedStateMgr shared state manager
* @param factory event state collection factory
* @param attributeName the attribute name, if {@code null} then a default name is used
*/
protected XAItemStateManager(SharedItemStateManager sharedStateMgr,
EventStateCollectionFactory factory,
String attributeName,
ItemStateCacheFactory cacheFactory) {
super(sharedStateMgr, factory, cacheFactory);
if (attributeName != null) {
this.attributeName = attributeName;
} else {
this.attributeName = DEFAULT_ATTRIBUTE_NAME;
}
}
/**
* Creates a new {@code XAItemStateManager} instance and registers it as an {@link ItemStateListener}
* with the given {@link SharedItemStateManager}.
*
* @param sharedStateMgr the {@link SharedItemStateManager}
* @param factory the {@link EventStateCollectionFactory}
* @param attributeName the attribute name, if {@code null} then a default name is used
* @param cacheFactory the {@link ItemStateCacheFactory}
* @return a new {@code XAItemStateManager} instance
*/
public static XAItemStateManager createInstance(SharedItemStateManager sharedStateMgr,
EventStateCollectionFactory factory, String attributeName, ItemStateCacheFactory cacheFactory) {
XAItemStateManager mgr = new XAItemStateManager(sharedStateMgr, factory, attributeName, cacheFactory);
sharedStateMgr.addListener(mgr);
return mgr;
}
/**
* Set optional virtual item state provider.
*/
public void setVirtualProvider(VirtualItemStateProvider virtualProvider) {
this.virtualProvider = virtualProvider;
}
/**
* {@inheritDoc}
*/
public void associate(TransactionContext tx) {
ChangeLog txLog = null;
if (tx != null) {
txLog = (ChangeLog) tx.getAttribute(attributeName);
if (txLog == null) {
txLog = new ChangeLog();
tx.setAttribute(attributeName, txLog);
}
}
this.txLog = txLog;
}
/**
* {@inheritDoc}
*/
public void beforeOperation(TransactionContext tx) {
ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName);
if (txLog != null) {
commitLogs.put(Thread.currentThread(), txLog);
}
}
/**
* {@inheritDoc}
*/
public void prepare(TransactionContext tx) throws TransactionException {
ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName);
if (txLog != null && txLog.hasUpdates()) {
try {
if (virtualProvider != null) {
updateVirtualReferences(txLog);
}
update = sharedStateMgr.beginUpdate(txLog, factory, virtualProvider);
} catch (ReferentialIntegrityException rie) {
txLog.undo(sharedStateMgr);
throw new TransactionException("Unable to prepare transaction.", rie);
} catch (ItemStateException ise) {
txLog.undo(sharedStateMgr);
throw new TransactionException("Unable to prepare transaction.", ise);
}
}
}
/**
* {@inheritDoc}
*/
public void commit(TransactionContext tx) throws TransactionException {
ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName);
if (txLog != null && txLog.hasUpdates()) {
try {
update.end();
} catch (ItemStateException ise) {
txLog.undo(sharedStateMgr);
throw new TransactionException("Unable to commit transaction.", ise);
}
txLog.reset();
}
}
/**
* {@inheritDoc}
*/
public void rollback(TransactionContext tx) {
ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName);
if (txLog != null && txLog.hasUpdates()) {
if (update != null) {
update.cancel();
}
txLog.undo(sharedStateMgr);
}
}
/**
* {@inheritDoc}
*/
public void afterOperation(TransactionContext tx) {
commitLogs.remove(Thread.currentThread());
}
/**
* Returns the current change log. First tries thread-local change log,
* then instance-local change log. Returns <code>null</code> if no
* change log was found.
*/
public ChangeLog getChangeLog() {
ChangeLog changeLog = (ChangeLog) commitLogs.get(Thread.currentThread());
if (changeLog == null) {
changeLog = txLog;
}
return changeLog;
}
/**
* @throws UnsupportedOperationException always.
*/
protected ChangeLog getChanges() {
throw new UnsupportedOperationException("getChanges");
}
//-----------------------------------------------------< ItemStateManager >
/**
* {@inheritDoc}
* <p>
* If this state manager is committing changes, this method first checks
* the commitLog ThreadLocal. Else if associated to a transaction check
* the transactional change log. Fallback is always the call to the base
* class.
*/
public ItemState getItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
if (virtualProvider != null && virtualProvider.hasItemState(id)) {
return virtualProvider.getItemState(id);
}
// 1) check local changes
ChangeLog changeLog = super.getChanges();
ItemState state = changeLog.get(id);
if (state != null) {
return state;
}
// 2) check tx log
changeLog = getChangeLog();
if (changeLog != null) {
state = changeLog.get(id);
if (state != null) {
return state;
}
}
// 3) fallback to base class
return super.getItemState(id);
}
/**
* {@inheritDoc}
* <p>
* If this state manager is committing changes, this method first checks
* the commitLog ThreadLocal. Else if associated to a transaction check
* the transactional change log. Fallback is always the call to the base
* class.
*/
public boolean hasItemState(ItemId id) {
if (virtualProvider != null && virtualProvider.hasItemState(id)) {
return true;
}
// 1) check local changes
ChangeLog changeLog = super.getChanges();
try {
ItemState state = changeLog.get(id);
if (state != null) {
return true;
}
} catch (NoSuchItemStateException e) {
// marked removed in local ism
return false;
}
// if we get here, then there is no item state with
// the given id known to the local ism
// 2) check tx log
changeLog = getChangeLog();
if (changeLog != null) {
try {
ItemState state = changeLog.get(id);
if (state != null) {
return true;
}
} catch (NoSuchItemStateException e) {
// marked removed in tx log
return false;
}
}
// 3) fallback to shared ism
return sharedStateMgr.hasItemState(id);
}
/**
* {@inheritDoc}
* <p>
* If this state manager is committing changes, this method first
* checks the commitLog ThreadLocal. Else if associated to a transaction
* check the transactional change log. Fallback is always the call to
* the base class.
*/
public NodeReferences getNodeReferences(NodeId id)
throws NoSuchItemStateException, ItemStateException {
if (virtualProvider != null && virtualProvider.hasNodeReferences(id)) {
return virtualProvider.getNodeReferences(id);
}
return getReferences(id);
}
/**
* {@inheritDoc}
* <p>
* If this state manager is committing changes, this method first
* checks the commitLog ThreadLocal. Else if associated to a transaction
* check the transactional change log. Fallback is always the call to
* the base class.
*/
public boolean hasNodeReferences(NodeId id) {
if (virtualProvider != null && virtualProvider.hasNodeReferences(id)) {
return true;
}
try {
return getReferences(id).hasReferences();
} catch (ItemStateException e) {
return false;
}
}
/**
* {@inheritDoc}
* <p>
* If associated with a transaction, simply merge the changes given to
* the ones already known (removing items that were first added and
* then again deleted).
*/
protected void update(ChangeLog changeLog)
throws ReferentialIntegrityException, StaleItemStateException,
ItemStateException {
if (txLog != null) {
txLog.merge(changeLog);
} else {
super.update(changeLog);
}
}
//-------------------------------------------------------< implementation >
/**
* Returns the node references for the given <code>id</code>.
*
* @param id the node references id.
* @return the node references for the given <code>id</code>.
* @throws ItemStateException if an error occurs while reading from the
* underlying shared item state manager.
*/
private NodeReferences getReferences(NodeId id)
throws ItemStateException {
NodeReferences refs;
try {
refs = super.getNodeReferences(id);
} catch (NoSuchItemStateException e) {
refs = new NodeReferences(id);
}
// apply changes from change log
ChangeLog changes = getChangeLog();
if (changes != null) {
// check removed reference properties
for (PropertyState prop : filterReferenceProperties(changes.deletedStates())) {
InternalValue[] values = prop.getValues();
for (int i = 0; i < values.length; i++) {
if (values[i].getNodeId().equals(id)) {
refs.removeReference(prop.getPropertyId());
break;
}
}
}
// check added reference properties
for (PropertyState prop : filterReferenceProperties(changes.addedStates())) {
InternalValue[] values = prop.getValues();
for (int i = 0; i < values.length; i++) {
if (values[i].getNodeId().equals(id)) {
refs.addReference(prop.getPropertyId());
break;
}
}
}
// check modified properties
for (ItemState state : changes.modifiedStates()) {
if (state.isNode()) {
continue;
}
try {
PropertyState old = (PropertyState) sharedStateMgr.getItemState(state.getId());
if (old.getType() == PropertyType.REFERENCE) {
// remove if one of the old values references the node
InternalValue[] values = old.getValues();
for (int i = 0; i < values.length; i++) {
if (values[i].getNodeId().equals(id)) {
refs.removeReference(old.getPropertyId());
break;
}
}
}
} catch (NoSuchItemStateException e) {
// property is stale
}
PropertyState prop = (PropertyState) state;
if (prop.getType() == PropertyType.REFERENCE) {
// add if modified value references node
InternalValue[] values = prop.getValues();
for (int i = 0; i < values.length; i++) {
if (values[i].getNodeId().equals(id)) {
refs.addReference(prop.getPropertyId());
break;
}
}
}
}
}
return refs;
}
/**
* Takes an iterator over {@link ItemState}s and returns a new iterator that
* filters out all but REFERENCE {@link PropertyState}s.
*
* @param itemStates item state source iterator.
* @return iterator over reference property states.
*/
private Iterable<PropertyState> filterReferenceProperties(
final Iterable<ItemState> itemStates) {
return new Iterable<PropertyState>() {
@SuppressWarnings("unchecked")
public Iterator<PropertyState> iterator() {
return (Iterator<PropertyState>) new FilterIterator(
itemStates.iterator(), new Predicate() {
public boolean evaluate(Object object) {
ItemState state = (ItemState) object;
if (!state.isNode()) {
PropertyState prop = (PropertyState) state;
return prop.getType() == PropertyType.REFERENCE;
}
return false;
}
});
}
};
}
/**
* Determine all node references whose targets only exist in the view of
* this transaction and store the modified view back to the virtual provider.
* @param changes change log
* @throws ItemStateException if an error occurs
*/
private void updateVirtualReferences(ChangeLog changes) throws ItemStateException {
ChangeLog references = new ChangeLog();
for (ItemState state : changes.addedStates()) {
if (!state.isNode()) {
PropertyState prop = (PropertyState) state;
if (prop.getType() == PropertyType.REFERENCE) {
InternalValue[] vals = prop.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
addVirtualReference(
references, prop.getPropertyId(),
vals[i].getNodeId());
}
}
}
}
for (ItemState state : changes.modifiedStates()) {
if (!state.isNode()) {
PropertyState newProp = (PropertyState) state;
PropertyState oldProp =
(PropertyState) getItemState(state.getId());
if (oldProp.getType() == PropertyType.REFERENCE) {
InternalValue[] vals = oldProp.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
removeVirtualReference(
references, oldProp.getPropertyId(),
vals[i].getNodeId());
}
}
if (newProp.getType() == PropertyType.REFERENCE) {
InternalValue[] vals = newProp.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
addVirtualReference(
references, newProp.getPropertyId(),
vals[i].getNodeId());
}
}
}
}
for (ItemState state : changes.deletedStates()) {
if (!state.isNode()) {
PropertyState prop = (PropertyState) state;
if (prop.getType() == PropertyType.REFERENCE) {
InternalValue[] vals = prop.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
removeVirtualReference(
references, prop.getPropertyId(),
vals[i].getNodeId());
}
}
}
}
virtualProvider.setNodeReferences(references);
}
/**
* Add a virtual reference from some reference property to a virtual node.
* Ignored if <code>refsId.getTargetId()</code> does not denote a
* virtual node.
* @param sourceId property id
* @param targetId target node id
*/
private void addVirtualReference(
ChangeLog references, PropertyId sourceId, NodeId targetId)
throws NoSuchItemStateException, ItemStateException {
NodeReferences refs = references.getReferencesTo(targetId);
if (refs == null) {
refs = virtualProvider.getNodeReferences(targetId);
}
if (refs == null && virtualProvider.hasItemState(targetId)) {
refs = new NodeReferences(targetId);
}
if (refs != null) {
refs.addReference(sourceId);
references.modified(refs);
}
}
/**
* Remove a virtual reference from some reference property to a virtual node.
* Ignored if <code>refsId.getTargetId()</code> does not denote a
* virtual node.
* @param sourceId property id
* @param targetId target node id
*/
private void removeVirtualReference(
ChangeLog references, PropertyId sourceId, NodeId targetId)
throws NoSuchItemStateException, ItemStateException {
NodeReferences refs = references.getReferencesTo(targetId);
if (refs == null) {
refs = virtualProvider.getNodeReferences(targetId);
}
if (refs == null && virtualProvider.hasItemState(targetId)) {
refs = new NodeReferences(targetId);
}
if (refs != null) {
refs.removeReference(sourceId);
references.modified(refs);
}
}
/**
* {@inheritDoc}
*
* Check whether the shared state modified is contained in our transactional
* log: in that case, update its state as well, as it might get reused
* in a subsequent transaction (see JCR-1554).
*/
public void stateModified(ItemState modified) {
ChangeLog changeLog = (ChangeLog) commitLogs.get(Thread.currentThread());
if (changeLog != null) {
ItemState local;
if (modified.getContainer() != this) {
// shared state was modified
try {
local = changeLog.get(modified.getId());
if (local != null && local.isConnected()) {
local.pull();
}
} catch (NoSuchItemStateException e) {
log.warn("Modified state marked for deletion: " + modified.getId());
}
}
}
super.stateModified(modified);
}
}