Tuesday, September 11, 2007

Unit Testing Struts 2.0 (Part 3)

Like Dan commented on the Unit Testing Struts 2.0 (Part 2) post, Struts 2.0 has changed it's API enough to make my previous code not work on latest version. So, in response to his comment, here is what I am using right now. Credit where it's due, the setup code is the one that The Arsenalist pointed out on his blog post Unit Testing Struts 2 Actions wired with Spring using JUnit. Again, credit where it's due...my thanks go to "The Arsenalist" for posting his solution.


/**
* Class for easier support of Struts related
* testing. Takes care of all the configuration details
* that allow test classes to create beans (Spring),
* actions (Struts), intercepted actions (Struts).
* Class is singleton to minimize hit of initializing
* Struts and related infrastructure (e.g. Hibernate).
*
* Adapted from code from "The Arsenalist" (http://arsenalist.com/),
* see http://arsenalist.com/2007/06/18/unit-testing-struts-2-actions-spring-junit/
*/
public class StrutsTestCaseSupport {

/**
* Singleton variable
*/
public static StrutsTestCaseSupport _theInstance = null;

/**
* Servlet context
*/
private ServletContext servletContext = null;

/**
* Request dispatcher
*/
private Dispatcher dispatcher = null;

/**
* Singleton access
*/
public static synchronized StrutsTestCaseSupport getInstance()
throws Exception {
if ( _theInstance == null ) {
_theInstance = new StrutsTestCaseSupport();
}
return _theInstance;
}

/**
* Class constructor, take care of Struts initializations
*/
private StrutsTestCaseSupport ()
throws Exception {
String[] config = new String[] { "/WEB-INF/applicationContext.xml" };

// Link the servlet context and the Spring context
servletContext = new MockServletContext(new FileSystemResourceLoader());
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setServletContext(servletContext);
appContext.setConfigLocations(config);
appContext.refresh();
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, appContext);

// Use spring as the object factory for Struts
StrutsSpringObjectFactory ssf = new StrutsSpringObjectFactory(null, null, servletContext);
ssf.setApplicationContext(appContext);
StrutsSpringObjectFactory.setObjectFactory(ssf);

// Dispatcher is the guy that actually handles all requests. Pass in
// an empty Map as the parameters but if you want to change stuff like
// what config files to read, you need to specify them here
// (see Dispatcher's source code)
dispatcher = new Dispatcher(servletContext, new HashMap());
dispatcher.init();
Dispatcher.setInstance(dispatcher);
}

/**
* create a bean from the object factory (all wired up from Spring)
*
* @param beanName the name of the bean to get from the object factory
* @param extraContent any extra content information to pass along to the bean building
* process
* @return the object factory created bean
* @throws Exception on processing, configuration errors, test failure
*/
public Object createBean ( String beanName, Map extraContext )
throws Exception {
ObjectFactory objectFactory = dispatcher.getContainer().getInstance(ObjectFactory.class);
return objectFactory.buildBean(beanName,extraContext);
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace )
throws Exception {
// create a proxy class which is just a wrapper around the action call.
// The proxy is created by checking the namespace and name against the
// struts.xml configuration
ActionProxy proxy = dispatcher.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(actionNamespace, actionName, null, true, false);

// by default, don't pass in any request parameters
proxy.getInvocation().getInvocationContext().setParameters(new HashMap());

// by default, pass along an empty session map
proxy.getInvocation().getInvocationContext().setSession(new HashMap());

// set the actions context to the one which the proxy is using
ServletActionContext.setContext(proxy.getInvocation().getInvocationContext());
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletActionContext.setRequest(request);
ServletActionContext.setResponse(response);
ServletActionContext.setServletContext(servletContext);

// set proper URI
request.setRequestURI(actionNamespace + "/" + actionName);

return proxy;
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param sessionMap the request/invocation session map (for http session map mocking)
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace, Map sessionMap )
throws Exception {

// create an action proxy as usual
ActionProxy actionProxy = createActionProxy(actionName,actionNamespace);

// set the session map in the action proxy's invocation
actionProxy.getInvocation().getInvocationContext().setSession(sessionMap);

return actionProxy;
}

/**
* create an action object, bypass all it's stacks. Have it properly injected
* according to configurations.
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @return the properly injected action
* @throws Exception on processing, configuration errors, test failure
*/
public Object createAction ( String actionName, String actionNamespace )
throws Exception {
ActionProxy actionProxy = createActionProxy(actionName,actionNamespace);
return actionProxy.getAction();
}

/**
* Access to the proxy's response content as a string
* @param proxy the proxy to get the string response for
* @return the proxy's response content as a string
*/
public static String getResponseContentAsString ( ActionProxy proxy )
throws java.io.UnsupportedEncodingException {
return ((MockHttpServletResponse)ServletActionContext.getResponse()).getContentAsString();
}

/**
* Set a hostname in proxy request
* @param proxy the proxy to set hostname to
* @param serverName the server name to set for this proxy's call
*/
public static void setRequestServerName ( ActionProxy proxy, String serverName ) {
((MockHttpServletRequest)ServletActionContext.getRequest()).setServerName(serverName);
}
}


Like mentioned in previous postings, using this class is pretty straightforward, within your test you just do for beans:


YourClass yourInstance = (YourClass)StrutsTestCaseSupport.getInstance().createBean("yourBeanId",new HashMap());


For Actions:

ActionProxy proxy = StrutsTestCaseSupport.getInstance().createActionProxy(yourActionId,yourContextPath);
MyActionClass myActionInstance = (MyActionClass)StrutsTestCaseSupport.getInstance().createAction(yourActionId,yourContextPath);


This code is currently being used with Struts 2.0.8.

Wednesday, August 29, 2007

Return of the Stuff!

Here we go again, it's that time of the year where good things are about to happen. September is marked by two major events: the release of Halo 3 ;-) and the return of the No Fluff Just Stuff conference (now called the New England Software Symposium). I have written about this even before but it does not hurt to repeat it...this is a really impressive conference and a must if you live around the Boston area. It takes place during a Friday afternoon and over the weekend. This is actually a great decision since it simplifies a lot the "need to be out of work" argument with your bosses. Sure it takes over your weekend but, if you're in this business you are most probably hooked enough to this stuff for this not to be a problem. The attendance to this event is capped at around 250 which is another great feature since it does allow you to be in those rooms listening to the presentations in a much close environment. You get to interact with the presenters both during the talks and during the breaks by mingling in lunch tables or just approaching them directly. The topics are amazing and, like I mentioned before (see here), the really annoying part of the even is choosing which sessions to go to. The price for the event is really a find...pretty affordable even if, like me, you do not get your company to sponsor you
and pay out of your own pocket.


This fall, the Boston even takes place on the 14th (PM), 15th and 16th of September in Framingham, Massachusetts. For all the details, check out the event site.


I got my place already booked, what are you waiting for ?

Friday, July 06, 2007

Problems with Keyboard Mapping when Running vncviewer on Ubuntu

In the hope that this adds to the list of solutions out there that can actually help people...
If you are having keyboard mapping issues when trying to connect via vncviewer to a machine running vncserver on Ubuntu Feisty (7.04) with gnome, try this:

Create a ~/.vncrc file and add to it:

        $vncStartup = "<your_home_dir_here>/.vnc/xstartup"; 

Create a ~/.vnc/xstartup file (or edit) and make sure it contains:
        xrdb $HOME/.Xresources
gnome-wm &
gnome-panel &
nautilus --no-default-window &
gnome-cups-icon &amp;
gnome-volume-manager &
cd ~
xterm &


Solved the problem for me. Credit where it belongs, found the solution here: http://ubuntuforums.org/showthread.php?t=382441

My take on it, posts do help find solutions, here is my contribution to try helping others find a solution quicker.

Saturday, May 05, 2007

Pity the User

I have recently got the opportunity to get my hands on a copy of Vista Ultimate at MS employee prices. I jumped at the opportunity and, as soon as I ensured all my crucial apps actually ran on Vista (VMWare Workstation in particular), proceeded to install it on my laptop. I spend my working hours (and others for that matter) using this laptop so, although I surely did not get to explore all the niceties of Vista, I got the daily use experience of running it. The first impression was the "Wow" that MS advertises so much. It is indeed an eye-candy-filled OS. It looks awesome and all the UI interactions have been tuned to please the eye, Windows Aero is indeed really enticing. But...the niceties of a new UI only last so much...My daily work is spent mostly on another OS environment. All my work is done on Linux and I had been used to depend on VMWare Workstation to be able to get the best of both worlds and jump around as needed. My experience with XP and VMWare Workstation had been impeccable and I could not recommend it more...Then came Vista...oh well...Suddenly my Pentium 4 3.4Ghz with Hyperthreading, 2Gb RAM started grinding to a halt. Although a year old now, this is not (I believe) a run-of-the-mill laptop...A laptop with these specs should be able to handle this OS plus the apps that I needed to run on top of it! My frustration grew when looking at memory usage on Vista...Just starting up brought me to 600Mb usage, with VMWare I was up to 1.6Gb...this on a 2Gb RAM machine...And I started looking through the nice UI and thinking seriously that I could not work daily with this...The initial "Wow" turned into "Wow, this is unbearable!". So, after some serious consideration I decided it was time to byte the bullet and wipe out my system and replace it by something more snappy that actually made good use of my hardware...Ubuntu 7.04 to the rescue! Ok, I am not religious about the OS war and am one of the people who tries to take advantage of whatever each has to offer so, being the gamer that I am, I left Vista on a dual boot setup alongside Ubuntu (I still have hopes of playing Halo 2 on Vista someday to get my achievements! ). Now I am running Linux as a first OS...my life became a lot less stressing...and I can still run Windows XP or Win 2K on a VMWare Workstation virtual host for all the testing I need to do...The best of both worlds I would say...

With this Windows Vista we are bound to see a new need to get new hardware where once what you had was just fine...and, to be honest, without any real *day to day* real important enhancements that I can see (again, I stress the *day to day*... you might do cool and important things on Vista but I'm betting these are completely irrelevant for the common user).

But I believe Ubuntu 7.04 (and Linux in general) is still not there as well...I had to jump through some serious loops to get my Wireless card and my sound card to work on my laptop...It has evolved immensely no doubts. I still remember the old Linux installations of the early nineties where you really had to be courageous and curious to even try it. But good as it is this is hardly mass user ready...

Pity the user indeed!

Thursday, March 29, 2007

Unit Testing Struts 2.0 (Part 2)

Edit: for latest on this see recent post.


In response to a comment made to Unit Testing Struts 2.0, here is the updated, complete code for Struts 2.0 testing. Hope this is useful. Have questions ? Want to discuss any of this ? Just drop me a line...Read on, includes support class code, small snippets for creation of Spring beans, Struts 2.0 actions, Struts 2.0 action proxies.


import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ActionProxyFactory;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.entities.ActionConfig;

import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsStatics;
import org.apache.struts2.config.Settings;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.impl.StrutsActionProxyFactory;
import org.apache.struts2.spring.StrutsSpringObjectFactory;
import org.apache.struts2.views.freemarker.FreemarkerManager;

import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.mock.web.MockServletContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;

import java.lang.reflect.Method;

import java.net.URLEncoder;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;


/**
* Class for easier support of Struts related
* testing. Takes care of all the configuration details
* that allow test classes to create beans (Spring),
* actions (Struts), intercepted actions (Struts).
* Class is singleton to minimize hit of initializing
* Struts and related infrastructure (e.g. Hibernate).
*
* @author Francisco Assis Rosa
*/
public class StrutsTestCaseSupport {

/**
* Singleton variable
*/
public static StrutsTestCaseSupport _theInstance;

/**
* Singleton access
*/
public static synchronized StrutsTestCaseSupport getInstance() {
if ( _theInstance == null ) {
_theInstance = new StrutsTestCaseSupport();
}
return _theInstance;
}

/**
* Application context class (encapsulation of applicationContext.xml)
*/
ConfigurableWebApplicationContext _applicationContext;

/**
* Configuration Manager object, to allow for encapusulation of struts.xml,
* creation of actions and their proxied counterparts, creation of
* servlet context from this application context
*/
ConfigurationManager _configurationManager;

/**
* Class constructor, take care of Struts initializations
*/
private StrutsTestCaseSupport () {

// create the struts+spring integrated object factory
// set spring autowiring by name for spring object factory
Settings.set(StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,"name");
StrutsSpringObjectFactory objectFactory = new StrutsSpringObjectFactory();

// set system object facory
ObjectFactory.setObjectFactory(objectFactory);

// set action proxy factory
ActionProxyFactory.setFactory(new StrutsActionProxyFactory());

// create a web application context instance (for spring configuration)
_applicationContext = new XmlWebApplicationContext();

// get ahold of a servlet context to use in the creation of the application context
ServletContext servletContext = createOneServletContext(_applicationContext);
// complete application context initialization, pass in servlet
// context and config file location, force reading of config (via refresh)
_applicationContext.setServletContext(servletContext);
_applicationContext.setConfigLocations(new String[] {"WEB-INF/applicationContext.xml"});
_applicationContext.refresh();

// initialize the object factory with the mock servlet context, application context
objectFactory.init(servletContext);
objectFactory.setApplicationContext(_applicationContext);

// add a default dispatcher to the system
Dispatcher du = new Dispatcher(servletContext);
Dispatcher.setInstance(du);

// pass over to the configuration manager location where struts-default.xml,
// struts-plugin.xml and struts.xml can be found, force reading all
_configurationManager = new ConfigurationManager();
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts-default.xml", false));
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts-plugin.xml", false));
_configurationManager.addConfigurationProvider( new StrutsXmlConfigurationProvider("struts.xml", false));
_configurationManager.reload();
}

/**
* create a servlet context useable for a specific action
*
* @param applicationContext the application context to use in the servlet context
* @return the created servlet context
*/
protected ServletContext createOneServletContext (ConfigurableWebApplicationContext applicationContext) {
// create a servlet context for this action, use FileSystemResourceLoader for
// context to find configuration files
ServletContext servletContext = (ServletContext) new MockServletContext(new FileSystemResourceLoader());

// initialize freemarker manager config parameter to null (let FreemarkerManager figure
// out configuration location out of ServletContext)
Settings.set(StrutsConstants.STRUTS_I18N_ENCODING, "UTF-8");
servletContext.setAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY,null);

// hand over application context to servlet context
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, applicationContext);

return servletContext;
}

/**
* Build one action context for an accessmethod and an access url
*
* @param serverName the hostname that the request will need to hook up to
* @param accessMethod http method to use (e.g. 'get', 'post', 'put', etc)
* @param accessUrl the url to access
* @return the map for the action's context
* @throws Exception on processing, configuration errors, test failure
*/
public Map buildActionContext ( String serverName, String accessMethod, String accessUrl, Map requestParamMap )
throws Exception {
// get ahold of a brand new servlet context
ServletContext servletContext = createOneServletContext(_applicationContext);

// create fake request and response objects
MockHttpServletRequest request = new MockHttpServletRequest(servletContext,accessMethod,accessUrl);
MockHttpServletResponse response = new MockHttpServletResponse();

// set request server name
request.setServerName(serverName);

// add request parameters
if ( "get".equals(accessMethod) ) {
for ( String oneParamName : requestParamMap.keySet() ) {
request.addParameter(oneParamName,requestParamMap.get(oneParamName));
}
} else if ( "post".equals(accessMethod) ) {
String requestBody = "";
for ( String oneParamName : requestParamMap.keySet() ) {
if ( requestBody.length() > 0 ) {
requestBody += "&";
}
requestBody += oneParamName + "=" + URLEncoder.encode(requestParamMap.get(oneParamName),"UTF-8");
}
request.setContent(requestBody.getBytes());
}

// add context, request and response to an action context map
Map actionContext = new HashMap();
actionContext.put(StrutsStatics.SERVLET_CONTEXT,servletContext);
actionContext.put(StrutsStatics.HTTP_REQUEST,request);
actionContext.put(StrutsStatics.HTTP_RESPONSE,response);
actionContext.put(ActionContext.DEV_MODE,new Boolean(false));
// add request parameters to action context
Map actionContextParams = new HashMap();
for ( String oneParamName : requestParamMap.keySet() ) {
String[] paramValue = new String[1];
paramValue[0] = requestParamMap.get(oneParamName);
actionContextParams.put(oneParamName,paramValue);
}
actionContext.put(ActionContext.PARAMETERS,actionContextParams);

return actionContext;
}

/**
* create a bean from the object factory (all wired up from Spring)
*
* @param beanName the name of the bean to get from the object factory
* @param extraContent any extra content information to pass along to the bean building
* process
* @return the object factory created bean
* @throws Exception on processing, configuration errors, test failure
*/
public Object createBean ( String beanName, Map extraContext )
throws Exception {
return ObjectFactory.getObjectFactory().buildBean(beanName,extraContext);
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace, Map actionContext)
throws Exception {
return createActionProxy(actionName,actionNamespace,actionContext,new HashMap());
}

/**
* create an action proxied by it's interceptor stack
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @param sessionMap the request/invocation session map (for http session map mocking)
* @return the proxyed action
* @throws Exception on processing, configuration errors, test failure
*/
public ActionProxy createActionProxy ( String actionName, String actionNamespace, Map actionContext, Map sessionMap )
throws Exception {
ActionProxy actionProxy = ActionProxyFactory.getFactory().createActionProxy(_configurationManager.getConfiguration(),actionNamespace,actionName,actionContext);

// set the session map in the action proxy's invocation
actionProxy.getInvocation().getInvocationContext().setSession(sessionMap);

return actionProxy;
}

/**
* create an action object, bypass all it's stacks. Have it properly injected
* according to configurations.
*
* @param actionName the name/id for the action
* @param actionNameSpace the namespace for the action
* @param actionContext the action context for creating the proxy (created from buildActionContext)
* @return the properly injected action
* @throws Exception on processing, configuration errors, test failure
*/
public Object createAction ( String actionName, String actionNamespace, Map actionContext )
throws Exception {
// get ahold of the action's configuration via the XWorkConfigRetriever class
ActionConfig actionConfig = _configurationManager.getConfiguration().getRuntimeConfiguration().getActionConfig(actionNamespace,actionName);

// create one instance of the action to test using the object factory, pass in action config and context
return ObjectFactory.getObjectFactory().buildAction(actionName, actionNamespace, actionConfig, actionContext);
}



And using it to create any bean:


MyClass myObject = (MyClass)StrutsTestCaseSupport.getInstance().createBean("myBeanName",new HashMap());


Or to do a full fledged Struts 2.0 action proxy test:


// create action for ActionSearchTest
Map requestParameters = new HashMap();
requestParameters.put("searchMode","quick");
requestParameters.put("searchText","Testing");
Map actionContext = StrutsTestCaseSupport.getInstance().buildActionContext("struts.assisrosa.com","get","/search/results",requestParameters);

// create the proxy for the action, this encapsulates all
// the interception stack up to the real action
ActionProxy proxy = StrutsTestCaseSupport.getInstance().createActionProxy("results","/search",actionContext);

// let the full stack run
String result = proxy.execute();

// confirm result, any exception thrown will cause test to fail
assert result.equals("success");



Or a Struts 2.0 action test (no proxy in front of it):


MyAction myAction = (MyAction) StrutsTestCaseSupport.getInstance().createAction("hello","/site",actionContext);
String result = myAction.execute();
assert result.equals("myExpectedResult");

Monday, February 12, 2007

Continous Integration

Continuous integration is a practice introduced by Extreme Programming (XP) that brings in the idea that developers should check in their code often and that their work should be continuously integrated and tested to ensure that no error goes unnoticed (see article by Martin Fowler). The practice is tightly integrated with the concepts of source control, unit testing and automated building as these are the privileged means of bringing the code together and running tests on it. Continuous integration (CI) not only works nicely but does enforces some pretty important habits to developers.

  • Developers working on a CI project will be required to use source control for their code. Sounds like a basic tool for any development project but I've seen too many projects where source control is completely absent...CI simply requires it...nice.
  • Developers working on a CI project will be rewarded by putting in place as many unit tests as possible. How ? By seeing that less errors will sneak by because of the existence of this safety net of testing. It is pretty cool to see that you avoid putting mistakes into production because of this first line of defense. Better our CI screaming at us than our clients right ?
  • Developers working on a CI project get used to the concept of automated building and the concept of building from scratch. Again, often have I seen in the past cases where code that is not rebuilt regularly from scratch becomes too entangled in dependencies that prevent it from building from a clean slate.
All these make for pretty strong points in favor of CI. And it is not that hard to put it in place...There is a significant amount of tools that joined together make for a great CI platform. Just look around. A winning combination for me, developing in Java, has been:

  • Cruise Control. A CI framework that glues together all the components to provide a pretty decent CI setup. From HTML reporting to email notification of success failures of builds, this is a pretty cool tool to use.
  • Subversion. A version control system that addresses a lot of the typical weak spots of other version control systems (e.g. CVS).
  • Ant. An automated build tool for Java. I doubt anyone working in Java never heard of Ant.
  • TestNG. A really cool testing framework for Java. Check it out...
All of the above are free tools that you can get and play with, together they make up for a pretty strong CI.

CI takes a step ahead when talking about Continuous Database Integration (CDBI). Paul Duvall at Test Early has been doing some pretty interesting presentations on it. It does bring a new level of testing to your database-driven apps. One which enforces the cooperation between developers and database administrators and builds a structure that ensures that you can deploy your application at any point in time. If you ever seen a project where to redeploy in a new system you have to go chasing for the DB schema required to deploy a clean system, you know the kind of sorrows CDBI can save you from.

I simply *guarantee you* that if you ever start using CI, you will not want to work again without it.

Wednesday, February 07, 2007

Javascript: The Definitive Guide

The title does sound presumptuous but after reading it you can only agree that if there is any Javascript book worth reading, this is it. David Flanagan does a most excellent work of introducing the Javascript language and of exploring all the kinks and nice features that you can take from it.

Having known Javascript for sometime I could not avoid being wowed in some chapters by some cool features that I really did not know existed in the language.

This 5th edition is well worth it even if you read previous editions. I did have a previous version and found that this new edition brings a significant amount of new content worth spending your budget in.

The book is well structure dividing it's presentation in core javascript and client-side javascript. Something I really liked seeing.

The core Javascript section presents pretty successfully crucial aspects of the language like closures and prototyping (among many others)...Even if you already know Javascript you should give it a try...I'm almost sure you will learn something from it.

The client-side Javascript is where this book gets even more of its value... from a fantastic CSS reference chapter (I do not believe you need much more than this to get you rolling with CSS), to even handling, DOM navigation, XML handling and scripting with Java, Flash, charting with Javascript and CSS (way cool). A really interesting read.

Like I said before, if you're out to get just one (or your first) Javascript book, I believe this is it! An interesting and absolutely essential read for any web developer these days.

5 stars!