Using ServletContainerInitializer to configure RWT applications

11 10 2011

Today is my birthday! Happy Birthday to me… 30 years of age makes me kind of proud and thoughtful. Benjamin Franklin said:

At twenty years of age, the will reigns; at thirty, the wit; and at forty, the judgement.

And my last will – yesterday – was to think about another way of configuring RWT applications as described by Frank Appel. Of course, registering an ApplicationConfigurator as an OSGi Service is pretty easy (on IBM WebSphere Application Server, you can use BluePrint Declarative Services), but as far as the Servlet 3.0 API is available, we can maybe use the Pluggability API?

The result of my thoughts is a prototype that uses the ServletContainerInitializer mechanism that allows to install web frameworks at the web container (or within a web application) and to use the interfaces or annotations of the framework to automatically configure servlets, listeners, filters, …

The Concept

For RWT, such a ServletContainerInitializer could register the HttpServiceServlet and the ApplicationConfigurator service instances. For this, we need an interface or an annotation for the ServletContainerInitializer to recognize an RWT application to configure. We could already use the IEntryPoint interface, but this might conflict with applications implementing this interface that were already configured manually. So the best way would be to define a custom annotation that is only handled by the ServletContainerInitializer.

The Implementation

So I first created the interface (with the most important information for an application):

package org.eclipse.rap.rwt.osgi.servlet;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RWTApplication {

  /**
   * The servlet name and the name of the entry point.
   *
   * @return the servlet name and the name of the entry point
   */
  String value();

  /**
   * The title of the application shown in the browser window.
   *
   * @return the title of the application shown in the browser window
   */
  String title() default "";

}

In the web application (Web Archive Bundle, WAB), I wrote an application like this:

package org.eclipse.rap.internal.rwt.osgi.servlet;

import org.eclipse.rap.rwt.osgi.servlet.RWTApplication;
import org.eclipse.rwt.lifecycle.IEntryPoint;

@RWTApplication(value="demo", title="RWT Demo using Servlet Container Initializer")
public class DemoApplication implements IEntryPoint {

  /* (non-Javadoc)
   * @see org.eclipse.rwt.lifecycle.IEntryPoint#createUI()
   */
  public int createUI() {
    [...]
  }

}

Then, I wrote the ServletContainerInitializer. The problem is that the ServletContainerInitializer is invoked before the OSGi BundleContext is added to the ServletContext, so a ServletContextAttributeListener must be used to register the ApplicationConfigurator service instance to the OSGi platform. The HttpServiceServlet must be registered immediately, this must not be done after the web application’s startup phase (even not in a ServletContextListener registered by a ServletContainerInitializer.

package org.eclipse.rap.internal.rwt.osgi.servlet;

import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.servlet.annotation.HandlesTypes;

import org.eclipse.rap.rwt.osgi.servlet.RWTApplication;
import org.eclipse.rwt.application.ApplicationConfiguration;
import org.eclipse.rwt.application.ApplicationConfigurator;
import org.eclipse.rwt.branding.AbstractBranding;
import org.eclipse.rwt.lifecycle.IEntryPoint;
import org.osgi.framework.BundleContext;

@HandlesTypes(RWTApplication.class)
public class OSGiServiceInitializer implements ServletContainerInitializer {

  private static final Logger logger = Logger.getLogger(OSGiServiceInitializer.class.getName());

  private static final String BC = "osgi-bundlecontext";
  private static final String SERVLET_CLASS_NAME = "org.eclipse.equinox.http.servlet.HttpServiceServlet";
  private static final String SERVLET_NAME = "OSGiHttpServiceServlet";
  private static final String SERVLET_MAPPING = "/*";

  private static final String DYNAMICALLY_ADDED_SERVLET_ATTRIBUTE = "org.eclipse.rap.rwt.osgi.DYNAMICALLY_SERVLET";
  private static final Object DYNAMICALLY_ADDED_SERVLET_VALUE = Boolean.TRUE;

  public OSGiServiceInitializer() {
    super();
    logger.log(Level.INFO, "OSGiServiceInitializer registered to Web Container");
  }

  private static void addHttpServiceServlet(final ServletContext ctx) {
    // check if already registered
    final Map<String, ? extends ServletRegistration> servletRegistrations = ctx.getServletRegistrations();
    for (final Map.Entry<String, ? extends ServletRegistration> entry : servletRegistrations.entrySet()) {
      final ServletRegistration reg = entry.getValue();
      if (SERVLET_CLASS_NAME.equals(reg.getClassName())) {
        return;
      }
    }
    // register servlet
    ctx.addServlet(OSGiServiceInitializer.SERVLET_NAME, OSGiServiceInitializer.SERVLET_CLASS_NAME).addMapping(OSGiServiceInitializer.SERVLET_MAPPING);
    // set context attribute for dynamic registration
    ctx.setAttribute(OSGiServiceInitializer.DYNAMICALLY_ADDED_SERVLET_ATTRIBUTE, OSGiServiceInitializer.DYNAMICALLY_ADDED_SERVLET_VALUE);
  }

  private static void configure(final ServletContext ctx, final BundleContext bc, final Set<Class<? extends IEntryPoint>> applications) {
    for (final Class<? extends IEntryPoint> app : applications) {
      final RWTApplication appConfig = app.getAnnotation(RWTApplication.class);
      if (null != appConfig) {
        logger.log(Level.INFO, "RWT Application configured by OSGiServiceInitializer: " + app.getName());
        bc.registerService(ApplicationConfigurator.class.getName(),
          new ApplicationConfigurator() {

            @Override
            public void configure(ApplicationConfiguration configuration) {
              configuration.addEntryPoint(appConfig.value(), app);
              configuration.addBranding(new AbstractBranding() {
                @Override
                public String getServletName() {
                  return appConfig.value();
                }

                @Override
                public String getTitle() {
                  return "".equals(appConfig.title()) ? null : appConfig.title();
                }

                @Override
                public String getDefaultEntryPoint() {
                  return appConfig.value();
                }
              });
            }
          }, null);
        }
      }
  }

  /*
   * (non-Javadoc)
   *
   * @see javax.servlet.ServletContainerInitializer#onStartup(java.util.Set,
   * javax.servlet.ServletContext)
   */
  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException {
    if (null != classes) {
      classes.remove(RWTApplication.class);
    }
    if (null != classes && !classes.isEmpty()) {
      logger.log(Level.INFO, "ServletContext configured by OSGiServiceInitializer because of found RWT application(s)");
      OSGiServiceInitializer.addHttpServiceServlet(ctx);
      // RWT Application(s) detected
      final BundleContext bc = (BundleContext) ctx.getAttribute(OSGiServiceInitializer.BC);
      if (null != bc) {
        OSGiServiceInitializer.configure(ctx, bc, (Set) classes);
      } else {
        ctx.addListener(new ServletContextAttributeListener() {

          @Override
          public void attributeReplaced(ServletContextAttributeEvent evt) {
            if (OSGiServiceInitializer.BC.equals(evt.getName())) {
              final BundleContext bc = (BundleContext) evt.getValue();
              if (null != bc) {
                OSGiServiceInitializer.configure(ctx, bc, (Set) classes);
              }
            }
          }

          @Override
          public void attributeRemoved(ServletContextAttributeEvent evt) {}

          @Override
          public void attributeAdded(ServletContextAttributeEvent evt) {
            attributeReplaced(evt);
          }

        });
      }

    }
  }

}

The RWTApplication annotation and the ServletContainerInitializer must be packaged within a JAR file (does not have to be an OSGi bundle). This JAR file must be available within the web application’s classpath during application startup.

Conclusion

Of course, this is just an experiment for today, but could be a suggestion to provide RWT for web applications by installing the framework at the web container instead of the web application. At least, I have to say: The will pushed me, the wit will advance. With this in mind: Cheers!

Advertisements