001: /*
002: * Copyright (c) 2002-2003 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.workflow.spi;
006:
007: import com.opensymphony.user.EntityNotFoundException;
008: import com.opensymphony.user.Group;
009: import com.opensymphony.user.User;
010: import com.opensymphony.user.UserManager;
011:
012: import com.opensymphony.workflow.AbstractWorkflow;
013: import com.opensymphony.workflow.QueryNotSupportedException;
014: import com.opensymphony.workflow.Workflow;
015: import com.opensymphony.workflow.WorkflowException;
016: import com.opensymphony.workflow.basic.BasicWorkflow;
017: import com.opensymphony.workflow.loader.WorkflowDescriptor;
018: import com.opensymphony.workflow.query.Expression;
019: import com.opensymphony.workflow.query.FieldExpression;
020: import com.opensymphony.workflow.query.NestedExpression;
021: import com.opensymphony.workflow.query.WorkflowExpressionQuery;
022: import com.opensymphony.workflow.query.WorkflowQuery;
023:
024: import junit.framework.TestCase;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import java.util.Collections;
030: import java.util.Date;
031: import java.util.HashMap;
032: import java.util.List;
033: import java.util.Map;
034:
035: /**
036: * This test case is functional in that it attempts to validate the entire
037: * lifecycle of a workflow. This is also a good resource for beginners
038: * to OSWorkflow. This class is extended to for various SPI's.
039: *
040: * @author Eric Pugh (epugh@upstate.com)
041: */
042: public abstract class AbstractFunctionalWorkflowTest extends TestCase {
043: //~ Static fields/initializers /////////////////////////////////////////////
044:
045: private static final String USER_TEST = "test";
046:
047: //~ Instance fields ////////////////////////////////////////////////////////
048:
049: protected Log log;
050: protected Workflow workflow;
051: protected WorkflowDescriptor workflowDescriptor;
052:
053: //~ Constructors ///////////////////////////////////////////////////////////
054:
055: public AbstractFunctionalWorkflowTest(String s) {
056: super (s);
057: log = LogFactory.getLog(getClass());
058: }
059:
060: //~ Methods ////////////////////////////////////////////////////////////////
061:
062: public void testExampleWorkflow() throws Exception {
063: WorkflowQuery query;
064:
065: String workflowName = getWorkflowName();
066: assertTrue("canInitialize for workflow " + workflowName
067: + " is false", workflow
068: .canInitialize(workflowName, 100));
069:
070: long workflowId = workflow.initialize(workflowName, 100,
071: new HashMap());
072: String workorderName = workflow.getWorkflowName(workflowId);
073: workflowDescriptor = workflow
074: .getWorkflowDescriptor(workorderName);
075:
076: if (log.isDebugEnabled()) {
077: log.debug("Name of workorder:" + workorderName);
078: }
079:
080: assertTrue(
081: "Expected external-permission permA in step 1 not found",
082: workflow.getSecurityPermissions(workflowId, null)
083: .contains("permA"));
084:
085: List currentSteps = workflow.getCurrentSteps(workflowId);
086: assertEquals("Unexpected number of current steps", 1,
087: currentSteps.size());
088: assertEquals("Unexpected current step", 1, ((Step) currentSteps
089: .get(0)).getStepId());
090:
091: List historySteps = workflow.getHistorySteps(workflowId);
092: assertEquals("Unexpected number of history steps", 0,
093: historySteps.size());
094:
095: if (log.isDebugEnabled()) {
096: log.debug("Perform Finish First Draft");
097: }
098:
099: workflow.doAction(workflowId, 1, Collections.EMPTY_MAP);
100:
101: int[] actions = workflow.getAvailableActions(workflowId,
102: Collections.EMPTY_MAP);
103: assertEquals(3, actions.length);
104: historySteps = workflow.getHistorySteps(workflowId);
105: assertEquals("Unexpected number of history steps", 1,
106: historySteps.size());
107:
108: Step historyStep = (Step) historySteps.get(0);
109: assertEquals(USER_TEST, historyStep.getCaller());
110: assertNull(historyStep.getDueDate());
111:
112: // check system date, add in a 1 second fudgefactor.
113: assertTrue("history step finish date "
114: + historyStep.getFinishDate() + " is in the future!",
115: (historyStep.getFinishDate().getTime() - 1000) < System
116: .currentTimeMillis());
117: logActions(actions);
118:
119: if (log.isDebugEnabled()) {
120: log.debug("Perform Finish Foo");
121: }
122:
123: workflow.doAction(workflowId, 12, Collections.EMPTY_MAP);
124:
125: //Step lastHistoryStep = historyStep;
126: historySteps = workflow.getHistorySteps(workflowId);
127: assertEquals("Unexpected number of history steps", 2,
128: historySteps.size());
129:
130: if (log.isDebugEnabled()) {
131: log.debug("Perform Stay in Bar");
132: }
133:
134: workflow.doAction(workflowId, 113, Collections.EMPTY_MAP);
135: actions = workflow.getAvailableActions(workflowId,
136: Collections.EMPTY_MAP);
137: assertEquals(2, actions.length);
138: assertTrue((actions[0] == 13) && (actions[1] == 113));
139: logActions(actions);
140:
141: //historyStep = (Step) historySteps.get(0);
142: //assertEquals(lastHistoryStep.getId(), historyStep.getId());
143: if (log.isDebugEnabled()) {
144: log.debug("Perform Finish Bar");
145: }
146:
147: workflow.doAction(workflowId, 13, Collections.EMPTY_MAP);
148: actions = workflow.getAvailableActions(workflowId,
149: Collections.EMPTY_MAP);
150: assertEquals(1, actions.length);
151: logActions(actions);
152:
153: if (log.isDebugEnabled()) {
154: log.debug("Perform Finish Baz");
155: }
156:
157: workflow.doAction(workflowId, 14, Collections.EMPTY_MAP);
158: actions = workflow.getAvailableActions(workflowId,
159: Collections.EMPTY_MAP);
160: logActions(actions);
161: historySteps = workflow.getHistorySteps(workflowId);
162: assertEquals("Unexpected number of history steps", 5,
163: historySteps.size());
164:
165: if (log.isDebugEnabled()) {
166: log.debug("Perform Finish Editing");
167: }
168:
169: workflow.doAction(workflowId, 3, Collections.EMPTY_MAP);
170: actions = workflow.getAvailableActions(workflowId,
171: Collections.EMPTY_MAP);
172: assertEquals(3, actions.length);
173: logActions(actions);
174:
175: if (log.isDebugEnabled()) {
176: log.debug("Perform Publish Doc");
177: }
178:
179: workflow.doAction(workflowId, 7, Collections.EMPTY_MAP);
180: actions = workflow.getAvailableActions(workflowId,
181: Collections.EMPTY_MAP);
182: assertEquals(1, actions.length);
183: logActions(actions);
184:
185: if (log.isDebugEnabled()) {
186: log.debug("Perform Publish Document");
187: }
188:
189: workflow.doAction(workflowId, 11, Collections.EMPTY_MAP);
190:
191: actions = workflow.getAvailableActions(workflowId,
192: Collections.EMPTY_MAP);
193: assertEquals(0, actions.length);
194: historySteps = workflow.getHistorySteps(workflowId);
195: assertEquals("Unexpected number of history steps", 8,
196: historySteps.size());
197:
198: query = new WorkflowQuery(WorkflowQuery.OWNER,
199: WorkflowQuery.CURRENT, WorkflowQuery.EQUALS, USER_TEST);
200:
201: try {
202: List workflows = workflow.query(query);
203: assertEquals("Unexpected number of workflow query results",
204: 1, workflows.size());
205:
206: WorkflowQuery queryLeft = new WorkflowQuery(
207: WorkflowQuery.OWNER, WorkflowQuery.CURRENT,
208: WorkflowQuery.EQUALS, USER_TEST);
209: WorkflowQuery queryRight = new WorkflowQuery(
210: WorkflowQuery.STATUS, WorkflowQuery.CURRENT,
211: WorkflowQuery.EQUALS, "Finished");
212: query = new WorkflowQuery(queryLeft, WorkflowQuery.AND,
213: queryRight);
214: workflows = workflow.query(query);
215: assertEquals("Unexpected number of workflow query results",
216: 1, workflows.size());
217: } catch (QueryNotSupportedException ex) {
218: System.out.println("query not supported");
219: }
220: }
221:
222: public void testExceptionOnIllegalStayInCurrentStep()
223: throws Exception {
224: String workflowName = getWorkflowName();
225: assertTrue("canInitialize for workflow " + workflowName
226: + " is false", workflow
227: .canInitialize(workflowName, 100));
228:
229: try {
230: long workflowId = workflow.initialize(workflowName, 200,
231: new HashMap());
232: fail("initial action result specified target step of current step. Succeeded but should not have.");
233: } catch (WorkflowException e) {
234: // expected, no such thing as current step for initial action
235: }
236: }
237:
238: public void testMetadataAccess() throws Exception {
239: String workflowName = getWorkflowName();
240: long workflowId = workflow.initialize(workflowName, 100,
241: new HashMap());
242: WorkflowDescriptor wfDesc = workflow
243: .getWorkflowDescriptor(workflowName);
244:
245: Map meta = wfDesc.getMetaAttributes();
246: assertTrue("missing metadata", (meta.get("workflow-meta1"))
247: .equals("workflow-meta1-value"));
248: assertTrue("missing metadata", (meta.get("workflow-meta2"))
249: .equals("workflow-meta2-value"));
250:
251: meta = wfDesc.getStep(1).getMetaAttributes();
252: assertTrue("missing metadata", (meta.get("step-meta1"))
253: .equals("step-meta1-value"));
254: assertTrue("missing metadata", (meta.get("step-meta2"))
255: .equals("step-meta2-value"));
256:
257: meta = wfDesc.getAction(1).getMetaAttributes();
258: assertTrue("missing metadata", (meta.get("action-meta1"))
259: .equals("action-meta1-value"));
260: assertTrue("missing metadata", (meta.get("action-meta2"))
261: .equals("action-meta2-value"));
262: }
263:
264: public void testWorkflowExpressionQuery() throws Exception {
265: List workflows;
266: WorkflowExpressionQuery query;
267:
268: String workflowName = getWorkflowName();
269: assertTrue("canInitialize for workflow " + workflowName
270: + " is false", workflow
271: .canInitialize(workflowName, 100));
272:
273: //------------------- FieldExpression.OWNER + FieldExpression.CURRENT_STEPS ----------------------
274: query = new WorkflowExpressionQuery(new FieldExpression(
275: FieldExpression.OWNER, FieldExpression.CURRENT_STEPS,
276: FieldExpression.EQUALS, USER_TEST));
277:
278: try {
279: workflows = workflow.query(query);
280: assertEquals("empty OWNER+CURRENT_STEPS", 0, workflows
281: .size());
282: } catch (QueryNotSupportedException e) {
283: log.error("Store does not support query");
284:
285: return;
286: }
287:
288: long workflowId = workflow.initialize(workflowName, 100,
289: new HashMap());
290: workflows = workflow.query(query);
291: assertEquals("OWNER+CURRENT_STEPS", 1, workflows.size());
292:
293: //------------------- FieldExpression.NAME + FieldExpression.ENTRY ----------------------------------
294: query = new WorkflowExpressionQuery(new FieldExpression(
295: FieldExpression.NAME, FieldExpression.ENTRY,
296: FieldExpression.EQUALS, "notexistingname"));
297: workflows = workflow.query(query);
298: assertEquals("empty NAME+ENTRY", 0, workflows.size());
299:
300: query = new WorkflowExpressionQuery(new FieldExpression(
301: FieldExpression.NAME, FieldExpression.ENTRY,
302: FieldExpression.EQUALS, workflowName));
303: workflows = workflow.query(query);
304: assertEquals("NAME+ENTRY", 1, workflows.size());
305:
306: //------------------- FieldExpression.STATE + FieldExpression.ENTRY ----------------------------------
307: query = new WorkflowExpressionQuery(new FieldExpression(
308: FieldExpression.STATE, FieldExpression.ENTRY,
309: FieldExpression.EQUALS, new Integer(
310: WorkflowEntry.COMPLETED)));
311: workflows = workflow.query(query);
312: assertEquals("empty STATE+ENTRY", 0, workflows.size());
313:
314: query = new WorkflowExpressionQuery(new FieldExpression(
315: FieldExpression.STATE, FieldExpression.ENTRY,
316: FieldExpression.EQUALS, new Integer(
317: WorkflowEntry.ACTIVATED)));
318: workflows = workflow.query(query);
319: assertEquals("STATE+ENTRY", 1, workflows.size());
320:
321: // --------------------------- empty nested query : AND ---------------------------------
322: Expression queryLeft = new FieldExpression(
323: FieldExpression.OWNER, FieldExpression.CURRENT_STEPS,
324: FieldExpression.EQUALS, USER_TEST);
325: Expression queryRight = new FieldExpression(
326: FieldExpression.STATUS, FieldExpression.CURRENT_STEPS,
327: FieldExpression.EQUALS, "Finished");
328: query = new WorkflowExpressionQuery(new NestedExpression(
329: new Expression[] { queryLeft, queryRight },
330: NestedExpression.AND));
331: workflows = workflow.query(query);
332: assertEquals("empty nested query AND", 0, workflows.size());
333:
334: // -------------------------- negated nested query: AND ----------------------------------
335: queryLeft = new FieldExpression(FieldExpression.OWNER,
336: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
337: USER_TEST);
338: queryRight = new FieldExpression(FieldExpression.STATUS,
339: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
340: "Finished", true);
341: query = new WorkflowExpressionQuery(new NestedExpression(
342: new Expression[] { queryLeft, queryRight },
343: NestedExpression.AND));
344: workflows = workflow.query(query);
345: assertEquals("negated nested query AND", 1, workflows.size());
346:
347: // -------------------------- nested query: AND + same context ------------------------------------------
348: queryRight = new FieldExpression(FieldExpression.STATUS,
349: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
350: "Underway");
351: query = new WorkflowExpressionQuery(new NestedExpression(
352: new Expression[] { queryLeft, queryRight },
353: NestedExpression.AND));
354: workflows = workflow.query(query);
355: assertEquals("nested query AND", 1, workflows.size());
356:
357: // ------------------------- empty nested query: OR + mixed context -------------------------------------
358: queryLeft = new FieldExpression(FieldExpression.FINISH_DATE,
359: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
360: new Date());
361: queryRight = new FieldExpression(FieldExpression.STATUS,
362: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
363: "Finished");
364: query = new WorkflowExpressionQuery(new NestedExpression(
365: new Expression[] { queryLeft, queryRight },
366: NestedExpression.OR));
367:
368: try {
369: workflows = workflow.query(query);
370: assertEquals("empty nested query OR + mixed context", 0,
371: workflows.size());
372: } catch (QueryNotSupportedException e) {
373: log.warn("Query not supported: " + e);
374: }
375:
376: // ------------------------- negated nested query: OR -------------------------------------
377: queryLeft = new FieldExpression(FieldExpression.FINISH_DATE,
378: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
379: new Date());
380: queryRight = new FieldExpression(FieldExpression.STATUS,
381: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
382: "Finished", true);
383: query = new WorkflowExpressionQuery(new NestedExpression(
384: new Expression[] { queryLeft, queryRight },
385: NestedExpression.OR));
386:
387: try {
388: workflows = workflow.query(query);
389: assertEquals("negated nested query OR", 1, workflows.size());
390: } catch (QueryNotSupportedException e) {
391: log.warn("Query not supported: " + e);
392: }
393:
394: // ------------------------- nested query: OR + mixed context -------------------------------------
395: queryLeft = new FieldExpression(FieldExpression.FINISH_DATE,
396: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
397: new Date());
398: queryRight = new FieldExpression(FieldExpression.NAME,
399: FieldExpression.ENTRY, FieldExpression.EQUALS,
400: workflowName);
401: query = new WorkflowExpressionQuery(new NestedExpression(
402: new Expression[] { queryLeft, queryRight },
403: NestedExpression.OR));
404:
405: try {
406: workflows = workflow.query(query);
407: assertEquals("nested query OR + mixed context", 1,
408: workflows.size());
409: } catch (QueryNotSupportedException e) {
410: log.warn("Query not supported: " + e);
411: }
412:
413: // --------------------- START_DATE+CURRENT_STEPS -------------------------------------------------
414: //there should be one step that has been started
415: query = new WorkflowExpressionQuery(new FieldExpression(
416: FieldExpression.START_DATE,
417: FieldExpression.CURRENT_STEPS, FieldExpression.LT,
418: new Date(System.currentTimeMillis() + 1000)));
419: workflows = workflow.query(query);
420: assertEquals(
421: "Expected to find one workflow step that was started",
422: 1, workflows.size());
423:
424: // --------------------- empty FINISH_DATE+HISTORY_STEPS -------------------------------------------
425: //there should be no steps that have been completed
426: query = new WorkflowExpressionQuery(new FieldExpression(
427: FieldExpression.FINISH_DATE,
428: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
429: new Date()));
430: workflows = workflow.query(query);
431: assertEquals(
432: "Expected to find no history steps that were completed",
433: 0, workflows.size());
434:
435: // =================================================================================================
436: workflow.doAction(workflowId, 1, Collections.EMPTY_MAP);
437:
438: // --------------------- START_DATE+HISTORY_STEPS -------------------------------------------------
439: //there should be two step that have been started
440: query = new WorkflowExpressionQuery(new FieldExpression(
441: FieldExpression.START_DATE,
442: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
443: new Date(System.currentTimeMillis() + 1000)));
444: workflows = workflow.query(query);
445: assertEquals(
446: "Expected to find 1 workflow step in the history for entry #"
447: + workflowId, 1, workflows.size());
448:
449: // --------------------- FINISH_DATE+HISTORY_STEPS -------------------------------------------
450: query = new WorkflowExpressionQuery(new FieldExpression(
451: FieldExpression.FINISH_DATE,
452: FieldExpression.HISTORY_STEPS, FieldExpression.LT,
453: new Date(System.currentTimeMillis() + 1000)));
454: workflows = workflow.query(query);
455: assertEquals(
456: "Expected to find 1 history steps that was completed",
457: 1, workflows.size());
458:
459: // --------------------- ACTION + HISTORY_STEPS ----------------------------------------------
460: query = new WorkflowExpressionQuery(new FieldExpression(
461: FieldExpression.ACTION, FieldExpression.HISTORY_STEPS,
462: FieldExpression.EQUALS, new Integer(1)));
463: workflows = workflow.query(query);
464: assertEquals("ACTION + HISTORY_STEPS", 1, workflows.size());
465:
466: // --------------------- STEP + HISTORY_STEPS ----------------------------------------------
467: query = new WorkflowExpressionQuery(new FieldExpression(
468: FieldExpression.STEP, FieldExpression.HISTORY_STEPS,
469: FieldExpression.EQUALS, new Integer(1)));
470: workflows = workflow.query(query);
471: assertEquals("STEP + HISTORY_STEPS", 1, workflows.size());
472:
473: // --------------------- CALLER + HISTORY_STEPS --------------------------------------------
474: query = new WorkflowExpressionQuery(new FieldExpression(
475: FieldExpression.CALLER, FieldExpression.HISTORY_STEPS,
476: FieldExpression.EQUALS, USER_TEST));
477: workflows = workflow.query(query);
478: assertEquals("CALLER + HISTORY_STEPS", 1, workflows.size());
479:
480: //----------------------------------------------------------------------------
481: // ----- some more tests using nested expressions
482: long workflowId2 = workflow.initialize(workflowName, 100,
483: Collections.EMPTY_MAP);
484: workflow.changeEntryState(workflowId, WorkflowEntry.SUSPENDED);
485: queryRight = new FieldExpression(FieldExpression.STATE,
486: FieldExpression.ENTRY, FieldExpression.EQUALS,
487: new Integer(WorkflowEntry.ACTIVATED));
488: queryLeft = new FieldExpression(FieldExpression.STATE,
489: FieldExpression.ENTRY, FieldExpression.EQUALS,
490: new Integer(WorkflowEntry.SUSPENDED));
491: query = new WorkflowExpressionQuery(new NestedExpression(
492: new Expression[] { queryLeft, queryRight },
493: NestedExpression.OR));
494: workflows = workflow.query(query);
495: assertEquals(2, workflows.size());
496:
497: queryLeft = new FieldExpression(FieldExpression.OWNER,
498: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
499: USER_TEST);
500: queryRight = new FieldExpression(FieldExpression.STATUS,
501: FieldExpression.CURRENT_STEPS, FieldExpression.EQUALS,
502: "Finished", true);
503: query = new WorkflowExpressionQuery(new NestedExpression(
504: new Expression[] { queryLeft, queryRight },
505: NestedExpression.AND));
506: workflows = workflow.query(query);
507: assertEquals("Expected to find 2 workflows in current steps",
508: 2, workflows.size());
509: }
510:
511: public void testWorkflowQuery() throws Exception {
512: WorkflowQuery query = null;
513: List workflows;
514:
515: String workflowName = getWorkflowName();
516: assertTrue("canInitialize for workflow " + workflowName
517: + " is false", workflow
518: .canInitialize(workflowName, 100));
519:
520: try {
521: query = new WorkflowQuery(WorkflowQuery.OWNER,
522: WorkflowQuery.CURRENT, WorkflowQuery.EQUALS,
523: USER_TEST);
524: workflows = workflow.query(query);
525: assertEquals(0, workflows.size());
526: } catch (QueryNotSupportedException e) {
527: log.error("Store does not support query");
528: }
529:
530: try {
531: long workflowId = workflow.initialize(workflowName, 100,
532: new HashMap());
533: workflows = workflow.query(query);
534: assertEquals(1, workflows.size());
535: } catch (QueryNotSupportedException e) {
536: log.error("Store does not support query");
537: }
538:
539: try {
540: WorkflowQuery queryLeft = new WorkflowQuery(
541: WorkflowQuery.OWNER, WorkflowQuery.CURRENT,
542: WorkflowQuery.EQUALS, USER_TEST);
543: WorkflowQuery queryRight = new WorkflowQuery(
544: WorkflowQuery.STATUS, WorkflowQuery.CURRENT,
545: WorkflowQuery.EQUALS, "Finished");
546: query = new WorkflowQuery(queryLeft, WorkflowQuery.AND,
547: queryRight);
548: workflows = workflow.query(query);
549: assertEquals(0, workflows.size());
550:
551: queryRight = new WorkflowQuery(WorkflowQuery.STATUS,
552: WorkflowQuery.CURRENT, WorkflowQuery.EQUALS,
553: "Underway");
554: query = new WorkflowQuery(queryLeft, WorkflowQuery.AND,
555: queryRight);
556: workflows = workflow.query(query);
557: assertEquals(1, workflows.size());
558: } catch (QueryNotSupportedException e) {
559: log.error("Store does not support query");
560: }
561: }
562:
563: protected void setUp() throws Exception {
564: workflow = new BasicWorkflow(USER_TEST);
565:
566: UserManager um = UserManager.getInstance();
567: assertNotNull("Could not get UserManager", um);
568:
569: try {
570: um.getUser(USER_TEST);
571: } catch (EntityNotFoundException enfe) {
572: User test = um.createUser(USER_TEST);
573: test.setPassword("test");
574:
575: Group foos = um.createGroup("foos");
576: Group bars = um.createGroup("bars");
577: Group bazs = um.createGroup("bazs");
578: test.addToGroup(foos);
579: test.addToGroup(bars);
580: test.addToGroup(bazs);
581: }
582: }
583:
584: protected String getWorkflowName() {
585: return getClass().getResource("/samples/example.xml")
586: .toString();
587: }
588:
589: protected void logActions(int[] actions) {
590: for (int i = 0; i < actions.length; i++) {
591: String name = workflowDescriptor.getAction(actions[i])
592: .getName();
593: int actionId = workflowDescriptor.getAction(actions[i])
594: .getId();
595:
596: if (log.isDebugEnabled()) {
597: log.debug("Actions Available: " + name + " id:"
598: + actionId);
599: }
600: }
601: }
602: }
|