Home / Java WURFL API


Luca Passani

Java WURFL API
Introduction
Javadocs


SourceForge.net Logo
 

New WURFL Java API
by Luca Passani
passani at eunet dot no

Here is the Java version of the new API. The one that implements all the nice features mentioned in the introduction.

If you are in a hurry to get started with the new API, you can download the WURFL Hello World application directly from Sourceforge: unpack the package into your tomcat webapps/ directory, and just hit http://yourserver[:port]/wurfl-helloworld-{version}/ with your firefox.

Make sure that you have the good User-Agent Switcher FireFox plug-in by Chris Pederick installed. This will allow you to see how your "Hello World" will be delivered in multiple languages: WML, XHTML and CHTML.


The new Java API is no rocket science to anyone who has used WURFL before. Some new concepts will need to be explained, though. No better place to start than a good old HelloWorld.java servlet:
package net.sourceforge.wurfl.core.example;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sourceforge.wurfl.core.CapabilityNotDefinedException;
import net.sourceforge.wurfl.core.Device;
import net.sourceforge.wurfl.core.MarkUp;
import net.sourceforge.wurfl.core.WURFLHolder;
import net.sourceforge.wurfl.core.WURFLManager;

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

public class HelloWorld extends HttpServlet {

    private static final String XHTML_ADV = "xhtmladv.jsp";
    private static final String XHTML_SIMPLE = "xhtmlmp.jsp";
    private static final String CHTML = "chtml.jsp";
    private static final String WML = "wml.jsp";

    /** Logger */
    private final Log log = LogFactory.getLog(getClass());

    protected void processRequest(
                       HttpServletRequest request,
                       HttpServletResponse response)
                   throws ServletException, IOException {

        WURFLHolder wurflHolder = (WURFLHolder) getServletContext()
                .getAttribute("net.sourceforge.wurfl.core.WURFLHolder");

        WURFLManager wurfl = wurflHolder.getWURFLManager();

        Device device = wurfl.getDeviceForRequest(request);
       
        log.debug("Device: " + device.getId());

        MarkUp markUp = device.getMarkUp();

        String jspView = null;

        if (MarkUp.XHTML_ADVANCED.equals(markUp)) {
            jspView = XHTML_ADV;
        } else if (MarkUp.XHTML_SIMPLE.equals(markUp)) {
            jspView = XHTML_SIMPLE;
        } else if (MarkUp.CHTML.equals(markUp)) {
            jspView = CHTML;
        } else if (MarkUp.WML.equals(markUp)) {
            jspView = WML;
        }

        log.info("View: " + jspView);

        // MIME type is decided by JSP. Only in case of XHTML 
        // will we need to multi-serve i.e text/html vs application/xml+xhtml vs
        // application/vnd.wap.xml+xhtml
        if (markUp == MarkUp.XHTML_ADVANCED || markUp == MarkUp.XHTML_SIMPLE) {
            String contentType = "text/html";
            try {
                contentType = device.getCapability("xhtmlmp_preferred_mime_type");
            } catch (CapabilityNotDefinedException e) {
                throw new RuntimeException(
                        "Somethingh is seriously wrong with your WURFL:"
                                + e.getLocalizedMessage(), e);
            }

            request.setAttribute("contentType", contentType);
            log.debug("ContentType: " + contentType);
        }

        request.setAttribute("markUp", markUp);
        request.setAttribute("device", device);

        request.getRequestDispatcher("WEB-INF/jsp/" + jspView)
                    .forward(request,response);
  }

  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                 throws ServletException, IOException {
    processRequest(request, response);
  }


  protected void doPost(HttpServletRequest request,
                        HttpServletResponse response) 
                 throws ServletException, IOException {
    processRequest(request, response);
  }
}
No initialization code for the API anymore? Correct. No initialization no more.

Someone somewhere has already initialized everything for you. In the case of a servlet, this may be the work of either /WEB-INF/web.xml or the Spring Framework, a great (and popular) Java framework which we adopted to bring the WURFL API to new levels of customizability.

Note: there is a small but useful addition to the API: you can now use a compressed (.zip or .gz) wurfl.xml file in your WEB-INF. I am sure this possibility will be welcome by many now. Regular wurfl.xml file has reached a size of several Mega Bytes.
In the case of an archived file, the API will take the first compressed file in the archive (no need to call it wurfl.xml). Actually, once you become really familiar with the API and Spring, you can build a class that finds the wurfl.xml in any way you want: just make sure that the class implements the WURFLResource Java interface, and you'll be able to plug it into the API.
Finally, patch files may be archived too (did I mention that you can now have as many patch files as you want?).


For those of you who would like to see the code and understand how the new heuristics work in more depth, here comes the package. Unfold the archive in a directory of its own and run "ant test". This will run the Unit Tests to check that all of those weird UA strings are correctly matched.

Time to look at the main new concepts of the new API.

New Concepts: WURFLHolder, WURFLManager and Device

If you remember the ObjectsManager singleton in the old API, a similar role is now played by WURFLHolder as an entry point for all of the other objects in the API.
WURFLHolder looks like a singleton, but it is not. A new WURFLHolder is created for every application, so you can have multiple WURFL apps running with different wurfl.xml on the same application server (this was not easy with singletons), an issue that adopters of the API have complained about at regular intervals.

The wurflHolder object is initialised with the corresponding attribute ("net.sourceforge.wurfl.core.WURFLHolder") in the ServletContext. As we will see later on in this document, the attribute has been put there through either the web.xml descriptor or through Spring wiring.
For the time being, just have faith that the attribute will point to the wurflHolder object one way or another.

If you were familiar with the old UAMAnager in the old API, WURFLManager plays a similar role: Thanks to the WURFLManager you can map any HTTP request to a Device (the old API would return the WURFL ID, but we have decided to do things a bit more Object-Oriented-ly).
Theoretically, the framework could use any headers to figure out which Device to return. Out of the box, the API still relies on the User-Agent string exclusively (albeit with greatly enhanced heuristics).

getDeviceForRequest() is the method that performs the magic of returning the Device object. It comes with three signatures, since the parameter can be on of the following three types: HTTP Request (javax.servlet.http.HttpServletRequest), User-Agent String and WURFLRequest, a java type we have introduced to capture only the headers the API cares about.

I am using the term "type" a bit loosely here. The fact is that the new API relies heavily on Interfaces. This is what will allow you to override specific components with your own implementations. Rather then writing "Classes (actually an Interface)" at every step, I'll opt for the term "type", which may jar to the ear of your average Java programmer, but is probably correct.


As I mentioned, there is now a Device type which will allow you to retrieve all the information about a device and its capabilities. The functions that used to be part of the old CapabilityMatrix are now part of Device. In particular, the function performed by the old capabilityMatrix.getDeviceIDFromUALoose(id,capa_name) is now the job of device.getCapability(capa_name):

 java.util.Map getCapabilities()
 Returns a device's capabilities name-value map.
 java.lang.String getCapability(java.lang.String name)
 Return a device's capability value.
 java.lang.String getId()
 Return the WURFL id of this device
 java.lang.String getUserAgent()
 Return the user-agent string for this device as defined in WURFL.

Now that you know how to get to a Device object, you can make total sense of the HelloWorld servlet above and, more importantly, you'll be able to migrate existing WURFL apps to the new API in a matter of hours.

Let's see how WURFLHolder can be instantiated in a declarative way through the web.xml.

Configuring with /WEB.INF/web.xml

(You can find the complete web.xml that comes with the distribution here)

The web.xml configuration is simple. The API distribution already contains a Listener class which does all the work for you.

if you are not sure about what a ServletContextListener is, it is simply a class which can be associated to a servlet context and is invoked when the context starts or stops.

The class is called net.sourceforge.wurfl.core.web.WURFLServletContextListener and it will take care of configuring net.sourceforge.wurfl.core.WURFLHolder as a wurflHolder.
<listener>
  <listener-class>
     net.sourceforge.wurfl.core.web.WURFLServletContextListener
  </listener-class>
</listener>
In practice, the Listener will programmatically create an instance of the wurflHolder object and attach it to the "net.sourceforge.wurfl.core.WURFLHolder" attribute.
From this moment, the object is available to the servlet, which will use it as the launching pad for the rest of the API, as you have seen already in the HelloWorld servlet above.

In addition to this class, the WURFLServletContextListener will also load the wurfl.xml file by reading the "wurfl" parameter from the web.xml itself.
<context-param>
  <param-name>wurfl</param-name>
  <param-value>/WEB-INF/wurfl.zip</param-value>
</context-param>
In case you also want to use one or more patch files, the syntax for the extra context-param is:
<context-param>
  <param-name>wurflPatch</param-name>
  <param-value>/WEB-INF/patch_1.xml,/WEB-INF/patch_2.xml</param-value>
</context-param>
web.xml is the easiest way to configure the API for your needs.

A day may come when you are willing to trade that simplicity with the possibility of making the API do more for you. On that day, you will be thankful to the Spring framework.

Configuring with Spring

As with any Open-Source project, you can always change and rebuild a modified version of the API for yourself. As a matter of fact, many did exactly this with the old API.

In case you do need to change certain features, the new API offers a more elegant way to implement what you need. In case you want to change the implementation of one or more components in the API, you do not need to change existing code or even recompile the library. You can provide your own alternate implementation of that component and plug it into the API through so-called "wiring": open a file called /WEB-INF/wurfl-default-ctx.xml and tell the WURFL API to load your classes instead. If you are new to Spring, this may sound strange. Yet, I promise you, it will all look simpler (and powerful!) once you understand Spring or, more generally, the pleasures of Inversion of Control (IoC).
If you want to know what you can configure in the new API and how, keep reading.

A look into /WEB-INF/wurfl-default-ctx.xml

wurfl-default-ctx.xml is the file that tells the API where to find the resources and the components which make it work

Important Note: you do not need to know anything about Spring in order to use the new API. The default wurfl-default-ctx.xml that comes with the distribution will make the API work out of the box.


Important Note 2: wurfl-default-ctx.xml will be read by the Spring framework, which in turn needs the web.xml to be initialised properly. In case you are using Spring, the web.xml will declare the following parameters:
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/wurfl-default-ctx.xml</param-value>
</context-param>
  :
<listener>
  <listener-class>
    org.springframework.web.context.ContextLoaderListener
  </listener-class>
</listener>
The effect of this is to delegate control of all the nice features in the API to Spring and its /WEB-INF/wurfl-default-ctx.xml configuration file.


If you want to modify this configuration, you need to understand a few new concepts.

Following the Spring spirit of Inversion of Control (IoC), objects are no longer hard-wired into the API itself. Rather, you have a Spring configuration file that tells the API where to find the resources and the parts of the program which, collectively, combine the API.
For example, if you decide to place your WURFL in a database or modify the behavior of the API in some contexts, you do not need recompile the whole API. Instead, you simply plug-in your new class into the existing API.

Finding the wurfl.xml and patches and generating a WURFLModel
For example, the WURFL repository and patch files are encapsulated into Java objects I refer to as "resources".
Normally, net.sourceforge.wurfl.core.resource.XMLResource will load a resource. This is a class in the API which knows how to parse an XML file containing wurfl.xml or a patch file.

XMLResource is not the class we are using in the example below (from the wurfl-default-ctx.xml). In a web Application context, referring to absolute paths outside of the web application context is frowned upon in respected Java programming circles. For this reason, the API also offers net.sourceforge.wurfl.core.resource.SpringXMLResource, a WURFLResource which can accept a Spring resource and make it digestable to XMLResource, which is still active behind the scenes (both implement the WURFLResource interface).
<bean id="wurflModel"
       class="net.sourceforge.wurfl.core.resource.DefaultWURFLModel" >
  <constructor-arg index="0">
    <bean class="net.sourceforge.wurfl.core.resource.SpringXMLResource">
      <constructor-arg value="/WEB-INF/wurfl.zip" />
    </bean>
  </constructor-arg>
  <constructor-arg index="1">
    <bean class="net.sourceforge.wurfl.core.resource.WURFLResources">
      <constructor-arg>
        <list>
          <bean class="net.sourceforge.wurfl.core.resource.SpringXMLResource">
            <constructor-arg value="/WEB-INF/web_browsers_patch.xml" />
          </bean>
        </list>
      </constructor-arg>
    </bean>
  </constructor-arg>
</bean>
The main Java bean to access the WURFL Object Model is WURFLModel. DefaultWURFLModel lets you access the Java objects which contain the WURFL device information and the capabilities.
The example above shows you the typical way to load wurfl.xml and one or more patch files of your choice.

Of course, were you to come up with your own way of creating a WURFL model, you could plug it in here and the rest of the API would be equally happy. In that case, you would need to implement the WURFLModel methods, such as: getDeviceById(), getDeviceHierarchy() and so on...

MatcherManager, WURFLManager, WURFLHolder and other amenities

The Spring configuration will plug in more (and more fine-grained) objects than what we have seen with the web.xml example.
The net.sourceforge.wurfl.core.handlers.matchers.MatcherManager is one such object:
<bean id="matcherManager"
     class="net.sourceforge.wurfl.core.handlers.matchers.MatcherManager">
   <constructor-arg ref="wurflModel" />
</bean>
MatcherManager is a class, not an interface. For this reason, it is one of the few components that cannot be plugged with the new API. We figured that this function was too tightly coupled with the framework as a whole, to go out of our way to abstract its implementation

MatcherManager is responsible for managing all of the heuristics. Heuristics implement the logic to:
  • categorize a WURFL User-Agent string into its logical family (for ex: if UA contains "Nokia", place it in the Nokia family)
  • match the UA string in a WURFLRequest object with the right WURFL entry (for ex., if UA string contains "Nokia", leverage the Nokia family and heuristics to find a match)
It goes without saying that the MatcherManager needs the wurflModel.


Configuring Device
The Device interface and the DefaultDevice implementation is the object that gives you access to all the capabilities of a given device.
Believe it or not, the Device object can be configured in a few ways.
<bean id="deviceProvider"
      class="net.sourceforge.wurfl.core.DefaultDeviceProvider">
  <constructor-arg ref="wurflModel" />
</bean>
I already explained the wurflModel, so it is probably no surprise that the Device object needs it.


Service Configuration
WURFLService is a Java bean which can access the repository and sits half way between the API and the low level data. WURFLManager will rely on the WURFLService to retrieve Device objects.
<bean id="wurflService"
      class="net.sourceforge.wurfl.core.DefaultWURFLService">
  <constructor-arg ref="matcherManager" />
  <constructor-arg ref="deviceProvider" />
</bean>
An added role of the WURFLService is to be a "Façade" to the CacheProvider, MatcherManager and DeviceProvider.

WURFLManager
WURFLManager can be retrieved directly from the WURFLHolder. It is the main interface to build a WURFLRequest and obtain a Device.
<bean id="wurflManager" class="net.sourceforge.wurfl.core.DefaultWURFLManager">
   <constructor-arg ref="wurflService" />
</bean>

WURFLUtils
This class collects a number of introspective utility functions, such as:
  • Load the list of Devices
  • Load device by WURFL ID
  • Navigate device hierarchy
  • Retrieve the version of the repository
  • and more...

<bean id="wurflUtils" class="net.sourceforge.wurfl.core.WURFLUtils">
  <constructor-arg ref="wurflModel" />
</bean>

WURFLHolder
We already described this class when we introduced the servlet. It's the entry point for the API, thanks to its referenced to WURFLManager and WURFLUtils.
<bean id="wurflHolder" class="net.sourceforge.wurfl.core.DefaultWURFLHolder">
  <constructor-arg ref="wurflManager" />
  <constructor-arg ref="wurflUtils" />
</bean>


ServletContextAttributeExporter
This is simply a Java bean which associates a Spring bean to the Servlet Context through the attribute (not very differently from what the WURFLServletContextListener did in the simple web.xml configurarion we illustrated at the beginning).
<bean
  class="org.springframework.web.context.support.ServletContextAttributeExporter">
  <property name="attributes">
    <map>
      <entry key="net.sourceforge.wurfl.core.WURFLHolder" value-ref="wurflHolder" />
    </map>
  </property>
</bean>


More Advanced usages of Spring configuration
The wurfl-default-ctx.xml file we described in this document is still sort of minimalistic compared to the granularity of the types which combine the API. We did not want to make it too difficult for beginners.
If you have specific needs, you can override multiple classes with your own custom implementations.
Just to give you an example, UserAgentResolver, DeviceProvider, UserAgentNormalizer, WURFLRequestFactory, etc. are all examples of Java interfaces which you can override with your own custom implementations. You will need to plug your classes in with more sophisticated Spring config files than wurfl-default-ctx.xml.
Just to give you a taste of what is possible, take a look at the following /WEB-INF/wurfl-custom-ctx.xml to get a feeling of what is possible.


Enjoy!
Luca Passani


Copyright © 2007-2009, Luca Passani