Subj : Getting Rhino debugger working in servlets To : netscape.public.mozilla.jseng From : Jon Brisbin Date : Wed Sep 14 2005 12:49 pm Here's the promised write-up on how I got the Rhino debugger to work in my servlets. I've also created a jakarta-taglibs tag library for Rhino so I can do server-side JavaScript without having to patch Tomcat. If anyone's interested in using this, then let me know and I'll post the code. There's probably easier and more efficient ways to do this. Please jump in if you see something that could be improved. But this works for me. First and foremost: DO NOT USE Context.enter() when you debug! This will really get you. That's why I had to dump BSF. I tried incorporating debugging into the BSF JavaScript engine, but it was just easier to use Rhino directly. This code is designed to be run in debug or non-debug modes with only a few lines of code here and there to distinguish between them. Okay. First, I declare a ContextFactory to run my code within and, based on my debug variable, use either the one from the Rhino shell (just because it's easier and that's how the debugger seems to like it) or do a Context.enter() and get that ContextFactory--if and only if I'm not debugging. As a general rule I never, ever declare a variable without setting it to null, but I don't do that here to generate a compiler error if I screw up my code and have a situation where I don't set the ContextFactory. Ditto for the scope object later on. ContextFactory ctxFac; if ( debug ) { ctxFac = org.mozilla.javascript.tools.shell.Main.shellContextFactory; Main.mainEmbedded( ctxFac, new ScopeProvider() { public Scriptable getScope() { return org.mozilla.javascript.tools.shell.Main.getGlobal(); } }, "Taglib JavaScript" ); } else { ctxFac = Context.enter().getFactory(); } Notice that I go ahead and get the GUI instantiated right away, which also puts listeners on the context you'll eventually be using, so that when you do all your scope.put() stuff, that will get picked up by the GUI. You can, of course, replace the anonymous class with a real one, which the debugger code uses all over the place (the IProxy stuff) but that's just a little too confusing for me. This works and is readily apparent what its function is. I also use an anonymous class to actually do the work I want done within that Context (notice that I trap any error for the "finally" block, which does a Context.exit() if I'm not debugging: Object returnValue; try { returnValue = ctxFac.call( new ContextAction() { public Object run( Context cx ) { String scriptStr = null; Scriptable scope; if ( debug ) { scope = org.mozilla.javascript.tools.shell.Main.getGlobal(); } else { scope = new ImporterTopLevel( cx ); } .... YOUR CODE GOES HERE ... // You have to catch all Exceptions. // If you need to bubble them up, then you have // to throw a RuntimeException since the 'run' // method doesn't declare any Exceptions // to be thrown. } }); } catch ( Exception e ) { throw new JspException( e ); // this example is from the JSP taglib, obviously, } finally { if ( !debug ) { try { Context.exit(); } catch ( IllegalStateException ignored ) {} } } If you have a scope that you need "persisted" from one call of JavaScript to the next (which I use in my application since I might have some JavaScript dynamically "sourced" in, like in a shell script, to define commonly used functions and variables) then you'll need to set this scope's parent to that shared one if you're not debugging. Keep in mind that this codes actually appears in the "YOUR CODE GOES HERE" area mentioned above: Scriptable scope = cx.initStandardObjects(); // Actually only needed if parentScope doesn't have them already // You'll probably want to change this to taste. Object returnValue = new Object(); Scriptable parentScope = (Scriptable) _args.get( Globals.Keys.JS_CONTEXT ); if ( parentScope != null ) { /* * Yank the environment to prevent circular references. * This is application-specific, but is the environment * in which my parentScope is stored, so keep that in * mind if you use another method to persist your parent * scope. In this example _args == scope.get("env") */ if ( parentScope.has( "env", parentScope ) ) parentScope.delete( "env" ); if ( debug ) { scope = org.mozilla.javascript.tools.shell.Main.getGlobal(); /* * Make sure my pre-existing JavaScript objects, which might * have been created without debugging, are available in the * the script I actually WILL be debugging. BUT, if the parent * scope is actually the shell's Global instance, which might * happen if my parent scope got created in JavaScript that was * also debugged, don't set a parent because it's redundant and * most likely cause a StackOverflowError if you do :-) */ if ( !( parentScope instanceof Global ) ) scope.setParentScope( parentScope ); } else { scope.setParentScope( parentScope ); } } else { if ( debug ) scope = org.mozilla.javascript.tools.shell.Main.getGlobal(); else _args.put( Globals.Keys.JS_CONTEXT, scope ); // save for later } .... SET OTHER OBJECTS IN YOUR SCOPE HERE ... .... e.g. HttpServletRequest/Response, etc ... This next block is only important if you're reading your JavaScript from a file. You'll want to preserve the line breaks because it'll be nigh on unreadable in the debugger if you don't (I load the files myself, instead of letting the Context object do that for me, because I allow the developer to use a comma-delimited list of files to load and cat together so I can store my JavaScript functions in files grouped by functionality and share only certain functions for other uses; feel free to skip this and just replace the empty quotes in the "evaluateString" line below with the filename of your JavaScript): if ( src != null ) { StringBuffer sb = new StringBuffer(); BufferedReader frdr; for ( String srcFile : src.split( "," ) ) { // <-- THIS IS JDK 1.5 SPECIFIC try { frdr = new BufferedReader( new FileReader( srcFile ) ); String line = null; while ( ( line = frdr.readLine() ) != null ) { sb.append( line + "\n" ); } frdr.close(); } catch ( FileNotFoundException e ) { log.error( e ); // Might want to do something special e.g. send 404 error } catch ( IOException e ) { log.error( e ); // Might want to handle this differently e.g. send 500 error } scriptStr = sb.toString() + scriptStr; } frdr = null; sb = null; } else { // This is set up to be either-or. Either you // load your JavaScript from a file or you put it // in some kind of a tag--in this example the // JSP taglib tag. Another place where I use // this code, I actually append to the front // of whatever script I put in a CDATA in the // XML, thus making it act like an "include" scriptStr = bodyContent.getString(); } Now that that's all taken care of, let's run it! Object returnValue = cx.evaluateString( scope, scriptStr.trim(), "", 1, null ); if ( null == returnValue ) returnValue = new Object(); // My application requires a non-null value here return returnValue; Notice that you have to start on line "1" for some reason, otherwise the arrow will be one line off in the debugger window. It has no effect if you're not debugging, so I just left it like this to cut down on the if(debug) checks. I think that should give you the critical parts needed to enable debugging anywhere you run JavaScript--even though the impetus for me to do this was to use it in a servlet. I use versions of this same code in a Jelly tag, a JSP tag, and am planning a JMS/JMX non-web version as well for my data replicator, which is straight SQL right now. Please don't hesitate to ask if you have any questions. -- Jon Brisbin Webmaster NPC International, Inc. .