/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.cache.interceptors;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeFactory;
import org.jboss.cache.NodeNotExistsException;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.config.Option;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.marshall.MethodCall;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jboss.cache.notifications.Notifier;
import static org.jboss.cache.notifications.event.NodeModifiedEvent.ModificationType.*;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.optimistic.DefaultDataVersion;
import org.jboss.cache.optimistic.TransactionWorkspace;
import org.jboss.cache.optimistic.WorkspaceNode;
import org.jboss.cache.transaction.GlobalTransaction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
/**
* Operations on nodes are done on the copies that exist in the workspace rather than passed down
* to the {@link org.jboss.cache.interceptors.CallInterceptor}. These operations happen in this interceptor.
*
* @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
* @author <a href="mailto:stevew@jofti.com">Steve Woodcock (stevew@jofti.com)</a>
*/
public class OptimisticNodeInterceptor extends OptimisticInterceptor
{
/**
* Needed for the creation of workspace nodes based on underlying nodes in the cache.
*/
private NodeFactory nodeFactory;
private Notifier notifier;
private long lockAcquisitionTimeout;
@Inject
protected void injectDependencies(Notifier notifier, NodeFactory nodeFactory)
{
this.notifier = notifier;
this.nodeFactory = nodeFactory;
}
@Override
public void setCache(CacheSPI cache)
{
super.setCache(cache);
lockAcquisitionTimeout = cache.getConfiguration().getLockAcquisitionTimeout();
}
public OptimisticNodeInterceptor()
{
initLogger();
}
@Override
public Object invoke(InvocationContext ctx) throws Throwable
{
MethodCall m = ctx.getMethodCall();
Object[] args = m.getArgs();
Object result = null;
if (MethodDeclarations.isCrudMethod(m.getMethodId()))
{
GlobalTransaction gtx = getGlobalTransaction(ctx);
TransactionWorkspace workspace = getTransactionWorkspace(gtx);
Fqn fqn = getFqn(args, m.getMethodId());
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, MethodDeclarations.removeNodeMethodLocal_id != m.getMethodId(), true);
// in the case of a data gravitation cleanup, if the primary Fqn does not exist the backup one may.
if (workspaceNode == null && m.getMethodId() == MethodDeclarations.dataGravitationCleanupMethod_id)
{
workspaceNode = fetchWorkspaceNode(ctx, getBackupFqn(args), workspace, true, true);
}
if (workspaceNode != null)
{
// use explicit versioning
if (ctx.getOptionOverrides() != null && ctx.getOptionOverrides().getDataVersion() != null)
{
// if the method call is a move() then barf. Note that remove calls will set data versions explicitly, regardless.
if (ctx.isOriginLocal() && m.getMethodId() == MethodDeclarations.moveMethodLocal_id)
throw new CacheException("Setting a data version while performing a move() is not supported!!");
workspace.setVersioningImplicit(false);
DataVersion version = ctx.getOptionOverrides().getDataVersion();
workspaceNode.setVersion(version);
if (trace) log.trace("Setting versioning for node " + workspaceNode.getFqn() + " to explicit");
workspaceNode.setVersioningImplicit(false);
}
else
{
if (trace) log.trace("Setting versioning for node " + workspaceNode.getFqn() + " to implicit");
workspaceNode.setVersioningImplicit(true);
}
}
else
{
// "fail-more-silently" patch thanks to Owen Taylor - JBCACHE-767
if ((ctx.getOptionOverrides() == null || !ctx.getOptionOverrides().isFailSilently()) && MethodDeclarations.isPutMethod(m.getMethodId()))
{
throw new CacheException("Unable to set node version for " + fqn + ", node is null.");
}
}
switch (m.getMethodId())
{
case MethodDeclarations.moveMethodLocal_id:
Fqn parentFqn = (Fqn) args[1];
moveNodeAndNotify(parentFqn, workspaceNode, workspace, ctx);
break;
case MethodDeclarations.putDataMethodLocal_id:
putDataMapAndNotify((Map<Object, Object>) args[2], false, workspace, workspaceNode, ctx);
break;
case MethodDeclarations.putDataEraseMethodLocal_id:
putDataMapAndNotify((Map<Object, Object>) args[2], (Boolean) args[args.length - 1], workspace, workspaceNode, ctx);
break;
case MethodDeclarations.putKeyValMethodLocal_id:
case MethodDeclarations.putForExternalReadMethodLocal_id:
Object key = args[2];
Object value = args[3];
result = putDataKeyValueAndNotify(key, value, workspace, workspaceNode, ctx);
break;
case MethodDeclarations.removeNodeMethodLocal_id:
result = removeNode(workspace, workspaceNode, true, ctx);
break;
case MethodDeclarations.removeKeyMethodLocal_id:
Object removeKey = args[2];
result = removeKeyAndNotify(removeKey, workspace, workspaceNode, ctx);
break;
case MethodDeclarations.removeDataMethodLocal_id:
removeDataAndNotify(workspace, workspaceNode, ctx);
break;
case MethodDeclarations.dataGravitationCleanupMethod_id:
result = nextInterceptor(ctx);
default:
if (log.isWarnEnabled()) log.warn("Cannot handle CRUD method " + m);
break;
}
addToModificationList(gtx, m, ctx);
}
else
{
switch (m.getMethodId())
{
case MethodDeclarations.getKeyValueMethodLocal_id:
result = getValueForKeyAndNotify(args, getTransactionWorkspace(getGlobalTransaction(ctx)), ctx);
break;
case MethodDeclarations.getKeysMethodLocal_id:
result = getKeysAndNotify(args, getTransactionWorkspace(getGlobalTransaction(ctx)), ctx);
break;
case MethodDeclarations.getDataMapMethodLocal_id:
result = getDataAndNotify(args, getTransactionWorkspace(getGlobalTransaction(ctx)), ctx);
break;
case MethodDeclarations.getChildrenNamesMethodLocal_id:
result = getChildNamesAndNotify(args, getTransactionWorkspace(getGlobalTransaction(ctx)), ctx);
break;
case MethodDeclarations.getNodeMethodLocal_id:
result = getNodeAndNotify(args, getTransactionWorkspace(getGlobalTransaction(ctx)), ctx);
break;
default:
if (trace) log.trace("read Method " + m + " called - Not handling, passing on.");
result = nextInterceptor(ctx);
break;
}
}
return result;
}
/**
* Extracts the Fqn from the array of arguments passed in
*
* @param args array of args
* @return fqn
*/
private Fqn getFqn(Object[] args, int methodId)
{
return (Fqn) args[methodId == MethodDeclarations.moveMethodLocal_id || methodId == MethodDeclarations.dataGravitationCleanupMethod_id ? 0 : 1];
}
/**
* Retrieves a backup fqn in an array of arguments. This is typically used to parse arguments from a data gravitation cleanup method.
*
* @param args array of arguments to parse
* @return an Fqn
*/
private Fqn getBackupFqn(Object[] args)
{
return (Fqn) args[1];
}
/**
* Adds a method call to the modification list of a given transaction's transaction entry
*
* @param gtx transaction
* @param m methodcall to add
*/
private void addToModificationList(GlobalTransaction gtx, MethodCall m, InvocationContext ctx)
{
Option opt = ctx.getOptionOverrides();
if (opt == null || !opt.isCacheModeLocal())
{
txTable.addModification(gtx, m);
if (log.isDebugEnabled()) log.debug("Adding Method " + m + " to modification list");
}
if (cache.getCacheLoaderManager() != null) txTable.addCacheLoaderModification(gtx, m);
}
// -----------------------------------------------------------------
// Methods that mimic core functionality in the cache, except that they work on WorkspaceNodes in the TransactionWorkspace.
// -----------------------------------------------------------------
/**
* Performs a move within the workspace.
*
* @param parentFqn parent under which the node is to be moved
* @param node node to move
* @param ws transaction workspace
* @param ctx
*/
private void moveNodeAndNotify(Fqn parentFqn, WorkspaceNode node, TransactionWorkspace ws, InvocationContext ctx)
{
Fqn nodeFqn = node.getFqn();
if (nodeFqn.isRoot())
{
log.warn("Attempting to move the root node. Not taking any action, treating this as a no-op.");
return;
}
WorkspaceNode oldParent = fetchWorkspaceNode(ctx, nodeFqn.getParent(), ws, false, true);
if (oldParent == null) throw new NodeNotExistsException("Node " + nodeFqn.getParent() + " does not exist!");
if (parentFqn.equals(oldParent.getFqn()))
{
log.warn("Attempting to move a node in same place. Not taking any action, treating this as a no-op.");
return;
}
// retrieve parent
WorkspaceNode parent = fetchWorkspaceNode(ctx, parentFqn, ws, false, true);
if (parent == null) throw new NodeNotExistsException("Node " + parentFqn + " does not exist!");
Object nodeName = nodeFqn.getLastElement();
// now that we have the parent and target nodes:
// first correct the pointers at the pruning point
oldParent.removeChild(new Fqn(nodeName));
// parent pointer is calculated on the fly using Fqns.
// now adjust Fqns of node and all children.
Fqn nodeNewFqn = new Fqn(parent.getFqn(), nodeFqn.getLastElement());
// pre-notify
notifier.notifyNodeMoved(nodeFqn, nodeNewFqn, true, ctx);
recursiveMoveNode(ctx, node, parent.getFqn(), ws);
// remove old nodes. this may mark some nodes which have already been moved as deleted
removeNode(ws, node, false, ctx);
// post-notify
notifier.notifyNodeMoved(nodeFqn, nodeNewFqn, false, ctx);
}
/**
* Moves a node to a new base.
*
* @param node node to move
* @param newBase new base Fqn under which the given node will now exist
* @param ws transaction workspace
*/
private void recursiveMoveNode(InvocationContext ctx, WorkspaceNode node, Fqn newBase, TransactionWorkspace ws)
{
Fqn newFqn = new Fqn(newBase, node.getFqn().getLastElement());
WorkspaceNode movedNode = fetchWorkspaceNode(ctx, newFqn, ws, true, true);
movedNode.putAll(node.getData());
// process children
for (Object n : node.getChildrenNames())
{
WorkspaceNode child = fetchWorkspaceNode(ctx, new Fqn(node.getFqn(), n), ws, false, true);
if (child != null) recursiveMoveNode(ctx, child, newFqn, ws);
}
}
private void putDataMapAndNotify(Map<Object, Object> data, boolean eraseExisitng, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
{
if (workspaceNode == null)
throw new NodeNotExistsException("optimisticCreateIfNotExistsInterceptor should have created this node!");
// pre-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), true, PUT_MAP, workspaceNode.getData(), ctx);
if (eraseExisitng) workspaceNode.clearData();
workspaceNode.putAll(data);
workspace.addNode(workspaceNode);
// post-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), false, PUT_MAP, workspaceNode.getData(), ctx);
}
private Object putDataKeyValueAndNotify(Object key, Object value, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
{
if (workspaceNode == null)
throw new NodeNotExistsException("optimisticCreateIfNotExistsInterceptor should have created this node!");
Map addedData = Collections.singletonMap(key, value);
// pre-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), true, PUT_DATA, workspaceNode.getData(), ctx);
Object old = workspaceNode.put(key, value);
workspace.addNode(workspaceNode);
// post-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), false, PUT_DATA, addedData, ctx);
return old;
}
private boolean removeNode(TransactionWorkspace workspace, WorkspaceNode workspaceNode, boolean notify, InvocationContext ctx) throws CacheException
{
// it is already removed - we can ignore it
if (workspaceNode == null) return false;
Fqn parentFqn = workspaceNode.getFqn().getParent();
WorkspaceNode parentNode = fetchWorkspaceNode(ctx, parentFqn, workspace, false, true);
if (parentNode == null) throw new NodeNotExistsException("Unable to find parent node with fqn " + parentFqn);
// pre-notify
if (notify) notifier.notifyNodeRemoved(workspaceNode.getFqn(), true, workspaceNode.getData(), ctx);
Fqn nodeFqn = workspaceNode.getFqn();
parentNode.removeChild(nodeFqn.getLastElement());
SortedMap<Fqn, WorkspaceNode> tailMap = workspace.getNodesAfter(workspaceNode.getFqn());
for (WorkspaceNode toDelete : tailMap.values())
{
if (toDelete.getFqn().isChildOrEquals(nodeFqn))
{
if (trace) log.trace("marking node " + toDelete.getFqn() + " as deleted");
toDelete.markAsDeleted(true);
}
else
{
break;// no more children, we came to the end
}
}
// post-notify
if (notify) notifier.notifyNodeRemoved(workspaceNode.getFqn(), false, null, ctx);
return workspaceNode.getNode().isValid();
}
private Object removeKeyAndNotify(Object removeKey, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
{
if (workspaceNode == null) return null;
// pre-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), true, REMOVE_DATA, workspaceNode.getData(), ctx);
Object old = workspaceNode.remove(removeKey);
workspace.addNode(workspaceNode);
Map removedData = Collections.singletonMap(removeKey, old);
// post-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), false, REMOVE_DATA, removedData, ctx);
return old;
}
private void removeDataAndNotify(TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
{
if (workspaceNode == null) return;
Map data = new HashMap(workspaceNode.getData());
// pre-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), true, REMOVE_DATA, data, ctx);
workspaceNode.clearData();
workspace.addNode(workspaceNode);
// post-notify
notifier.notifyNodeModified(workspaceNode.getFqn(), false, REMOVE_DATA, data, ctx);
}
private Object getValueForKeyAndNotify(Object[] args, TransactionWorkspace workspace, InvocationContext ctx)
{
Fqn fqn = (Fqn) args[0];
Object key = args[1];
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
if (workspaceNode == null)
{
if (trace) log.debug("Unable to find node " + fqn + " in workspace.");
return null;
}
else
{
//add this node into the wrokspace
notifier.notifyNodeVisited(fqn, true, ctx);
Object val = workspaceNode.get(key);
workspace.addNode(workspaceNode);
notifier.notifyNodeVisited(fqn, false, ctx);
return val;
}
}
private Object getNodeAndNotify(Object[] args, TransactionWorkspace workspace, InvocationContext ctx)
{
Fqn fqn = (Fqn) args[0];
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
if (workspaceNode == null)
{
if (trace) log.trace("Unable to find node " + fqn + " in workspace.");
return null;
}
else if (workspaceNode.isDeleted())
{
if (trace) log.trace("Attempted to retrieve node " + fqn + " but it has been deleted!");
return null;
}
else
{
notifier.notifyNodeVisited(fqn, true, ctx);
workspace.addNode(workspaceNode);
notifier.notifyNodeVisited(fqn, false, ctx);
return workspaceNode.getNode();
}
}
private Object getKeysAndNotify(Object[] args, TransactionWorkspace workspace, InvocationContext ctx)
{
Fqn fqn = (Fqn) args[0];
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
if (workspaceNode == null)
{
if (trace) log.trace("unable to find node " + fqn + " in workspace.");
return null;
}
else
{
notifier.notifyNodeVisited(fqn, true, ctx);
Object keySet = workspaceNode.getKeys();
workspace.addNode(workspaceNode);
notifier.notifyNodeVisited(fqn, false, ctx);
return keySet;
}
}
private Object getDataAndNotify(Object[] args, TransactionWorkspace workspace, InvocationContext ctx)
{
Fqn fqn = (Fqn) args[0];
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
if (workspaceNode == null)
{
if (trace) log.trace("unable to find node " + fqn + " in workspace.");
return null;
}
else
{
notifier.notifyNodeVisited(fqn, true, ctx);
Object data = workspaceNode.getData();
workspace.addNode(workspaceNode);
notifier.notifyNodeVisited(fqn, false, ctx);
return data;
}
}
private Object getChildNamesAndNotify(Object[] args, TransactionWorkspace workspace, InvocationContext ctx)
{
Fqn fqn = (Fqn) args[0];
WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
if (workspaceNode == null)
{
if (trace) log.trace("Unable to find node " + fqn + " in workspace.");
return null;
}
else
{
notifier.notifyNodeVisited(fqn, true, ctx);
Object nameSet = workspaceNode.getChildrenNames();
workspace.addNode(workspaceNode);
notifier.notifyNodeVisited(fqn, false, ctx);
return nameSet;
}
}
// -----------------------------------------------------------------
// Methods to help retrieval of nodes from the transaction workspace.
// -----------------------------------------------------------------
/**
* Retrieves a node for a given Fqn from the workspace. If the node does not exist in the workspace it is retrieved
* from the cache's data structure. Note that at no point is a NEW node created in the underlying data structure.
* That is up to the {@link org.jboss.cache.interceptors.OptimisticCreateIfNotExistsInterceptor}.
* <p/>
* If the node requested is in the workspace but marked as deleted, this method will NOT retrieve it, unless undeleteIfNecessary
* is true, in which case the node's <tt>deleted</tt> property is set to false first before being retrieved.
*
* @param fqn Fqn of the node to retrieve
* @param workspace transaction workspace to look in
* @param undeleteIfNecessary if the node is in the workspace but marked as deleted, this meth
* @param includeInvalidNodes
* @return a node, if found, or null if not.
*/
private WorkspaceNode fetchWorkspaceNode(InvocationContext ctx, Fqn fqn, TransactionWorkspace workspace, boolean undeleteIfNecessary, boolean includeInvalidNodes)
{
WorkspaceNode workspaceNode = workspace.getNode(fqn);
// if we do not have the node then we need to add it to the workspace
if (workspaceNode == null)
{
NodeSPI node = peekNode(ctx, fqn, false, true, includeInvalidNodes);
if (node == null) return null;
GlobalTransaction gtx = ctx.getGlobalTransaction();
workspaceNode = lockAndCreateWorkspaceNode(nodeFactory, node, workspace, gtx, lockAcquisitionTimeout);
// and add the node to the workspace.
workspace.addNode(workspaceNode);
}
// Check that the workspace node has been marked as deleted.
if (workspaceNode.isDeleted())
{
if (trace) log.trace("Node " + fqn + " has been deleted in the workspace.");
if (undeleteIfNecessary)
{
undeleteWorkspaceNode(workspaceNode, fetchWorkspaceNode(ctx, fqn.getParent(), workspace, undeleteIfNecessary, includeInvalidNodes));
}
else if (!includeInvalidNodes)
{
// don't return deleted nodes if undeleteIfNecessary is false!
workspaceNode = null;
}
}
// set implicit node versioning flag.
if (workspaceNode != null && !(workspaceNode.getVersion() instanceof DefaultDataVersion))
{
workspaceNode.setVersioningImplicit(false);
}
// now make sure all parents are in the wsp as well
if (workspaceNode != null && !fqn.isRoot())
fetchWorkspaceNode(ctx, fqn.getParent(), workspace, false, includeInvalidNodes);
return workspaceNode;
}
}