001: /*
002: * <copyright>
003: *
004: * Copyright 2002-2004 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026: package org.cougaar.pizza.plugin;
027:
028: import org.cougaar.core.blackboard.IncrementalSubscription;
029: import org.cougaar.core.component.ServiceBroker;
030: import org.cougaar.core.plugin.ComponentPlugin;
031: import org.cougaar.core.service.DomainService;
032: import org.cougaar.core.service.LoggingService;
033: import org.cougaar.pizza.Constants;
034: import org.cougaar.pizza.asset.PizzaAsset;
035: import org.cougaar.pizza.asset.PropertyGroupFactory;
036: import org.cougaar.planning.ldm.PlanningFactory;
037: import org.cougaar.planning.ldm.asset.Asset;
038: import org.cougaar.planning.ldm.asset.Entity;
039: import org.cougaar.planning.ldm.asset.NewItemIdentificationPG;
040: import org.cougaar.planning.ldm.plan.Allocation;
041: import org.cougaar.planning.ldm.plan.AllocationResult;
042: import org.cougaar.planning.ldm.plan.AspectType;
043: import org.cougaar.planning.ldm.plan.AspectValue;
044: import org.cougaar.planning.ldm.plan.Expansion;
045: import org.cougaar.planning.ldm.plan.NewTask;
046: import org.cougaar.planning.ldm.plan.PlanElement;
047: import org.cougaar.planning.ldm.plan.Preference;
048: import org.cougaar.planning.ldm.plan.Relationship;
049: import org.cougaar.planning.ldm.plan.RelationshipSchedule;
050: import org.cougaar.planning.ldm.plan.ScoringFunction;
051: import org.cougaar.planning.ldm.plan.Task;
052: import org.cougaar.planning.ldm.plan.Verb;
053: import org.cougaar.planning.plugin.util.PluginHelper;
054: import org.cougaar.util.UnaryPredicate;
055:
056: import java.util.ArrayList;
057: import java.util.Collection;
058: import java.util.Enumeration;
059: import java.util.Iterator;
060: import java.util.Vector;
061:
062: /**
063: * This plugin orders the pizza for a pizza party. It subscribes to a {@link PizzaPreferences} object
064: * published by the {@link InvitePlugin} when it knows how much of what kind of pizza to order.
065: * <p>
066: * On receiving the PizzaPreferences, it creates and publishes a Task with the Verb
067: * "Order" and a Direct Object of type Pizza (Asset). Next, it expands the Order Task
068: * into a workflow of two subtasks, one subtask per type of pizza with a quantity
069: * Preference for the number of servings needed. To place the order, it allocates the subtasks
070: * to its pizza provider, Joes Local Pizza Shack. This customer/provider relationship is
071: * defined in the XML configuration files and is established when the agents start up. See
072: * PizzaNode1.xml and PizzaNode2.xml for details.
073: * <p>
074: * In this example, the plugin cannot successfully complete the pizza order because Joe's
075: * Local Pizza Shack doesn't make veggie pizzas. The final result is a failed Expansion
076: * on the parent order Task due to the failed Allocation of the veggie pizza subtask.
077: */
078: public class PlaceOrderPlugin extends ComponentPlugin {
079: // Logger for the plugin
080: protected LoggingService logger;
081: // DomainService is needed to get the Planning Factory
082: protected DomainService domainService;
083: // PlanningFactory is needed to create tasks and task-related components
084: protected PlanningFactory planningFactory;
085:
086: // Subscription to this agent's Entity
087: protected IncrementalSubscription selfSub;
088: // Subscription to PizzaPreferences
089: protected IncrementalSubscription pizzaPrefSub;
090: // Subscription to Allocations on pizza order tasks
091: protected IncrementalSubscription allocationSub;
092: // Subscription to Expansions on order tasks
093: protected IncrementalSubscription expansionSub;
094:
095: /**
096: * Services that are absolutely required by the plugin can be loaded via introspection
097: * by the binding utility instead of explicitly getting each service from the
098: * ServiceBroker in load(). The setter methods are called after the component is
099: * constructed but before the state methods such as initialize, load,
100: * setupSubscriptions, etc. If the service is not available at that time, the component
101: * will be unloaded.
102: */
103: public void setDomainService(DomainService aDomainService) {
104: domainService = aDomainService;
105: }
106:
107: /**
108: * Loads services used by the plugin.
109: */
110: public void load() {
111: super .load();
112: // ServiceBroker handles the getting and releasing of services.
113: ServiceBroker sb = getServiceBroker();
114: logger = (LoggingService) sb.getService(this ,
115: LoggingService.class, null);
116: planningFactory = (PlanningFactory) domainService
117: .getFactory(org.cougaar.planning.ldm.PlanningDomain.PLANNING_NAME);
118: // No longer need the DomainService, release it.
119: sb.releaseService(this , DomainService.class, domainService);
120: }
121:
122: /**
123: * Initialize the subcriptions the plugin is interested in: self Entity, the PizzaPreferences,
124: * and existing Allocations or Expansions or Order Tasks.
125: */
126: protected void setupSubscriptions() {
127: selfSub = (IncrementalSubscription) getBlackboardService()
128: .subscribe(SELF_PRED);
129: pizzaPrefSub = (IncrementalSubscription) getBlackboardService()
130: .subscribe(PIZZA_PREF_PRED);
131: allocationSub = (IncrementalSubscription) getBlackboardService()
132: .subscribe(ALLOCATION_PRED);
133: expansionSub = (IncrementalSubscription) blackboard
134: .subscribe(EXPANSION_PRED);
135: }
136:
137: /**
138: * When there are changes to the plugins's subscriptions, we:
139: * 1) When the PizzaPreferences arrives, create the root Order Task, and expand
140: * it by the types of pizza. Then allocate these to the available provider.
141: * 2) Propagate up any changed Allocation Results
142: * 3) See if the root Expansion changed. If so, we may be done.
143: */
144: protected void execute() {
145: // The PizzaPreferences object contains the party invitation responses,
146: // and indicates we should start.
147: // Get any just added PizzaPreferences object.
148: PizzaPreferences pizzaPrefs = getPizzaPreferences();
149: if (pizzaPrefs != null) {
150: // We get in here only when a PizzaPreferences object has just been added. So
151: // in our application, this should happen exactly once.
152:
153: // Create the parent order task.
154: Task orderTask = createOrderTask();
155: // Create a subtask for each pizza preference.
156: Collection pizzaSubtasks = createPizzaSubtasks(pizzaPrefs,
157: orderTask);
158: // Expand the order task and add the subtasks to the workflow.
159: makeExpansion(orderTask, pizzaSubtasks);
160: // Allocate all pizza subtasks to the agent's pizza provider.
161: allocateSubtasks(pizzaSubtasks, getProvider());
162: }
163:
164: // Update changes to the results of the allocation - propagate the changes up
165: updateOrderAllocationResults();
166:
167: // Check for changes on the Expansion - we may be done
168: Expansion exp = getChangedExpansion();
169: if (exp != null) {
170: if (logger.isDebugEnabled()) {
171: logger.debug(" Change received on the expansion "
172: + printExpansionResults(exp));
173: }
174:
175: // If the Expansion is confident, then we are done. Print the results.
176: // Note that just because we're done, doesn't mean we have our pizza
177: if (exp.getReportedResult().getConfidenceRating() == 1.0) {
178: logExpansionResults(exp);
179: } // Expansion is confident
180: } // have an updated expansion
181: }
182:
183: /**
184: * Log (at SHOUT) the results of the given Expansion, showing
185: * what was ordered, how much, from whom, and with what success.
186: * @param exp The Expansion of the root Order task, whose details we print
187: */
188: protected void logExpansionResults(Expansion exp) {
189: boolean succ = false;
190: if (exp != null) {
191: Task t = exp.getTask();
192: Verb v = t.getVerb();
193: succ = exp.getReportedResult().isSuccess();
194:
195: // The sub-tasks (1 per pizza type)
196: Enumeration subs = exp.getWorkflow().getTasks();
197:
198: logger.shout("Pizza " + v + " Task "
199: + (succ ? "SUCCEEDED " : "FAILED "));
200:
201: // Log details of each sub-task
202: while (subs.hasMoreElements()) {
203: Task sub = (Task) subs.nextElement();
204:
205: // Get number servings, type of pizza ordered
206: double qty = sub.getPreferredValue(AspectType.QUANTITY);
207: String piztype = sub.getDirectObject()
208: .getItemIdentificationPG()
209: .getItemIdentification();
210:
211: // Get the store we ordered from
212: String store = ((Allocation) sub.getPlanElement())
213: .getAsset().getItemIdentificationPG()
214: .getItemIdentification();
215:
216: // Remember that each sub-task succeeds or fails independently
217: boolean subSucc = sub.getPlanElement()
218: .getReportedResult().isSuccess();
219: logger.shout(" " + store + " could"
220: + (subSucc ? "" : " NOT") + " handle " + v
221: + " for " + qty + " servings of " + piztype);
222: } // loop over sub-tasks
223: }
224:
225: if (succ)
226: logger.shout("The Party is on!");
227: else
228: logger
229: .shout("Can't get the pizza I need! The party guests will not be happy....");
230: }
231:
232: /**
233: * Returns any added PizzaPreferences object from the PizzaPreferences Subscription. Checks the
234: * added collection on the subscription and returns the first element in the
235: * collection. Will return null if the added collection is empty.
236: *<p>
237: * Note that since we only check the Added list, this method will only return an object
238: * once in our application. This keeps the plugin from publishing
239: * the Order Tasks each time the plugin runs.
240: *
241: * @return first PizzaPrefereneces object from the subscription
242: */
243: protected PizzaPreferences getPizzaPreferences() {
244: for (Iterator i = pizzaPrefSub.getAddedCollection().iterator(); i
245: .hasNext();) {
246: if (logger.isDebugEnabled()) {
247: logger.debug(" found pizzaPrefs " + pizzaPrefSub);
248: }
249: // This plugin expects only one PizzaPreferences object
250: return (PizzaPreferences) i.next();
251: }
252: return null;
253: }
254:
255: /**
256: * Returns a Task for ordering pizza. Creates a Task with Verb "Order", makes a Pizza
257: * asset and sets it as the direct object of the task. Next, it publishes the task to
258: * the blackboard.
259: *
260: * @return a task for ordering pizza
261: */
262: protected Task createOrderTask() {
263: Task orderTask = makeTask(Constants.Verbs.ORDER,
264: makePizzaAsset(Constants.PIZZA));
265: getBlackboardService().publishAdd(orderTask);
266: return orderTask;
267: }
268:
269: /**
270: * Returns a Collection of subtasks for ordering meat and veggie pizzas. Creates a meat
271: * pizza subtask and a veggie pizza subtask. The number of individuals requesting meat
272: * and veggie pizzas are obtained from the PizzaPreferences object. A quantity
273: * Preference is created to represent the number of servings needed and is added to the
274: * appropriate subtask. The parentTask is set as the parent of the subtasks.
275: *
276: * @param pizzaPrefs contains the number of people requesting meat or veggie pizzas
277: * @param parentTask the parent of the subtasks
278: * @return a Collection of pizza subtasks
279: */
280: protected Collection createPizzaSubtasks(
281: PizzaPreferences pizzaPrefs, Task parentTask) {
282: ArrayList subtasks = new ArrayList(2);
283:
284: if (pizzaPrefs.getNumMeat() > 0) {
285: // Create the meat pizza subtask
286: PizzaAsset meatPizza = makePizzaAsset(Constants.MEAT_PIZZA);
287: // Add meat properties to the pizza
288: // Note the use of our Pizza-domain PropertyGroupFactory, which
289: // has the custom newFooPG methods.
290: meatPizza.addOtherPropertyGroup(PropertyGroupFactory
291: .newMeatPG());
292: NewTask meatPizzaTask = makeTask(Constants.Verbs.ORDER,
293: meatPizza);
294: // Make a quantity Preference to represent the number of requests for meat pizza
295: Preference meatPref = makeQuantityPreference(pizzaPrefs
296: .getNumMeat());
297: meatPizzaTask.setPreference(meatPref);
298: meatPizzaTask.setParentTask(parentTask);
299: subtasks.add(meatPizzaTask);
300: }
301:
302: if (pizzaPrefs.getNumVeg() > 0) {
303: // Create the veggie pizza subtask
304: PizzaAsset veggiePizza = makePizzaAsset(Constants.VEGGIE_PIZZA);
305: // Add veggie properties to the pizza
306: veggiePizza.addOtherPropertyGroup(PropertyGroupFactory
307: .newVeggiePG());
308: NewTask veggiePizzaTask = makeTask(Constants.Verbs.ORDER,
309: veggiePizza);
310: // Make a quantity Preference to represent the number of servings needed for veggie pizza
311: Preference veggiePref = makeQuantityPreference(pizzaPrefs
312: .getNumVeg());
313: veggiePizzaTask.setPreference(veggiePref);
314: veggiePizzaTask.setParentTask(parentTask);
315: subtasks.add(veggiePizzaTask);
316: }
317:
318: return subtasks;
319: }
320:
321: /**
322: * Creates and adds an Expansion on a Task and publishes it to the blackboard. An
323: * Expansion is created containing a Workflow of the specified subtasks. This Expansion
324: * is set to the parent task. The subtasks are published to the blackboard first, then
325: * the Expansion is published.
326: *
327: * @param parentTask the parent task to be expanded
328: * @param subtasks the collection of subtasks to be added to the Workflow of Expansion
329: */
330: protected void makeExpansion(Task parentTask, Collection subtasks) {
331: /**
332: * This helper method creates the Expansion, adds the subtasks to the Workflow, sets
333: * the parent task, and sets the estimated AllocationResult to null.
334: */
335: Expansion expansion = PluginHelper.wireExpansion(parentTask,
336: new Vector(subtasks), planningFactory);
337: // Logical order of publish: tasks first, then the expansion that contains them.
338: for (Iterator iterator = subtasks.iterator(); iterator
339: .hasNext();) {
340: Task subtask = (Task) iterator.next();
341: getBlackboardService().publishAdd(subtask);
342: }
343: getBlackboardService().publishAdd(expansion);
344: if (logger.isDebugEnabled()) {
345: logger.debug(" publishing subtasks and expansion ");
346: }
347: }
348:
349: /**
350: * Creates and publishes Allocations for each subtask in the collection. The Allocation
351: * includes: the provider assigned to compelete the task, an estimated AllocationResult,
352: * and the Role of PizzaProvider. The estimated AllocationResult includes: an
353: * estimated confidence rating of 0.25 and success is true. Valid range for the
354: * confidence rating is between 0 and 1. The assumption is, as the subtasks are
355: * completed, the allocation results will become closer to 1. The Allocations are then
356: * published to the blackboard.
357: *
358: * @param subtasks a collection of subtasks to be allocated
359: * @param provider the provider to which the subtasks are allocated
360: */
361: protected void allocateSubtasks(Collection subtasks, Entity provider) {
362: if (provider == null) {
363: if (logger.isErrorEnabled()) {
364: logger
365: .error("No pizza provider found -- can't order pizza! Check config files for provider relationship. (Did you start PizzaNode2?)");
366: }
367: return;
368: }
369: for (Iterator iter = subtasks.iterator(); iter.hasNext();) {
370: Task task = (Task) iter.next();
371: // create Result for the given task, using the planningFactory, with
372: // a confidence of 0.25, estimate success=true
373: AllocationResult ar = PluginHelper
374: .createEstimatedAllocationResult(task,
375: planningFactory, 0.25, true);
376: // Create an allocation for the given task & plan, allocating the task to
377: // the given asset - an Entity asset, which will cause the task
378: // to be copied to the Agent that represents that Entity.
379: // Give the allocation the initial Estimated result, and indicate
380: // that the Entity is performing the PizzaProvider role for you
381: // in handling this Task
382: Allocation alloc = planningFactory.createAllocation(task
383: .getPlan(), task, (Asset) provider, ar,
384: Constants.Roles.PIZZAPROVIDER);
385: getBlackboardService().publishAdd(alloc);
386: if (logger.isDebugEnabled()) {
387: logger.debug(" allocating task " + task
388: + " to pizza provider: " + provider);
389: }
390: }
391: }
392:
393: /**
394: * Updates the estimated AllocationResults with reported AllocationResults.
395: * <p>
396: * Remember that the infrastructure sets the ReportedResult for you (in this case,
397: * by copying it from the provider agent when the provider settles the Task), but
398: * you must copy that up typically.
399: * <p>
400: * Checks the
401: * changed collection on the Allocation subscription. If the collection is not empty,
402: * it retrieves the Allocation PlanElement from the subscription. If the reported
403: * AllocationResults have changed, the results are udpated and a publish change is
404: * called on the PlanElement. The reported AllocationResults are changed by the
405: * provider assigned to the task.
406: */
407: protected void updateOrderAllocationResults() {
408: for (Iterator i = allocationSub.getChangedCollection()
409: .iterator(); i.hasNext();) {
410: PlanElement pe = (PlanElement) i.next();
411: if (logger.isDetailEnabled()) {
412: logger.detail("Updating the allocation results on "
413: + printAllocationResults(pe));
414: }
415: /**
416: * Looks for differences between reported and estimated allocation results. If they
417: * are not equal the reported value is copied into the estimated value. The updating
418: * of results should be done in order for the results to continue to flow up the
419: * PlanElement chain.
420: */
421: if (PluginHelper.updatePlanElement(pe)) {
422: getBlackboardService().publishChange(pe);
423: }
424: }
425: }
426:
427: /**
428: * Returns the Expansion that was changed (the one and only typically).
429: * <p>
430: * Checks the changed collection on the
431: * Subscription. If the collection is not empty, it returns the first element. If empty
432: * it returns null.
433: *
434: * @return the Expansion that was changed if any
435: */
436: protected Expansion getChangedExpansion() {
437: for (Iterator i = expansionSub.getChangedCollection()
438: .iterator(); i.hasNext();) {
439: // This plugin expects only one expansion
440: return (Expansion) i.next();
441: }
442: return null;
443: }
444:
445: /**
446: * Returns a NewTask. Creates a task with the specified verb and sets the Asset as the
447: * direct object.
448: * @param verb the verb of the newtask
449: * @param directObject asset that this Task is acting on
450: * @return a NewTask
451: */
452: protected NewTask makeTask(Verb verb, Asset directObject) {
453: NewTask newTask = planningFactory.newTask();
454: newTask.setVerb(verb);
455: newTask.setPlan(planningFactory.getRealityPlan()); // Plan is bogus - always use Reality
456: newTask.setDirectObject(directObject);
457: return newTask;
458: }
459:
460: /**
461: * Creates an instance of a PizzaAsset of the
462: * specified asset type. Adds an ItemIdentification property group to the pizzaAsset
463: * instance marking the pizza type.
464: *
465: * @param assetType the name of the type of asset (veg/meat)
466: * @return a PizzaAsset
467: */
468: protected PizzaAsset makePizzaAsset(String assetType) {
469: // Note that the PizzaPrototypePlugin registered the PizzaAsset prototype with
470: // the planning factory, so now the planning factory knows
471: // how to create a pizza asset for us.
472: PizzaAsset pizzaAsset = (PizzaAsset) planningFactory
473: .createInstance(Constants.PIZZA);
474: NewItemIdentificationPG itemIDPG = PropertyGroupFactory
475: .newItemIdentificationPG();
476: itemIDPG.setItemIdentification(assetType);
477: pizzaAsset.setItemIdentificationPG(itemIDPG);
478: return pizzaAsset;
479: }
480:
481: /**
482: * Returns a quantity Preference, representing the number of servings of pizza to order.
483: * <p>
484: * Creates a Preference with a Strictly-at ScoringFunction and a
485: * quantity AspectType from the specified value. In other words,
486: * says I want this many, and you get 0 credit for any more or less.
487: *
488: * @param value the number required
489: * @return a Preference requesting that number of items exactly
490: */
491: protected Preference makeQuantityPreference(int value) {
492: ScoringFunction sf = ScoringFunction
493: .createStrictlyAtValue(AspectValue.newAspectValue(
494: AspectType.QUANTITY, value));
495: Preference pref = planningFactory.newPreference(
496: AspectType.QUANTITY, sf);
497: return pref;
498: }
499:
500: /**
501: * Returns a pizza provider for this agent. Get all relationships that match the Role
502: * of PizzaProvider from the RelationshipSchedule. Return the provider Entity from
503: * the first Relationship found. If there are no pizza provider relationships, null
504: * will be returned.
505: *
506: * @return a pizza provider Entity
507: */
508: protected Entity getProvider() {
509: Entity provider = null; // return null if no PizzaProvider relationships found
510: // Get the RelationshipSchedule for this agent
511: RelationshipSchedule relSched = getSelfEntity()
512: .getRelationshipSchedule();
513: // Find all relationships matching the role of pizza provider
514: Collection relationships = relSched
515: .getMatchingRelationships(Constants.Roles.PIZZAPROVIDER);
516: for (Iterator iterator = relationships.iterator(); iterator
517: .hasNext();) {
518: Relationship r = (Relationship) iterator.next();
519: provider = (Entity) relSched.getOther(r);
520: break; // we only need one
521: }
522: return provider;
523: }
524:
525: /**
526: * Returns the Entity representing the agent. Checks the self entity subscription and
527: * returns the first element. In this example, there should be only one self entity.
528: * Will return null if the subscription is empty.
529: *
530: * @return the Entity representing the agent.
531: */
532: protected Entity getSelfEntity() {
533: if (selfSub.isEmpty()) {
534: if (logger.isErrorEnabled()) {
535: logger
536: .error(" Self Entity subscription is empty, this should not happen!!!!");
537: }
538: return null;
539: }
540: return (Entity) selfSub.first();
541: }
542:
543: protected String printExpansionResults(Expansion exp) {
544: return "the reported AllocationResults are [isSuccess: "
545: + exp.getReportedResult().isSuccess()
546: + " confidence rating: "
547: + exp.getReportedResult().getConfidenceRating() + " ]";
548: }
549:
550: protected String printAllocationResults(PlanElement pe) {
551: return "task UID: " + pe.getTask().getUID() + " estimated: "
552: + pe.getEstimatedResult() + " reported: "
553: + pe.getReportedResult();
554: }
555:
556: /**
557: * This predicate matches the Entity object of the agent.
558: */
559: protected static final UnaryPredicate SELF_PRED = new UnaryPredicate() {
560: public boolean execute(Object o) {
561: if (o instanceof Entity) {
562: return ((Entity) o).isLocal();
563: }
564: return false;
565: }
566: };
567:
568: /**
569: * This predicate matches PizzaPreferences objects.
570: */
571: protected static final UnaryPredicate PIZZA_PREF_PRED = new UnaryPredicate() {
572: public boolean execute(Object o) {
573: return (o instanceof PizzaPreferences);
574: }
575: };
576:
577: /**
578: * This predicate matches Allocations on "Order" tasks.
579: */
580: protected static final UnaryPredicate ALLOCATION_PRED = new UnaryPredicate() {
581: public boolean execute(Object o) {
582: if (o instanceof Allocation) {
583: Task task = ((Allocation) o).getTask();
584: return task.getVerb().equals(Constants.Verbs.ORDER);
585: }
586: return false;
587: }
588: };
589:
590: /**
591: * This predicate matches Expansions on "Order" tasks.
592: */
593: protected static final UnaryPredicate EXPANSION_PRED = new UnaryPredicate() {
594: public boolean execute(Object o) {
595: if (o instanceof Expansion) {
596: Task task = ((Expansion) o).getTask();
597: return task.getVerb().equals(Constants.Verbs.ORDER);
598: }
599: return false;
600: }
601: };
602: }
|