Wednesday, December 19, 2007

Stripes (1.5) Updates Are Coming...

There's been a lot going on with Stripes lately. A lot of users have been asking when 1.5 is due out. I don't believe there is a set date yet but I do know that something is coming quickly. There's a lot of new features and improvements in the trunk right now that are making it into 1.5. I'm going to try and get together a list and post them here with explinations.

Also, there will be a new website (http://www.stripesframework.org). The current website (wiki) will still be there with all the wonderful documentation that Stripes is known for but now there will be a more elegant entry point.

So check back and I'll see if I can gather up that feature/improvement list for my next post.

Friday, November 16, 2007

Stripes TypeConverters: Populating Domain Objects

Most web frameworks today have type converters. Type converters are chunks of code that takes query parameters as Strings and convert them into their appropriate data type. So numbers become Integers, dates become Dates, etc. Most frameworks also allow you to create custom type converters. For example, you might want to convert a currency form field into a Money object. That's all well and good. But we can take it a bit further with Stripes.

A common scenario in the web world is requesting data from the server and displaying it on the page. I mean, really, all web applications do is send data and get data and display the data they got. Nothing too complex. How we populate domain objects based on query parameters is where everyone's code can differ. Most common is for an action to process the query parameters and we get the one(s) we need and use them to lookup information in a database, shove those into some domain objects or properties of some sort, throw them in the request and forward to our view. Again, nothing too complex.

There are many times when we need to populate the same domain object(s) based on the same parameter on many different pages. I'll set up a scenario that we can use throughout the rest of this article. You created a User management screen. Displaying a list of users you want to edit one. The query parameter you pass in is the user.id. So we might have some code in our Stripes action bean that looks something like this:


private User user;
private UserDao userDao;

public User getUser()
{
return this.user;
}

public void setUser(User user)
{
this.user = user;
}

@SpringBean("userDao")
public void setUserDao(UserDao userDao)
{
this.userDao = userDao;
}

public Resolution edit()
{
user = UserDao.getUser(user.getId());
return new ForwardResolution(EDIT_USER_PAGE);
}


We could of course also discuss the pros and cons of doing the same thing in a method annotated with @After/@Before but for the sake of simplicity we'll say that's possible as well and move on.

So the client comes back and decides that they need an additional page where the administrator can click on a product and see all the users that purchased this product. And further more he can click on a user and view some details about that user. We begin to build the new action and view and notice that we've got to duplicate some code we just wrote. We need the UserDao again and we need to do a lookup so we copy and paste, test it, it works. Yea! And then we need another page, and another page, and we copy and paste and copy and paste. At this point we've got the same code written 10 times. That's bad. So we decide hey, we are using Stripes, let's use a TypeConverter to do this.

Stripes has a TypeConverter interface that requires two methods:

convert(String input, Class targetType, Collection errors)
setLocale(Locale locale)

We are only going to worry about convert right now. So we begin our custom type converter:


public class UserTypeConverter implements TypeConverter
{
private UserDao userDAo;

@SpringBean("userDao")
public void setUserDao(UserDao userDao)
{
this.userDao = userDao;
}

public User convert(String id, Class user, Collection error)
{
if (id != null)
{
User user = userDao.getUser(id);
return user;
}
return null;
}
}


This is pretty straightforward. We are just looking the user up from our UserDao using an id. But how does that id get there? Pretty simple as well. Let's look at our action again, only this time using the converter.


User user;

public void setUser(User user)
{
this.user = user;
}

public User getUser()
{
return this.user;
}

public Resolution edit()
{
return new ForwardResolution(EDIT_USER_PAGE);
}


And the URL might looking something like this:

http://localhost:8080/app/User.action?edit=&user=1234

Anytime the id is not null, user gets populated. If it is null, for example when adding a new user, user will only be populated from the normal form to object binding that Stripes normally does. But how does our action know that User is supposed to use the UserTypeConverter? Well, if we weren't needing the Spring Bean injection magic would could have simply done this in our action.

@Validate(converter=UserTypeConverter.class)
User user;

But since we need the Spring Bean magic we need to create our own TypeConverterFactory. Again, another interface. Stripes uses a default to init all the built in type converters. Here is what one might look like:


public class CustomTypeConverterFactory extends DefaultTypeConverterFactory
{
public TypeConverter getInstance(Class clazz, Locale locale) throws Exception
{
TypeConverter tc = super.getInstance(clazz, locale);
ServletContext sc = getConfiguration().getServletContext();

SpringHelper.injectBeans(tc, sc);

return tc;
}

@Override
public void init(Configuration configuration) {
super.init(configuration);
add(User.class, UserTypeConverter.class);
}
}


And then we need to tell Stripes to use our CustomTypeConverterFactory. In the web.xml in the StripesFilter init-params add the following param-name:

TypeConverterFactory.Class

and param-value:

com.app.package.foo.CustomTypeConverterFactory

And we're done. Comments welcome.

Thursday, November 15, 2007

Spring's DAO Support..Other choices?

I've gotten really spoiled to Spring's DAO support classes, Transaction management, etc. But as any good developer should I begin to wonder what alternatives are out there? Do java developers have another choice aside from rolling our own?

Just looking for comments.

Monday, November 12, 2007

Stripes Interceptor Tutorial

Note: I still can't get code to display very well on blogger. Sorry for the formatting issues. If you need to the source code is linked at the bottom of this article if it makes it easier to follow along.

I've been using Stripes for both large and small applications for about a year. While I am of the camp that there is no golden hammer I've not run across a project I am involved with where Stripes doesn't make sense. I've been wanting to write more articles on Stripes. While the documentation on the site is great for folks to get started it is lacking in the area of best practices and general tutorials on different aspects of the framework. One of my favaorite things about Stripes (and there are many) is how easy it is to extend the framework to suit my needs and most of the time I don't even need to dig into the source. A lot of tiimes that power comes in the form of an Interceptor.

Interceptors are not a new concept. I believe my first experince with them in web applications was when working with WebWork several years ago. Stripes has a couple of Interceptors already:

  • SpringInterceptor - Allows you to inject Spring Beans into controllers using the @SpringBean method level annotation.
  • BeforeAfterMethodInterceptor - Allows you to execute methods before and/or after specific Lifecycle Stages using @Before and @After method level annotations (AOP like behavior)

Note: Both the Interceptors mentioned above work in combination with annotations but not all have to.

In this article I want to show how to create an interceptor and we'll also create an annotation that will be used with the interceptor. I'll identify a problem that Stripes by itself doesn't solve and show how to implement the solution with a simple Interceptor. At times you may see mention of concepts that you won't be familiar with if not familiar with Stripes. While I'll try and explain these a bit as I go I may link to the Stripes documentation on the subject so as not to repeat information. I also want to keep this article as concise and to the point as possible.

The Problem

Ajax is everywhere and one common problem I've run into in the past is dealing with the browser caching the response from an ajax request. This seems to happen more often than not in Internet Explorer. What we want is to prevent this as much as possible when needed.

Typical Solutions

Probably the most common talked about solution is appending a random number on the end of the request. This tricks the browser into thinking it is a different response. Wikipedia's entry on XmlHttpRequest offers a different and slightly more complex solution which you can read about here: http://en.wikipedia.org/wiki/Xmlhttprequest#Caching. The problem with that is most of us are using a JavaScript library like jQuery or Prototype and that would cause us to go hacking inside their code. Not ideal. And another approache is to add header information to the response. In java it goes something like this:

HttpServletResponse response = context.getResponse();

response.setDateHeader("Expires", 0);

response.setHeader("Cache-control", "no-cache");

response.setHeader("Pragma", "no-cache");

Finding a centralized place for that code is the key. One solution on the Stripes mailing list was having a disableCaching() method in an ActionBean base class. When you wanted this to execute you would do something like this:

@Before(stages = LifecycleStage.ResolutionExecution)

@Override

protected void disableCaching() {

super.disableCaching();

}

And while that works it is a bit more verbose than need be.

The Interceptor Solution

What we want to do is take the above code and centralize it in an Interceptor that executes on the LifecycleStage.ResolutionExecution stage however we want to be able to control when the code gets executed. To do this we need two things:

  1. @NoCache annotation - We'll use this on methods to denote we want no caching. We'll also allow a boolean argument to indicate whether it should be on or off. This is important if we want to override a class level annotation on one or more methods.
  2. NoCacheInterceptor - We'll use this to check for the @NoCache annotation and shove the no cache header directives into the response.

The @NoCache annotation is pretty straightfoward:

@Retention(RetentionPolicy.RUNTIME)

@Target( {ElementType.METHOD, ElementType.TYPE})

@Documented

public @interface NoCache

{

boolean value() default true;

}

Save that in a file called NoCache.java in whatever package you desire.

Next is the interceptor which requires a bit more discussion. A Stripes interceptor implements an interface called Interceptor. Interceptor requires one method be implemented; public Resolution intercept(ExecutionContext ctx) throws Exception. Stripes will call this method on any configured interceptor during the specified LifecycleStage. Whoa, how do we specify the LifecycleStage? Whoa!!! What's with all this LifecycleStage talk?

Segway..Stripes has five Lifecycle Stages. Instead of repeating what is already well documented I'll let you read about them at your lesiure here.

And we're back...So we need to create this Interceptor and tell it when to execute the intercept method. We do that by specifying an annotation at the class level:

@Intercepts(LifecycleStage.ResolutionExecution)

public class NoCacheInterceptor implements Interceptor {

...

}

Now we should define our intercept method:

public Resolution intercept(ExecutionContext ctx) throws Exception {
...
}

Inside this method we need to define several objects we are going to use:

(1) final Configuration config = StripesFilter.getConfiguration();
(2) final ActionResolver resolver = config.getActionResolver();
(3) final ActionBeanContext context = ctx.getActionBeanContext();
(4) final ActionBean actionBean = resolver.getActionBean(context);
(5) final Class beanClass = actionBean.getClass();
(6) final String eventName = resolver.getEventName(beanClass, context);

  1. The Stripes Configuration class holds all the configuration information passed into Stripes from the web.xml. This includes all configured Interceptors.
  2. The ActionResolver will allows us to get which Method was requested from the ActionBean during the request.
  3. The ActionBeanContext is used to help us get an instance of the requested ActionBean
  4. The requested ActionBean.
  5. We need the actual Class.
  6. The eventName or requested method to call.

The next thing we need to do is get the Method or handler if the eventName doesn't exist. The reason for this is because if you request a URL and there is no eventName specified Stripes will look for a default handler which is a method denoted by the @DefaultHandler annotation. And then if this doesn't exist we need to throw an error and inform the user.

Good Practice: All ActionBeans should have a default handler in case an eventName was not given.

final Method handler;
if (eventName != null) {
handler = resolver.getHandler(beanClass, eventName);
} else {
handler = resolver.getDefaultHandler(beanClass);
if (handler != null) {
context.setEventName(resolver.getHandledEvent(handler));
}
}

// Insist that we have a handler
if (handler == null) {
throw new StripesServletException(
"No handler method found for request with ActionBean ["
+ beanClass.getName() + "] and eventName [ "
+ eventName + "]");
}

Don't worry too much about understanding every bit of that. Just know that we need the handler and we need to throw an error if one doesn't exist. The next thing we need to do is actually look for the @NoCache annotation. If we find it we set the no cache header junk and tell the ExecutionContext to proceed with the request.

if (isCachingDisabled(handler, beanClass)) {
HttpServletResponse response = context.getResponse();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-control", "no-cache");
response.setHeader("Pragma", "no-cache");
}
return ctx.proceed();

Now we'll look at the isCachingDisabled method. We pass this method the handler and also the beanClass.

protected boolean isCachingDisabled(Method method, Class beanClass) {
....
}

To make sure that we are as performant as possible Stripes caches interceptor instances. Because of this we can also cache data within the interceptor. In this case once a @NoCache annotation has been found we want to cache this fact so the next time this interceptor is run with this beanClass we can just check the cache and not have to go through the annotation reflection checks. We first want to check the handler (Method) being called because it override class level annotations. If no annotation is found then we check for a class annotation.

So first, lets check the cache:

CacheKey cacheKey = new CacheKey(method, beanClass);
Boolean disabled = cache.get(cacheKey);
if (disabled != null) {
return disabled;
}

Don't worry about CacheKey yet. It's a simple class and we'll discuss it last. So if we found cache we return the value we found. Otherwise, we need to check for the annotation:

NoCache annotation = method.getAnnotation(NoCache.class);
if (annotation != null) {
disabled = annotation.value();
} else {
// search the method's class and its superclasses
Class clazz = beanClass;
do {
annotation = clazz.getAnnotation(NoCache.class);
clazz = clazz.getSuperclass();
} while (clazz != null && annotation == null);

if (annotation != null) {
disabled = annotation.value();
} else {
disabled = false;
}
}

So basically we look for an annotation on the method. If we find it we return its value. Otherwise, we check the class. We also want to check all super classes incase the user is extending a Base ActionBean of sorts.

Good Practice: Creating a BaseActionBean and having all your ActionBeans extend that base class will save a lot of boilerplace code and make developing with Stripes a lot simpler.

So once we either find an annotation or not we want to cache it and then return what we found:

cache.put(cacheKey, disabled);
return disabled;

The last thing for code is the CacheKey class. We want to make sure that we store unique keys when checking for cache. That way we always know we have the correct cache for the correct beanClass that was requested. Here's the class. I just make it an inner class of the interceptor.

private static final class CacheKey {
private Method method;
private Class beanClass;
private int hashCode;

public CacheKey(Method method, Class beanClass) {
super();
this.method = method;
this.beanClass = beanClass;
this.hashCode = method.hashCode() * 37 + beanClass.hashCode();
}

@Override
public boolean equals(Object obj) {
CacheKey that = (CacheKey) obj;
return this.method.equals(that.method)
&& this.beanClass.equals(that.beanClass);
}

@Override
public int hashCode() {
return hashCode;
}

@Override
public String toString() {
return beanClass.getName() + "." + method.getName();
}
}

Note: In Stripes 1.5, yet to be released, there will be a core set of interceptors on by default. This NoCache interceptor will be one of them along with BeforeAfterMethodInterceptor.

And that's it. Now to use it you might do something like this:

public class SomeActionBean extends BaseActionBean {

@NoCache
public Resolution ajaxEvent() {
// return some ajaxy stuff here
}
}

If you needed all but one method to turn caching off you might do something like this:

@NoCache
public class SomeActionBean extends BaseActionBean {

@NoCache(false)
public Resolution ajaxEvent() {
// return some ajaxy stuff here
}
}

I've included the full source for the interceptor and annotation at the following URL.

NoCache.tar.gz


Monday, October 15, 2007

JavaRebel: Interesting but still iffy

I tried JavaRebel today and for the most part it does what it says. It was really cool to see a single class updated in Tomcat without Tomcat reloading the application. However, there were a couple of gotchas that I am not sure how to overcome if possible at all.

1. For some reason a fellow developer where I work needed to get the package name of a class at runtime. So we have getClass().getPackage().getName(); Once JavaRebel is in place getPackage() returns null. Remove JavaRebel and it works fine. I assume this has something to do with a class wrapper.

2. I primarily use Stripes and one of the things Stripes tries to do is cache configuration information at startup. For example, to inject a Spring Bean into a Stripes ActionBean I simply provide an @SpringBean for a bean setter. If I decide to inject a new/different Spring Bean into the ActionBean, even though it already exists in the spring context, the class reloads but Stripes doesn't know about it. Is this a JavaRebel flaw? No, not really. As I said, it works as stated. But this does limit the effectiveness for me.

I still think it is slick and I am anxious to see how java development improves for us web developers as JavaRebel improves.

Thursday, October 11, 2007

Stripes Presentation KC JUG Post Mortem

Last night's presentation on Stripes at the Kansas City Java Users Group went well. It was my first time doing that kind of a presentation. There was quite a bit of code to discuss and I wish I'd had more time. Overall I felt that I explained things well and I did get quite a bit of positive feedback from the attendees. I hope that I'll have the opportunity to do more of these types of presentations. I'm considering a JQuery presentation for early next year.

Monday, October 08, 2007

Halo 3 Looks Amazing

I played the original Halo on PC when it came out years ago. Never played Halo 2. Looking at Halo 3 it really looks amazing. Halo was a blast. So would I be nuts to want an XBox just so I can play Halo 3? I'm not a big gamer which is to say that when I do want to waste time I really have to like the game. So I doubt I'd buy a whole library of games if I did purchase an XBox.

Is it worth it?

Spring MVC Seems Overly Complicated

Chris Sawer wrote an article on Using Spring's MVC framework for web form validation. I haven't used Spring MVC a lot so I am not here to say he did it right or wrong. However, if he did it the right way I'm glad I am not using Spring MVC.

I thought it might be a good idea to show an alternative using the same example Chris used but I'll show mine in Stripes. I'm not going to repeat Chris' code here so you can follow along using another tab or open another browser window.

So the first thing Chris mentions beyond the web.xml file is the configuration for the controller bean in servlet configuration xml file. Stripes doesn't require controllers (Stripes calls them Action Beans) to be configured anywhere so we've already saved a bit of configuration.

Next Chris shows the SpringTestController.java and the SpringTestValidator; both of which are required to validate a form. Here is the Stripes version (Since I have no idea what properties a DataBean refers to I'll use my own object):

The code formatting didn't turn out exactly as I had planned so bare with me until I find a better solution. The gist of the post just be understandable, however.


import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.RedirectResolution;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.validation.Validate;
import net.sourceforge.stripes.validation.ValidateNestedProperties;
import org.apache.log4j.Logger;
import com.miadidas.model.User;

public class StripesTestActionBean implements ActionBean {

protected final Logger logger = Logger.getLogger(StripesTestActionBean.class);
private ActionBeanContext context;

@ValidateNestedProperties({
@Validate(field = "username", required = true),
@Validate(field = "password", required = true)
})
private User user;

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

public ActionBeanContext getContext() {
return this.context;
}

public void setContext(ActionBeanContext context) {
this.context = context;
}

public Resolution doSubmitAction() {
/* The givenData object now contains the successfully validated
* data from the form, so can be written to a database, or whatever.
*/
return new RedirectResolution("We would redirect to a new view at this point");
}
}


And now the JSP, however mine may be a bit different because of the DataBean I was unclear on:


<%@ taglib prefix="stripes" uri="http://stripes.sourceforge.net/stripes.tld"%>
<html>
<head>
    <title>Stripes Test</title>
</head>
<body>
<stripes:errors />
<stripes:form beanclass="com.test.web.action.StripesTestActionBean" method="post">
    <fieldset>
        <p>
            Username: <stripes:text name="username" />
        </p>
        <p>
            Password: <stripes:password name="password" />
        </p>
        <p>
            <stripes:submit name="doSubmitAction" value="Login" />
        </p>
    </fieldset>
</stripes:form>
</body>
</html>


And that's it. To me, its simpler. I'd like to hear what you think.

Monday, September 17, 2007

Stripes Presentation Kansas City JUG Oct 10th

I'll be doing a presentation on Stripes October 10th at the Kansas City Java Users Group. See details here.

Wednesday, September 12, 2007

Requirements Exceeding Web Capabilities

Let me start off with the fact that I like HTML. I like JavaScript. I like CSS. I'm rather proud of my abilities in combining these technologies to make some really interactive sites. The problem is that I keep seeing requirements come down that make using these age old and proven technologies extremely difficult. So there's prototype, scriptaculous, JQuery, Dojo, GWT; I could go on and on. And to a fault these libraries do ease the pain. Well, some of them. Some of them actually cause me more pain.

I'm leading a project now and I spoke with one of my developers and asked him if he thought this would make a really great Desktop application. He agreed. Unfortunately, it's not what the client wants. In fact, Its not what any client wants these days. Its all about the DubDubDub. Enter Flex and Silverlight.

I don't dislike Flex and I know it has been around for a year or so and it is going open source, yada yada and I've not even glanced at Silverlight so I won't bother commenting on it aside from saying it locks you into the .NET world. And looking at Air, linux is once again an afterthought for the flash guys.

It seems to me that web developers are sitting in a very difficult era in web development where clients want the bells and whistles of a true rich client application that runs in a web browser but aren't willing to commit to new technologies (Flex, etc) that actually make these applications possible. What's a developer to do?

Monday, September 10, 2007

Java Forums

I'm in search of a new stomping ground. I've been a member at JavaRanch for 6 years and a staff member for about 4 but I'm needing to part ways and I'm looking for a new community to be a part of and help if I can. I know about Sun's forums and Javalobby's forums but I'm not too impressed with either community. JavaRanch is full of really nice folks and I'm looking for something similar. Any suggestions?

Friday, September 07, 2007

DZone Aggregated on Javablogs?

Does anyone else think that DZone should not be aggregated on Javablogs? I find it pretty annoying that on Thursday I jump to a blog entry from Javablogs to the author's site and then on Friday I see the same blog entry on Javablogs but this time I'm jumped to DZone.

Look, I like DZone. I prefer it to Digg for my technology related information. And I have them subscribed in my Google Reader. It would be one thing if DZone published their own content and every now and again thought a blog was worth linking. I've done that. It happens. But that's not how they operate. They are another version of Javablogs and shouldn't be listed on Javablogs.

Wednesday, September 05, 2007

Invalid Property Binding Feedback

Matt Raible blogged today about why Struts 2 sucks. At the same time he concluded that Stripes sucks as well. Why? Because neither framework handle invalid property binding feedback properly. Well, let's discuss what might be proper and let's talk about Stripes because I love Stripes. I want Stripes to be the best it can be and we can find ways to improve it, let's do that.

Stripes doesn't use OGNL. When Tim designed Stripes he used OGNL in the beginning and later found out that he could write something a bit smaller and faster that was specific for Stripes and remove another dependency from the project. I think that is great. One less JAR I need to fetch.

One of the many nice things about Stripes (specifically over Struts) is the ability to create pages without wiring up the actions on the back end. with Struts (this could be different in Struts 2, someone please correct me) you had to have your form beans, validation, XML all correct and complete before you could even view your JSP. With Stripes, if a binding resource is missing it's ignored. Is this a flaw? I don't believe so.

When you submit the form or when the page requires binding, which typically isn't on view, Stripes will tell you what you've missed. It's in the debug log, not on screen. So if you had a form that required a first name and a last name. In your action bean you have two strings, firstName and lastName. On the JSP you fat finger lastName to lkastName, the form will display just fine. This is what I would want. I want to see my form. When I submit the form, this is what I get in the debug:

DEBUG Could not bind property with name [lkastName] to bean of type: FormTestActionBean : Bean class com.app.web.action.FormTestActionBean does not contain a property called 'lkastName'. As a result the following expression could not be evaluated: lkastName

And now I know what I need to fix. I'm not 100% sure that Matt views this as a flaw or if maybe he doesn't understand that Stripes does this. From his post I am led to believe that Matt wants immediate feedback if there are binding issues, even prior to submitting the form. I don't agree that this should be an issue. What do you think?

Java Desktop Jobs

After reviewing Filthy Rich Clients I got all excited about Desktop Java again. It did get me wondering though if there are jobs out there for Swing folks. I'm not looking for one but I am curious what the market is like in this time of Ajax and Flex. Does anyone have any stats or information regarding this?

Tuesday, September 04, 2007

Book Review: Filthy Rich Clients


This review will be in two parts. I was contacted about reviewing Filthy Rich Clients by Chet Haase and Romain Guy and at the same time asked to review Safari's "Rough Cuts" subscription. So first, let's get the not so good out of the way.

Safari's "Rough Cuts" is similar to Manning's MEAP (Manning Early Access Program). If you aren't familiar with either one it's basically a way to read a book as chapters are made available online. The difference, as far as I can tell, between MEAP and Rough Cuts is that chapters released through MEAP are complete whereas chapters released through Rough Cuts often times are incomplete. That isn't to say that reading the final draft of a book versus the Rough Cut version is significantly different but there were things missing like diagrams that are referred to in the text. I may be in the minority here but I actually find them useful in most cases and when a book is talking about a diagram its nice to be able to see it. Add that to my dislike of Safari's interface for reading books online and I give it a thumbs down. I'd rather just read the book when it is complete than have incomplete chapters made available.

And now on to some good news. FRC is a great book. The community has needed a book like this for a long time and I can't think of any better folks to write this kind of book than Chet and Romain. I've been following their blogs for a long time and am a big fan of all the cool things Romain has done with Swing.

FRC is a book about "Developing Animated and Graphical Effects for Desktop Java Applications". Yes, that is on the cover but I couldn't think of a better way to describe this book. FRC gives some much needed insight into the inner workings of Swing, AWT, and Java2D and how they all interact. Sure, you can scour the web and locate a lot of articles and blogs that talk about this but its nice to finally have this in one place. But don't worry, its not that deep. Its just enough to help you understand when things start getting cool later on.

The book does assume a basic understanding of desktop development with Java. If you are new to Swing you might want to get a few basics down before delving into FRC. The book reads very well, even going between the two authors, which you can easily tell who wrote what. If its technical it was probably Chet. If it was pretty it was probably Romain. Not to say that Romain isn't technical in his own right.

There are plenty of code fragments in the book to help convey the author's points. In fact, there is a lot. More than I expected. On a sad note, even though all the examples are available on the book's web site they are Netbeans projects. I download a few and tried to compile them with Ant from the command line but they always complained about Netbeans dependencies. Granted, I didn't research this much so the problem may have easily been resolved.

The book is fun and results are immediate. I've already started trying to come up with a side project for myself just to try out a lot of the techniques described in FRC. I'd recommend this book to anyone looking to spruce up their existing desktop applications or design something entirely new and original. Great work Chet and Romain!!