View Javadoc

1   /*
2    *  Copyright 1999-2004 The Apache Software Foundation.
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *  http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.apache.struts.flow;
17  
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.util.Iterator;
21  import java.util.LinkedList;
22  import java.util.List;
23  import java.util.Map;
24  
25  import javax.servlet.ServletException;
26  import javax.servlet.http.HttpServletRequest;
27  import javax.servlet.http.HttpServletResponse;
28  
29  import org.apache.commons.chain.web.servlet.ServletWebContext;
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.struts.action.Action;
33  import org.apache.struts.action.ActionErrors;
34  import org.apache.struts.action.ActionForm;
35  import org.apache.struts.action.ActionForward;
36  import org.apache.struts.action.ActionMapping;
37  import org.apache.struts.action.ActionMessages;
38  import org.apache.struts.flow.core.javascript.ConversionHelper;
39  import org.apache.struts.flow.core.Interpreter;
40  import org.apache.struts.flow.json.JSONArray;
41  import org.apache.struts.flow.json.JSONSerializer;
42  import org.mozilla.javascript.Scriptable;
43  
44  /***  Executes scripts and continuations */
45  public class FlowAction extends Action {
46  
47      private static final Log log = LogFactory.getLog(FlowAction.class);
48      
49      /***
50       *  Gets the interpreter when no script is specified
51       *
52       *@return    The interpreter value
53       */
54      protected Interpreter getInterpreter(String prefix) {
55          return (Interpreter) servlet.getServletContext().getAttribute(FlowPlugIn.INTERPRETER_KEY+"/"+prefix);
56      }
57      
58      /***
59       *  Gets the interpreter for the requested script
60       *
61       *@param script The script to load the interpeter for
62       *@return    The interpreter value
63       */
64      protected Interpreter getInterpreter(String prefix, String script) {
65          Map map = (Map)servlet.getServletContext().getAttribute(FlowPlugIn.INTERPRETERS_KEY+"/"+prefix);
66          return (Interpreter) map.get(script);
67      }
68  
69  
70      /***
71       *  Handle by either starting a new Control Flow or continuing an existing
72       *  one. The logic is:<br />
73       *  - If request contains contid, then continue existing flow.<br />
74       *  - Else if the action mapping parameter attribute has the contid, then
75       *  continue the existing flow.<br />
76       *  - Else start a new flow.<br />
77       *  The name of the function to execute for a new flow should be specified
78       *  in the <code>function</code> property of the custom action mapping
79       *  class, <code>FlowMapping</code>
80       *
81       *@param  request        the request send by the client to the server
82       *@param  response       the response send by the server to the client
83       *@param  mapping        the action mapping
84       *@param  form           the action form
85       *@return                the action forward
86       *@exception  Exception  If something goes wrong
87       */
88      public ActionForward execute(ActionMapping mapping,
89              ActionForm form,
90              HttpServletRequest request,
91              HttpServletResponse response)
92               throws Exception {
93  
94          FlowMapping flowMapping = null;
95          if (mapping instanceof FlowMapping) {
96              flowMapping = (FlowMapping) mapping;
97          } else {
98              throw new ServletException("FlowMapping implementation of ActionMapping required");
99          }
100         // Create and populate a Context for this request
101         ServletWebContext context = new ServletWebContext();
102         context.initialize(servlet.getServletContext(), request, response);
103         context.put(Constants.ACTION_SERVLET_KEY,
104                 this.servlet);
105         context.put(Constants.ACTION_CONFIG_KEY,
106                 mapping);
107         context.put(Constants.ACTION_FORM_KEY,
108                 form);
109         context.put(Constants.MESSAGE_RESOURCES_KEY,
110                 getResources(request));
111         context.put(Constants.ACTION_KEY, this);
112 
113         String contid = request.getParameter("contid");
114        
115         Interpreter interp = null;
116         if (flowMapping.getScript() == null) {
117             interp = getInterpreter(flowMapping.getModuleConfig().getPrefix());
118         } else {
119             interp = getInterpreter(flowMapping.getModuleConfig().getPrefix(), flowMapping.getScript());
120         }
121 
122         // A FlowCall means the request came from client-side javascript that
123         // expects the return type to be JSON
124         boolean isFlowCall = (request.getParameter("FlowCall") != null);
125         String func = null;
126         if (isFlowCall) {
127             func = request.getParameter("FlowCall");
128         } else {
129             func = flowMapping.getFunction();
130             if (func == null || func.length() == 0) {
131                 func = mapping.getParameter();
132             }
133         }
134         
135  
136         if (contid == null || contid.length() == 0) {
137 
138             // --- start a new flow
139 
140             List args = new LinkedList();
141 
142             if (func == null || func.length() == 0) {
143                 throw new ServletException("You must specify a function name to call");
144             } else {
145                 
146                 // validate JSON in the body of a flowcall
147                 if (isFlowCall) {
148                     StringBuffer sb = new StringBuffer();
149                     char[] buffer = new char[1024];
150                     int len = 0;
151                     BufferedReader reader = request.getReader();
152                     while ((len = reader.read(buffer)) > 0 ) {
153                         sb.append(buffer, 0, len);
154                     }
155                     String json = sb.toString();
156                     if (log.isDebugEnabled()) {
157                         log.debug("processing json:"+json);
158                     }
159                     if (isValidJSON(json)) {
160                         context.put("json", json);
161                     }
162                     
163                 }
164                 
165                 // call control script function
166                 Object ret = interp.callFunction(func, args, context);
167 
168                 // retrieve page, continuation ID, and attributes from chain context
169                 String page = (String) ConversionHelper.jsobjectToObject(context.get(Constants.FORWARD_NAME_KEY));
170                 contid = (String) context.get(Constants.CONTINUATION_ID_KEY);
171                 Scriptable bizdata = (Scriptable) context.get(Constants.BIZ_DATA_KEY);
172                 Map atts = null;
173                 if (bizdata != null) {
174                     atts = ConversionHelper.jsobjectToMap(bizdata);
175                 }  else if (ret != null && ret instanceof Scriptable) {
176                     atts = ConversionHelper.jsobjectToMap((Scriptable) ret);
177                 } 
178                 
179                 // if a flowcall, return pure json 
180                 if (isFlowCall) {
181                     String json = new JSONSerializer().serialize(atts);
182                     if (log.isDebugEnabled()) {
183                         log.debug("returning json: "+json);
184                     }
185                     response.getWriter().write(json);
186                     response.getWriter().flush();
187                     response.getWriter().close();
188                     return null;
189                 } else {
190                     return dispatchToPage(request, response, mapping, page, contid, atts);
191                 }
192             }
193         } else {
194             // --- continue an existing flow
195 
196             // kick off continuation
197             context.put("id", "5");
198             
199             interp.handleContinuation(
200                     request.getParameter("contid"), new LinkedList(), context);
201 
202             // retrieve page, continuation ID, and attributes from chain context
203             String page = (String) context.get(Constants.FORWARD_NAME_KEY);
204             contid = (String) context.get(Constants.CONTINUATION_ID_KEY);
205             Scriptable bizdata = (Scriptable) context.get(Constants.BIZ_DATA_KEY);
206             Map atts = null;
207             if (bizdata != null) {
208                 atts = ConversionHelper.jsobjectToMap(bizdata);
209             }    
210             return dispatchToPage(request, response, mapping, page, contid, atts);
211         }
212     }
213 
214 
215     /***
216      *  Add continuation ID and attributes to request scope, dispatch to page.
217      *
218      *@param  request            The request
219      *@param  response           The response
220      *@param  page               The action forward name
221      *@param  contid             Continuation ID to be set in request.
222      *@param  atts               Attributes to be set in request.
223      *@param  mapping            The action mapping
224      *@return
225      *@throws  ServletException
226      *@throws  IOException
227      */
228     private ActionForward dispatchToPage(
229             HttpServletRequest request, HttpServletResponse response, ActionMapping mapping,
230             String page, String contid, Map atts)
231              throws ServletException, IOException {
232 
233         // Probably only need to process if the response hasn't already been committed.  This
234         // should let flow code be able to completely handle a request if desired.
235         if (!response.isCommitted()) {
236             request.setAttribute("contid", contid);
237 
238             if (atts != null) {
239                 Iterator attkeys = atts.keySet().iterator();
240                 while (attkeys.hasNext()) {
241                     String attkey = (String) attkeys.next();
242                     request.setAttribute(attkey, ConversionHelper.jsobjectToObject(atts.get(attkey)));
243                 }
244             }    
245 
246             ActionForward af = mapping.findForward(page);
247             if (af == null) {
248                 af = new ActionForward(page);
249             }
250             return af;
251         }   
252         return null;
253     }
254     
255     private boolean isValidJSON(String val) {
256         try {
257             if (val != null && val.length() > 1) {
258                 JSONArray obj = new JSONArray(val);
259                 if (log.isDebugEnabled()) {
260                     log.debug("Valid JSON");
261                 }
262                 return true;
263             } else {
264                 if (log.isDebugEnabled()) {
265                     log.debug("No JSON detected");
266                 }
267             }
268         } catch (Exception ex) {
269             log.warn("Invalid JSON object", ex);
270         }
271         return false;
272     }
273         
274 
275 
276     // These methods seem necessary as some scripting engines are not able to
277     // access Action's protected methods.
278 
279     /***
280      *  Saves a token
281      *
282      *@param  req  The request object
283      */
284     public void saveToken(HttpServletRequest req) {
285         super.saveToken(req);
286     }
287 
288 
289     /***
290      *  Checks to see if the request is cancelled
291      *
292      *@param  req  The request object
293      *@return      True if cancelled
294      */
295     public boolean isCancelled(HttpServletRequest req) {
296         return super.isCancelled(req);
297     }
298 
299 
300     /***
301      *  Checks to see if the token is valid
302      *
303      *@param  req  The request object
304      *@return      True if valid
305      */
306     public boolean isTokenValid(HttpServletRequest req) {
307         return super.isTokenValid(req);
308     }
309 
310 
311     /***
312      *  Resets the token
313      *
314      *@param  req  The request object
315      */
316     public void resetToken(HttpServletRequest req) {
317         super.resetToken(req);
318     }
319 
320 
321     /***
322      *  Saves the messages to the request
323      *
324      *@param  req  The request object
325      *@param  mes  The action messages
326      */
327     public void saveMessages(HttpServletRequest req, ActionMessages mes) {
328         super.saveMessages(req, mes);
329     }
330 
331 
332     /***
333      *  Saves the errors to the request
334      *
335      *@param  req   The request object
336      *@param  errs  The action errors
337      */
338     public void saveErrors(HttpServletRequest req, ActionErrors errs) {
339         super.saveErrors(req, errs);
340     }
341 
342 }
343