/* * ForEachForkAH.java * * Created on November 24, 2007, 8:43 PM * * Copyright 2007, Britt Miner. * * * * compsForForking * * #{tempForEachForkAHInputItem.getType().getCode()} * * inputItem * * true * * true * * noMatch * * true * * false * * noList * */ package org.bminer.jbpm.pd.ah; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.*; import org.bminer.jbpm.pd.GenericHandler; import org.jbpm.graph.def.ActionHandler; import org.jbpm.graph.def.Node; import org.jbpm.graph.def.Transition; import org.jbpm.graph.exe.ExecutionContext; import org.jbpm.graph.exe.Token; /** * * @author bminer */ public class ForEachForkAH extends GenericHandler implements ActionHandler { public Log log = LogFactory.getLog(this.getClass()); // transitions with this prefix won't be used private String RESERVED_TRANS_PREFIX = "_sys_"; // collection of items for which we will create sub tokens private String inputCollectionVar; // property to be accessed to retrieve a String value used for transition matching // if the collection is not of Strings private String inputItemProperty; // variable name under which to store the inputItem object that caused the creation // of this new child token. The variable will be stored against the new child token. private String inputItemMappedName; // match inputCollection items to corresponding transition 'name', or create tokens // for 'all' combinations of collection items and transitions. private boolean matchItemWithTrans = true; // 'true' will be used to reduce the inputCollection to a set prior to matching with // the available transitions private boolean matchUnique = false; // default transition to take for a inputCollection item that has no matching transition name, // otherwise no token will be generated for that collection item private String noMatchTrans; // true if a noMatchTrans should be taken for EACH un-matched item, false if the // noMatchTrans should be taken only once. private boolean noMatchDoTransOnce = true; // true if matched transitions should be taken when a no-match exists, or false // if only the noMatchTrans(s) should be taken private boolean noMatchDoMatched = false; // transition to take if there is nothing in the collection, otherwise // nothing will be done and we'll halt execution in this node private String noListTrans; public void execute(final ExecutionContext ec) throws Exception { Collection inputs = null; //collection of items for which we would like to fork final Token token = ec.getToken(); final Node node = ec.getNode(); /* If there is an input list, we'll try to create tokens based on it's entries. * If there is no list, we'll take the 'noListTrans' transition if * it has been specified, otherwise we'll do nothing and end execution in this node. */ //check for a list... boolean haveList = false; if (inputCollectionVar == null || inputCollectionVar.length() == 0) { log.warn("No 'inputCollectionVar' has been configured for this node."); }else{ Object o = ec.getContextInstance().getVariable(inputCollectionVar); if(o == null) { log.warn("The process variable '"+inputCollectionVar+"' does not exist"); }else{ try{ inputs = (Collection)o; if(inputs.size() > 0) { haveList = true; }else{ log.warn("The input collection '"+inputCollectionVar+"' is empty."); } }catch(Exception e) { log.warn("The process variable '"+inputCollectionVar+"' is not a java.util.Collection! " + e.getMessage()); } } } //bail if there is no list... if( !haveList ) { if(noListTrans != null && noListTrans.length() > 0) { node.leave(ec, noListTrans); }else{ log.warn("...and no default transition is configured either--we're stuck here."); } }else{ //build the available transition list List transList = this.processTransitions(node); // list of token/transition sets that will need to be signaled //for items with matching transitions when 'matchItemWithTtrans' is 'true' List matchedArgSets = new LinkedList(); //for items that are not matched to specific transtions, or generally when 'matchItemWithTtrans' is 'false' List unmatchedArgSets = new LinkedList(); // Check the matchCriteria. If we should match all we'll simply send // tokens for each item in the list down every configured transition, except that no // tokens will be sent down the noMatchTrans, the noListTrans, // or any transition whose name begins with "_sys_". // If we should match item names with equivalent transition names, then we'll send a token // for each item in the list only down those transitions whose names match the item. If an // item has no correspondingly named transition, it's token will be sent down the 'noMatchTrans' // transition if one is configured, or if not, no token will be created. // Reduce the inputs to a Set if so configured if(matchUnique) { inputs = new HashSet(inputs); } Map transitionNameCounts = new HashMap(); boolean haveANoMatchEntry = false; for(Object inputItemObj: inputs) { String inputItemName = this.getStringValue(inputItemObj, inputItemProperty, ec); if(!matchItemWithTrans) { for(Object o2: transList) { String transName = (String)o2; String newTokenName = node.getName() + "." + inputItemName + "-" + transName; if(!matchUnique) { //Ensure this new token's name is unique Integer transNameCount = transitionNameCounts.get(inputItemName + transName); if(transNameCount == null) transNameCount = 0; transitionNameCounts.put(inputItemName + transName, ++transNameCount); newTokenName += ("_" + transNameCount); } unmatchedArgSets.add(new Object[] {newTokenName, transName}); } }else if(matchItemWithTrans) { String transName = null; //Do we have a valid transition for this input item? if(transList.contains(inputItemName)) { transName = inputItemName; }else if(noMatchTrans != null && noMatchTrans.length() > 0) { if(!noMatchDoTransOnce || !haveANoMatchEntry) { transName = noMatchTrans; haveANoMatchEntry = true; } } // If we haven't assigned a transition name by now, we're simply // going to ingore that entry. if(transName != null) { String newTokenName = node.getName() + "." + inputItemName + "-" + transName; if(!matchUnique) { //Ensure this new token's name is unique Integer transNameCount = transitionNameCounts.get(transName); if(transNameCount == null) transNameCount = 0; transitionNameCounts.put(transName, ++transNameCount); newTokenName += ("_" + transNameCount); } //is this transition a valid trans or the configured noMatchTrans? if(transName.equals(noMatchTrans)) { unmatchedArgSets.add(new Object[] {newTokenName, transName, inputItemObj}); }else{ matchedArgSets.add(new Object[] {newTokenName, transName, inputItemObj}); } } } } List argSets = new LinkedList(); argSets.addAll(unmatchedArgSets); if(noMatchDoMatched || unmatchedArgSets.isEmpty()) { argSets.addAll(matchedArgSets); } // Create each token. List tokensToAdvance = new ArrayList(); for (int i = 0; i < argSets.size(); i++) { final Object[] args = (Object[]) argSets.get(i); String newTokenName = (String) args[0]; String transitionName = (String) args[1]; Object inputItem = (Object) args[2]; Token newToken = new Token(token, newTokenName); newToken.setTerminationImplicit(true); ec.getJbpmContext().getSession().save(newToken); //store the inputItem against the new token if(inputItemMappedName != null) { ec.getContextInstance().createVariable(inputItemMappedName, inputItem, newToken); } //record each token and transition pair... Object[] tokInfo = new Object[2]; tokInfo[0] = newToken; tokInfo[1] = transitionName; tokensToAdvance.add(tokInfo); } //Advance each token -- why we don't advance the tokens as we create them: // if the first token we created and imediately advanced was to execute straight through // to the join, the join would pick up one token out of only one yet created and move on; // subsequently created tokens would be left behind in the process. for(Object[] tokenInfo : tokensToAdvance){ Token tok = (Token)tokenInfo[0]; String transitionName = (String)tokenInfo[1]; node.leave(new ExecutionContext(tok), transitionName); } } } /* * Generate a list of available transitions out of this node, excluding any * reserved transitions. */ private List processTransitions(Node node) { List transList = new ArrayList(); List transs = node.getLeavingTransitions(); for(int i = 0; i < transs.size(); i++) { Transition trans = (Transition)transs.get(i); if(!trans.getName().equals(noMatchTrans) && !trans.getName().equals(noListTrans) && !trans.getName().startsWith(RESERVED_TRANS_PREFIX)) { transList.add(trans.getName()); } } return transList; } /* * Returns a String value from the specified inputItem object, generating the value * via one of the four following methods, listed in order of preference: * 1) specified property string is an EL that is evaluated against the inputItem * 2) inputItem is itself a String. * 3) from the specified property via an accessor method (supporting 'get*', 'is*', or 'has*'). * 4) calling toString() on the object. */ private String getStringValue(Object inputItem, String property, ExecutionContext ec) throws Exception { String valueStr = null; // If the specified property string is an EL, evaluate that to a String and return it. // If the object itself is already a String, fine; // If no property has been specified to reference, we'll simply call the object's toString() method. // If a property has been specified, call an accessor method for the property, followed // by toString() if that call fails. if(this.hasEL(property)){ ec.setVariable("tempForEachForkAHInputItem", inputItem); log.debug("EL expression: '"+property+"'."); valueStr = (String)this.evaluateEL(property, ec); ec.setVariable("tempForEachForkAHInputItem", null); }else if(inputItem instanceof java.lang.String) { valueStr = (String)inputItem; }else if(property == null || property.length() == 0) { valueStr = inputItem.toString(); }else { // upper case the first letter in preparation for building the accessor method name... String uProperty = null; if(property.length() == 1) { uProperty = property.toUpperCase(); }else{ uProperty = property.substring(0, 1).toUpperCase() + property.substring(1, property.length()); } //try to access the property through any of the supported accessor methods... Object valueObj = null; try { valueObj = inputItem.getClass().getMethod("get"+uProperty).invoke(inputItem); }catch(Exception e) { try { valueObj = inputItem.getClass().getMethod("is"+uProperty).invoke(inputItem); }catch(Exception e2) { try { valueObj = inputItem.getClass().getMethod("has"+uProperty).invoke(inputItem); }catch(Exception e3) { log.warn("No functioning 'get', 'is', or 'has' accessor method for property '"+ property+"'. Using toString() for a value instead: "+e3.getMessage()); valueObj = inputItem.toString(); } } } // now be sure we have a String... if(valueObj instanceof java.lang.String) { valueStr = (String)valueObj; }else{ try{ valueStr = String.valueOf(valueObj); }catch(Exception e) { log.warn("Property '"+property+"' is not a String and is not convertible to a String via String.valueOf(). Using toString() for a value instead: "+e.getMessage()); valueStr = inputItem.toString(); } } } return valueStr; } public void setInputCollectionVar(String inputCollectionVar) { if(inputCollectionVar != null) inputCollectionVar = inputCollectionVar.trim(); this.inputCollectionVar = inputCollectionVar; } public void setInputItemProperty(String inputItemProperty) { if(inputItemProperty != null) inputItemProperty = inputItemProperty.trim(); this.inputItemProperty = inputItemProperty; } public void setInputItemMappedName(String inputItemMappedName) { this.inputItemMappedName = inputItemMappedName; } public void setMatchItemWithTrans(boolean matchItemWithTrans) { this.matchItemWithTrans = matchItemWithTrans; } public void setMatchUnique(boolean matchUnique) { this.matchUnique = matchUnique; } public void setNoMatchTrans(String noMatchTrans) { if(noMatchTrans != null) noMatchTrans = noMatchTrans.trim(); this.noMatchTrans = noMatchTrans; } public void setNoMatchDoTransOnce(boolean noMatchDoTransOnce) { this.noMatchDoTransOnce = noMatchDoTransOnce; } public void setNoMatchDoMatched(boolean noMatchDoMatched) { this.noMatchDoMatched = noMatchDoMatched; } public void setNoListTrans(String noListTrans) { if(noListTrans != null) noListTrans = noListTrans.trim(); this.noListTrans = noListTrans; } }