001: /*
002: * <copyright>
003: *
004: * Copyright 1999-2004 Honeywell Inc
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:
027: package org.cougaar.logistics.plugin.packer;
028:
029: import org.cougaar.core.blackboard.IncrementalSubscription;
030:
031: import org.cougaar.glm.ldm.Constants;
032:
033: import org.cougaar.planning.ldm.asset.Asset;
034: import org.cougaar.planning.ldm.asset.ItemIdentificationPG;
035: import org.cougaar.planning.ldm.asset.TypeIdentificationPG;
036:
037: import org.cougaar.planning.ldm.plan.AllocationResult;
038: import org.cougaar.planning.ldm.plan.AllocationResultDistributor;
039: import org.cougaar.planning.ldm.plan.AspectType;
040: import org.cougaar.planning.ldm.plan.AspectValue;
041: import org.cougaar.planning.ldm.plan.Expansion;
042: import org.cougaar.planning.ldm.plan.NewWorkflow;
043: import org.cougaar.planning.ldm.plan.PlanElement;
044: import org.cougaar.planning.ldm.plan.PrepositionalPhrase;
045: import org.cougaar.planning.ldm.plan.Task;
046: import org.cougaar.planning.plugin.util.PluginHelper;
047: import org.cougaar.util.Sortings;
048: import org.cougaar.util.UnaryPredicate;
049:
050: import java.util.ArrayList;
051: import java.util.Collection;
052: import java.util.Comparator;
053: import java.util.Enumeration;
054: import java.util.HashMap;
055: import java.util.HashSet;
056: import java.util.Iterator;
057: import java.util.Map;
058: import java.util.Set;
059: import java.util.TreeSet;
060: import java.util.Vector;
061:
062: /**
063: * Packer - handles packing supply requests
064: *
065: */
066: public abstract class ALPacker extends GenericPlugin {
067: private int ADD_TASKS = 0;
068: private int REMOVE_TASKS = 0;
069: private double ADD_TONS = 0;
070: private double REMOVE_TONS = 0;
071: Map receiverToType = new HashMap();
072:
073: /**
074: * Packer - constructor
075: */
076: public ALPacker() {
077: super ();
078: }
079:
080: /**
081: * getSortFunction - returns comparator to be used in sorting the tasks to be
082: * packed. Default implementation sorts on end time.
083: *
084: * @return Comparator
085: */
086: public Comparator getSortFunction() {
087: return new SortByEndTime();
088: }
089:
090: /**
091: * getAllocationResultDistributor - returns the AllocationResultDistributor be
092: * used in distributing allocation result for the transport task among the initial
093: * supply tasks. Defaults to
094: * ProportionalDistributor.DEFAULT_PROPORTIONAL_DISTRIBUTOR;
095: *
096: * @return AllocationResultDistributor
097: */
098: public AllocationResultDistributor getAllocationResultDistributor() {
099: return ProportionalDistributor.DEFAULT_PROPORTIONAL_DISTRIBUTOR;
100: }
101:
102: /**
103: * getPreferenceAggregator - returns PreferenceAggregator for setting the
104: * start/end times on the transport tasks. Defaults to DefaultPreferenceAggregator.
105: *
106: * @return PreferenceAggregator
107: */
108: public PreferenceAggregator getPreferenceAggregator() {
109: return new DefaultPreferenceAggregator(getAlarmService());
110: }
111:
112: /**
113: * getAggregationClosure - return AggregationClosure to be used for creating
114: * transport tasks
115: */
116: public abstract AggregationClosure getAggregationClosure(
117: ArrayList tasks);
118:
119: public int getTaskQuantityUnit() {
120: return Sizer.TONS;
121: }
122:
123: /**
124: * processNewTasks - handle new ammo supply tasks
125: * Called within GenericPlugin.execute.
126: *
127: * @param newTasks Enumeration of the new tasks
128: */
129: public void processNewTasks(Enumeration newTasks) {
130: ArrayList tasks = new ArrayList();
131:
132: double tonsReceived = 0;
133:
134: while (newTasks.hasMoreElements()) {
135: Task task = (Task) newTasks.nextElement();
136: if (task.getPlanElement() != null) {
137: getLoggingService()
138: .warn(
139: "Packer: Unable to pack - "
140: + task.getUID()
141: + " - task already has a PlanElement - "
142: + task.getPlanElement()
143: + ".\n"
144: + "Is the UniversalAllocator also handling Supply tasks in this node?");
145: } else {
146: ADD_TASKS++;
147:
148: double taskWeight = Sizer.getTaskMass(task,
149: getTaskQuantityUnit()).getShortTons();
150: ADD_TONS += taskWeight;
151: tonsReceived += taskWeight;
152: tasks.add(task);
153:
154: if (getLoggingService().isInfoEnabled()) {
155: getLoggingService().info(
156: "Packer: Got a task - " + task.getUID()
157: + " from " + task.getSource()
158: + " with " + taskWeight
159: + " tons of ammo.");
160: }
161: }
162: }
163:
164: if (tasks.size() == 0) {
165: return;
166: }
167:
168: if (getLoggingService().isInfoEnabled()) {
169: getLoggingService()
170: .info(
171: "Packer - number of added SUPPLY tasks: "
172: + ADD_TASKS
173: + ", aggregated quantity from added SUPPLY tasks: "
174: + ADD_TONS + " tons, "
175: + " this cycle got " + tasks.size()
176: + " tasks, " + tonsReceived
177: + " tons.");
178: }
179:
180: double tonsPacked = doPacking(tasks, getSortFunction(),
181: getPreferenceAggregator(),
182: getAllocationResultDistributor());
183:
184: if ((tonsPacked > tonsReceived + 0.1)
185: || (tonsPacked < tonsReceived - 0.1)) {
186: if (getLoggingService().isWarnEnabled()) {
187: getLoggingService().warn(
188: "Packer - received " + tonsReceived
189: + " tons but packed " + tonsPacked
190: + " tons, (total received " + ADD_TONS
191: + " vs total packed "
192: + Filler.TRANSPORT_TONS
193: + ") for tasks : ");
194: Task t = null;
195: for (Iterator iter = tasks.iterator(); iter.hasNext();) {
196: t = (Task) iter.next();
197: getLoggingService().warn("\t" + t.getUID());
198: getLoggingService()
199: .warn(
200: " Quantity : "
201: + t
202: .getPreferredValue(AspectType.QUANTITY));
203: }
204: }
205: }
206: }
207:
208: /**
209: * processChangedTasks - handle changed supply tasks
210: * Called within GenericPlugin.execute.
211: * Rescind current PlanElement and reprocess tasks.
212: *
213: * @param changedTasks Enumeration of changed ammo supply tasks. Ignored.
214: */
215: public void processChangedTasks(Enumeration changedTasks) {
216: Vector changedTaskList = new Vector();
217: while (changedTasks.hasMoreElements()) {
218: Task task = (Task) changedTasks.nextElement();
219: changedTaskList.add(task);
220: if (getLoggingService().isDebugEnabled()) {
221: getLoggingService().debug(
222: "Packer - handling changed task - "
223: + task.getUID() + " from "
224: + task.getSource());
225: }
226:
227: PlanElement pe = task.getPlanElement();
228: if (pe != null) {
229: AllocationResult ar;
230: if (pe.getReportedResult() != null) {
231: ar = pe.getReportedResult();
232: } else {
233: ar = pe.getEstimatedResult();
234: }
235: // make sure that we got atleast a valid reported OR estimated allocation result
236: if (ar != null) {
237: double taskWeight = PluginHelper.getARAspectValue(
238: ar, AspectType.QUANTITY);
239: ADD_TONS -= taskWeight;
240: ADD_TASKS--;
241: }
242: if (pe instanceof Expansion) {
243: Enumeration tasks = ((Expansion) pe).getWorkflow()
244: .getTasks();
245: while (tasks.hasMoreElements()) {
246: Task t = (Task) tasks.nextElement();
247: ((NewWorkflow) t.getWorkflow()).removeTask(t);
248: publishRemove(t);
249: }
250: }
251: publishRemove(pe);
252: }
253: }
254: processNewTasks(changedTaskList.elements());
255: }
256:
257: /**
258: * processRemovedTasks - handle removed supply tasks
259: * Called within GenericPlugin.execute.
260: * **** Tasks are currently ignored ****
261: */
262: public void processRemovedTasks(Enumeration removedTasks) {
263: boolean anyRemoved = false;
264: Set toSubtract = new HashSet();
265: while (removedTasks.hasMoreElements()) {
266: anyRemoved = true;
267: Task task = (Task) removedTasks.nextElement();
268:
269: if (getLoggingService().isInfoEnabled()) {
270: getLoggingService().info(
271: "Packer: Got a removed task - " + task
272: + " from " + task.getSource());
273: }
274:
275: REMOVE_TASKS++;
276: REMOVE_TONS += task.getPreferredValue(AspectType.QUANTITY);
277:
278: if (getLoggingService().isInfoEnabled()) {
279: getLoggingService()
280: .info(
281: "Packer - number of removed SUPPLY tasks: "
282: + REMOVE_TASKS
283: + ", aggregated quantity from removed SUPPLY tasks: "
284: + REMOVE_TONS + " tons.");
285: }
286:
287: /*
288: Expansion exp = (Expansion) task.getPlanElement();
289: if (exp == null) {
290: if (getLoggingService().isInfoEnabled()) {
291: getLoggingService().info("Packer - no plan element for remove task " + task.getUID()+ " so subtracting it.");
292: }
293: toSubtract.add (task);
294: }
295: else {
296: Enumeration subtaskEnum = exp.getWorkflow().getTasks();
297: for (;subtaskEnum.hasMoreElements();) {
298: toSubtract.add(subtaskEnum.nextElement());
299: }
300: }
301: */
302: }
303:
304: /*
305: if (anyRemoved) {
306: Collection unplannedInternal = getBlackboardService().query(new UnaryPredicate() {
307: public boolean execute(Object obj) {
308: if (obj instanceof Task) {
309: Task task = (Task) obj;
310: return ((task.getPrepositionalPhrase(GenericPlugin.INTERNAL) != null) &&
311: task.getPlanElement() == null);
312: }
313: return false;
314: }
315: }
316: );
317:
318: // any tasks we're going to put back into milvans, don't subtract from our totals
319: toSubtract.removeAll(unplannedInternal);
320: handleUnplanned(unplannedInternal);
321:
322: }
323: */
324:
325: Set unplannedInternal = new HashSet();
326: for (Iterator iter = allInternalTasks.getCollection()
327: .iterator(); iter.hasNext();) {
328: Task internalTask = (Task) iter.next();
329: if (internalTask.getPlanElement() == null) {
330: unplannedInternal.add(internalTask);
331: }
332: }
333:
334: toSubtract = new HashSet(allInternalTasks
335: .getRemovedCollection());
336:
337: unplannedInternal.removeAll(toSubtract);
338: handleUnplanned(unplannedInternal);
339: toSubtract.addAll(unplannedInternal);
340:
341: if (getLoggingService().isInfoEnabled()) {
342: getLoggingService().info(
343: "Packer - subtracting " + toSubtract.size()
344: + " tasks.");
345: }
346:
347: for (Iterator iter = toSubtract.iterator(); iter.hasNext();) {
348: subtractTaskFromReceiver((Task) iter.next());
349: }
350: }
351:
352: protected void handleUnplanned(Collection unplanned) {
353: if (getLoggingService().isInfoEnabled())
354: getLoggingService().info(
355: "Packer: found " + unplanned.size()
356: + " tasks -- replanning them!");
357:
358: for (Iterator iter = unplanned.iterator(); iter.hasNext();) {
359: Task task = (Task) iter.next();
360: if (getLoggingService().isInfoEnabled()) {
361: getLoggingService().info(
362: "Packer: replanning " + task.getUID());
363: }
364:
365: ArrayList copy = new ArrayList();
366: copy.add(task);
367: AggregationClosure ac = getAggregationClosure(copy);
368:
369: Filler fil = new Filler(null, this , ac,
370: getAllocationResultDistributor(),
371: getPreferenceAggregator());
372:
373: fil.handleUnplanned(task);
374: }
375: }
376:
377: /**
378: * doPacking - packs specified set of supply tasks.
379: * Assumes that it's called within an open/close transaction.
380: * @param tasks ArrayList with the tasks which should be packed
381: * @param sortfun BinaryPredicate to be used in sorting the tasks
382: * @param prefagg PreferenceAggregator for setting the start/end times on the
383: * transport tasks.
384: * @param ard AllocationResultDistributor to be used in distributing allocation results
385: * for the transport task amount the initial supply tasks. *
386: */
387: protected double doPacking(ArrayList tasks, Comparator sortfun,
388: PreferenceAggregator prefagg,
389: AllocationResultDistributor ard) {
390:
391: // Divide into 'pack together' groups
392: Collection packGroups = groupByAggregationClosure(tasks);
393:
394: double totalPacked = 0;
395:
396: for (Iterator iterator = packGroups.iterator(); iterator
397: .hasNext();) {
398: ArrayList packList = (ArrayList) iterator.next();
399: // sort them, if appropriate
400: if (sortfun != null) {
401: packList = (ArrayList) Sortings.sort(packList, sortfun);
402: }
403:
404: if (getLoggingService().isDebugEnabled()) {
405: getLoggingService()
406: .debug(
407: "Packer: about to build the sizer in doPacking.");
408: }
409:
410: AggregationClosure ac = getAggregationClosure(packList);
411:
412: // now we set the double wheel going...
413: Sizer sz = new Sizer(packList, this , getTaskQuantityUnit());
414:
415: if (getLoggingService().isDebugEnabled()) {
416: getLoggingService()
417: .debug(
418: "Packer: about to build the filler in doPacking.");
419: }
420:
421: Filler fil = new Filler(sz, this , ac, ard, prefagg);
422:
423: if (getLoggingService().isDebugEnabled()) {
424: getLoggingService()
425: .debug(
426: "Packer: about to run the wheelz in doPacking.");
427: }
428:
429: totalPacked += fil.execute();
430: }
431:
432: return totalPacked;
433: }
434:
435: protected IncrementalSubscription allInternalTasks;
436:
437: protected void setupSubscriptions() {
438: super .setupSubscriptions();
439: ProportionalDistributor.DEFAULT_PROPORTIONAL_DISTRIBUTOR
440: .setLoggingService(getLoggingService());
441: /*
442: unplannedInternalTasks = (IncrementalSubscription) subscribe(new UnaryPredicate() {
443: public boolean execute(Object obj) {
444: if (obj instanceof Task) {
445: Task task = (Task) obj;
446: return ((task.getPrepositionalPhrase(GenericPlugin.INTERNAL) != null) &&
447: task.getPlanElement() == null);
448: }
449: return false;
450: }
451: }
452: );
453:
454: */
455:
456: allInternalTasks = (IncrementalSubscription) subscribe(new UnaryPredicate() {
457: public boolean execute(Object obj) {
458: if (obj instanceof Task) {
459: Task task = (Task) obj;
460: return (task
461: .getPrepositionalPhrase(GenericPlugin.INTERNAL) != null);
462: }
463: return false;
464: }
465: });
466: }
467:
468: protected void updateAllocationResult(
469: IncrementalSubscription planElements) {
470: // Make sure that quantity preferences get returned on the allocation
471: // results. Transport thread may not have filled them in.
472: Enumeration changedPEs = planElements.getChangedList();
473: while (changedPEs.hasMoreElements()) {
474: PlanElement pe = (PlanElement) changedPEs.nextElement();
475:
476: // Only update the plan element if this is a change to the reported
477: // result.
478: if (PluginHelper.checkChangeReports(planElements
479: .getChangeReports(pe),
480: PlanElement.ReportedResultChangeReport.class)
481: && PluginHelper.updatePlanElement(pe)) {
482: boolean needToCorrectQuantity = false;
483:
484: AllocationResult estimatedAR = pe.getEstimatedResult();
485: double prefValue = pe.getTask().getPreference(
486: AspectType.QUANTITY).getScoringFunction()
487: .getBest().getAspectValue().getValue();
488:
489: AspectValue[] aspectValues = estimatedAR
490: .getAspectValueResults();
491:
492: // Possibly need to add quantity to list of aspects if it's not there in the first place.
493: // Couldn't see that this was in fact necessary so leaving it out for the moment
494: // Gordon Vidaver 08/23/02
495:
496: boolean foundQuantity = false;
497: for (int i = 0; i < aspectValues.length; i++) {
498: if (aspectValues[i].getAspectType() == AspectType.QUANTITY) {
499: if (aspectValues[i].getValue() != prefValue) {
500: // set the quantity to be the preference quantity
501: aspectValues[i] = aspectValues[i]
502: .dupAspectValue(prefValue);
503: needToCorrectQuantity = true;
504: }
505: foundQuantity = true;
506: break;
507: }
508: }
509:
510: if (!foundQuantity) {
511: AspectValue[] copy = new AspectValue[aspectValues.length + 1];
512: System.arraycopy(aspectValues, 0, copy, 0,
513: aspectValues.length);
514: copy[aspectValues.length] = AspectValue
515: .newAspectValue(AspectType.QUANTITY,
516: prefValue);
517: aspectValues = copy;
518: }
519:
520: if (needToCorrectQuantity) {
521: if (getLoggingService().isDebugEnabled()) {
522: getLoggingService().debug(
523: "Packer.updateAllocationResult - fixing quantity on estimated AR of pe "
524: + pe.getUID());
525: }
526:
527: AllocationResult correctedAR = new AllocationResult(
528: estimatedAR.getConfidenceRating(),
529: estimatedAR.isSuccess(), aspectValues);
530:
531: pe.setEstimatedResult(correctedAR);
532: }
533:
534: publishChange(pe);
535: }
536: }
537: }
538:
539: /**
540: * SortByEndTime - sorts tasks by end date, earliest first
541: */
542: private class SortByEndTime implements Comparator {
543:
544: /*
545: * compare - compares end date of the 2 tasks.
546: * Compares its two arguments for order. Returns a negative integer, zero, or a
547: * positive integer as the first argument is less than, equal
548: * to, or greater than the second.
549: */
550: public int compare(Object first, Object second) {
551: Task firstTask = null;
552: Task secondTask = null;
553:
554: if (first instanceof Task) {
555: firstTask = (Task) first;
556: }
557:
558: if (second instanceof Task) {
559: secondTask = (Task) second;
560: }
561:
562: if ((firstTask == null) && (secondTask == null)) {
563: return 0;
564: } else if (firstTask == null) {
565: return -1;
566: } else if (secondTask == null) {
567: return 1;
568: } else {
569: return (firstTask
570: .getPreferredValue(AspectType.END_TIME) > secondTask
571: .getPreferredValue(AspectType.END_TIME)) ? 1
572: : -1;
573: }
574: }
575:
576: /**
577: * Indicates whether some other object is "equal to" this Comparator.
578: * This method must obey the general contract of Object.equals(Object).
579: * Additionally, this method can return true only if the specified Object is
580: * also a comparator and it imposes the same ordering as this comparator. Thus,
581: * comp1.equals(comp2) implies that sgn(comp1.compare(o1,
582: * o2))==sgn(comp2.compare(o1, o2)) for every object reference o1 and o2.
583: */
584: public boolean equals(Object o) {
585: return (o.getClass() == SortByEndTime.class);
586: }
587: }
588:
589: protected void addToReceiver(String receiverID, String type,
590: double newQuantity) {
591: Map typeToQuantity = (Map) receiverToType.get(receiverID);
592: if (typeToQuantity == null) {
593: receiverToType.put(receiverID,
594: typeToQuantity = new HashMap());
595: }
596:
597: Double currentQ = (Double) typeToQuantity.get(type);
598: double current = 0;
599:
600: if (currentQ != null) {
601: current = currentQ.doubleValue();
602: }
603:
604: typeToQuantity.put(type, new Double(current + newQuantity));
605: }
606:
607: private static final String UNKNOWN = "UNKNOWN";
608:
609: protected void subtractTaskFromReceiver(Task task) {
610: TypeIdentificationPG typeIdentificationPG = task
611: .getDirectObject().getTypeIdentificationPG();
612: String typeID;
613: if (typeIdentificationPG != null) {
614: typeID = typeIdentificationPG.getTypeIdentification();
615: if ((typeID == null) || (typeID.equals(""))) {
616: typeID = UNKNOWN;
617: }
618:
619: } else {
620: typeID = UNKNOWN;
621: }
622: Object receiver = task
623: .getPrepositionalPhrase(Constants.Preposition.FOR);
624:
625: if (receiver != null)
626: receiver = ((PrepositionalPhrase) receiver)
627: .getIndirectObject();
628:
629: String receiverID;
630:
631: // Add field with recipient
632: if (receiver == null) {
633: receiverID = UNKNOWN;
634: if (getLoggingService().isErrorEnabled()) {
635: getLoggingService().error(
636: "Filler.addContentsInfo - Task "
637: + task.getUID() + " had no FOR prep.");
638: }
639: } else if (receiver instanceof String) {
640: receiverID = (String) receiver;
641: } else if (!(receiver instanceof Asset)) {
642: receiverID = UNKNOWN;
643: } else {
644: ItemIdentificationPG itemIdentificationPG = ((Asset) receiver)
645: .getItemIdentificationPG();
646: if ((itemIdentificationPG == null)
647: || (itemIdentificationPG.getItemIdentification() == null)
648: || (itemIdentificationPG.getItemIdentification()
649: .equals(""))) {
650: receiverID = UNKNOWN;
651: } else {
652: receiverID = itemIdentificationPG
653: .getItemIdentification();
654: }
655: }
656:
657: double quantity = task.getPreferredValue(AspectType.QUANTITY);
658: if (getLoggingService().isInfoEnabled()) {
659: getLoggingService().info(
660: "Subtracting - " + task.getUID() + " for "
661: + receiverID + " - " + typeID + " - "
662: + quantity);
663: }
664: subtractFromReceiver(receiverID, typeID, quantity);
665: }
666:
667: protected void subtractFromReceiver(String receiverID, String type,
668: double newQuantity) {
669: Map typeToQuantity = (Map) receiverToType.get(receiverID);
670: if (typeToQuantity == null) {
671: receiverToType.put(receiverID,
672: typeToQuantity = new HashMap());
673: }
674:
675: Double currentQ = (Double) typeToQuantity.get(type);
676: double current = 0;
677:
678: if (currentQ != null) {
679: current = currentQ.doubleValue();
680: }
681:
682: typeToQuantity.put(type, new Double(current - newQuantity));
683: }
684:
685: protected void reportQuantities() {
686: for (Iterator iter = receiverToType.keySet().iterator(); iter
687: .hasNext();) {
688: Object receiver = iter.next();
689: Map typeToQuantity = (Map) receiverToType.get(receiver);
690: if (typeToQuantity == null) {
691: if (getLoggingService().isErrorEnabled()) {
692: getLoggingService().error(
693: "ALPacker: no type->quantity map for "
694: + receiver);
695: }
696: }
697: Set types = new TreeSet(typeToQuantity.keySet()); // sorted
698: for (Iterator iter2 = types.iterator(); iter2.hasNext();) {
699: Object type = iter2.next();
700: if (getLoggingService().isInfoEnabled()) {
701: getLoggingService().info(
702: "\t" + receiver + " : " + type + " - "
703: + typeToQuantity.get(type));
704: }
705: }
706: }
707: }
708:
709: protected abstract Collection groupByAggregationClosure(
710: Collection tasks);
711: }
|