View Javadoc

1   /*
2    * This software was designed and created by Jason Carroll.
3    * Copyright (c) 2002, 2003, 2004 Jason Carroll.
4    * The author can be reached at jcarroll@cowsultants.com
5    * ITracker website: http://www.cowsultants.com
6    * ITracker forums: http://www.cowsultants.com/phpBB/index.php
7    *
8    * This program is free software; you can redistribute it and/or modify
9    * it only under the terms of the GNU General Public License as published by
10   * the Free Software Foundation; either version 2 of the License, or
11   * (at your option) any later version.
12   *
13   * This program is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU General Public License for more details.
17   */
18  
19  package org.itracker.services.util;
20  
21  import java.lang.reflect.Field;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  
30  import org.apache.log4j.Logger;
31  import org.apache.struts.action.ActionErrors;
32  import org.apache.struts.action.ActionMessage;
33  import org.apache.struts.action.ActionMessages;
34  import org.apache.struts.validator.ValidatorForm;
35  import org.itracker.core.resources.ITrackerResources;
36  import org.itracker.model.NameValuePair;
37  import org.itracker.model.ProjectScript;
38  import org.itracker.services.exceptions.WorkflowException;
39  
40  import bsh.Interpreter;
41  
42  /**
43   * Contains utilities used when displaying and processing workflow and field events
44   */
45  public class WorkflowUtilities  {
46      
47      /** Fires for each field when building the form.  Mainly used to build dynamic list options. */
48      public static final int EVENT_FIELD_ONPOPULATE = 1;
49      /** NOT CURRENTLY IMPLEMENTED.  Use the onPopulate event instead. In the future, this event may be implemented to allow for list sorting after list value population. */
50      public static final int EVENT_FIELD_ONSORT = 2;
51      /** Fires to set the current value of a form field.  This will overwrite any data in the form field pulled from the database. */
52      public static final int EVENT_FIELD_ONSETDEFAULT = 3;
53      /** Fires on validation of the form field. */
54      public static final int EVENT_FIELD_ONVALIDATE = 4;
55      /** Fires after validation, but before the data is committed to the database. */
56      public static final int EVENT_FIELD_ONPRESUBMIT = 5;
57      /** Fires after all data is submitted to the db for all fields. Performed right before the response is sent. */
58      public static final int EVENT_FIELD_ONPOSTSUBMIT = 6;
59      
60      private static final Logger logger = Logger.getLogger(WorkflowUtilities.class);
61      
62      public WorkflowUtilities() {
63      }
64      
65      /**
66       * Returns a title of workflow event, according to selected locale.
67       * @param value is an identifier of incoming event.
68       * @param locale is a selected locale.
69       * @return a name of event or something like "MISSING KEY: <resourceBundleKey>".
70       */
71      public static String getEventName(int value, Locale locale) {
72          final String eventName = getEventName(Integer.toString(value), locale);
73  //        assert null != eventName : "event name should never be null.";
74          return eventName;
75      }
76      
77      public static String getEventName(String value, Locale locale) {
78          return ITrackerResources.getString(ITrackerResources.KEY_BASE_WORKFLOW_EVENT + value, locale);
79      }
80      
81      /**
82       * Returns an array of pairs (eventName, eventId), where eventName
83       * is an event title, according to selected locale.
84       * @param locale is a selected locale.
85       * @return an array of pairs (eventName, eventId), which is never null.
86       */
87      public static NameValuePair[] getEvents(Locale locale) {
88          NameValuePair[] eventNames = new NameValuePair[6];
89          eventNames[0] = new NameValuePair(getEventName(EVENT_FIELD_ONPOPULATE, locale), Integer.toString(EVENT_FIELD_ONPOPULATE));
90          eventNames[1] = new NameValuePair(getEventName(EVENT_FIELD_ONSORT, locale), Integer.toString(EVENT_FIELD_ONSORT));
91          eventNames[2] = new NameValuePair(getEventName(EVENT_FIELD_ONSETDEFAULT, locale), Integer.toString(EVENT_FIELD_ONSETDEFAULT));
92          eventNames[3] = new NameValuePair(getEventName(EVENT_FIELD_ONVALIDATE, locale), Integer.toString(EVENT_FIELD_ONVALIDATE));
93          eventNames[4] = new NameValuePair(getEventName(EVENT_FIELD_ONPRESUBMIT, locale), Integer.toString(EVENT_FIELD_ONPRESUBMIT));
94          eventNames[5] = new NameValuePair(getEventName(EVENT_FIELD_ONPOSTSUBMIT, locale), Integer.toString(EVENT_FIELD_ONPOSTSUBMIT));
95          return eventNames;
96      }
97      
98      /**
99       * Select a list of NameValuePair objects from provided map object according
100      * to fieldId selector. Typesafe version of #getListOptions(Map, Integer)
101      * @param listOptions is a map, with stored NameValuePair objects lists
102      * associated with specific integer id.
103      * @param fieldId is a selector from map.
104      * @return a list of objects, which may be empty, but never null.
105      */
106     public static List<NameValuePair> getListOptions(Map<Integer, List<NameValuePair>> listOptions, int fieldId) {
107         return getListOptions(listOptions, Integer.valueOf(fieldId));
108     }
109     
110     /**
111      * Select a list of NameValuePair objects from provided map object according
112      * to fieldId selector.
113      * @param listOptions is a map, with stored NameValuePair objects lists
114      * associated with specific integer id.
115      * @param fieldId is a selector from map.
116      * @return a list of objects, which may be empty, but never null.
117      */
118     @SuppressWarnings("unchecked")
119     public static List<NameValuePair> getListOptions(Map listOptions, Integer fieldId) {
120         List<NameValuePair> options = new ArrayList<NameValuePair>();
121         
122         if(listOptions != null && listOptions.size() != 0 && fieldId != null) {
123             Object mapOptions = listOptions.get(fieldId);
124             if(mapOptions != null) {
125                 options = (List<NameValuePair>) mapOptions;
126             }
127         }
128         
129         return options;
130     }
131     
132     /**
133      * The most general way to run scripts. All matching of event and fields
134      * are embedded within. As a result, currentValues parameter will
135      * contain updated values and form will contain new default values
136      * if appropriate.
137      * @param projectScriptModels is a list of scripts.
138      * @param event is an event type.
139      * @param currentValues is a map of current values to fields.
140      * @param currentErrors is a container for errors.
141      * @param form contains default values of fields.
142      * @throws org.itracker.services.exceptions.WorkflowException
143      */
144     public static void processFieldScripts(List<ProjectScript> projectScriptModels, int event, Map<Integer, List<NameValuePair>> currentValues, ActionMessages currentErrors, ValidatorForm form) throws WorkflowException {
145         if(projectScriptModels == null || projectScriptModels.size() == 0) {
146             return;
147         }
148         logger.debug("Processing " + projectScriptModels.size() + " field scripts for project " + projectScriptModels.get(0).getProject().getId());
149         
150         List<ProjectScript> scriptsToRun = new ArrayList<ProjectScript>();
151         for(int i = 0; i < projectScriptModels.size(); i++) {
152             if(projectScriptModels.get(i).getScript().getEvent() == event) {
153                 int insertIndex = 0;
154                 for(insertIndex = 0; insertIndex < scriptsToRun.size(); insertIndex++) {
155                     if(projectScriptModels.get(i).getPriority() < ((ProjectScript) scriptsToRun.get(insertIndex)).getPriority()) {
156                         break;
157                     }
158                 }
159                 scriptsToRun.add(insertIndex,projectScriptModels.get(i));
160             }
161         }
162         logger.debug(scriptsToRun.size() + " eligible scripts found for event " + event);
163         
164         if (currentValues == null) {
165             currentValues = new HashMap<Integer, List<NameValuePair>>();
166         }
167         
168         for (int i = 0; i < scriptsToRun.size(); i++) {
169             ProjectScript currentScript = (ProjectScript) scriptsToRun.get(i);
170             try {
171                 logger.debug("Running script " + currentScript.getScript().getId() + " with priority " + currentScript.getPriority());
172                 List<NameValuePair> currentValue = currentValues.get(currentScript.getFieldId());
173                 
174                 logger.debug("Before script current value for field " + IssueUtilities.getFieldName(currentScript.getFieldId()) + " (" + currentScript.getFieldId() + ") is " + (currentValue == null ? "NULL" : "'" + currentValue.toString() + "' (" + currentValue.getClass().getName() + "'"));
175                 currentValue = processFieldScript(currentScript, event, currentScript.getFieldId(), currentValue, currentErrors, form);
176                 logger.debug("After script current value for field " + IssueUtilities.getFieldName(currentScript.getFieldId()) + " (" + currentScript.getFieldId() + ") is " + (currentValue == null ? "NULL" : "'" + currentValue.toString() + "' (" + currentValue.getClass().getName() + "'"));
177                 
178                 currentValues.put(currentScript.getFieldId(), currentValue);
179             } catch(WorkflowException we) {
180                 logger.error("Error processing script " + currentScript.getScript().getId() + ": " + we.getMessage());
181             }
182         }
183     }
184     
185     /**
186      * Run appropriate script, selecting it from provided list by matching
187      * event and field.
188      * @param projectScripts is a list of provided scripts.
189      * @param event is an event type.
190      * @param fieldId is a field, associated with event.
191      * @param currentValue is a set of current values.
192      * @param currentErrors is a container for errors.
193      * @param form is a form, holder of default values.
194      * @return new set of values.
195      * @throws org.itracker.services.exceptions.WorkflowException
196      */
197     public static List<NameValuePair> processFieldScripts(List<ProjectScript> projectScripts, int event, Integer fieldId, List<NameValuePair> currentValue, ActionErrors currentErrors, ValidatorForm form) throws WorkflowException {
198         if(projectScripts == null || projectScripts.size() == 0 || fieldId == null) {
199             return null;
200         }
201         logger.debug("Processing " + projectScripts.size() + " field scripts for project " + projectScripts.get(0).getProject().getId());
202         
203         List<ProjectScript> scriptsToRun = new LinkedList<ProjectScript>();
204         for(int i = 0; i < projectScripts.size(); i++) {
205             if(projectScripts.get(i).getScript().getEvent() == event && fieldId.equals(projectScripts.get(i).getFieldId())) {
206                 int insertIndex = 0;
207                 for(insertIndex = 0; insertIndex < scriptsToRun.size(); insertIndex++) {
208                     if(projectScripts.get(i).getPriority() < ((ProjectScript) scriptsToRun.get(insertIndex)).getPriority()) {
209                         break;
210                     }
211                 }
212                 scriptsToRun.add(insertIndex,projectScripts.get(i));
213             }
214         }
215         logger.debug(scriptsToRun.size() + " eligible scripts found for event " + event + " on field " + fieldId);
216         
217         for(int i = 0; i < scriptsToRun.size(); i++) {
218             ProjectScript currentScript = (ProjectScript) scriptsToRun.get(i);
219             try {
220                 logger.debug("Running script " + currentScript.getScript().getId() + " with priority " + currentScript.getPriority());
221                 currentValue = processFieldScript(currentScript, event, fieldId, currentValue, currentErrors, form);
222             } catch(WorkflowException we) {
223                 logger.error("Error processing script " + currentScript.getScript().getId() + ": " + we.getMessage());
224             }
225         }
226         
227         return currentValue;
228     }
229     
230     /**
231      * Run provided BEANSHELL script against form instance, taking into account
232      * incoming event type, field raised an event and current values.
233      * As a result, a set of new current values is returned and if
234      * appropriate, default values are changed in form.
235      * TODO: should issue, project, user, services be available too?
236      * 
237      * @param projectScript is a script to run.
238      * @param event is an event type.
239      * @param fieldId is a field id associated with event.
240      * @param currentValues is a set of current values.
241      * @param currentErrors is a container for occured errors.
242      * @param form is a form instance, holding values.
243      * @return new current values.
244      * @throws org.itracker.services.exceptions.WorkflowException
245      */
246     public static List<NameValuePair> processFieldScript(ProjectScript projectScript, int event, Integer fieldId, List<NameValuePair> currentValues, ActionMessages currentErrors, ValidatorForm form) throws WorkflowException {
247         if (projectScript == null) {
248             throw new WorkflowException("ProjectScript was null.", WorkflowException.INVALID_ARGS);
249         }
250         if (currentErrors == null) {
251             throw new WorkflowException("Errors was null.", WorkflowException.INVALID_ARGS);
252         }
253         
254         try {
255             Interpreter bshInterpreter = new Interpreter();
256             bshInterpreter.set("event", event);
257             bshInterpreter.set("fieldId", fieldId);
258             // TODO: remove currentValue from bshInterpreter, it's a collection of current values.
259             bshInterpreter.set("currentValue", currentValues);
260             bshInterpreter.set("currentValues", currentValues);
261             bshInterpreter.set("currentErrors", currentErrors);
262             bshInterpreter.set("currentForm", form);
263             bshInterpreter.eval(projectScript.getScript().getScript());
264             // TODO: is this necessary? It should stay the same list-object..
265 //            currentValues = (List<NameValuePair>)bshInterpreter.get("currentValue");
266             if (logger.isDebugEnabled()) {
267             	logger.debug("processFieldScript: Script returned current value of '" + currentValues + "' (" + (currentValues != null ? currentValues.getClass().getName() : "NULL") + ")");
268             }
269             if (event == EVENT_FIELD_ONSETDEFAULT && form != null && currentValues != null) {
270             	if (logger.isDebugEnabled()) {
271             		logger.debug("processFieldScript: Setting current form field value for field " + IssueUtilities.getFieldName(projectScript.getFieldId()) + " to '" + currentValues + "'");
272             	}
273                 setFormProperty(form, projectScript.getFieldId(), currentValues);
274             }
275         } catch(Exception e) {
276             logger.error("processFieldScript: Error processing field script.", e);
277             // TODO: error-handling..?
278             currentErrors.add(ActionMessages.GLOBAL_MESSAGE, 
279             		new ActionMessage("itracker.web.error.system.message", 
280             				new Object[]{ITrackerResources.getString("itracker.web.attr.script"), // Script
281             					e.getMessage()
282             }));
283         }
284         if (logger.isDebugEnabled()) {
285         	logger.debug("processFieldScript: returning " + currentValues + ", errors: " + currentErrors);
286         }
287         return currentValues;
288     }
289     
290     @SuppressWarnings("unchecked")
291     private static void setFormProperty(ValidatorForm form, Integer fieldId, Object currentValue) {
292         String fieldName = IssueUtilities.getFieldName(fieldId);
293         int fieldType = IssueUtilities.getFieldType(fieldId);
294         if(fieldType == IssueUtilities.FIELD_TYPE_SINGLE) {
295             try {
296                 Field formField = form.getClass().getField( fieldName );
297                 if ( formField != null ) {
298                     formField.set( form , currentValue );
299                 } else {
300                     throw new IllegalArgumentException( "no field with name "
301                             + fieldName + " found in form " + form );
302                 }
303             } catch ( NoSuchFieldException e ) {
304                 e.printStackTrace();
305             } catch ( IllegalAccessException e ) {
306                 e.printStackTrace();
307             }
308         } else if(fieldType == IssueUtilities.FIELD_TYPE_INDEXED) {
309             try {
310                 Object indexedField = null;
311                 Field formField = form.getClass().getField(fieldName);
312                 indexedField = formField.get( form );
313                 if ( indexedField instanceof List ) {
314                     ((List)indexedField).set( 0 , currentValue );
315                 } else if ( indexedField instanceof Collection ) {
316                     ((Collection)indexedField).add( currentValue );
317                 } else {
318                     throw new IllegalArgumentException( "field with name "
319                             + fieldName + " found in form " + form + " is of unknown type" );
320                 }
321             } catch ( NoSuchFieldException e ) {
322                 e.printStackTrace();
323             } catch ( IllegalAccessException e ) {
324                 e.printStackTrace();
325             }
326         } else if(fieldType == IssueUtilities.FIELD_TYPE_MAP) {
327             try {
328                 Object indexedField = null;
329                 Field formField = form.getClass().getField( fieldName );
330                 indexedField = formField.get( form );
331                 if ( indexedField instanceof Map ) {
332                     ((Map)indexedField).put( fieldId.toString(), currentValue );
333                 } else {
334                     throw new IllegalArgumentException( "field with name "
335                             + fieldName + " found in form " + form + " is of unknown type" );
336                 }
337             } catch ( NoSuchFieldException e ) {
338                 e.printStackTrace();
339             } catch ( IllegalAccessException e ) {
340                 e.printStackTrace();
341             }
342         }
343     }
344     
345 }