001: package org.tigris.scarab.workflow;
002:
003: /* ================================================================
004: * Copyright (c) 2000-2003 CollabNet. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowlegement: "This product includes
019: * software developed by Collab.Net <http://www.Collab.Net/>."
020: * Alternately, this acknowlegement may appear in the software itself, if
021: * and wherever such third-party acknowlegements normally appear.
022: *
023: * 4. The hosted project names must not be used to endorse or promote
024: * products derived from this software without prior written
025: * permission. For written permission, please contact info@collab.net.
026: *
027: * 5. Products derived from this software may not use the "Tigris" or
028: * "Scarab" names nor may "Tigris" or "Scarab" appear in their names without
029: * prior written permission of Collab.Net.
030: *
031: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
032: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
033: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
034: * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
035: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
036: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
037: * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
038: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
039: * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
040: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
041: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042: *
043: * ====================================================================
044: *
045: * This software consists of voluntary contributions made by many
046: * individuals on behalf of Collab.Net.
047: */
048:
049: import org.tigris.scarab.om.*;
050: import org.tigris.scarab.tools.localization.L10NKeySet;
051: import org.tigris.scarab.tools.localization.L10NMessage;
052: import org.tigris.scarab.tools.localization.LocalizationKey;
053: import org.tigris.scarab.util.Log;
054: import org.tigris.scarab.util.ScarabException;
055:
056: import org.apache.torque.TorqueException;
057: import org.apache.fulcrum.security.entity.Role;
058:
059: import java.util.ArrayList;
060: import java.util.Iterator;
061: import java.util.List;
062:
063: /**
064: * Simple implementation of Workflow, relies on the Transition tables, where every record defines a
065: * transition available for a given Role, from an option to another.
066: * <ul>
067: * <li>If there are no transitions defined, it will always return true</li>
068: * <li>If the "from" option of a transition is null, it will mean "Any option", and the "To" will
069: * be available from any option.</li>
070: * <li>If the "to" option of a transition is null, any option will be available from the "From" option.</li>
071: * <li>If both the 'to' and 'from' options are null, the role will be able to change freely from one value
072: * to another.</li>
073: * If any transition happens to have associated Conditions, will only be available when this condition
074: * evals to true.
075: * </ul>
076: */
077: public class CheapWorkflow extends DefaultWorkflow {
078:
079: /**
080: * Returns true if the transition from the option fromOption to toOption is
081: * allowed for the current user.
082: *
083: */
084: public boolean canMakeTransition(ScarabUser user,
085: AttributeOption fromOption, AttributeOption toOption,
086: Issue issue) {
087: boolean result = false;
088: List allTransitions = null;
089: Module module = null;
090: try {
091: if (fromOption.equals(toOption)) {
092: result = true;
093: } else {
094: if (TransitionPeer.hasDefinedTransitions(toOption
095: .getAttribute())) {
096: allTransitions = TransitionPeer.getTransitions(
097: fromOption, toOption);
098: allTransitions = filterConditionalTransitions(
099: allTransitions, issue);
100: module = issue.getModule();
101: Iterator iter = allTransitions.iterator();
102: while (!result && iter.hasNext()) {
103: Object obj = iter.next();
104: Transition tran = (Transition) obj;
105: Role requiredRole = tran.getRole();
106: if (requiredRole != null) { // A role is required for this transition to be
107: // allowed
108: result = user.hasRoleInModule(requiredRole,
109: module);
110: } else {
111: result = true;
112: }
113: }
114: } else {
115: result = true;
116: }
117: }
118: } catch (TorqueException te) {
119: Log.get(this .getClass().getName()).error(
120: "canMakeTransition: " + te);
121: }
122: return result;
123: }
124:
125: /**
126: * Returns true if at least one transition from the fromOption
127: * to any other option is allowed on the given attribute in the scope
128: * of the given IssueType and for the current user.
129: * @throws TorqueException
130: */
131: public boolean canMakeTransitionsFrom(ScarabUser user,
132: IssueType issueType, Attribute attribute,
133: AttributeOption fromOption) throws ScarabException {
134: Module module = user.getCurrentModule();
135: boolean result = false;
136: List availableOptions = getAvailableOptions(issueType,
137: attribute, module);
138: List allTransitions = TransitionPeer.getTransitionsFrom(
139: availableOptions, attribute, fromOption);
140: Iterator iter = allTransitions.iterator();
141: if (!iter.hasNext()) {
142: return true; // no transition rules available -> any transition possible
143: }
144:
145: while (!result && iter.hasNext()) {
146: Object obj = iter.next();
147: Transition tran = (Transition) obj;
148:
149: if (transitionIsSupportedByOptions(tran, availableOptions)) {
150: Role requiredRole = tran.getRole();
151: if (requiredRole != null) { // A role is required for this transition to be
152: // allowed
153: result = user.hasRoleInModule(requiredRole, module);
154: } else {
155: result = true;
156: }
157: }
158: }
159: return result;
160: }
161:
162: /**
163: * Returns the list of transitions allowed for the current user
164: * in the current module/issueType/attribute combination
165: * @throws TorqueException
166: */
167: public List getTransitions(ScarabUser user, IssueType issueType,
168: Attribute attribute) throws ScarabException {
169: Module module = user.getCurrentModule();
170: List result = null;
171: List availableOptions = getAvailableOptions(issueType,
172: attribute, module);
173:
174: Iterator optionsIter = availableOptions.iterator();
175: while (optionsIter.hasNext()) {
176: RModuleOption rmoduleOption = (RModuleOption) optionsIter
177: .next();
178: AttributeOption fromOption;
179: try {
180: fromOption = rmoduleOption.getAttributeOption();
181: } catch (TorqueException te) {
182: L10NMessage msg = new L10NMessage(
183: L10NKeySet.ExceptionTorqueGeneric, te);
184: throw new ScarabException(msg);
185: }
186: List list = getTransitionsFrom(user, issueType, attribute,
187: fromOption);
188: if (list != null) {
189: if (result == null) {
190: result = list;
191: } else {
192: result.addAll(list);
193: }
194: }
195: }
196: return result;
197: }
198:
199: /**
200: * Returns the tree of transitions
201: * in the current module/issueType/attribute combination.
202: * @throws TorqueException
203: */
204: public TransitionNode getTransitionTree(ScarabUser user,
205: IssueType issueType, Attribute attribute)
206: throws ScarabException {
207: Module module = user.getCurrentModule();
208: TransitionNode result = null;
209: List availableOptions = getAvailableOptions(issueType,
210: attribute, module, false);
211: List visitedTransitions = null;
212: Iterator optionsIter = availableOptions.iterator();
213: while (optionsIter.hasNext()) {
214: RModuleOption rmoduleOption = (RModuleOption) optionsIter
215: .next();
216: AttributeOption fromOption;
217: try {
218: fromOption = rmoduleOption.getAttributeOption();
219: } catch (TorqueException te) {
220: L10NMessage msg = new L10NMessage(
221: L10NKeySet.ExceptionTorqueGeneric, te);
222: throw new ScarabException(msg);
223: }
224: List list = getTransitionsFrom(user, issueType, attribute,
225: fromOption, false);
226: if (list != null) {
227: if (result == null) {
228: result = new TransitionNode(fromOption);
229: }
230: for (int index = 0; index < list.size(); index++) {
231: Transition t = (Transition) list.get(index);
232: if (visitedTransitions == null) {
233: visitedTransitions = new ArrayList();
234: }
235: if (visitedTransitions.contains(t)) {
236: continue;
237: }
238: visitedTransitions.add(t);
239: TransitionNode child = result.addNode(t);
240: getTransitionTree(user, issueType, attribute, t
241: .getTo(), child, visitedTransitions);
242: }
243: }
244: }
245: return result;
246: }
247:
248: /**
249: * Returns the list of transitions allowed for the current user
250: * in the current module/issueType/attribute combination
251: * @throws TorqueException
252: */
253: public void getTransitionTree(ScarabUser user, IssueType issueType,
254: Attribute attribute, AttributeOption fromOption,
255: TransitionNode node, List visitedTransitions)
256: throws ScarabException {
257: List list = getTransitionsFrom(user, issueType, attribute,
258: fromOption, false);
259: if (list != null) {
260: for (int index = 0; index < list.size(); index++) {
261: Transition t = (Transition) list.get(index);
262: if (visitedTransitions.contains(t)) {
263: continue;
264: }
265: visitedTransitions.add(t);
266: TransitionNode child = node.addNode(t);
267: AttributeOption toOption = t.getTo();
268: TransitionNode parent = node.getParent();
269: while (parent != null
270: && !parent.getOption().equals(toOption)) {
271: parent = parent.getParent();
272: }
273: if (parent == null) {
274: getTransitionTree(user, issueType, attribute, t
275: .getTo(), child, visitedTransitions);
276: }
277: }
278: }
279: }
280:
281: /**
282: * @param issueType
283: * @param attribute
284: * @param module
285: * @return
286: * @throws ScarabException
287: */
288: private List getAvailableOptions(IssueType issueType,
289: Attribute attribute, Module module) throws ScarabException {
290: return getAvailableOptions(issueType, attribute, module, true);
291: }
292:
293: /**
294: * @param issueType
295: * @param attribute
296: * @param module
297: * @return
298: * @throws ScarabException
299: */
300: private List getAvailableOptions(IssueType issueType,
301: Attribute attribute, Module module, boolean activeOnly)
302: throws ScarabException {
303: List availableOptions;
304: try {
305: availableOptions = module.getOptionTree(attribute,
306: issueType, activeOnly);
307: } catch (TorqueException e) {
308: LocalizationKey key = L10NKeySet.ExceptionTorqueGeneric;
309: L10NMessage msg = new L10NMessage(key, e);
310: throw new ScarabException(msg, e);
311: }
312: return availableOptions;
313: }
314:
315: /**
316: * Returns the list of transitions allowed for the current user
317: * in the current module/issueType/attribute combination
318: * starting from fromOption.
319: * @throws TorqueException
320: */
321: public List getTransitionsFrom(ScarabUser user,
322: IssueType issueType, Attribute attribute,
323: AttributeOption fromOption) throws ScarabException {
324: return getTransitionsFrom(user, issueType, attribute,
325: fromOption, false);
326: }
327:
328: private List getTransitionsFrom(ScarabUser user,
329: IssueType issueType, Attribute attribute,
330: AttributeOption fromOption, boolean activeOnly)
331: throws ScarabException {
332: Module module = user.getCurrentModule();
333: List result;
334: List availableOptions = getAvailableOptions(issueType,
335: attribute, module, activeOnly);
336: List allTransitions = TransitionPeer.getTransitionsFrom(
337: availableOptions, attribute, fromOption);
338: Iterator iter = allTransitions.iterator();
339: if (iter.hasNext()) {
340: result = new ArrayList();
341: while (iter.hasNext()) {
342: Object obj = iter.next();
343: Transition transition = (Transition) obj;
344:
345: boolean isSupportedByOptions = transitionIsSupportedByOptions(
346: transition, availableOptions);
347: if (isSupportedByOptions || !activeOnly) {
348: boolean addTransition;
349:
350: if (activeOnly) {
351: Role requiredRole = transition.getRole();
352: if (requiredRole != null) {
353: // A role is required for this transition to be allowed
354: addTransition = user.hasRoleInModule(
355: requiredRole, module);
356: } else {
357: addTransition = true;
358: }
359: } else {
360: addTransition = true;
361: }
362:
363: if (addTransition) {
364: result.add(transition);
365: }
366: }
367: }
368: } else {
369: result = null; // no transitions defined -> all transitions allowed
370: }
371: return result;
372: }
373:
374: /**
375: * It is possible that a defined transition can not be processed, because
376: * either the source option or the target option is not available in the
377: * current scope. This may happen when an option has been defined in the
378: * global attributes section, but later removed in the module scope.
379: * @param t
380: * @param availableOptions
381: * @return
382: */
383: private static boolean transitionIsSupportedByOptions(Transition t,
384: List availableOptions) {
385: Integer fromId = t.getFromOptionId();
386: Integer toId = t.getToOptionId();
387: Iterator iter = availableOptions.iterator();
388: int count = 0;
389:
390: if (toId == null) {
391: // allow any target option -> return true if at least one available option exists
392: return availableOptions.size() > 0;
393: }
394:
395: if (fromId == null || !fromId.equals(toId)) {
396: if (fromId == null || fromId.intValue() == 0) {
397: // fromId is either any option (null), or emtpy option (0)
398: count++;
399: }
400: while (iter.hasNext() && count < 2) {
401: RModuleOption attributeOption = (RModuleOption) iter
402: .next();
403: Integer id = attributeOption.getOptionId();
404: if ((fromId != null && id.equals(fromId))
405: || id.equals(toId)) {
406: count++;
407: }
408: }
409: }
410: return (count < 2) ? false : true;
411: }
412:
413: /**
414: * Filter the allowed transitions so only those not-conditioned,
415: * those whose condition fulfill, and those not restricted by
416: * the blocking condition, will remain.
417: * @param transitions
418: * @param issue
419: * @return
420: * @throws TorqueException
421: */
422: public List filterConditionalTransitions(List transitions,
423: Issue issue) throws TorqueException {
424: try {
425: boolean blockedIssue = issue.isBlocked();
426: if (transitions != null) {
427: for (int i = transitions.size() - 1; i >= 0; i--) {
428: Transition tran = (Transition) transitions.get(i);
429: if (blockedIssue && tran.getDisabledIfBlocked()) {
430: transitions.remove(i);
431: continue;
432:
433: }
434: List conditions = tran.getConditions();
435: if (null != conditions && conditions.size() > 0) {
436: boolean bRemove = true;
437: for (Iterator itReq = conditions.iterator(); bRemove
438: && itReq.hasNext();) {
439: Condition cond = (Condition) itReq.next();
440: Attribute requiredAttribute = cond
441: .getAttributeOption()
442: .getAttribute();
443: Integer optionId = cond.getOptionId();
444: AttributeValue av = issue
445: .getAttributeValue(requiredAttribute);
446: if (av != null) {
447: Integer issueOptionId = av
448: .getOptionId();
449: if (issueOptionId != null
450: && issueOptionId
451: .equals(optionId)) {
452: bRemove = false;
453: }
454: }
455: }
456: if (bRemove) {
457: transitions.remove(i);
458: }
459: }
460: }
461: }
462: } catch (Exception e) {
463: Log.get().error("filterConditionalTransitions: " + e);
464: }
465:
466: return transitions;
467: }
468: }
|