001: /*
002: * Bossa Workflow System
003: *
004: * $Id: Case.java,v 1.56 2004/03/03 22:27:42 gdvieira Exp $
005: *
006: * Copyright (C) 2003,2004 OpenBR Sistemas S/C Ltda.
007: *
008: * This file is part of Bossa.
009: *
010: * Bossa is free software; you can redistribute it and/or modify it
011: * under the terms of version 2 of the GNU General Public License as
012: * published by the Free Software Foundation.
013: *
014: * This program is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: * General Public License for more details.
018: *
019: * You should have received a copy of the GNU General Public
020: * License along with this program; if not, write to the
021: * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
022: * Boston, MA 02111-1307, USA.
023: */
024:
025: package com.bigbross.bossa.wfnet;
026:
027: import java.io.IOException;
028: import java.io.InvalidObjectException;
029: import java.io.Serializable;
030: import java.util.ArrayList;
031: import java.util.Collection;
032: import java.util.Collections;
033: import java.util.HashMap;
034: import java.util.Iterator;
035: import java.util.List;
036: import java.util.Map;
037:
038: import org.apache.bsf.BSFException;
039: import org.apache.bsf.BSFManager;
040:
041: import com.bigbross.bossa.Bossa;
042: import com.bigbross.bossa.BossaException;
043: import com.bigbross.bossa.resource.Resource;
044: import com.bigbross.bossa.resource.ResourceRegistry;
045:
046: /**
047: * This class represents a specific instance of a case type. It
048: * holds the current state of a case. <p>
049: *
050: * @author <a href="http://www.bigbross.com">BigBross Team</a>
051: */
052: public class Case implements Serializable {
053:
054: private int id;
055:
056: private CaseType caseType;
057:
058: private int[] marking;
059:
060: private ResourceRegistry resources;
061:
062: private Map workItems;
063:
064: private Map activities;
065:
066: private int activitySequence;
067:
068: private Map attributes;
069:
070: private transient BSFManager bsf;
071:
072: private WFNetEvents eventQueue;
073:
074: /**
075: * Creates a new case, using the provided token count and attributes.
076: * Appropriate work items are activated according to the provided
077: * data. <p>
078: *
079: * @param caseType the case type of this case.
080: * @param state the initial token count as a map (<code>String</code>,
081: * <code>Integer</code>), indexed by the place id.
082: * This state map must have a token count for every place.
083: * @param attributes the initial attributes as a map (<code>String</code>,
084: * <code>Object</code>) of variables names (as used in
085: * edge weight expressions) and Java objects. The Java
086: * objects should be understandable by the underlying
087: * BSF engine being used.
088: * @exception SetAttributeException if the underlying expression
089: * evaluation system has problems setting an attribute.
090: * @exception EvaluationException if an expression evaluation error
091: * occurs.
092: */
093: Case(CaseType caseType, Map state, Map attributes)
094: throws BossaException {
095:
096: this .caseType = caseType;
097:
098: this .marking = new int[state.size()];
099: Iterator i = state.keySet().iterator();
100: while (i.hasNext()) {
101: String placeId = (String) i.next();
102: int newCount = ((Integer) state.get(placeId)).intValue();
103: marking[getCaseType().getPlace(placeId).getIndex()] = newCount;
104: }
105:
106: this .activities = new HashMap();
107: this .activitySequence = 1;
108: this .eventQueue = new WFNetEvents();
109:
110: this .attributes = new HashMap();
111: this .bsf = new BSFManager();
112: /* A SetAttributeException can be thrown here. */
113: declare(attributes);
114:
115: Collection ts = caseType.getTransitions();
116: workItems = new HashMap(ts.size());
117: for (i = ts.iterator(); i.hasNext();) {
118: Transition t = (Transition) i.next();
119: /* An EvaluationException can be thrown here. */
120: workItems.put(t.getId(), new WorkItem(this , t,
121: isFireable(t)));
122: }
123:
124: this .id = caseType.nextCaseId();
125: this .resources = new ResourceRegistry(Integer.toString(id));
126: }
127:
128: /**
129: * Returns the id of this case. <p>
130: *
131: * @return the id of this case.
132: */
133: public int getId() {
134: return id;
135: }
136:
137: /**
138: * Returns the case type of this case. <p>
139: *
140: * @return the case type of this case.
141: */
142: public CaseType getCaseType() {
143: return this .caseType;
144: }
145:
146: /**
147: * Returns the bossa engine this case is part, if any. <p>
148: *
149: * @return the bossa engine this case is part, <code>null</code> if not
150: * part of a bossa engine.
151: */
152: Bossa getBossa() {
153: if (getCaseType() != null
154: && getCaseType().getCaseTypeManager() != null) {
155: return getCaseType().getCaseTypeManager().getBossa();
156: } else {
157: return null;
158: }
159: }
160:
161: /**
162: * Returns the resource registry with the local resources of this
163: * case. <p>
164: *
165: * @return the resource registry with the local resources of this
166: * case.
167: */
168: ResourceRegistry getResourceRegistry() {
169: return resources;
170: }
171:
172: /**
173: * Returns all local resources of this case. <p>
174: *
175: * @return a list of all local resources of this case.
176: */
177: public List getResources() {
178: return resources.getResources();
179: }
180:
181: /**
182: * Returns the state of the case, that is, how many tokens are in each
183: * place. <p>
184: *
185: * @return the token count as a map (<code>String</code>,
186: * <code>Integer</code>), indexed by the place id.
187: */
188: public Map getState() {
189: HashMap state = new HashMap(marking.length);
190: Iterator i = getCaseType().getPlaces().iterator();
191: while (i.hasNext()) {
192: Place p = (Place) i.next();
193: state.put(p.getId(), new Integer(marking[p.getIndex()]));
194: }
195: return state;
196: }
197:
198: /**
199: * Changes the state of the case, that is, how many tokens are in each
200: * place. This method bypasses the usual transition firing process and
201: * should be used with caution to override the normal sequence of
202: * activities. <p>
203: *
204: * @param state the token count as a map (<code>String</code>,
205: * <code>Integer</code>), indexed by the place id. Only the
206: * places present in this map have their token count modified,
207: * the others are unchanged.
208: * @exception EvaluationException if an expression evaluation error
209: * occurs. If this exception is thrown the state of this case
210: * may be left inconsistent.
211: */
212: public void setState(Map state) throws BossaException {
213: WFNetTransaction setState = new SetState(this , state);
214: getBossa().execute(setState);
215: }
216:
217: /**
218: * Changes the state of the case, that is, how many tokens are in each
219: * place. This method bypasses the usual transition firing process and
220: * should be used with caution to override the normal sequence of
221: * activities. <p>
222: *
223: * This method will not persist the result of its activation and should
224: * be used only internally as a part of a persistent transaction. <p>
225: *
226: * @param state the token count as a map (<code>String</code>,
227: * <code>Integer</code>), indexed by the place id. Only the
228: * places present in this map have their token count modified,
229: * the others are unchanged.
230: * @exception EvaluationException if an expression evaluation error
231: * occurs. If this exception is thrown the state of this case
232: * may be left inconsistent.
233: */
234: void setStateImpl(Map state) throws BossaException {
235: if (state.size() == 0) {
236: return;
237: }
238: Iterator i = state.keySet().iterator();
239: while (i.hasNext()) {
240: String placeId = (String) i.next();
241: int newCount = ((Integer) state.get(placeId)).intValue();
242: marking[getCaseType().getPlace(placeId).getIndex()] = newCount;
243: eventQueue.newPlaceEvent(getBossa(),
244: WFNetEvents.ID_SET_TOKENS, this , getCaseType()
245: .getPlace(placeId), newCount);
246: }
247: deactivate();
248: List activated = activate();
249: eventQueue.newCaseEvent(getBossa(), WFNetEvents.ID_SET_STATE,
250: this );
251: eventQueue.notifyAll(getBossa());
252: processTimedFiring(activated);
253: }
254:
255: /**
256: * Returns the current attributes of this case. <p>
257: *
258: * @return the attributes as an unmodifiable map (<code>String</code>,
259: * <code>Object</code>) of variables names (as used in
260: * edge weight expressions) and Java objects.
261: */
262: public Map getAttributes() {
263: return Collections.unmodifiableMap(attributes);
264: }
265:
266: /**
267: * Returns the list of currently fireable work items associated
268: * with this case. <p>
269: *
270: * @return A list with the fireable work items of this case.
271: */
272: public List getWorkItems() {
273: ArrayList items = new ArrayList(workItems.size());
274:
275: for (Iterator i = workItems.values().iterator(); i.hasNext();) {
276: WorkItem wi = (WorkItem) i.next();
277: if (wi.isFireable()) {
278: items.add(wi);
279: }
280: }
281:
282: return items;
283: }
284:
285: /**
286: * Returns a specific work item, selected by its id. <p>
287: *
288: * @param id the work item id.
289: * @return the work item, <code>null</code> if there is no work item
290: * with this id.
291: */
292: public WorkItem getWorkItem(String id) {
293: return (WorkItem) workItems.get(id);
294: }
295:
296: /**
297: * Returns a list of activities associated with this case. <p>
298: *
299: * @return The list of activities of this case.
300: */
301: public List getActivities() {
302: List acts = new ArrayList(activities.size());
303: acts.addAll(activities.values());
304: return acts;
305: }
306:
307: /**
308: * Returns a specific activity, selected by its id. <p>
309: *
310: * @param id the activity id.
311: * @return the activity, <code>null</code> if there is no activity
312: * with this id.
313: */
314: public Activity getActivity(int id) {
315: return (Activity) activities.get(new Integer(id));
316: }
317:
318: /**
319: * Returns the next activity id for this case. <p>
320: *
321: * @return the next activity id.
322: */
323: int nextActivityId() {
324: return activitySequence++;
325: }
326:
327: /**
328: * Indicates if this case is a template case of some case type. <p>
329: *
330: * @return <code>true</code> is this case is a template,
331: * <code>false</code> otherwise.
332: */
333: boolean isTemplate() {
334: return id == 0;
335: }
336:
337: /**
338: * Declares an attribute to be used at expression evaluation. <p>
339: *
340: * @param id the attribute identifier.
341: * @param value a Java object with the attribute value. This object
342: * should be understandable by the underlying
343: * BSF engine being used.
344: * @exception SetAttributeException if the underlying expression
345: * evaluation system has problems setting an attribute.
346: */
347: void declare(String id, Object value) throws SetAttributeException {
348: try {
349: bsf.declareBean(id, value, value.getClass());
350: attributes.put(id, value);
351: } catch (BSFException e) {
352: throw new SetAttributeException("Could not set variable '"
353: + id + "'", e);
354: }
355: }
356:
357: /**
358: * Declares all the attributes to be used at expression evaluation. <p>
359: *
360: * @param attributes the attributes to be declared as a map
361: * (<code>String</code>, <code>Object</code>) of
362: * variables names (as used in edge weight expressions)
363: * and Java objects. The Java objects should be
364: * understandable by the underlying BSF engine being used.
365: * @exception SetAttributeException if the underlying expression
366: * evaluation system has problems setting an attribute.
367: */
368: void declare(Map attributes) throws SetAttributeException {
369: if (attributes != null) {
370: Iterator it = attributes.entrySet().iterator();
371: while (it.hasNext()) {
372: Map.Entry attribute = (Map.Entry) it.next();
373: declare((String) attribute.getKey(), attribute
374: .getValue());
375: }
376: }
377: }
378:
379: /**
380: * Evaluates an integer expression using the local attributes of this
381: * case. <p>
382: *
383: * @param expression the expression to be evaluated.
384: * @return The expression result.
385: * @exception EvaluationException if an evaluation error occurs.
386: */
387: int eval(String expression) throws EvaluationException {
388: try {
389: Object result = bsf.eval("javascript", "WFNet", 0, 0,
390: expression);
391: if (result instanceof Number) {
392: return ((Number) result).intValue();
393: } else if (result instanceof Boolean) {
394: return ((Boolean) result).booleanValue() ? 1 : 0;
395: } else {
396: throw new EvaluationException("'" + result
397: + "' is not a number or boolean.");
398: }
399: } catch (BSFException e) {
400: throw new EvaluationException("Error in the expression "
401: + "evaluation sub-system.", e);
402: }
403: }
404:
405: /**
406: * Tries to activate all deactivated transitions of this case. <p>
407: *
408: * @return a list of the activated work items.
409: * @exception EvaluationException if an expression evaluation error
410: * occurs.
411: */
412: private List activate() throws EvaluationException {
413: ArrayList activated = new ArrayList(workItems.size());
414: for (Iterator i = workItems.values().iterator(); i.hasNext();) {
415: WorkItem wi = (WorkItem) i.next();
416: if (!wi.isFireable()) {
417: if (wi.update()) {
418: activated.add(wi);
419: eventQueue.newWorkItemEvent(getBossa(),
420: WFNetEvents.ID_WORK_ITEM_ACTIVE, wi, null);
421: }
422: }
423: }
424: return activated;
425: }
426:
427: /**
428: * Tries to deactivate all activated transitions of this case. <p>
429: *
430: * @exception EvaluationException if an expression evaluation error
431: * occurs.
432: */
433: private void deactivate() throws EvaluationException {
434: for (Iterator i = workItems.values().iterator(); i.hasNext();) {
435: WorkItem wi = (WorkItem) i.next();
436: if (wi.isFireable()) {
437: if (!wi.update()) {
438: eventQueue
439: .newWorkItemEvent(getBossa(),
440: WFNetEvents.ID_WORK_ITEM_INACTIVE,
441: wi, null);
442: }
443: }
444: }
445: }
446:
447: /**
448: * Indicates if a transition is fireable, that is, if it is an actual
449: * work item. <p>
450: *
451: * @param t the transition.
452: * @return <code>true</code> if the transition is fireable;
453: * <code>false</code> otherwise.
454: * @exception EvaluationException if an expression evaluation error
455: * occurs.
456: */
457: boolean isFireable(Transition t) throws EvaluationException {
458: for (Iterator i = t.getInputEdges().iterator(); i.hasNext();) {
459: Edge e = (Edge) i.next();
460: if (marking[e.getPlace().getIndex()] < e.input(this )) {
461: return false;
462: }
463: }
464: return true;
465: }
466:
467: /**
468: * Opens a work item. A open work item is represented by
469: * an activity and is locked to the resource who opened it. The actual
470: * completion of the work item in handled by the created activity. <p>
471: *
472: * This method will not persist the result of its activation and should
473: * be used only internally as a part of a persistent transaction. <p>
474: *
475: * @param wi the work item to be opened.
476: * @param resource the resource that is opening the work item.
477: * @return The activity created by the opening of this work item,
478: * <code>null</code> if the work item could not be opened.
479: * @exception EvaluationException if an expression evaluation error
480: * occurs. If this exception is thrown the state of this case
481: * may be left inconsistent.
482: */
483: Activity open(WorkItem wi, Resource resource) throws BossaException {
484:
485: if (!wi.isFireable()) {
486: return null;
487: }
488:
489: if (isTemplate()) {
490: /* An EvaluationException can be consistently thrown here. */
491: Case caze = caseType.openCaseImpl(null);
492: return caze.open(caze.getWorkItem(wi.getId()), resource);
493: }
494:
495: List edges = wi.getTransition().getInputEdges();
496: for (Iterator i = edges.iterator(); i.hasNext();) {
497: Edge e = (Edge) i.next();
498: /* An EvaluationException can be inconsistently thrown here. */
499: int tokenNumber = e.input(this );
500: this .marking[e.getPlace().getIndex()] -= tokenNumber;
501: eventQueue.newPlaceEvent(getBossa(),
502: WFNetEvents.ID_REMOVE_TOKENS, this , e.getPlace(),
503: tokenNumber);
504: }
505: /* An EvaluationException can be inconsistently thrown here. */
506: deactivate();
507:
508: Resource group = getResourceRegistry().getResource(wi.getId());
509: if (group == null) {
510: group = getResourceRegistry().createResourceImpl(
511: wi.getId(), false);
512: }
513: group.includeImpl(resource, false);
514:
515: Activity activity = new Activity(wi, resource);
516: activities.put(new Integer(activity.getId()), activity);
517:
518: eventQueue.newWorkItemEvent(getBossa(),
519: WFNetEvents.ID_OPEN_WORK_ITEM, wi, resource);
520: eventQueue.notifyAll(getBossa());
521:
522: return activity;
523: }
524:
525: /**
526: * Closes and finishes an activity. Call this method when the
527: * activity is successfully completed. <p>
528: *
529: * Closes the <code>Case</code> when the last activity is closed, that is,
530: * no work items to open nor open activities remain. <p>
531: *
532: * An attribute mapping should be passed when this method is called.
533: * This is a (<code>String</code>, <code>Object</code>) mapping of
534: * variables names (as used in edge weight expressions) and Java objects.
535: * The Java objects should be understandable by the underlying BSF
536: * engine being used (for JavaScript, <code>Boolean</code>,
537: * <code>Integer</code> and <code>String</code> are known to work).
538: * The attributes provided will overwrite current set attributes and
539: * the value of these attributes will be used when evaluating edge
540: * weights. If you do not want to set any new attribute, use
541: * <code>null</code> as the attribute mapping. <p>
542: *
543: * This method will not persist the result of its activation and should
544: * be used only internally as a part of a persistent transaction. <p>
545: *
546: * @param activity the activity to be closed.
547: * @param newAttributes the attributes mapping.
548: * @return <code>true</code> if the activity is succesfully opened,
549: * <code>false</code> otherwise.
550: * @exception SetAttributeException if the underlying expression
551: * evaluation system has problems setting an attribute.
552: * @exception EvaluationException if an expression evaluation error
553: * occurs. If this exception is thrown the state of this case
554: * may be left inconsistent.
555: */
556: boolean close(Activity activity, Map newAttributes)
557: throws BossaException {
558:
559: if (!activities.containsKey(new Integer(activity.getId()))) {
560: return false;
561: }
562:
563: declare(newAttributes);
564:
565: List edges = activity.getTransition().getOutputEdges();
566: for (Iterator i = edges.iterator(); i.hasNext();) {
567: Edge e = (Edge) i.next();
568: /* An EvaluationException can be inconsistently thrown here. */
569: int tokenNumber = e.output(this );
570: this .marking[e.getPlace().getIndex()] += tokenNumber;
571: eventQueue.newPlaceEvent(getBossa(),
572: WFNetEvents.ID_ADD_TOKENS, this , e.getPlace(),
573: tokenNumber);
574: }
575:
576: /* An EvaluationException can be inconsistently thrown here. */
577: List activated = activate();
578: activities.remove(new Integer(activity.getId()));
579:
580: eventQueue.newActivityEvent(getBossa(),
581: WFNetEvents.ID_CLOSE_ACTIVITY, activity);
582: eventQueue.notifyAll(getBossa());
583:
584: if (getWorkItems().size() == 0 && activities.size() == 0) {
585: caseType.closeCase(this );
586: } else {
587: processTimedFiring(activated);
588: }
589:
590: return true;
591: }
592:
593: /**
594: * Cancel an activity. Call this method if the activity could not
595: * be completed. The related work item will return to the list of
596: * available work items and can be opened again. <p>
597: *
598: * This method will not persist the result of its activation and should
599: * be used only internally as a part of a persistent transaction. <p>
600: *
601: * @param activity the activity to be canceled.
602: * @return <code>true</code> if the activity is succesfully canceled,
603: * <code>false</code> otherwise.
604: * @exception EvaluationException if an expression evaluation error
605: * occurs. If this exception is thrown the state of this case
606: * may be left inconsistent.
607: */
608: boolean cancel(Activity activity) throws EvaluationException {
609:
610: if (!activities.containsKey(new Integer(activity.getId()))) {
611: return false;
612: }
613:
614: List edges = activity.getTransition().getInputEdges();
615: for (Iterator i = edges.iterator(); i.hasNext();) {
616: Edge e = (Edge) i.next();
617: /* An EvaluationException can be inconsistently thrown here. */
618: int tokenNumber = e.input(this );
619: this .marking[e.getPlace().getIndex()] += tokenNumber;
620: eventQueue.newPlaceEvent(getBossa(),
621: WFNetEvents.ID_ADD_TOKENS, this , e.getPlace(),
622: tokenNumber);
623: }
624:
625: Resource resource = activity.getResource();
626: Resource group = getResourceRegistry().getResource(
627: activity.getWorkItemId());
628: group.removeImpl(resource, false);
629:
630: /* An EvaluationException can be inconsistently thrown here. */
631: activate();
632: activities.remove(new Integer(activity.getId()));
633:
634: eventQueue.newActivityEvent(getBossa(),
635: WFNetEvents.ID_CANCEL_ACTIVITY, activity);
636: eventQueue.notifyAll(getBossa());
637:
638: return true;
639: }
640:
641: /**
642: * Performs the required actions related to timed firing in a list
643: * of activated work items. <p>
644: *
645: * @param activated the list of activated work items.
646: * @exception EvaluationException if an expression evaluation error
647: * occurs. If this exception is thrown the state of this case
648: * may be left inconsistent.
649: */
650: private void processTimedFiring(List activated)
651: throws BossaException {
652: for (Iterator i = activated.iterator(); i.hasNext();) {
653: WorkItem wi = (WorkItem) i.next();
654: /*
655: * We may have a deep, nested chain of timed firings.
656: * Before acting, check if the work item is still active.
657: * *And* we only process zero timeouts for now.
658: */
659: if (wi.isFireable() && wi.getTransition().getTimeout() == 0) {
660: Resource timerResource = getResourceRegistry()
661: .getResource("__timer");
662: if (timerResource == null) {
663: timerResource = getResourceRegistry()
664: .createResourceImpl("__timer", false);
665: }
666: Activity a = open(wi, timerResource);
667: close(a, null);
668: }
669: }
670: }
671:
672: /**
673: * Closes this case. A case will close automatically when there are no
674: * active work items. This method should only be used to override
675: * the normal case closing procedure and prematurely abort a case. <p>
676: *
677: * @return <code>true</code> if the case could be closed,
678: * <code>false</code> otherwise.
679: * @exception PersistenceException if an error occours when making the
680: * execution of this method persistent.
681: */
682: public boolean closeCase() throws BossaException {
683: WFNetTransaction closeCase = new CloseCase(this );
684: return ((Boolean) getBossa().execute(closeCase)).booleanValue();
685: }
686:
687: /**
688: * @see java.io.Serializable
689: */
690: private void readObject(java.io.ObjectInputStream in)
691: throws IOException, ClassNotFoundException {
692: in.defaultReadObject();
693: /*
694: * Restores the state of the non serializable BSFManager object.
695: */
696: bsf = new BSFManager();
697: Iterator it = attributes.entrySet().iterator();
698: while (it.hasNext()) {
699: Map.Entry attr = (Map.Entry) it.next();
700: try {
701: bsf.declareBean((String) attr.getKey(),
702: attr.getValue(), attr.getValue().getClass());
703: } catch (BSFException e) {
704: throw new InvalidObjectException(
705: "Could not restore the "
706: + "BSFmanager object: " + e.toString());
707: }
708: }
709: }
710: }
|