Monday, December 29, 2008

Preventing NullPointerException

When a NullPointerException (NPE) is thrown, it can be hard to trace back to the bug that causes it, especially if it comes from an instance variable of a mutable class, where the execution of buggy code may have finished long before the NPE is thrown. With the wide adoption of the best practice of immutability and IoC container, it is not seen as frequently as before and most of the time it is easy to locate the bug, such as a missing setter injection in the Spring application context. Probably as a result, recently some developers seem to have relaxed on null checking as I have seem some codes in an open soure project that completely lacks both null checking and documentation of the preconditions on methods.

This week, I have seen two good practices (IMHO) of null checking and related documentation:

The first is a static method named T checkNotNull(T reference, String referenceName) in class Preconditions I found in the Activity Stream project. This class is modelled after a similar class from Google Collections API. Note that it does not share any of its source code.

This reminds me of the static methods void notNull(Object object) and void notNull(Object object, String message) from Validate class in Apache Commons Lang that are used a lot in my previous job for null checking:

import static org.apache.commons.lang.Validate.notNull;
...
public class Foo
{
private final Bar bar;

public Foo(Bar bar)
{
notNull(bar, "Bar must not be null.");
this.bar = bar;
...
}
...
}

The T checkNotNull(T reference, String referenceName) has the advantage of returning the parameter, thus it can be assigned right after the null checking:

import static com.atlassian.streams.util.Preconditions.checkNotNull;
...
public class Foo
{
private final Bar bar;

public Foo(Bar bar)
{
this.bar = checkNotNull(bar, "Bar");
...
}
...
}

Similar to null checking, quite often there is also a need to guard against empty String, blank String (containing only whitespaces, see StringUtils), empty Collection, null-containing Collection (containing a null element), empty Array, null-containing Array and empty Map, etc.

Luckily, Validate provides most of these checks:
  • void notEmpty(Collection collection, String referenceName)
  • void noNullElements(Collection collection, String referenceName)
  • void notEmpty(String string, String referenceName)
  • void notEmpty(Object[] array, String referenceName)
  • void noNullElements(Object[] array, String referenceName)
  • void notEmpty(Map map, String referenceName)

Unfortunately, they all return void and cannot be chained, and it can force you to write your own little static method to do all the checks, or use it before the proper check:

import static org.apache.commons.lang.Validate.*;
...
public class Foo extends Bar
{
public Foo(List list)
{
super(notEmptyNoNullElements(list));
}

private static notEmptyNoNullElements(Collection collection)
{
notEmpty(collection);
noNullElements(collection);
}
...
}

So I have added the following methods to Preconditions:
  • <T, C extends Collection<T>> C notEmpty(C collection, String name)
  • <T, C extends Collection<T>> C noNullElements(C collection, String name)
  • <T, C extends Collection<T>> C notEmptyNoNullElements(C collection, String name)
  • <T> T[] notEmpty(T[] array, String name)
  • <T> T[] noNullElements(T[] array, String name)
  • <T> T[] notEmptyNoNullElements(T[] array, String name)
  • <K, V> Map<K, V> notEmpty(Map<K, V> map, String name)
  • String notBlank(String text, String name)

The other example is actually in the Google Gadgets API. All the optional parameters are prefixed with "opt_", making it very obvious. So instead of just documenting in javadoc, we can also given a more intention-revealing name to parameters, such as:

/**
* Do something.
* @param bar Used to do something. Cannot be <code>null</code>. Mandatory...
* @param baz Used to do something. Can be <code>null</code>. Optional...
*/
public void foo(Bar bar, Baz optBaz)
{
...
}

Finally, Preconditions probably should not sit in streams-core. It would be more useful if it is moved to some core projects, such as atlassian-core, so that different teams do not have to reinvent the wheel.

Wednesday, July 23, 2008

Unable to install equinox p2 plugins for Eclipse Ganymede

I have been trying to install "build utility feature for equinox p2 plugins", on which Spring IDE Eclipse plugin and M2Eclipse plugin have a dependency. But I'm constantly getting an "Invalid zip file format" error:

An error occurred while collecting items to be installed
Error closing the output stream for master-equinox-p2/org.eclipse.update.feature/1.0.0.v20080506-4--8Mc44yANsYbyiqu-z-uDo0 on repository file:/C:/eclipse-3.4-ganymede/.
Error unzipping C:\DOCUME~1\Alex\LOCALS~1\Temp\master-equinox-p2_1.0.0.v20080506-4--8Mc44yANsYbyiqu-z-uDo044914.jar: Invalid zip file format


It seems to be unable to download a good copy of master-equinox-p2 JAR file. I have cleaned up all the temporary internet files and tried to install on a different machine but still no luck. I suspect that the mirror site is corrupted, probably with a bad signature or something. But it doesn't seem to allow me to choose mirror site in Ganymede any more. Urrghh!

Saturday, June 14, 2008

Which is the hottest Java web framework that people want to learn?

A recent post on The "Break it Down" Blog, Which is the Hottest Java Web Framework? Or Maybe Not Java? has attracted lots of attention, including that of Java Web Frameworks Guru Matt Raible.

The author excluded Tapestry and Stripes because of the high noise from the common usage of these terms.

Some readers, including myself, commented that the high search rate may reflect that some frameworks, like JSF, especially JSF 1.1, are so bad that people encounter problems all the time and have to rely on Google to search for solutions.

In order to find out how people really like to learn about those frameworks mentioned in the post, I tweaked the search terms a bit by adding the word "tutorial", as I reckon anyone who wants to learn a web technology is likely to search for a tutorial on that technology, only if they speak English...

And here are the result.

Comparing JSF, Struts 2, Spring MVC / Spring Webflow, JBoss Seam and Apache Wicket:

As we can see, JSF is much more popular than all other Java web frameworks and is very popular in India, Hong Kong, Czech, Singapore and the Philippines. Struts 2 ranked second, slightly better than Spring MVC and Seam. And Wicket didn't even have enough search volume to rank.

Then I compared Struts 2, Spring MVC / Spring Webflow, JBoss Seam, Tapestry and Grails:

Basically, there is little search volume for tutorials on Grails, Tapestry and Stripes (not shown in this image). This is probably an indication that the official website for these frameworks have good documentation and tutorials, where JSF is only a specification and you have to find tutorials elsewhere.

Here the interesting observation is: Struts 2 is very popular in India and Brazil; Seam is far more popular in Austria, Spring MVC is especially popular in London and Grails has been taken up quite well in Germany. And surprsingly, even Australia has more search volumes for these frameworks than the United States.

Finally, I compared Ruby on Rails, Adobe Flex, JSF, Struts 2 and Spring MVC / Spring Webflow:



Hmmm, Ruby on Rails, Adobe Flex and JSF are popular to the same level. However, the popularity of JSF has been on a plateau for the past few years and has begun to decline. Ruby on Rails also starts to show signs of decline, while Flex is rising sharply.

And Ruby on Rails are very popular in San Francisco and San Jose, CA, the Philippines and Sweden. And Flex is rather popular in Brazil and Europe.

Saturday, March 8, 2008

More on How to make Acegi work on Sun Application Server 8.x?

Applying the solution mentioned in my previous post IMHO: How to make Acegi work on Sun Application Server 8.x? has been proved working quite well. Well, until I encountered the same ClassCastException again. This time it was because the access was denied. Looking into the stack trace, I found that it was because Acegi's AccessDeniedHandlerImpl forwards the SecurityContextHolderAwareRequestWrapper to show the error page.

Similar to the solution in the previous post, I creates a NullPrincipalAccessDeniedHandlerImpl, which extends Acegi's AccessDeniedHandlerImpl but wrap the HttpServletRequest with the NullPrincipalHttpServletRequestWrapper.

Following is the source code of NullPrincipalAccessDeniedHandlerImpl:

package au.net.ozgwei.util.spring.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.ui.AccessDeniedHandlerImpl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import au.net.ozgwei.util.httpservlet.NullPrincipalHttpServletRequestWrapper;

/**
* An Acegi AccessDeniedHandler implementation designed specific to get around
* the bug in Sun Application Server 8.x where a custom security framework's
* implementation of Principal is casted to Sun Application Server's own
* implementation.
*
* @author Alex
* @version 1.0
*/
public class NullPrincipalAccessDeniedHandlerImpl extends
AccessDeniedHandlerImpl {

@SuppressWarnings("unused")
private static final Log log =
LogFactory.getTrace(
NullPrincipalAccessDeniedHandlerImpl.class);

/**
* Default no-arg constructor.
*/
public NullPrincipalAccessDeniedHandlerImpl() {
super();
}

@Override
public void handle(ServletRequest aRequest, ServletResponse aResponse,
AccessDeniedException aAccessDeniedException) throws IOException,
ServletException {

super.handle(new NullPrincipalHttpServletRequestWrapper(
(HttpServletRequest)aRequest), aResponse, aAccessDeniedException);
}

}

Of course, the Spring application context must be changed to replace the original AccessDeniedHandler implementation with this in the definition of the exceptionTranslationFilter bean:
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<ref local="authenticationProcessingFilterEntryPoint"/>
</property>

<property name="accessDeniedHandler">
<bean class="au.com.cardlink.common.util.spring.security.NullPrincipalAccessDeniedHandlerImpl">
<property name="errorPage" value="/faces/ForbiddenAccess.jsp"/>
</bean>
</property>

</bean>

Now it works even if user's access to a protected URL is denied by Acegi.

Friday, February 22, 2008

12 Technologies I Would Like to Grasp in 2008

  1. Grails
  2. Groovy
  3. OSGi
  4. Spring Batch
  5. Spring Security 2.0
  6. Spring Web Services
  7. AspectJ
  8. JBoss Seam
  9. RichFaces
  10. Mule
  11. JavaFX
  12. Selenium

Wednesday, February 6, 2008

Grails 1.0 is Out!

Grails 1.0 was finally released yesterday!
To quote from the official website: "The Search Is Over!"
After a long and exciting wait, Grails, the response to Ruby on Rails (RoR) from the Java land, has reached maturity.
It adopts "Convention over Configuration" (CoC), which has been made popular by RoR.
It's built on top of solid frameworks, such as Spring, Hibernate & Sitemesh, allowing developers to quickly develop web application with a focus on CRUD operations on database.
It also has a healthy plugin system to allow contributors to develope plugins, such as Acegi plugin.
When developing with Grails, you program in Groovy, a powerful scripting language that runs seamlessly on the JVM. It has all the powers that Ruby has and maybe more.
Enjoy the journey to Grails! I'm sure I will.

Monday, January 21, 2008

How to make Acegi work on Sun Application Server 8.x?

We are currently developing an application that employs Acegi and runs on Sun Application Server 8 (Sun AS 8.x).

Being a mature and widely adopted security solution, we did not have many problems until we encountered the following puzzling exception:
[#|2008-01-18T16:58:28.984+1100|SEVERE|sun-appserver-pe8.2|javax.enterprise.system.container.web|_ThreadID=22;|ApplicationDispatcher[/express_portal] Servlet.service() for servlet jsp threw exception
java.lang.ClassCastException: org.acegisecurity.providers.UsernamePasswordAuthenticationToken
at com.sun.web.server.J2EEInstanceListener.handleBeforeEvent(J2EEInstanceListener.java:130)
at com.sun.web.server.J2EEInstanceListener.instanceEvent(J2EEInstanceListener.java:68)
at org.apache.catalina.util.InstanceSupport.fireInstanceEvent(InstanceSupport.java:300)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:482)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:417)
at org.apache.catalina.core.ApplicationDispatcher.access$000(ApplicationDispatcher.java:80)
at org.apache.catalina.core.ApplicationDispatcher$PrivilegedForward.run(ApplicationDispatcher.java:95)
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:313)
at org.acegisecurity.ui.AccessDeniedHandlerImpl.handle(AccessDeniedHandlerImpl.java:65)
at org.acegisecurity.ui.ExceptionTranslationFilter.handleException(ExceptionTranslationFilter.java:166)
at org.acegisecurity.ui.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.providers.anonymous.AnonymousProcessingFilter.doFilter(AnonymousProcessingFilter.java:125)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:81)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.ui.AbstractProcessingFilter.doFilter(AbstractProcessingFilter.java:217)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.ui.logout.LogoutFilter.doFilter(LogoutFilter.java:106)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter(HttpSessionContextIntegrationFilter.java:229)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.concurrent.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:95)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:274)
at org.acegisecurity.util.FilterChainProxy.doFilter(FilterChainProxy.java:148)
at org.acegisecurity.util.FilterToBeanProxy.doFilter(FilterToBeanProxy.java:98)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.ApplicationFilterChain.access$000(ApplicationFilterChain.java:55)
at org.apache.catalina.core.ApplicationFilterChain$1.run(ApplicationFilterChain.java:161)
at java.security.AccessController.doPrivileged(Native Method)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:263)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:225)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:173)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:132)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:551)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:933)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:189)
at com.sun.enterprise.web.connector.grizzly.ProcessorTask.doProcess(ProcessorTask.java:604)
at com.sun.enterprise.web.connector.grizzly.ProcessorTask.process(ProcessorTask.java:475)
at com.sun.enterprise.web.connector.grizzly.ReadTask.executeProcessorTask(ReadTask.java:371)
at com.sun.enterprise.web.connector.grizzly.ReadTask.doTask(ReadTask.java:264)
at com.sun.enterprise.web.connector.grizzly.TaskBase.run(TaskBase.java:281)
at com.sun.enterprise.web.connector.grizzly.WorkerThread.run(WorkerThread.java:83)
|#]


We traced the application server and found that it was likely caused by the Sun AS 8.x trying to cast the Acegi-implemented Principal to an internal Sun AS implementation.

We did some googling and found that Andrey Grebnev blogged about this two years ago, and he suggested a workaround by overriding the getUserPrincipal() method (of SecurityContextHolderAwareRequestWrapper) by always returning a null.

Because SecurityContextHolderAwareRequestWrapper and its subclasses are used internally by Acegi, his workaround implied changing the source code of Acegi, which we are reluctant to do.

Realizing that the it was only unsafe for the HttpServletRequest to return the Acegi-implemented Principal when the servlet filter chain has been executed and the control is handed over to the AS and the running application, we came up with a NullPrincipalFilter that wraps the incoming HttpServletRequest with a NullPrincipalHttpServletRequestWrapper, which returns null for getUserPrincipal(), and hands the control over to the AS. This filter should always be placed at the end of the filter proxy chain in Acegi. And of course, the application must not use HttpServletRequest's getUserPrincipal() method to retrieve the user principal, which is very easy to do, as it can invoke SecurityContextHolder.getContext().getAuthentication() to achieve the same goal, without coupling to the Servlet API.

The following is the source code of NullPrincipalFilter:

package au.net.ozgwei.util.httpservlet;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* A filter designed specific to get around the bug in Sun Application Server
* 8.x where a custom security framework's implementation of Principal is
* casted to Sun Application Server's own implementation.
*
* @author Alex
* @version 1.0
*/
public class NullPrincipalFilter implements Filter {

@SuppressWarnings("unused")
private static final Log log = LogFactory.getLog(NullPrincipalFilter.class);

/* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
}

/* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aFileterChain) throws IOException, ServletException {
aFileterChain.doFilter(
new NullPrincipalHttpServletRequestWrapper(
(HttpServletRequest) aRequest), aResponse);
}

/* (non-Javadoc)
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig aArg0) throws ServletException {
}

}


And the source code of NullPrincipalHttpServletRequestWrapper:

package au.net.ozgwei.util.httpservlet;

import java.security.Principal;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* A HttpServletRequestWrapper that always return null for Principal.
*
* @author Alex
* @version 1.0
*/
public class NullPrincipalHttpServletRequestWrapper extends HttpServletRequestWrapper {

@SuppressWarnings("unused")
private static final Log log = LogFactory.getLog(
NullPrincipalHttpServletRequestWrapper.class);

public NullPrincipalHttpServletRequestWrapper(HttpServletRequest aReq) {
super(aReq);
}

@Override
public Principal getUserPrincipal() {
return null;
}

}


And a nullPrincipalFilter bean should be defined in the Spring application context and added to the end of the filterChainProxy, as following:
<bean class="org.acegisecurity.util.FilterChainProxy" id="filterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=concurrentSessionFilter,httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,nullPrincipalFilter
</value>
</property>
</bean>

<bean class="au.net.ozgwei.util.httpservlet.NullPrincipalFilter" id="nullPrincipalFilter">


Finally, this bug has been fixed in GlassFish eventually.

Wednesday, January 16, 2008

Gavin King: first impression (contrasting Spring guys)

Today I attended Red Hat's "Gavin King" event. While I have attended many Spring events in Sydney, this is my first time attending a Hibernate/JBoss/Red Hat event. And the impressions are quite different...

  • First impression - clothes:
    • The Spring guys are always well-suited and businessmen-like. They are often the centre of the crowd.
    • Gavin's clothes were the most casual in the room. Before his presentation, I thought that fitted T-shirt wearing guy in trendy jeans was some skateboarding Ruby programmer who happened to want to know something in the Java world...
  • Presentation style:
    • The presentations by the Spring guys are always very professional, with the right level in technology details according to the nature of the event, the structure well organised, and seemingly well rehearsed.
    • Gavin's presentation is, again, more casual, just like a technology chat. I don't know how much the other people in the audience know about Web Beans and Seam, but Gavin lost me a few times because I haven't been following what's happening in the JBoss world...
  • Technology inclination:
    • The Spring guys can be very pedantic (in a good way), always emphasising best practices, such as programming to interfaces, abstraction levels, separation of concerns, etc.
    • Gavin is more pragmatic. Interfaces did not even make it to his slides. His Web Beans JSR recommends to make business interface optional for EJB 3.1 in Java EE 6. He reckons AOP is too complex for ordinary Java developers, there are only a handful of cross-cutting concerns, and EJB interceptors are enough to get the job done.
    • Spring framework focuses on enterprise applications that are typically developed by financial institutions and usually involves lots of web services and enterprise application integration, and some of these enterprise applications may not even have a web tier.
    • Web Beans JSR, JBoss Seam and Rich Faces, promoted by Gavin, are all mostly relevant to web-focused projects. Web Beans and JBoss Seam are particularly designed to ease development burden on entity management website with lots of CRUD operations, which make them competitors of (J)Ruby on Rails and Grails. I'll try to compare these frameworks in a later post. Enterprise applications seem off the target.
    • The apparent weakness in the Spring framework are: no type-safety check in the application context until runtime, verbose XML configuration, no bean id (or name) namespaces and the statelessness of Spring-managed beans. However, the first two have been addressed by JavaConfig and XML namespace. Noticeably, JavaConfig also uses annotations, but only in the config class without polluting the service bean or the service client. The stateless singleton issue has also been tackled with 'scopes' and domain object dependency injection, which is enabled by Spring AOP.
    • Gavin loves type-safety check in Java, so he hates the lack of type-safety check in Spring XML configuration, and he embraces annotation wholeheartedly. So he prefers Google Guice to Spring for dependency injection. In contrast to Spring's JavaConfig, Google Guice's annotations are used everywhere, in the service bean, in the service client or both. JBoss Seam introduces lots of annotations, and Web Beans JSR is to make many of these annotations into Java EE standard. I don't remember how many times Gavin showed the definition of an annotation in his slides today. Probably a dozen! And he still relies on XML configuration to override annotations. He classifies services beans according to deployment, such as one for production, one for stubbing in testing. So what will you do if you have two classes with the same service API, both used in production environment? My guess is you need to write a new annotation to differentiate them... Seems overuse of annotations, doesn't it?
  • Hostility:
    • Spring guys rarely publicly show their hostility towards JBoss, though in after session chats, they describe JBoss Seam as a "big hack", "annotation hell", "technologically inferior" and "would have been just another web framework were it not for Gavin King's fame".
    • Gavin is more straight forward, rubbished Spring guys as "AOP nerds" during the session, and I wouldn't be surprised if he called Spring "XML hell". He deliberately omitted Spring when he enumerated the open source frameworks that have influenced Java EE.
  • The audience:
    • Spring events usually draw a huge audience. Many times, some people who came late had to stand in the back of the room for the whole session. They are almost always held in the evening.
    • Today's Hibernate event was held in the morning with only a few dozen people attending. One-third of the seats were empty.

Anyway, I was pretty impressed by Gavin's demo of fast web project development with JBoss AS, JBoss Seam, Rich Faces and JBoss Tools. I'll definitely give it a try when I have the time...