Source code: com/jcorporate/expresso/core/controller/Transition.java
1 /* ====================================================================
2 * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
3 *
4 * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in
15 * the documentation and/or other materials provided with the
16 * distribution.
17 *
18 * 3. The end-user documentation included with the redistribution,
19 * if any, must include the following acknowledgment:
20 * "This product includes software developed by Jcorporate Ltd.
21 * (http://www.jcorporate.com/)."
22 * Alternately, this acknowledgment may appear in the software itself,
23 * if and wherever such third-party acknowledgments normally appear.
24 *
25 * 4. "Jcorporate" and product names such as "Expresso" must
26 * not be used to endorse or promote products derived from this
27 * software without prior written permission. For written permission,
28 * please contact info@jcorporate.com.
29 *
30 * 5. Products derived from this software may not be called "Expresso",
31 * or other Jcorporate product names; nor may "Expresso" or other
32 * Jcorporate product names appear in their name, without prior
33 * written permission of Jcorporate Ltd.
34 *
35 * 6. No product derived from this software may compete in the same
36 * market space, i.e. framework, without prior written permission
37 * of Jcorporate Ltd. For written permission, please contact
38 * partners@jcorporate.com.
39 *
40 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
41 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
42 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
43 * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
44 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
45 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
46 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
47 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
48 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
49 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
50 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
51 * SUCH DAMAGE.
52 * ====================================================================
53 *
54 * This software consists of voluntary contributions made by many
55 * individuals on behalf of the Jcorporate Ltd. Contributions back
56 * to the project(s) are encouraged when you make modifications.
57 * Please send them to support@jcorporate.com. For more information
58 * on Jcorporate Ltd. and its products, please see
59 * <http://www.jcorporate.com/>.
60 *
61 * Portions of this software are based upon other open source
62 * products and are subject to their respective licenses.
63 */
64
65 package com.jcorporate.expresso.core.controller;
66
67 import com.jcorporate.expresso.core.ExpressoConstants;
68 import com.jcorporate.expresso.core.misc.ConfigManager;
69 import com.jcorporate.expresso.core.misc.StringDOMParser;
70 import com.jcorporate.expresso.core.misc.StringUtil;
71 import com.jcorporate.expresso.core.misc.URLUTF8Encoder;
72 import com.jcorporate.expresso.kernel.util.FastStringBuffer;
73 import org.apache.log4j.Logger;
74 import org.apache.struts.config.ActionConfig;
75 import org.w3c.dom.Document;
76 import org.w3c.dom.NamedNodeMap;
77 import org.w3c.dom.Node;
78 import org.w3c.dom.NodeList;
79
80 import javax.servlet.RequestDispatcher;
81 import javax.servlet.http.HttpServletRequest;
82 import javax.servlet.http.HttpServletResponse;
83 import java.io.IOException;
84 import java.util.Enumeration;
85 import java.util.Hashtable;
86 import java.util.Map;
87 import java.util.Vector;
88
89
90 /**
91 * <p>An Transition is a choice that the user can make that initiates
92 * either another sequence in this same controller or some new
93 * controller. A transition is one of the three types of objects that
94 * a controller produces when it enters a new state, the others
95 * being Input objects and Output objects.</p>
96 * <p>Another use of a Transition object is for internal transitioning between
97 * various controllers and their states. Typical example is as follows: <br/>
98 * <code>
99 * <pre>
100 * Transition t = new Transition();
101 * t.setControllerObject(com.myapp.MyController.class);
102 * t.setState("State2");
103 * t.addParameter("SampleParam","This is a parameter value");
104 * return t.transition();
105 * </pre>
106 * </code>
107 * </p>
108 * <p/>
109 * <h4>Recognized Attributes:</h4>
110 * <p>The following types are recognized by the expresso framework and
111 * automatically rendered: You may add your own types or ignore them
112 * if you are doing your own page rendering.</p>
113 * <p/>
114 * <p>header: Renders the transition in the jc-header class style</p>
115 * <p/>
116 * <p>button: Renders the transition as a button.
117 * <p/>
118 * <p><i>default behavior:</i> Renders as a clickable button</p>
119 */
120 public class Transition
121 extends ControllerElement
122 implements Cloneable,
123 java.io.Serializable {
124
125 private static Logger log = Logger.getLogger(Transition.class);
126
127 /**
128 * name of the controller object that created this transition
129 */
130 private String ownerObject = null;
131
132 /**
133 * The name of a controller object that handles this action
134 */
135 private String controllerObject = null;
136
137 /**
138 * The parameters to the controller object
139 */
140 private Hashtable params = new Hashtable(1);
141 private String myState = null;
142 private boolean returnToSender = false;
143
144 /**
145 * This is used to store a constructed parameter string to save on
146 * re-constructing multiple times.
147 */
148 private transient String cacheParamStringSansController = null;
149
150 /**
151 * This is used to store a constructed parameter string to save on
152 * re-constructing multiple times.
153 */
154 private transient String cacheParamStringWithController = null;
155
156
157 /**
158 * Used in place of ControllerResponse for URL encoding
159 */
160 private transient HttpServletResponse servletResponse = null;
161
162 /**
163 * Default Constructor. Normally you don't use this.
164 */
165 public Transition() {
166 }
167
168 /**
169 * Convenience method to transition to another state in this same controller.
170 *
171 * @param newState the new name of the state.
172 * @param myController An instantiated Controller object. Use a <code>ControllerFactory</code>
173 * to instantiate the controller if you must use this constructor.
174 */
175 public Transition(String newState, Controller myController) {
176
177 //
178 //Precalculate the class name because it can take significant CPU time.
179 //
180 String className = myController.getClass().getName();
181 myState = newState;
182 setControllerObject(className);
183 setOwnerController(className);
184 setName(newState);
185
186 //
187 //Performance: get the hashmap because that way we don't invoke a state
188 //copy for each object.
189 //
190 Hashtable allstates = myController.getStates();
191 State theState = null;
192 if (allstates != null) {
193 theState = (State) allstates.get(newState);
194 }
195
196 if (theState == null) {
197 throw new IllegalArgumentException("No such state as '" +
198 newState + "' in controller '" +
199 className +
200 "'");
201 }
202
203 setLabel(theState.getDescription());
204 addParam(Controller.STATE_PARAM_KEY, newState);
205 } /* Transition(String, Controller) */
206
207 /**
208 * Convenience constructor to create an action with a label
209 * and a controller already set.
210 *
211 * @param newLabel Label for the new action
212 * @param newObject The name of the object this action referred to
213 */
214 public Transition(String newLabel, String newObject) {
215 super();
216 setName(StringUtil.replace(newLabel, " ", ""));
217 setLabel(newLabel);
218 setControllerObject(newObject);
219 } /* Transition(String, String) */
220
221 /**
222 * Convenience constructor to create an action with a label
223 * and a controller already set.
224 *
225 * @param newName Name of this Transition object
226 * @param newLabel Label for the new action
227 * @param newObject The name of the Controller object this action referred to
228 */
229 public Transition(String newName, String newLabel, String newObject) {
230 super();
231 setName(newName);
232 setLabel(newLabel);
233 setControllerObject(newObject);
234 } /* Transition(String, String, String) */
235
236 /**
237 * Convenience method to allow for one line of code to construct a transition.
238 *
239 * @param name The name of the transition
240 * @param label The label to use for the transition
241 * @param controllerClass The <code>Class</code> of the controller to use
242 * @param controllerState The name of the controller's state.
243 */
244 public Transition(String name,
245 String label,
246 Class controllerClass,
247 String controllerState) {
248 super();
249 setName(name);
250 setLabel(label);
251 setControllerObject(controllerClass);
252 setState(controllerState);
253 }
254
255 /**
256 * Convenience method to allow for one line of code to construct a transition.
257 * The (internal) name of the transition will be the state name.
258 *
259 * @param label The label to use for the transition
260 * @param controllerClass The <code>Class</code> of the controller to use
261 * @param controllerState The name of the controller's state.
262 */
263 public Transition(String label,
264 Class controllerClass,
265 String controllerState) {
266 super();
267 setName(controllerState);
268 setLabel(label);
269 setControllerObject(controllerClass);
270 setState(controllerState);
271 }
272
273 /**
274 * Adds a parameter to a transition. These parameters are meant to
275 * be eventually consumed by the target controller via the request.getParameter()
276 * method.
277 *
278 * @param paramCode The code name of the parameter
279 * @param paramValue The value for the paramter
280 */
281 public synchronized void addParam(String paramCode, String paramValue) {
282 clearCache();
283 if (paramCode.equals(Controller.STATE_PARAM_KEY)) {
284 setState(StringUtil.notNull(paramValue));
285
286 return;
287 }
288 if (paramCode.equals(Controller.CONTROLLER_PARAM_KEY)) {
289 setControllerObject(StringUtil.notNull(paramValue));
290
291 return;
292 }
293
294 params.put(paramCode, StringUtil.notNull(paramValue));
295 } /* addParam(String, String) */
296
297 private synchronized void clearCache() {
298 cacheParamStringWithController = null;
299 cacheParamStringSansController = null;
300 }
301
302 /**
303 * Sets the target state to transition to.
304 *
305 * @param newState java.lang.String
306 */
307 public synchronized void setState(String newState) {
308 clearCache();
309 myState = newState;
310 }
311
312 /**
313 * Retrieve the currently set state
314 *
315 * @return java.lang.String
316 */
317 public String getState() {
318 if (myState == null) {
319 return getParam(Controller.STATE_PARAM_KEY);
320 }
321
322 return myState;
323 }
324
325 /**
326 * Returns a copy of itself
327 *
328 * @return a cloned and instantiated <code>Transition</code> object.
329 * @throws CloneNotSupportedException as required by the method
330 * signature.
331 */
332 public Object clone()
333 throws CloneNotSupportedException {
334 Transition t;
335
336 synchronized (this) {
337 t = (Transition) super.clone();
338 t.params = (Hashtable) params.clone();
339 t.controllerObject = controllerObject;
340 t.myState = myState;
341 t.returnToSender = returnToSender;
342 }
343
344 return t;
345 }
346
347 /**
348 * Call this method when the state/controller being transitioned to should return control back
349 * to the calling state once it has 'completed' successfully. Th 'completion' point depends on
350 * whether the transition is to an external (new controller) or internal state. For external
351 * transitions, the completion point is once the Final state has run successfully. For internal
352 * transitions, the completion point is once the called state has run successfully.
353 * <p/>
354 * When this method is called with a non-null response parameter, this indicates the currently
355 * running state should be the return address. If this method is called with a null response
356 * parameter then this indicates that the return address should be determined at transition
357 * execution time. This is useful/necessary when a transition is instantiated outside the scope
358 * of a state execution. For example, when the controllerSecurityTransition value is set in
359 * the controller constructor, the return state is not known/active.
360 *
361 * @param response the ControllerResponse object
362 * @throws ControllerException upon error
363 */
364 public void enableReturnToSender(ControllerResponse response)
365 throws ControllerException {
366 returnToSender = true;
367 String returnToSender = null;
368 if (response != null) {
369 FastStringBuffer fsb = FastStringBuffer.getInstance();
370 try {
371 Transition t = response.getCurrentState().getReturnToSender();
372 returnToSender = t.toXML(fsb).toString();
373 } finally {
374 fsb.release();
375 fsb = null;
376 }
377
378 if (isExternalTransition(response.getControllerClass())) {
379 if (getParam(Controller.CTL_SUCC_TRAN) == null) { //don't overwrite if already set while in run()
380 addParam(Controller.CTL_SUCC_TRAN, returnToSender);
381 }
382 } else {
383 if (getParam(Controller.STATE_SUCC_TRAN) == null) { //don't overwrite if already set while in run()
384 addParam(Controller.STATE_SUCC_TRAN, returnToSender);
385 }
386 }
387 }
388 }
389
390 /**
391 * Returns True if the destination for this transition is a different
392 * controller from the one currently active.
393 *
394 * @param runningController the name of the controller we're currently in.
395 * Usually you would
396 * use <code>this.getClass().getName()</code> within your own controller as
397 * a parameter.
398 * @return true if this is a transition to a another controller.
399 */
400 public boolean isExternalTransition(String runningController) {
401 if (controllerObject == null || controllerObject.equals(runningController)) {
402 return false;
403 } else {
404 return true;
405 }
406 }
407
408 /**
409 * Return the name of the controller object that this Transition
410 * referred to. If this is null, then it refers to the same controller
411 * object (e.g. intra-controller transition)
412 *
413 * @return The class name of the controller object this action
414 * refers to.
415 */
416 public String getControllerObject() {
417 if (controllerObject == null) {
418 return getParam(Controller.CONTROLLER_PARAM_KEY);
419 }
420
421 return controllerObject;
422 } /* getControllerObject() */
423
424 /**
425 * Sets the controller that created this transition. If controllerObject
426 * equals owner controller, then no controller= parameter is generated.
427 *
428 * @return the owner controller
429 */
430 public String getOwnerController() {
431 return this.ownerObject;
432 }
433
434 /**
435 * Return the value for a specific parameter for this transition object.
436 *
437 * @param paramCode The code (name) of the parameter
438 * @return The value of the parameter as a string
439 */
440 public String getParam(String paramCode) {
441 return (String) params.get(paramCode);
442 } /* getParam(String) */
443
444 /**
445 * Return the hashtable of parameters for this transition object.
446 * These parameters are to be handed to the new controller
447 * when the action is called.
448 *
449 * @return A hashtable of name/value pairs for the parameters
450 */
451 public Hashtable getParams() {
452 return params;
453 } /* getParams() */
454
455 /**
456 * @param includeControllerParameter whether to include controller param or not.
457 * @return parameter string which includes all params added to trans, as well
458 * as state param. controller param added optionally
459 */
460 public synchronized String getParamString(boolean includeControllerParameter) {
461 if (includeControllerParameter && cacheParamStringWithController == null) {
462 FastStringBuffer paramString = FastStringBuffer.getInstance();
463 try {
464 if (controllerObject != null) {
465 paramString.append("controller=");
466 paramString.append(controllerObject);
467 }
468
469 addNonControllerParams(paramString);
470
471 cacheParamStringWithController = paramString.toString();
472 } finally {
473 paramString.release();
474 paramString = null;
475 }
476
477 }
478
479 if (!includeControllerParameter && cacheParamStringSansController == null) {
480 FastStringBuffer paramString = FastStringBuffer.getInstance();
481 try {
482 addNonControllerParams(paramString);
483
484 cacheParamStringSansController = paramString.toString();
485 } finally {
486 paramString.release();
487 paramString = null;
488 }
489
490 }
491
492
493 if (includeControllerParameter) {
494 return cacheParamStringWithController;
495 } else {
496 return cacheParamStringSansController;
497 }
498
499 }
500
501 private void addNonControllerParams(FastStringBuffer paramString) {
502 if (this.params != null) {
503 if (!this.params.isEmpty()) {
504 String oneKey = null;
505
506 for (Enumeration e = this.params.keys(); e.hasMoreElements();) {
507 if (paramString.length() != 0) {
508 paramString.append("&");
509 }
510 oneKey = (String) e.nextElement();
511
512 //Encode user's Transition parameters otherwise is ueer's parameters has '&' then
513 //it will mess up the addButtonParams() method when using Tokenizer.
514 FastStringBuffer fsb = FastStringBuffer.getInstance();
515 try {
516 fsb.append(oneKey);
517 fsb.append("=");
518 fsb.append(URLUTF8Encoder.encode((String) this.params.get(oneKey)));
519 paramString.append(fsb.toString());
520 } finally {
521 fsb.release();
522 fsb = null;
523 }
524
525 }
526 }
527 }
528
529 String stateString = StringUtil.notNull(getState());
530 if (stateString.length() != 0) {
531 if (paramString.length() != 0) {
532 paramString.append("&");
533 }
534 paramString.append("state=");
535 paramString.append(stateString);
536 }
537 }
538
539 /**
540 * Return a string of the current params This is NOT URL encoded string.
541 * Use either getUrl() OR java.net.URLEncoder() to do this job.
542 *
543 * @return java.lang.String
544 */
545 public String getParamString() {
546
547 return getParamString(true);
548
549 } /* getParamString() */
550
551 /**
552 * This method invokes a new controller by dispatching to it rather than
553 * calling it directly. This is required when transitioning to external
554 * states so that Struts can setup the controller form.
555 * <p/>
556 * The currently active controller request is passed to the new state to
557 * simulate a direct call to the state. The request is then picked up
558 * by the controller's perform method. Any exceptions raised by the
559 * target state will filter back here to be passed on. This approach
560 * was needed in order to simulate a direct call the the state while at
561 * the same time allowing Struts to setup the controller form.
562 *
563 * @param request the <code>ControllerRequest</code> Object
564 * @return an instantiated <code>ControllerResponse</code> object
565 */
566 protected ControllerResponse newStateDispatch(ControllerRequest request)
567 throws ControllerException,
568 NonHandleableException {
569 ServletControllerRequest servletRequest = (ServletControllerRequest) request;
570 HttpServletRequest httpRequest = (HttpServletRequest) servletRequest.getServletRequest();
571 HttpServletResponse httpResponse = (HttpServletResponse) servletRequest.getServletResponse();
572 //Blank state required to avoid picking up an old state param while really we want
573 //to transition to the initial/default state (ie state is not specified)
574 // ActionMapping mm = ConfigManager.getMapping(controllerObject, "");
575 ActionConfig ac = ConfigManager.getActionConfig("", controllerObject, "");
576
577 if (ac == null) {
578 throw new ControllerException("Cannot transition to controller: " +
579 controllerObject +
580 " controller not defined in Struts configuration");
581 }
582
583 String apath = ac.getPath();
584 FastStringBuffer newURL = FastStringBuffer.getInstance();
585 RequestDispatcher dispatcher = null;
586 try {
587 newURL.append(apath);
588 newURL.append(".do");
589 newURL.append("?controller=" + getControllerObject());
590
591 if (getState() != null) {
592 newURL.append("&state=" + getState());
593 }
594
595 httpRequest.setAttribute(ExpressoConstants.CONTROLLER_REQUEST_KEY, request); //Push our request onto the 'queue'
596
597 String urlValue = newURL.toString();
598 dispatcher = httpRequest.getRequestDispatcher(urlValue);
599
600 if (dispatcher == null) {
601 throw new ControllerException("Request dispatcher was null - cannot include URL '" +
602 urlValue + "'");
603 }
604 } finally {
605 newURL.release();
606 newURL = null;
607 }
608 try {
609 dispatcher.include(httpRequest, httpResponse);
610 } catch (Exception e) {
611 throw new ControllerException(e);
612 }
613
614 //Pop our request & response off the 'queue'
615 Exception newStateException = (Exception) httpRequest.getAttribute(ExpressoConstants.NEWSTATE_EXCEPTION_KEY);
616
617 if (newStateException != null) { //bubble up any exception
618 if (newStateException instanceof ControllerException) {
619 throw (ControllerException) newStateException;
620 } else {
621 if (newStateException instanceof NonHandleableException) {
622 throw (NonHandleableException) newStateException;
623 } else {
624 throw (RuntimeException) newStateException;
625 }
626 }
627 }
628
629 request = (ControllerRequest) httpRequest.getAttribute(ExpressoConstants.CONTROLLER_REQUEST_KEY);
630 httpRequest.removeAttribute(ExpressoConstants.CONTROLLER_REQUEST_KEY);
631
632 ControllerResponse response = (ControllerResponse) httpRequest.getAttribute(
633 ExpressoConstants.CONTROLLER_RESPONSE_KEY);
634 httpRequest.removeAttribute(ExpressoConstants.CONTROLLER_RESPONSE_KEY);
635
636 return response;
637 }
638
639 /**
640 * Returns True if the destination for this transition is the same as the
641 * currently active state. Avoid infinite loop.
642 *
643 * @param runningState the current state
644 * @param runningController the current controller
645 * @return true if we're recusing to the same state and controller as we're
646 * already in.
647 */
648 public boolean isRecursiveTransition(String runningState,
649 String runningController) {
650 String targetController = StringUtil.notNull(controllerObject);
651 String targetState = StringUtil.notNull(myState);
652
653 if (targetController.equals(runningController) &&
654 targetState.equals(runningState)) {
655 return true;
656 }
657
658 return false;
659 }
660
661 /**
662 * Return the return-to-sender flag.
663 *
664 * @return <code>boolean</code>
665 */
666 public boolean isReturnToSenderEnabled() {
667 return returnToSender;
668 }
669
670 /**
671 * Returns a hidden form field string that is safe in either the GET or POST
672 * case.
673 * <p/>
674 * Creation date: (1/10/01 11:24:00 AM)
675 * author: Adam Rossi, PlatinumSolutions
676 *
677 * @return java.lang.String
678 */
679 public String getHTMLParamString() {
680 String paramString = this.getParamString();
681 paramString = URLUTF8Encoder.encode(paramString);
682
683 //
684 //Guess at a memory allocation to reduce re-allocation/copy
685 //Alloc Adjust was guessed at based upon watching string lengths and
686 //guessing a reasonable maximum size that would fit most applciations.
687 //
688
689 FastStringBuffer sb = FastStringBuffer.getInstance();
690 String returnValue = null;
691 try {
692 sb.append("<input type=\"HIDDEN\" name=\"");
693 sb.append(this.getName());
694 sb.append("_params");
695 sb.append("\" value=\"");
696 sb.append(paramString);
697 sb.append("\">");
698 sb.append("<input type=\"HIDDEN\" name=\"");
699 sb.append(this.getName());
700 sb.append("_encoding");
701 sb.append("\" value=\"u\">");
702 returnValue = sb.toString();
703 } finally {
704 sb.release();
705 sb = null;
706 }
707 return returnValue;
708 } /* getHTMLParamStriong() */
709
710 /**
711 * Set the Controller that this action referrs to
712 *
713 * @param newObject Name of the Controller object that this Transition refers to
714 */
715 public synchronized void setControllerObject(String newObject) {
716 clearCache();
717 controllerObject = newObject;
718 } /* setControllerObject(String) */
719
720 /**
721 * Mor Typesafe way of setting the controller object. Any typos will be caught
722 * at compile time. Example usage:
723 * <code><pre>
724 * Transition t = new Transition();
725 * t.setControllerObject(com.jcorporate.expresso.services.Status.class);
726 * </pre></code>
727 *
728 * @param c the class of the controller object to add.
729 */
730 public synchronized void setControllerObject(Class c) {
731 clearCache();
732 if (c != null) {
733 setControllerObject(c.getName());
734 } else {
735 setControllerObject((String) null);
736 }
737 }
738
739 /**
740 * Sets the controller that created this transition. If controllerObject
741 * equals owner controller, then no controller= parameter is generated.
742 *
743 * @param newController the classname of the new controller
744 */
745 public synchronized void setOwnerController(String newController) {
746 clearCache();
747 ownerObject = newController;
748 }
749
750 /**
751 * Set this transition's parameters to the passed in collection.
752 *
753 * @param newParams the new parameters in bulk
754 */
755 public synchronized void setParams(Hashtable newParams) {
756 clearCache();
757 params = new Hashtable(newParams);
758 }
759
760 /**
761 * This method will take the request parameters that were passed to this state
762 * and will copy them into this transition's parameters. These parameters
763 * will then be used when this state is reinvoked (return-to-sender) in order
764 * to re-establish the parameter context.
765 *
766 * @param newReturnToSenderRequest The <code>ControllerRequest</code> object
767 */
768 public synchronized void setReturnToSenderParms(ControllerRequest newReturnToSenderRequest) {
769 clearCache();
770 String oneParamName = null;
771 Object oneParamValue = null;
772 Hashtable params = newReturnToSenderRequest.getParameters();
773 Hashtable newParams = new Hashtable();
774
775 //Copy all parameters except hidden parameters xxx_params and xxx_encoding.
776 //The xxx_params have already been unencoded and untokenized into separate parameters.
777 //Passing the xxx_params caused problems when the transition was encoded again and sent to HTML.
778 //When the double-encoded parameter comes back with the next user request it is never unencoded.
779 //This ends up causing problems with the fromXML/toXML methods in Transition.
780 for (Enumeration e = params.keys(); e.hasMoreElements();) {
781 oneParamName = (String) e.nextElement();
782
783 if (!oneParamName.endsWith("_params") &&
784 !oneParamName.endsWith("_encoding") &&
785 !oneParamName.equals(Controller.STATE_PARAM_KEY) &&
786 !oneParamName.equals(Controller.CONTROLLER_PARAM_KEY)) {
787 oneParamValue = params.get(oneParamName);
788 newParams.put(oneParamName, oneParamValue);
789 }
790 }
791
792 setParams(newParams);
793 }
794
795 /**
796 * Convert the object to an xml fragment.
797 *
798 * @param stream an instantiated FastStringBuffer to which we append to.
799 * @return a FastStringBuffer object
800 */
801 public FastStringBuffer toXML(FastStringBuffer stream) {
802 stream.append("<transition");
803
804 if (this.getName() != null && this.getName().length() > 0) {
805 stream.append(" name=\"");
806 stream.append(StringUtil.xmlEscape(getName()));
807 stream.append("\"");
808 }
809
810 String controllerName = this.getControllerObject();
811
812 if (controllerName != null && controllerName.length() > 0) {
813 stream.append(" controller=\"");
814 stream.append(StringUtil.xmlEscape(controllerName));
815 stream.append("\"");
816 }
817
818 String stateName = this.getState();
819
820 if (stateName != null && stateName.length() > 0) {
821 stream.append(" state=\"");
822 stream.append(StringUtil.xmlEscape(stateName));
823 stream.append("\"");
824 }
825
826 stream.append(">\n");
827
828 Hashtable params = this.getParams();
829 String oneKey = null;
830
831 if (!params.isEmpty()) {
832 stream.append("\t<transition-parameters>\n");
833
834 for (Enumeration ap = params.keys(); ap.hasMoreElements();) {
835 oneKey = (String) ap.nextElement();
836 stream.append("\t\t<transition-param name=\"");
837 stream.append(StringUtil.xmlEscape(oneKey));
838 stream.append("\" value=\"");
839 stream.append(StringUtil.xmlEscape((String) params.get(oneKey)));
840 stream.append("\"/>\n");
841 }
842
843 stream.append("\t</transition-parameters>\n");
844 }
845
846 stream = super.toXML(stream);
847 stream.append("</transition>\n");
848
849 return stream;
850 }
851
852 /**
853 * Return a Transition based upon the String based xml fragment
854 *
855 * @param newTransition an xml fragment
856 * @return an instantiated Transition object
857 * @throws ControllerException upon error
858 */
859 public static Transition fromXML(String newTransition)
860 throws ControllerException {
861 StringDOMParser parser = new StringDOMParser();
862 Document d = parser.parseString(newTransition);
863
864 if (d == null) {
865 throw new ControllerException("Transition returned mal-formed XML document");
866 } else {
867 parser.dumpDOM(d);
868 }
869
870 return (Transition) fromXML(d);
871 }
872
873 /**
874 * Return a controller element based upon the xml fragment
875 *
876 * @param n a DOM Node object
877 * @return an instantiated ControllerElement
878 * @throws ControllerException upon error
879 */
880 public static ControllerElement fromXML(Node n)
881 throws ControllerException {
882
883 //If we're at the root node, then it'll be doc instead of input.
884 if (n.getNodeName().equals("#document")) {
885 return fromXML(n.getChildNodes().item(0));
886 }
887 if (!n.getNodeName().equals("transition")) {
888 return null;
889 }
890
891 Transition t = new Transition();
892
893 //Get node attributes
894 NamedNodeMap transitionAttributes = n.getAttributes();
895 Node attributeNode = transitionAttributes.getNamedItem("name");
896
897 if (attributeNode != null) {
898 String value = attributeNode.getNodeValue();
899
900 if (value != null) {
901 t.setName(value);
902 }
903 }
904
905 attributeNode = transitionAttributes.getNamedItem(Controller.CONTROLLER_PARAM_KEY);
906
907 if (attributeNode != null) {
908 String value = attributeNode.getNodeValue();
909
910 if (value != null) {
911 t.setControllerObject(value);
912 }
913 }
914
915 NodeList nl = n.getChildNodes();
916
917 for (int i = 0; i < nl.getLength(); i++) {
918 Node oneChild = nl.item(i);
919 String nodeName = oneChild.getNodeName();
920
921 if (nodeName.equals("transition-parameters")) {
922 NodeList parameters = oneChild.getChildNodes();
923
924 for (int j = 0; j < parameters.getLength(); j++) {
925 if (parameters.item(j).getNodeName().equals("transition-param")) {
926 NamedNodeMap paramAttributes = parameters.item(j).getAttributes();
927 Node paramAttribute = paramAttributes.getNamedItem("name");
928 String name = null;
929 String value = null;
930
931 if (paramAttribute != null) {
932 name = paramAttribute.getNodeValue();
933 }
934
935 paramAttribute = paramAttributes.getNamedItem("value");
936
937 if (paramAttribute != null) {
938 value = paramAttribute.getNodeValue();
939 }
940 if (name != null && value != null) {
941 t.addParam(name, value);
942 }
943 }
944 }
945 } else if (nodeName.equals("controller-element")) {
946 t = (Transition) ControllerElement.fromXML(oneChild, t);
947 }
948 }
949
950 return t;
951 }
952
953 /**
954 * Internal use for retrieving the URL that this transition points to.
955 *
956 * @param resolveControllerReference should the controller be resolved to
957 * a mapping, or should it just be the request path with a controller equals
958 * parameter. True if you want the URL mapped to a .do mapping.
959 * @return java.lang.String
960 * @throws ControllerException if there is an error or there is no controller
961 * response object available to help resoolve the reference.
962 */
963 public String getTheUrl(boolean resolveControllerReference) throws ControllerException {
964 String returnValue = null;
965 FastStringBuffer fsb = FastStringBuffer.getInstance();
966 try {
967 String paramString = "";
968
969 if (resolveControllerReference
970 && this.getControllerObject() != null
971 && this.getControllerObject().length() > 0) {
972
973 // try mapping first
974 try {
975 fsb.append(getMapping());
976 } catch (Exception e) {
977 // try response
978 ControllerResponse myResponse = getControllerResponse();
979
980 if (myResponse == null) {
981 throw new ControllerException(
982 "No controller object, nor controller response object available - cannot build URL");
983 }
984
985 fsb.clear();
986 fsb.append(myResponse.getRequestPath());
987 }
988
989 paramString = getParamString(false);
990
991 } else {
992
993 // try response first
994 ControllerResponse myResponse = getControllerResponse();
995
996 if (myResponse == null) {
997 try {
998 fsb.append(getMapping());
999 } catch (Exception e) {
1000 throw new ControllerException(
1001 "No controller param nor ControllerResponse available - cannot build URL");
1002 }
1003 } else {
1004 fsb.append(myResponse.getRequestPath());
1005 }
1006
1007 paramString = getParamString(true);
1008 }
1009
1010 if (paramString != null && paramString.length() > 0) {
1011 fsb.append("?");
1012 fsb.append(paramString);
1013 }
1014
1015 returnValue = fsb.toString();
1016 if (log.isDebugEnabled()) {
1017 log.debug("transition redirects to: " + returnValue);
1018 }
1019 } finally {
1020 fsb.release();
1021 fsb = null;
1022 }
1023 return returnValue;
1024 }
1025
1026 /**
1027 * Returns a URL reference for this transition. The URL does NOT include
1028 * the context path.
1029 *
1030 * @return java.lang.String
1031 * @throws ControllerException upon error
1032 */
1033 public String getUrl()
1034 throws ControllerException {
1035 return getTheUrl(true);
1036 }
1037
1038 /**
1039 * Similar to getURL but also includes the context path. Useful for working
1040 * with JSTL cout expressions inside an <a> link.
1041 * <p>If the ControllerResponse has been set and running in a servlet environment,
1042 * then this function also encodes the resulting URL with suitable session id's
1043 * if necessary too</p>
1044 * This URL is optimized, so it includes a Controller.CONTROLLER_PARAM_KEY param only if
1045 * the destination controller is different than the controller of the ControllerResponse (if
1046 * the response is known).
1047 *
1048 * @return java.lang.String
1049 * @throws ControllerException upon error.
1050 * @see #getTheUrl
1051 */
1052 public String getFullUrl() throws ControllerException {
1053 HttpServletResponse servResponse = this.getServletResponse();
1054 if (servResponse != null) {
1055 return servResponse.encodeURL(ConfigManager.getContextPath() + getTheUrl(true));
1056 } else {
1057 return ConfigManager.getContextPath() + getTheUrl(true);
1058 }
1059 }
1060
1061 /**
1062 * This function returns the mapping of the Struts action (including the
1063 * .do part) but without a context prepended to the mapping.
1064 *
1065 * @return java.lang.String
1066 */
1067 public String getMapping() throws ControllerException {
1068 ActionConfig actionConfig = ConfigManager.getActionConfig("",
1069 this.getControllerObject(), this.getState());
1070 if (actionConfig == null) {
1071 throw new ControllerException("Unable to locate action mapping for: "
1072 + this.getControllerObject()
1073 + " and state " + this.getState());
1074 }
1075 return actionConfig.getPath() + ".do";
1076 }
1077
1078
1079 /**
1080 * Run this transition - e.g. transition to the new state of the
1081 * specified controller object immediately, setting the specified
1082 * response to the response of this new controller/state, discarding
1083 * any previous response
1084 *
1085 * @param req The ControllerRequest object handed to you by the framework
1086 * @param res the ControllerResponse object handed to you by the framework
1087 * @return the ControllerResponse Object from the called controller
1088 * @throws ControllerException upon error
1089 * @throws NonHandleableException upon fatal error
1090 */
1091 public ControllerResponse transition(ControllerRequest req,
1092 ControllerResponse res)
1093 throws ControllerException,
1094 NonHandleableException {
1095 return transition(req, res, true);
1096 }
1097
1098 /**
1099 * Transition to a new controller and state by issuing a <code>Redirect</code>
1100 * request to the browser. This can only be used in a Servlet environment,
1101 * and is mainly useful when you want the URL for the browser to change after
1102 * a request, so for example, after making an online purchase, you display the
1103 * invoice, but if the user hit's refresh, you don't want the processing
1104 * to occur again.
1105 *
1106 * @param request The ControllerRequest object handed to you by the framework
1107 * @param response the ControllerResponse object handed to you by the framework
1108 */
1109 public void redirectTransition(ControllerRequest request,
1110 ControllerResponse response) throws ControllerException {
1111 try {
1112
1113 if (isRecursiveTransition(response.getCurrentState().getName(),
1114 response.getControllerClass())) {
1115 throw new ControllerException("State cannot transition to itself.");
1116 }
1117
1118 if (this.getControllerObject() == null) {
1119 throw new ControllerException("Transition.redirectTransition(): "
1120 + " controller object parameter must be set before calling this function");
1121 }
1122
1123 if (!(request instanceof ServletControllerRequest)) {
1124 log.error("Cannot redirect transition in a non-servlet environment. " +
1125 "Transitioning normally instead");
1126 try {
1127 this.transition(request, response);
1128 } catch (NonHandleableException ex) {
1129 log.error("Non Handleable Exception during transition", ex);
1130 throw new ControllerException(ex);
1131 }
1132 return;
1133 }
1134
1135 ServletControllerRequest scr = (ServletControllerRequest) request;
1136 HttpServletResponse hserv = (HttpServletResponse) scr.getServletResponse();
1137 this.setControllerResponse(response);
1138
1139
1140// ActionMapping mapping = ConfigManager.getMapping(this.getControllerObject(),
1141// this.getState());
1142 ActionConfig actionConfig = ConfigManager.getActionConfig("",
1143 this.getControllerObject(), this.getState());
1144 response.setRequestPath(actionConfig.getPath());
1145
1146 hserv.sendRedirect(((HttpServletRequest) scr.getServletRequest()).getContextPath() + this.getTheUrl(true));
1147 //WE have to set custom response to true, otherwise the framework
1148 //will attempt to send more html once the redirect is done, which
1149 //will cause exceptions
1150 response.setCustomResponse(true);
1151 } catch (IOException ex) {
1152 log.error("IO Error sending HTTP Redirect. Connection was broken.", ex);
1153 }
1154 }
1155
1156
1157 /**
1158 * Run this transition - e.g. transition to the new state of the
1159 * specified controller object immediately, setting the specified
1160 * response to the response of this new controller/state, discarding
1161 * any previous response (if "clear" is specified)
1162 * <p/>
1163 * NB: all parameters in the original request are discarded, except
1164 * those that are explicit params added to this Transition.
1165 * Therefore, if you have a param X in the original state,
1166 * and you need it in the upcoming
1167 * state, use addParam() to preserve it, OR use the ControllerResponse.setFormCache()
1168 * to save them before the transition(),
1169 *
1170 * @param req The ControllerRequest object handed to you by the framework
1171 * @param res the ControllerResponse object handed to you by the framework
1172 * @param clear True clears the response object before adding the outputs
1173 * from the called controller?
1174 * @return the ControllerResponse Object from the called controller
1175 * @throws ControllerException upon error
1176 * @throws NonHandleableException upon fatal error
1177 * @see ControllerResponse#setFormCache
1178 * <p/>
1179 * and response.getFormCache(paramName) to retrieve them
1180 * @see ControllerResponse#getFormCache(String)
1181 */
1182 public ControllerResponse transition(ControllerRequest req,
1183 ControllerResponse res, boolean clear)
1184 throws ControllerException,
1185 NonHandleableException {
1186 if (isRecursiveTransition(res.getCurrentState().getName(),
1187 res.getControllerClass())) {
1188 throw new ControllerException("State cannot transition to itself.");
1189 }
1190
1191 boolean externalTransition = isExternalTransition(res.getControllerClass());
1192 //Clear out parameters - Avoids junk building up (can cause infinite loops)
1193 req.setParameters(null);
1194
1195 //Pass the Transition object that will be executed by the called state when it needs to return.
1196 if (returnToSender) {
1197 enableReturnToSender(res);
1198 }
1199
1200 String oneParamKey = null;
1201 String oneParamValue = null;
1202
1203 for (Enumeration ep = params.keys(); ep.hasMoreElements();) {
1204 oneParamKey = (String) ep.nextElement();
1205 oneParamValue = (String) params.get(oneParamKey);
1206
1207 if (oneParamValue != null) {
1208 req.setParameter(oneParamKey, oneParamValue);
1209 }
1210 }
1211
1212 ControllerRequest newRequest = (ControllerRequest) req.clone();
1213 newRequest.setParameters(this.params);
1214 newRequest.setParameter(Controller.STATE_PARAM_KEY, StringUtil.notNull(this.getState()));
1215 newRequest.setParameter(Controller.CONTROLLER_PARAM_KEY, this.getControllerObject());
1216
1217 ControllerResponse newResponse = null;
1218
1219 if ((externalTransition) &&
1220 (req instanceof ServletControllerRequest)) {
1221 newResponse = newStateDispatch(req);
1222 } else {
1223 Controller c = null;
1224
1225 if (this.getControllerObject() == null) {
1226 c = ConfigManager.getControllerFactory().getController(newRequest);
1227 } else {
1228 c = ConfigManager.getControllerFactory().getController(this.getControllerObject());
1229 }
1230
1231 newResponse = c.newState(getState(), newRequest);
1232 }
1233 if (clear) {
1234 res.clearOutputCache();
1235 res.clearInputCache();
1236 res.clearTransitionCache();
1237 res.clearBlockCache();
1238 res.clearAttributes();
1239 }
1240
1241 res.setDBName(newResponse.getDBName());
1242 res.setCustomResponse(newResponse.isCustomResponse());
1243 res.setCurrentState(newResponse.getCurrentState());
1244 res.setStyle(newResponse.getStyle());
1245 res.setControllerClass(newResponse.getControllerClass());
1246 res.setSchemaStack(newResponse.getSchemaStack());
1247 res.setTitle(newResponse.getTitleKey());
1248
1249 Block oneBlock = null;
1250 Vector newBlocks = newResponse.getBlocks();
1251
1252 if (newBlocks != null) {
1253 for (Enumeration eb = newBlocks.elements(); eb.hasMoreElements();) {
1254 oneBlock = (Block) eb.nextElement();
1255 oneBlock.setControllerResponse(res);
1256 res.addBlock(oneBlock);
1257 }
1258 }
1259
1260 Output oneOutput = null;
1261 Vector newOutputs = newResponse.getOutputs();
1262
1263 if (newOutputs != null) {
1264 for (Enumeration eo = newOutputs.elements(); eo.hasMoreElements();) {
1265 oneOutput = (Output) eo.nextElement();
1266 oneOutput.setControllerResponse(res);
1267 res.addOutput(oneOutput);
1268 }
1269 }
1270
1271 Input oneInput = null;
1272 Vector newInputs = newResponse.getInputs();
1273
1274 if (newInputs != null) {
1275 for (Enumeration ei = newInputs.elements(); ei.hasMoreElements();) {
1276 oneInput = (Input) ei.nextElement();
1277 oneInput.setControllerResponse(res);
1278 res.addInput(oneInput);
1279 }
1280 }
1281
1282 Transition oneTransition = null;
1283 Vector newTransitions = newResponse.getTransitions();
1284
1285 if (newTransitions != null) {
1286 for (Enumeration et = newTransitions.elements();
1287 et.hasMoreElements();) {
1288 oneTransition = (Transition) et.nextElement();
1289 oneTransition.setControllerResponse(res);
1290 res.addTransition(oneTransition);
1291 }
1292 }
1293 if (StringUtil.notNull(res.getControllerClass()).length() == 0) {
1294 res.setControllerClass(getControllerObject());
1295 }
1296
1297 Map attributes = newResponse.getAttributes();
1298 if (attributes != null) {
1299 res.setAttributes(attributes);
1300 }
1301
1302 return res;
1303 }
1304
1305 /**
1306 * Gets an underlying ServletResponse if it has been set either through
1307 * setting the controller response or manually
1308 *
1309 * @return HttpServletResponse or NULL if not being used.
1310 */
1311 public HttpServletResponse getServletResponse() {
1312 return servletResponse;
1313 }
1314
1315 /**
1316 * Low level, sets the servlet response. Normally you won't use this function
1317 * except under specialty situations where you are using a Transition more
1318 * as a URL generator
1319 *
1320 * @param servletResponse a servlet response object for encoding URLs
1321 */
1322 public void setServletResponse(HttpServletResponse servletResponse) {
1323 this.servletResponse = servletResponse;
1324 }
1325
1326 /**
1327 * Override of the normal setControllerResponse so that the HttpServletResponse
1328 * is also set for this particular transition.
1329 *
1330 * @param newResponse the controllerResponse to set.
1331 */
1332 public synchronized void setControllerResponse(ControllerResponse newResponse) {
1333 super.setControllerResponse(newResponse);
1334 if (newResponse == null) {
1335 log.warn("Null 'newReponse'");
1336 return;
1337 }
1338 if (newResponse.getRequest() != null && (newResponse.getRequest() instanceof ServletControllerRequest)) {
1339
1340 this.setServletResponse(
1341 (HttpServletResponse) ((ServletControllerRequest) newResponse.getRequest()).getServletResponse());
1342 }
1343 }
1344} /* Transition */