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.core.javascript.fom;
17  
18  import java.awt.Dimension;
19  import java.awt.Toolkit;
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStreamReader;
23  import java.io.OutputStream;
24  import java.io.PushbackInputStream;
25  import java.io.Reader;
26  import java.util.ArrayList;
27  import java.util.*;
28  import java.util.Map;
29  
30  import org.apache.struts.flow.core.Logger;
31  import org.apache.struts.flow.core.Factory;
32  
33  import org.apache.commons.chain.web.WebContext;
34  import java.lang.reflect.InvocationTargetException;
35  //import org.apache.avalon.framework.activity.Initializable;
36  //import org.apache.avalon.framework.configuration.Configurable;
37  //import org.apache.avalon.framework.configuration.Configuration;
38  //import org.apache.avalon.framework.configuration.ConfigurationException;
39  //mport org.apache.avalon.framework.service.ServiceManager;
40  //mport org.apache.flow.ResourceNotFoundException;
41  //mport org.apache.flow.components.ContextHelper;
42  import org.apache.struts.flow.core.CompilingInterpreter;
43  import org.apache.struts.flow.core.Interpreter;
44  import org.apache.struts.flow.core.InvalidContinuationException;
45  import org.apache.struts.flow.core.*;
46  import org.apache.struts.flow.core.javascript.JSErrorReporter;
47  import org.apache.struts.flow.core.javascript.LocationTrackingDebugger;
48  //import org.apache.struts.flow.core.javascript.ScriptablePointerFactory;
49  //import org.apache.struts.flow.core.javascript.ScriptablePropertyHandler;
50  //import org.apache.flow.environment.ObjectModelHelper;
51  //import org.apache.flow.environment.Redirector;
52  //import org.apache.flow.environment.Request;
53  //import org.apache.flow.environment.Session;
54  //import org.apache.commons.jxpath.JXPathIntrospector;
55  //import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
56  import org.apache.struts.flow.core.source.Source;
57  //import org.apache.regexp.RE;
58  //import org.apache.regexp.RECompiler;
59  //import org.apache.regexp.REProgram;
60  import org.mozilla.javascript.Context;
61  import org.mozilla.javascript.EcmaError;
62  import org.mozilla.javascript.Function;
63  import org.mozilla.javascript.JavaScriptException;
64  import org.mozilla.javascript.NativeJavaClass;
65  import org.mozilla.javascript.NativeJavaPackage;
66  import org.mozilla.javascript.PropertyException;
67  import org.mozilla.javascript.Script;
68  import org.mozilla.javascript.ScriptRuntime;
69  import org.mozilla.javascript.Scriptable;
70  import org.mozilla.javascript.ScriptableObject;
71  import org.mozilla.javascript.WrappedException;
72  import org.mozilla.javascript.Wrapper;
73  import org.mozilla.javascript.WrapFactory;
74  import org.mozilla.javascript.continuations.Continuation;
75  import org.mozilla.javascript.tools.debugger.Main;
76  import org.mozilla.javascript.tools.shell.Global;
77  
78  import java.util.regex.Pattern;
79  import java.util.regex.Matcher;
80  
81  /***
82   * Interface with the JavaScript interpreter.
83   *
84   * @author <a href="mailto:ovidiu@apache.org">Ovidiu Predescu</a>
85   * @author <a href="mailto:crafterm@apache.org">Marcus Crafter</a>
86   * @since March 25, 2002
87   * @version CVS $Id: FOM_JavaScriptInterpreter.java 307410 2005-10-09 12:17:33Z reinhard $
88   */
89  public class FOM_JavaScriptInterpreter extends CompilingInterpreter {
90  
91      /***
92       * A long value is stored under this key in each top level JavaScript
93       * thread scope object. When you enter a context any scripts whose
94       * modification time is later than this value will be recompiled and reexecuted,
95       * and this value will be updated to the current time.
96       */
97      private final static String LAST_EXEC_TIME = "__PRIVATE_LAST_EXEC_TIME__";
98  
99      /***
100      * Prefix for session/request attribute storing JavaScript global scope object.
101      */
102     private static final String USER_GLOBAL_SCOPE = "FOM JavaScript GLOBAL SCOPE/";
103 
104     /***
105      * This is the only optimization level that supports continuations
106      * in the Christoper Oliver's Rhino JavaScript implementation
107      */
108     private static final int OPTIMIZATION_LEVEL = -2;
109 
110     /***
111      * When was the last time we checked for script modifications. Used
112      * only if {@link #reloadScripts} is true.
113      */
114     private long lastTimeCheck;
115 
116     /***
117      * Shared global scope for scripts and other immutable objects
118      */
119     private Global scope;
120 
121     /***
122      * List of <code>String</code> objects that represent files to be
123      * read in by the JavaScript interpreter.
124      */
125     private List topLevelScripts = new ArrayList();
126 
127     private boolean enableDebugger;
128     
129     private WrapFactory wrapFactory;
130     private Map flowVars = new HashMap();
131     private Map globalVars = new HashMap();
132 
133     /***
134      * JavaScript debugger: there's only one of these: it can debug multiple
135      * threads executing JS code.
136      */
137     private static Main debugger;
138 
139     static synchronized Main getDebugger() {
140         if (debugger == null) {
141             final Main db = new Main("Flow Debugger");
142             db.pack();
143             Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
144             size.width *= 0.75;
145             size.height *= 0.75;
146             db.setSize(size);
147             db.setExitAction(new Runnable() {
148                     public void run() {
149                         db.setVisible(false);
150                     }
151                 });
152             db.setOptimizationLevel(OPTIMIZATION_LEVEL);
153             db.setVisible(true);
154             debugger = db;
155             Context.addContextListener(debugger);
156         }
157         return debugger;
158     }
159     
160     /***
161      *  Gets the logger attribute of the JavaScriptInterpreter object
162      *
163      *@return    The logger value
164      */
165     public Logger getLogger() {
166         return Factory.getLogger();
167     }
168     
169     /***
170      *  Sets the wrap factory to use with Rhino
171      *
172      *@param wf The WrapFactory instance
173      */
174     public void setWrapFactory(WrapFactory wf) {
175         this.wrapFactory = wf;
176     }
177     
178     /***
179      *  Adds a class that will register a global variable
180      *
181      * @param reg The variable registrar
182      */
183     public void addGlobalVariable(String name, Object var) {
184         globalVars.put(name, var);
185     }
186 
187     /***
188      *  Removes a class that will register a global variable
189      *
190      * @param reg The variable registrar
191      */
192     public void removeGlobalVariable(String name) {
193         globalVars.remove(name);
194     }
195     
196     /***
197      *  Adds a class that will register a global variable
198      *
199      * @param reg The variable registrar
200      */
201     public void addFlowVariable(String name, FlowVariableFactory fac) {
202         flowVars.put(name, fac);
203     }
204 
205     /***
206      *  Removes a class that will register a global variable
207      *
208      * @param reg The variable registrar
209      */
210     public void removeFlowVariable(String name) {
211         flowVars.remove(name);
212     }
213     
214 
215     /***
216      *  Sets the interval between when the script should be looked at to see if
217      *  it needs to be reloaded
218      *
219      *@param  time  The interval time in milliseconds
220      */
221     public void setCheckTime(long time) {
222         checkTime = time;
223     }
224     
225     
226     /***
227      *  Sets whether to enable the debugger
228      *
229      *@param  val  The new debugger value
230      */
231     public void setDebugger(boolean val) {
232         enableDebugger = val;
233     }
234 
235 
236     /***
237      *  Sets whether to try to reload modified scripts or not
238      *
239      *@param  val  True to reload
240      */
241     public void setReloadScripts(boolean val) {
242         reloadScripts = val;
243     }
244 
245     /***
246      *  Initialize the global scope
247      *
248      *@exception  FlowException  If anything goes wrong
249      */
250     public void initialize() throws FlowException {
251         initialize(null);
252     }
253     
254 
255     /***
256      *  Initialize the global scope
257      *
258      *@exception  FlowException  If anything goes wrong
259      */
260     public void initialize(List classes) throws FlowException {
261         if (enableDebugger) {
262             if (getLogger().isDebugEnabled()) {
263                 getLogger().debug("Flow debugger enabled, creating");
264             }
265             getDebugger().doBreak();
266         }
267         Context context = Context.enter();
268         context.setOptimizationLevel(OPTIMIZATION_LEVEL);
269         context.setCompileFunctionsWithDynamicScope(true);
270         context.setGeneratingDebug(true);
271         if (wrapFactory != null) {
272             context.setWrapFactory(wrapFactory);
273         }
274 
275         try {
276             scope = new Global(context);
277             
278             // Register some handy classes with JavaScript, so we can make
279             // use of them from the flow layer.
280             initScope(context, scope);
281 
282             // Register any custom classes
283             if (classes != null) {
284                 for (Iterator i = classes.iterator(); i.hasNext(); ) {
285                     ScriptableObject.defineClass(scope, (Class)i.next());
286                 }
287             }
288             
289             // Access to Cocoon internal objects
290             FOM_Flow.init(scope);
291         } catch (Exception e) {
292             Context.exit();
293             e.printStackTrace();
294             throw new FlowException(e);
295         }
296     }
297     
298     
299     /***
300      *  Initialize the global scope
301      *
302      *@param  context                        The context
303      *@param  scope                          The scope to initialize
304      *@exception  IllegalAccessException     If anything goes wrong
305      *@exception  InstantiationException     If anything goes wrong
306      *@exception  InvocationTargetException  If anything goes wrong
307      *@exception  JavaScriptException        If anything goes wrong
308      */
309     protected void initScope(Context context, Global scope) throws IllegalAccessException,
310             InstantiationException, InvocationTargetException, JavaScriptException {
311         
312         WrapFactory factory = context.getWrapFactory();
313         for (Iterator i = globalVars.keySet().iterator(); i.hasNext(); ) {
314             String name = (String) i.next();
315             Object bean = globalVars.get(name);
316             
317             if (bean instanceof Scriptable) {
318                 scope.put(name, scope, (Scriptable)bean);
319             } else {
320                 Scriptable var = factory.wrapAsJavaObject(context, scope, bean, bean.getClass());
321                 scope.put(name, scope, var);
322             }
323         }        
324     }
325 
326 
327     /***
328      * Returns the JavaScript scope, a Scriptable object, from the user
329      * session instance. Each interpreter instance can have a scope
330      * associated with it.
331      *
332      * @return a <code>ThreadScope</code> value
333      */
334     private ThreadScope getSessionScope(WebContext ctx) throws Exception {
335         final String scopeID = USER_GLOBAL_SCOPE + getInterpreterID();
336 
337         ThreadScope scope = null;
338 
339         // Get/create the scope attached to the current context
340         //Session session = request.getSession(false);
341         //if (session != null) {
342         //    scope = (ThreadScope) session.getAttribute(scopeID);
343         //} else {
344         //    scope = (ThreadScope) request.getAttribute(scopeID);
345         //}
346         
347         // FIXME: This might create a unwanted session
348         scope = (ThreadScope) ctx.getSessionScope().get(scopeID);
349         if (scope == null) {
350             scope = (ThreadScope) ctx.getRequestScope().get(scopeID);
351         }
352         
353 
354         if (scope == null) {
355             scope = createThreadScope();
356             // Save scope in the request early to allow recursive Flow calls
357             ctx.getRequestScope().put(scopeID, scope);
358         }
359 
360         return scope;
361     }
362 
363     /***
364      * Associates a JavaScript scope, a Scriptable object, with
365      * {@link #getInterpreterID() identifier} of this {@link Interpreter}
366      * instance.
367      *
368      * @param scope a <code>ThreadScope</code> value
369      */
370     private void setSessionScope(ThreadScope scope, WebContext ctx) throws Exception {
371         if (scope.useSession) {
372             final String scopeID = USER_GLOBAL_SCOPE + getInterpreterID();
373 
374             // FIXME: Where "session scope" should go when session is invalidated?
375             // Attach the scope to the current context
376             try {
377                 ctx.getSessionScope().put(scopeID, scope);
378             } catch (IllegalStateException e) {
379                 // Session might be invalidated already.
380                 if (getLogger().isDebugEnabled()) {
381                     getLogger().debug("Got '" + e + "' while trying to set session scope.", e);
382                 }
383             }
384         }
385     }
386 
387     public static class ThreadScope extends ScriptableObject {
388         private static final String[] BUILTIN_PACKAGES = {"javax", "org", "com"};
389 
390         private ClassLoader classLoader;
391 
392         /* true if this scope has assigned any global vars */
393         boolean useSession;
394 
395         boolean locked = false;
396 
397         /***
398          * Initializes new top-level scope.
399          */
400         public ThreadScope(Global scope, Map flowVars, WrapFactory wrapFactory) throws Exception {
401             final Context context = Context.getCurrentContext();
402             if (wrapFactory != null) {
403                 context.setWrapFactory(wrapFactory);
404             }
405 
406             final String[] names = { "importClass" };
407             try {
408                 defineFunctionProperties(names,
409                                          ThreadScope.class,
410                                          ScriptableObject.DONTENUM);
411             } catch (PropertyException e) {
412                 throw new Error();  // should never happen
413             }
414 
415             setPrototype(scope);
416 
417             // We want this to be a new top-level scope, so set its
418             // parent scope to null. This means that any variables created
419             // by assignments will be properties of this.
420             setParentScope(null);
421 
422             // Put in the thread scope the Cocoon object, which gives access
423             // to the interpreter object, and some Cocoon objects. See
424             // FOM_Flow for more details.
425             final Object[] args = {};
426             FOM_Flow flow = (FOM_Flow) context.newObject(this,
427                                                                "FOM_Flow",
428                                                                args);
429             flow.setParentScope(this);
430             super.put("flow", this, flow);
431             
432             WrapFactory factory = context.getWrapFactory();
433             for (Iterator i = flowVars.keySet().iterator(); i.hasNext(); ) {
434                 String name = (String) i.next();
435                 FlowVariableFactory varfactory = (FlowVariableFactory) flowVars.get(name);
436                 
437                 Object bean = varfactory.getInstance(this, flow);
438                 
439                 if (bean instanceof Scriptable) {
440                     super.put(name, this, (Scriptable) bean);
441                 } else {
442                     Scriptable var = (Scriptable) factory.wrapAsJavaObject(context, this, bean, bean.getClass());
443                     super.put(name, this, var);
444                 }
445             }  
446 
447             defineProperty(LAST_EXEC_TIME,
448                            new Long(0),
449                            ScriptableObject.DONTENUM | ScriptableObject.PERMANENT);
450         }
451 
452         public String getClassName() {
453             return "ThreadScope";
454         }
455 
456         public void setLock(boolean lock) {
457             this.locked = lock;
458         }
459 
460         public void put(String name, Scriptable start, Object value) {
461             //Allow setting values to existing variables, or if this is a
462             //java class (used by importClass & importPackage)
463             if (this.locked && !has(name, start) && !(value instanceof NativeJavaClass)) {
464                 // Need to wrap into a runtime exception as Scriptable.put has no throws clause...
465                 throw new WrappedException (new JavaScriptException("Implicit declaration of global variable '" + name +
466                   "' forbidden. Please ensure all variables are explicitely declared with the 'var' keyword"));
467             }
468             this.useSession = true;
469             super.put(name, start, value);
470         }
471 
472         public void put(int index, Scriptable start, Object value) {
473             // FIXME(SW): do indexed properties have a meaning on the global scope?
474             if (this.locked && !has(index, start)) {
475                 throw new WrappedException(new JavaScriptException("Global scope locked. Cannot set value for index " + index));
476             }
477             this.useSession = true;
478             super.put(index, start, value);
479         }
480 
481         // Invoked after script execution
482         void onExec() {
483             this.useSession = false;
484             super.put(LAST_EXEC_TIME, this, new Long(System.currentTimeMillis()));
485         }
486 
487         /*** Override importClass to allow reloading of classes */
488         public static void importClass(Context ctx,
489                                        Scriptable thisObj,
490                                        Object[] args,
491                                        Function funObj) {
492             for (int i = 0; i < args.length; i++) {
493                 Object clazz = args[i];
494                 if (!(clazz instanceof NativeJavaClass)) {
495                     throw Context.reportRuntimeError("Not a Java class: " +
496                                                      Context.toString(clazz));
497                 }
498                 String s = ((NativeJavaClass) clazz).getClassObject().getName();
499                 String n = s.substring(s.lastIndexOf('.') + 1);
500                 thisObj.put(n, thisObj, clazz);
501             }
502         }
503 
504         public void setupPackages(ClassLoader cl) throws Exception {
505             final String JAVA_PACKAGE = "JavaPackage";
506             if (classLoader != cl) {
507                 classLoader = cl;
508                 Scriptable newPackages = new NativeJavaPackage("", cl);
509                 newPackages.setParentScope(this);
510                 newPackages.setPrototype(ScriptableObject.getClassPrototype(this, JAVA_PACKAGE));
511                 super.put("Packages", this, newPackages);
512                 for (int i = 0; i < BUILTIN_PACKAGES.length; i++) {
513                     String pkgName = BUILTIN_PACKAGES[i];
514                     Scriptable pkg = new NativeJavaPackage(pkgName, cl);
515                     pkg.setParentScope(this);
516                     pkg.setPrototype(ScriptableObject.getClassPrototype(this, JAVA_PACKAGE));
517                     super.put(pkgName, this, pkg);
518                 }
519             }
520         }
521 
522         public ClassLoader getClassLoader() {
523             return classLoader;
524         }
525     }
526 
527     private ThreadScope createThreadScope() throws Exception {
528         return new ThreadScope(scope, flowVars, wrapFactory);
529     }
530 
531     /***
532      * Returns a new Scriptable object to be used as the global scope
533      * when running the JavaScript scripts in the context of a request.
534      *
535      * <p>If you want to maintain the state of global variables across
536      * multiple invocations of <code>&lt;map:call
537      * function="..."&gt;</code>, you need to instanciate the session
538      * object which is a property of the flow object
539      * <code>var session = flow.session</code>. This will place the
540      * newly create Scriptable object in the user's session, where it
541      * will be retrieved from at the next invocation of {@link #callFunction}.</p>
542      *
543      * @exception Exception if an error occurs
544      */
545     private void setupContext(WebContext webctx, Context context,
546                               ThreadScope thrScope)
547     throws Exception {
548         // Try to retrieve the scope object from the session instance. If
549         // no scope is found, we create a new one, but don't place it in
550         // the session.
551         //
552         // When a user script "creates" a session using
553         // flow.createSession() in JavaScript, the thrScope is placed in
554         // the session object, where it's later retrieved from here. This
555         // behaviour allows multiple JavaScript functions to share the
556         // same global scope.
557 
558         FOM_Flow flow = (FOM_Flow) thrScope.get("flow", thrScope);
559         long lastExecTime = ((Long) thrScope.get(LAST_EXEC_TIME,
560                                                  thrScope)).longValue();
561         boolean needsRefresh = false;
562         if (reloadScripts) {
563             long now = System.currentTimeMillis();
564             if (now >= lastTimeCheck + checkTime) {
565                 needsRefresh = true;
566             }
567             lastTimeCheck = now;
568         }
569         
570         // We need to setup the FOM_Flow object according to the current
571         // request. Everything else remains the same.
572         ClassLoader contextClassloader = Thread.currentThread().getContextClassLoader();
573         thrScope.setupPackages(contextClassloader);
574         flow.pushCallContext(this, webctx, getLogger(), null);
575         
576         // Check if we need to compile and/or execute scripts
577         synchronized (compiledScripts) {
578             List execList = new ArrayList();
579             // If we've never executed scripts in this scope or
580             // if reload-scripts is true and the check interval has expired
581             // or if new scripts have been specified in the sitemap,
582             // then create a list of scripts to compile/execute
583             if (lastExecTime == 0 || needsRefresh || needResolve.size() > 0) {
584                 topLevelScripts.addAll(needResolve);
585                 if (lastExecTime != 0 && !needsRefresh) {
586                     execList.addAll(needResolve);
587                 } else {
588                     execList.addAll(topLevelScripts);
589                 }
590                 needResolve.clear();
591             }
592             // Compile all the scripts first. That way you can set breakpoints
593             // in the debugger before they execute.
594             for (int i = 0, size = execList.size(); i < size; i++) {
595                 String sourceURI = (String)execList.get(i);
596                 ScriptSourceEntry entry =
597                     (ScriptSourceEntry)compiledScripts.get(sourceURI);
598                 if (entry == null) {
599                     Source src = this.sourceresolver.resolveURI(sourceURI);
600                     entry = new ScriptSourceEntry(src);
601                     compiledScripts.put(sourceURI, entry);
602                 }
603                 // Compile the script if necessary
604                 entry.getScript(context, this.scope, needsRefresh, this);
605             }
606             // Execute the scripts if necessary
607             for (int i = 0, size = execList.size(); i < size; i++) {
608                 String sourceURI = (String) execList.get(i);
609                 ScriptSourceEntry entry =
610                     (ScriptSourceEntry) compiledScripts.get(sourceURI);
611                 long lastMod = entry.getSource().getLastModified();
612                 Script script = entry.getScript(context, this.scope, false, this);
613                 if (lastExecTime == 0 || lastMod > lastExecTime) {
614                     script.exec(context, thrScope);
615                     thrScope.onExec();
616                 }
617             }
618         }
619     }
620 
621     /***
622      * Compile filename as JavaScript code
623      *
624      * @param cx Rhino context
625      * @param fileName resource uri
626      * @return compiled script
627      */
628     Script compileScript(Context cx, String fileName) throws Exception {
629         Source src = this.sourceresolver.resolveURI(fileName);
630         if (src != null) {
631             synchronized (compiledScripts) {
632                 ScriptSourceEntry entry =
633                     (ScriptSourceEntry)compiledScripts.get(src.getURI());
634                 Script compiledScript = null;
635                 if (entry == null) {
636                     compiledScripts.put(src.getURI(),
637                             entry = new ScriptSourceEntry(src));
638                 } else {
639                     this.sourceresolver.release(src);
640                 }
641                 compiledScript = entry.getScript(cx, this.scope, false, this);
642                 return compiledScript;
643             }
644         }
645         throw new FlowException(fileName + ": not found");
646 
647     }
648 
649     protected Script compileScript(Context cx, Scriptable scope, Source src)
650     throws Exception {
651         PushbackInputStream is = new PushbackInputStream(src.getInputStream(), ENCODING_BUF_SIZE);
652         try {
653             String encoding = findEncoding(is);
654             Reader reader = encoding == null ? new InputStreamReader(is) : new InputStreamReader(is, encoding);
655             reader = new BufferedReader(reader);
656             Script compiledScript = cx.compileReader(scope, reader,
657                     src.getURI(), 1, null);
658             return compiledScript;
659         } finally {
660             is.close();
661         }
662     }
663     
664     // A charset name can be up to 40 characters taken from the printable characters of US-ASCII
665     // (see http://www.iana.org/assignments/character-sets). So reading 100 bytes should be more than enough.
666     private final static int ENCODING_BUF_SIZE = 100;
667     // Match 'encoding = xxxx' on the first line
668     Pattern encodingRE = Pattern.compile("^.*encoding//s*=//s*([^//s]*)");
669     
670     /***
671      * Find the encoding of the stream, or null if not specified
672      */
673     String findEncoding(PushbackInputStream is) throws IOException {
674         // Read some bytes
675         byte[] buffer = new byte[ENCODING_BUF_SIZE];
676         int len = is.read(buffer, 0, buffer.length);
677         // and push them back
678         is.unread(buffer, 0, len);
679         
680         // Interpret them as an ASCII string
681         String str = new String(buffer, 0, len, "ASCII");
682         Matcher re = encodingRE.matcher(str);
683         if (re.matches()) {
684             return re.group(1);
685         }
686         return null;
687     }
688 
689     /***
690      * Calls a JavaScript function, passing <code>params</code> as its
691      * arguments. In addition to this, it makes available the parameters
692      * through the <code>flow.parameters</code> JavaScript array
693      * (indexed by the parameter names).
694      *
695      * @param funName a <code>String</code> value
696      * @param params a <code>List</code> value
697      * @param redirector
698      * @exception Exception if an error occurs
699      */
700     public Object callFunction(String funName, List params, WebContext webctx)
701     throws Exception {
702         Context context = Context.enter();
703         context.setOptimizationLevel(OPTIMIZATION_LEVEL);
704         context.setGeneratingDebug(true);
705         context.setCompileFunctionsWithDynamicScope(true);
706         context.setErrorReporter(new JSErrorReporter(getLogger()));
707         if (wrapFactory != null) {
708             context.setWrapFactory(wrapFactory);
709         }
710         
711         LocationTrackingDebugger locationTracker = new LocationTrackingDebugger();
712         if (!enableDebugger) {
713             //FIXME: add a "tee" debugger that allows both to be used simultaneously
714             context.setDebugger(locationTracker, null);
715         }
716         Object ret = null;
717         ThreadScope thrScope = getSessionScope(webctx);
718         synchronized (thrScope) {
719             ClassLoader savedClassLoader =
720                 Thread.currentThread().getContextClassLoader();
721             FOM_Flow flow = null;
722             try {
723                 try {
724                     setupContext(webctx, context, thrScope);
725                     flow = (FOM_Flow) thrScope.get("flow", thrScope);
726 
727                     // Register the current scope for scripts indirectly called from this function
728                     //FOM_JavaScriptFlowHelper.setFOM_FlowScope(flow.getObjectModel(), thrScope);
729 
730                     if (enableDebugger) {
731                         if (!getDebugger().isVisible()) {
732                             // only raise the debugger window if it isn't already visible
733                             getDebugger().setVisible(true);
734                         }
735                     }
736 
737                     int size = (params != null ? params.size() : 0);
738                     Scriptable parameters = context.newObject(thrScope);
739                     for (int i = 0; i < size; i++) {
740                         Interpreter.Argument arg = (Interpreter.Argument)params.get(i);
741                         if (arg.name == null) {
742                             arg.name = "";
743                         }
744                         parameters.put(arg.name, parameters, arg.value);
745                     }
746                     flow.setParameters(parameters);
747 
748                     Object fun = ScriptableObject.getProperty(thrScope, funName);
749                     if (fun == Scriptable.NOT_FOUND) {
750                         throw new FlowException("Function \"javascript:" + funName + "()\" not found");
751                     }
752 
753                     thrScope.setLock(true);
754                     ret = ScriptRuntime.call(context, fun, thrScope, new Object[0], thrScope);
755                 } catch (JavaScriptException ex) {
756                     throw locationTracker.getException("Error calling flowscript function " + funName, ex);
757                 } catch (EcmaError ee) {
758                     throw locationTracker.getException("Error calling function " + funName, ee);
759                 } catch (WrappedException ee) {
760                     throw locationTracker.getException("Error calling function " + funName, ee);
761                 }
762             } finally {
763                 thrScope.setLock(false);
764                 setSessionScope(thrScope, webctx);
765                 if (flow != null) {
766                     flow.popCallContext();
767                 }
768                 Context.exit();
769                 Thread.currentThread().setContextClassLoader(savedClassLoader);
770             }
771         }
772         return ret;
773     }
774 
775     public void handleContinuation(String id, List params,
776                                    WebContext webctx) throws Exception
777     {
778         WebContinuation wk = continuationsMgr.lookupWebContinuation(id, getInterpreterID(), webctx);
779 
780         if (wk == null) {
781             /*
782              * Throw an InvalidContinuationException to be handled inside the
783              * <map:handle-errors> sitemap element.
784              */
785             throw new InvalidContinuationException("The continuation ID " + id + " is invalid.");
786         }
787 
788         Context context = Context.enter();
789         context.setOptimizationLevel(OPTIMIZATION_LEVEL);
790         context.setGeneratingDebug(true);
791         context.setCompileFunctionsWithDynamicScope(true);
792         LocationTrackingDebugger locationTracker = new LocationTrackingDebugger();
793         if (wrapFactory != null) {
794             context.setWrapFactory(wrapFactory);
795         }
796         if (!enableDebugger) {
797             //FIXME: add a "tee" debugger that allows both to be used simultaneously
798             context.setDebugger(locationTracker, null);
799         }
800 
801         // Obtain the continuation object from it, and setup the
802         // FOM_Flow object associated in the dynamic scope of the saved
803         // continuation with the environment and context objects.
804         Continuation k = (Continuation)wk.getContinuation();
805         ThreadScope kScope = (ThreadScope)k.getParentScope();
806         synchronized (kScope) {
807             ClassLoader savedClassLoader =
808                 Thread.currentThread().getContextClassLoader();
809             FOM_Flow flow = null;
810             try {
811                 Thread.currentThread().setContextClassLoader(kScope.getClassLoader());
812                 flow = (FOM_Flow)kScope.get("flow", kScope);
813                 kScope.setLock(true);
814                 flow.pushCallContext(this, webctx, getLogger(), wk);
815 
816                 // Register the current scope for scripts indirectly called from this function
817                 //FOM_JavaScriptFlowHelper.setFOM_FlowScope(flow.getObjectModel(), kScope);
818 
819                 if (enableDebugger) {
820                     getDebugger().setVisible(true);
821                 }
822                 Scriptable parameters = context.newObject(kScope);
823                 int size = params != null ? params.size() : 0;
824                 for (int i = 0; i < size; i++) {
825                     Interpreter.Argument arg = (Interpreter.Argument)params.get(i);
826                     parameters.put(arg.name, parameters, arg.value);
827                 }
828                 flow.setParameters(parameters);
829                 FOM_WebContinuation fom_wk = new FOM_WebContinuation(wk);
830                 fom_wk.setParentScope(kScope);
831                 fom_wk.setPrototype(ScriptableObject.getClassPrototype(kScope,
832                                                                        fom_wk.getClassName()));
833                 Object[] args = new Object[] {k, fom_wk};
834                 try {
835                     ScriptableObject.callMethod(flow,
836                                                 "handleContinuation", args);
837                 } catch (JavaScriptException ex) {
838                     throw locationTracker.getException("Error calling continuation", ex);
839 
840                 } catch (EcmaError ee) {
841                     throw locationTracker.getException("Error calling continuation", ee);
842 
843                 }
844             } finally {
845                 kScope.setLock(false);
846                 setSessionScope(kScope, webctx);
847                 if (flow != null) {
848                     flow.popCallContext();
849                 }
850                 Context.exit();
851                 Thread.currentThread().setContextClassLoader(savedClassLoader);
852             }
853         }
854     }
855 
856     private Throwable unwrap(JavaScriptException e) {
857         Object value = e.getValue();
858         while (value instanceof Wrapper) {
859             value = ((Wrapper)value).unwrap();
860         }
861         if (value instanceof Throwable) {
862             return (Throwable)value;
863         }
864         return e;
865     }
866 }