Showing posts with label jetty. Show all posts
Showing posts with label jetty. Show all posts

Saturday, August 22, 2009

Exposing Java Objects over HTTP with Jetty

Couple of weeks ago, I described how I exposed a Python object over HTTP using CherryPy. I wrote the code in there after a quick read of the CherryPy Tutorial, so it's quite basic and doesn't use any advanced CherryPy functionality. But what impressed me was the simplicity of CherryPy's approach to exposing the Python object - and since I am primarily a Java programmer, and therefore mostly need to expose Java objects, I decided to see if I could do something similar using Jetty. This post is a result of that decision.

Overview

I called the system JOH (for Java Over HTTP). It rhymes with D'oh (as in, D'oh! Why didn't I think of this before?). The diagram below illustrates the data flow. Imagine that you have one or more Java objects (the left most box) that you are currently calling directly from Java client code (the right most box). Exposing these objects over HTTP using JOH involves creating a Facade and plugging it into JOH on the server side. The Facade delegates to the Java Objects and decides how to serialize the outputs, in our example we convert our outputs to JSON. On the client side, the client now needs to go through an HTTP client which will deserialize the HTTP response into the desired Object.

As a user, the only significant thing you need to supply is the Facade class, which has a bunch of public methods which translate directly into servlet URI's. So a public method called getFoo() will be accessible over JOH using the URL http://.../getfoo. Unlike CherryPy, where each method is individually exposed, we rely on Java's visibility modifiers here - all public methods on the Facade are exposed - if you don't want to expose some method, change its visibility to protected or private.

Each method on the Facade takes a reference to the HttpServletRequest and HttpServletResponse. When delegating to methods of local Java objects, it extracts the relevant parameter from the request, validates and passes it on. On the way out, it serializes the output of the result and sticks it into the response. The serialization mechanism can probably be factored out into a Renderer abstraction, and multiple types of Renderer provided for different serialization mechanisms. So anyway, as you can see, most of the "magic" of exposing the Java object over HTTP happens in the Facade.

The Joh.expose method (called from the Facade's main() method) is responsible for exposing the Facade to Jetty's lifecycle via the JohHandler. Each incoming HTTP request is intercepted by the JohHandler, and converted into a method call on the Facade, which is then invoked reflectively. After the invocation, the Facade takes over and is responsible for sending out the response.

On the client side, we have a simple JOH Client which uses takes a URL and deserializes the response back to the user's requested class. Both the JSON serialization and deserialization use the Jackson JSON Processor. A similar facade exists on the client side to minimize disruption to client code, if it already exists - instead of calling the Java objects, the client code calls the equivalent methods on the client facade, which delegates to the JOH Client.

JOH Components

We describe the JOH components (in the pink boxes in the diagram above) individually below. If you just want to know how to use this stuff, then you can safely skip down to the next section on the Facade components (the light blue boxes in the diagram).

Joh.java

The Joh class is the main class and consists of a bunch of static methods. The main method is expose() which takes an instance of the Facade class to expose and a configuration file. If a configuration file is not supplied (i.e. null), suitable defaults are used to start the Jetty server. In addition, there are some generally useful utility methods in here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Source: src/main/java/com/mycompany/joh/Joh.java
package com.mycompany.joh;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;

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

import org.apache.commons.collections15.map.CaseInsensitiveMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.HttpStatus;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;

/**
 * Exposes a Java object reference.
 */
public class Joh {

  private final static Log LOG = LogFactory.getLog(Joh.class);
  
  public final static String HTTP_PORT_KEY = "_http_port";
  
  private final static int DEFAULT_HTTP_PORT = 8080;
  
  public static void expose(Object obj, Map<String,Object> config)
      throws Exception {
    Server server = new Server();
    Connector connector = new SocketConnector();
    if (config != null && config.containsKey(HTTP_PORT_KEY)) {
      connector.setPort((Integer) config.get(HTTP_PORT_KEY));
    } else {
      connector.setPort(DEFAULT_HTTP_PORT);
    }
    server.setConnectors(new Connector[] {connector});
    Handler handler = new JohHandler(obj);
    server.setHandler(handler);
    server.start();
    server.join();
  }
  
  public static Map<String,String> getParameters(
      HttpServletRequest request) {
    Map<String,String> parameters = 
      new CaseInsensitiveMap<String>();
    Map<String,String[]> params = request.getParameterMap();
    for (String key : params.keySet()) {
      parameters.put(key, StringUtils.join(params.get(key), ","));
    }
    return parameters;
  }

  public static void error(Exception e, HttpServletRequest request,
      HttpServletResponse response) {
    response.setContentType("text/html");
    try {
      PrintWriter responseWriter = response.getWriter();
      responseWriter.println("<html><head><title>Error Page</title></head>");
      responseWriter.println("<body><font color=\"red\">");
      e.printStackTrace(responseWriter);
      responseWriter.println("</font></body></html>");
      responseWriter.flush();
      responseWriter.close();
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      LOG.error(e);
    } catch (IOException ioe) {
      LOG.error(ioe);
    }
  }
}

JohHandler.java

The JohHandler hooks into the Jetty request lifecycle, so that the JohHandler is invoked to handle a request. The URI of the request is mapped to a method of the Facade class, and any required method parameters are extracted from the request parameters. The invocation of the method on the Facade will cause the response to be populated with the JSON serialized result of the method call.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Source: src/main/java/com/mycompany/joh/JohHandler.java
package com.mycompany.joh;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;

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

import org.apache.commons.collections15.map.CaseInsensitiveMap;
import org.mortbay.jetty.HttpConnection;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.handler.AbstractHandler;

/**
 * Hook into the Jetty infrastructure.
 */
public class JohHandler extends AbstractHandler {

  private Map<String,Method> methodMap = 
    new CaseInsensitiveMap<Method>();
  
  private Object javaObject;
  
  public JohHandler(Object obj) {
    super();
    this.javaObject = obj;
    Method[] methods = obj.getClass().getMethods();
    for (Method method : methods) {
      methodMap.put(method.getName(), method);
    }
  }
  
  public void handle(String target, HttpServletRequest request,
      HttpServletResponse response, int dispatch) 
      throws IOException, ServletException {
    Request req = (request instanceof Request ? 
      (Request) request : 
      HttpConnection.getCurrentConnection().getRequest());
    // strip off the leading "/" and lowercase the target. The target is
    // the same as the requestURI from the HttpServletRequest object.
    String methodName = request.getRequestURI().substring(1);
    if (methodMap.containsKey(methodName)) {
      Method method = methodMap.get(methodName);
      try {
        method.invoke(javaObject, new Object[] {request, response});
        response.setStatus(HttpServletResponse.SC_OK);
      } catch (InvocationTargetException e) {
        Joh.error(e, request, response);
      } catch (IllegalAccessException e) {
        Joh.error(e, request, response);
      }
    } else {
      Joh.error(new Exception("No such method: " + methodName), 
        request, response);
    }
    req.setHandled(true);
  }
}

JohClient.java

The JobClient is a standard Apache HTTP client that passes in HTTP GET calls to Jetty and gets back a JSON response. It then deserializes the JSON response into the object expected by the client. Here is the code for it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Source: src/main/java/com/mycompany/joh/JohClient.java
package com.mycompany.joh;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.codehaus.jackson.map.ObjectMapper;

/**
 * Simple Jetty HTTP Client to test our Joh enabled BlogDict.
 * @author Sujit Pal
 * @version $Revision$
 */
public class JohClient {

  public Object request(String url, Class<?> clazz) {
    HttpClient client = new HttpClient();
    GetMethod method = new GetMethod(url);
    method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
      new DefaultHttpMethodRetryHandler(3, false));
    try {
      int status = client.executeMethod(method);
      if (status != HttpStatus.SC_OK) {
        throw new Exception(method.getStatusText() + 
          " [" + method.getStatusCode() + "]");
      }
      ObjectMapper mapper = new ObjectMapper();
      return mapper.readValue(method.getResponseBodyAsStream(), clazz);
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      method.releaseConnection();
    }
  }
}

Some examples of calling this component are described in the Facade Components section below.

Facade Components

The original component to be exposed is our tired-but-tested BlogDict class which reads a text file and builds an internal data structure, then exposes methods which allows a caller to query parts of the data structure. Here is the code in Java (last week's post contains the Python version).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Source: src/test/java/com/mycompany/joh/BlogDict.java
package com.mycompany.joh;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

/**
 * Simple Java Object to be exposed by JOH.
 */
public class BlogDict {

  private String file;
  
  private Set<String> labels;
  private Map<String,Set<String>> synonyms;
  private Map<String,Set<String>> categories;
  
  public BlogDict(String file) {
    this.file = file;
    init();
  }
  
  public Set<String> getLabels() {
    return labels;
  }
  
  public Set<String> getSynonyms(String label) {
    if (synonyms.containsKey(label)) {
      return synonyms.get(label);
    } else {
      return Collections.emptySet();
    }
  }
  
  public Set<String> getCategories(String label) {
    if (categories.containsKey(label)) {
      return categories.get(label);
    } else {
      return Collections.emptySet();
    }
  }
  
  protected void init() {
    this.labels = new HashSet<String>();
    this.synonyms = new HashMap<String,Set<String>>();
    this.categories = new HashMap<String,Set<String>>();
    try {
      BufferedReader reader = new BufferedReader(new FileReader(file));
      String line = null;
      while ((line = reader.readLine()) != null) {
        if (line.startsWith("#")) {
          continue;
        }
        String[] cols = StringUtils.splitPreserveAllTokens(line, ":");
        this.labels.add(cols[0]);
        this.synonyms.put(cols[0], new HashSet<String>(
          Arrays.asList(StringUtils.split(cols[1], ","))));
        this.categories.put(cols[0], new HashSet<String>(
          Arrays.asList(StringUtils.split(cols[2], ","))));
      }
      reader.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}

The BlogDictFacade provides a Facade that delegates to the BlogDict class and serializes the output into JSON and sends it back in the HTTP response. Each of the public getXXX() methods in the BlogDict has an analog in the BlogDictFacade, although the method signature is different and there is no return type. Here is the code - showing it is probably clearer than explaining it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Source: src/test/java/com/mycompany/joh/BlogDictFacade.java
package com.mycompany.joh;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

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

import org.codehaus.jackson.map.ObjectMapper;

/**
 * The web based facade that will be plugged into JOH.
 */
public class BlogDictFacade {

  private BlogDict blogDict;
  
  public BlogDictFacade(String file) {
    this.blogDict = new BlogDict(file);
  }
  
  protected void init() { /* nothing to do here */ }
  
  protected void destroy() { /* nothing to do here */ }
  
  public void getLabels(HttpServletRequest request, 
      HttpServletResponse response) {
    Set<String> labels = blogDict.getLabels();
    response.setContentType("application/x-javascript");
    try {
      PrintWriter responseWriter = response.getWriter();
      ObjectMapper mapper = new ObjectMapper();
      mapper.writeValue(responseWriter, labels);
      responseWriter.flush();
      responseWriter.close();
    } catch (IOException e) {
      Joh.error(e, request, response);
    }
  }
  
  public void getSynonyms(HttpServletRequest request, 
      HttpServletResponse response) {
    Map<String,String> parameters = Joh.getParameters(request);
    if (parameters.containsKey("label")) {
      Set<String> synonyms = 
        blogDict.getSynonyms(parameters.get("label"));
      response.setContentType("application/x-javascript");
      try {
        PrintWriter responseWriter = response.getWriter();
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(responseWriter, synonyms);
        responseWriter.flush();
        responseWriter.close();
      } catch (IOException e) {
        Joh.error(e, request, response);
      }
    } else {
      Joh.error(new Exception("Parameter 'label' not provided"), 
        request, response);
    }
  }
  
  public void getCategories(HttpServletRequest request, 
      HttpServletResponse response) {
    Map<String,String> parameters = Joh.getParameters(request);
    if (parameters.containsKey("label")) {
      Set<String> categories = 
        blogDict.getCategories(parameters.get("label"));
      response.setContentType("application/x-javascript");
      try {
        PrintWriter responseWriter = response.getWriter();
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(responseWriter, categories); 
        responseWriter.flush();
        responseWriter.close();
      } catch (IOException e) {
        Joh.error(e, request, response);
      }
    } else {
      Joh.error(new Exception("Parameter 'label' not provided"), 
        request, response);
    }
  }
  
  public static void main(String[] argv) throws Exception {
    Map<String,Object> config = new HashMap<String,Object>();
    config.put(Joh.HTTP_PORT_KEY, new Integer(8080));
    Joh.expose(new BlogDictFacade("/home/sujit/bin/blog_dict.txt"), 
      config);
  }
}

Since we are really just testing this stuff at this point, I don't have any client code, so I decided to do away with the client side Facade and just write a unit test that goes directly against the JohClient. This is shown below, to illustrate usage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Source: src/test/java/com/mycompany/joh/JohClientTest.java
package com.mycompany.joh;

import java.util.Set;

import junit.framework.Assert;

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

/**
 * Test for JohClient.
 */
public class JohClientTest {

  private final Log log = LogFactory.getLog(getClass());
  
  @Test
  public void testGetLabel() throws Exception {
    JohClient client = new JohClient();
    Set<String> labels = (Set<String>) client.request(
      "http://localhost:8080/getlabels", Set.class);
    log.debug("labels=" + labels);
    Assert.assertNotNull(labels);
    Assert.assertTrue(labels.contains("crawling"));
  }
  
  @Test
  public void testGetSynonyms() throws Exception {
    JohClient client = new JohClient();
    Set<String> synonyms = (Set<String>) client.request(
      "http://localhost:8080/getsynonyms?label=crawling", Set.class);
    log.debug("synonyms(crawling)=" + synonyms);
    Assert.assertNotNull(synonyms);
    Assert.assertTrue(synonyms.contains("crawler"));
  }
  
  @Test
  public void testGetCategories() throws Exception {
    JohClient client = new JohClient();
    Set<String> categories = (Set<String>) client.request(
      "http://localhost:8080/getcategories?label=crawling", Set.class);
    log.debug("categories(crawling)=" + categories);
    Assert.assertNotNull(categories);
    Assert.assertTrue(categories.contains("lucene"));
  }
}

Shell script

This is Java, so we need to build a shell script to run the server. A simple shell script with all the dependencies in the classpath is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/bash
# Source: src/main/scripts/joh_blogdict.sh
PROJECT_HOME=$HOME/src/gclient
M2_REPO=$HOME/.m2/repository
CLASSPATH=\
$M2_REPO/commons-lang/commons-lang/2.3/commons-lang-2.3.jar:\
$M2_REPO/org/apache/commons/collections15/4.01/collections15-4.01.jar:\
$M2_REPO/commons-logging/commons-logging/1.1/commons-logging-1.1.jar:\
$M2_REPO/log4j/log4j/1.2.14/log4j-1.2.14.jar:\
$M2_REPO/org/mortbay/jetty/jetty/6.1.5/jetty-6.1.5.jar:\
$M2_REPO/org/mortbay/jetty/jetty-util/6.1.5/jetty-util-6.1.5.jar:\
$M2_REPO/org/mortbay/jetty/servlet-api-2.5/6.1.5/servlet-api-2.5-6.1.5.jar:\
$M2_REPO/org/codehaus/jackson/core/1.2.0/core-1.2.0.jar:\
$M2_REPO/org/codehaus/jackson/mapper/1.2.0/mapper-1.2.0.jar:\
$PROJECT_HOME/target/classes:\
$PROJECT_HOME/target/test-classes

java -cp $CLASSPATH com.mycompany.joh.BlogDictFacade 2>&1 | tee $0.log

After running this script on the command prompt, you can hit the BlogDict using either a browser or something like JohClientTest shown above. To shutdown the server, hit CTRL+C.

Conclusion

Although a lot of code is shown here on this blog, in reality, a user who is looking for functionality to expose a Java object needs to only create the Facade object and plug it into JOH using the Joh.expose() call. So the approach is very similar to CherryPy's. In the same spirit, there is no attempt to force the user (via interface, etc) to conform to a specific approach - this is more of a prescriptive approach.

Saturday, September 06, 2008

JMX for Scripts : RSS Adapter

Last week, I added scheduling and monitoring capabilities to the basic MBean server to manage scripts I described the week before. Now imagine that the server was deployed to a bank of 10 or 50 or more machines on which scripts are being run. If you were to depend on the notifications alone, this would be fine, but there may still be situations where you want a "bird's eye view" of your entire system, perhaps to show your boss, or even for yourself. This post describes an RSS adapter that returns an RSS feed of the status of all the scripts being managed by its containing MBean server.

Anyway, back to the RSS Adapter. I initially figured that it should be modelled after the HTML Server Adapter, so I peeked at the OpenDMK sources (which is where the JMX tools code originally came from), but it seemed to be too much infrastructure for what I had in mind, so I fell back to using Jetty. I ended up creating an MBean that that instantiates a Jetty Handler listening on port 9081. Code inside the Handler queries the MBean server for the ScriptAdapter MBeans, gets the Status attribute values, then creates and serializes a SyndFeed object using ROME. Here is the code:

// Source: src/main/java/com/mycompany/myapp/RssAdapterServerMBean.java
package com.mycompany.myapp;

public interface RssAdapterServerMBean {
  
  public void start();
  public void stop();
}

Here is the implementation for the RssAdapterServer MBean. The code queries the MBean server for the ScriptAdapters - information about the mechanics of which came from Eamonn McManus's blog, which is also where I got the pointer about OpenDMK being the source for the JMX tools project.

// Source: src/main/java/com/mycompany/myapp/RssAdapterServer.java
package com.mycompany.myapp;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.Query;
import javax.management.QueryExp;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mortbay.jetty.Handler;
import org.mortbay.jetty.HttpStatus;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;

import com.sun.syndication.feed.WireFeed;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.WireFeedOutput;

public class RssAdapterServer implements RssAdapterServerMBean {

  private class StatusTriple {
    public ObjectName script;
    public URL httpUrl;
    public String status;
  };
  
  private int port;
  private String httpAdapterHostPort;
  
  private Server rssServer;
  
  public void setPort(int port) {
    this.port = port;
  }

  public void setHttpAdapterHostPort(String httpAdapterHostPort) {
    this.httpAdapterHostPort = httpAdapterHostPort;
  }
  
  public void start() {
    Handler handler = new AbstractHandler() {
      public void handle(String target, HttpServletRequest request,
          HttpServletResponse response, int dispatch) throws IOException,
          ServletException {
        response.setContentType("text/xml");
        PrintWriter writer = response.getWriter();
        writer.println(getScriptStatusRss());
        writer.flush();
        writer.close();
        response.setStatus(HttpStatus.ORDINAL_200_OK);
        ((Request) request).setHandled(true);
      }
    };
    this.rssServer = new Server(port);
    rssServer.setHandler(handler);
    try {
      rssServer.start();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void stop() {
    try {
      rssServer.stop();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private final String getScriptStatusRss() {
    List<StatusTriple> triples = new ArrayList<StatusTriple>();
    List<MBeanServer> mbeanServers = 
      MBeanServerFactory.findMBeanServer(null);
    if (mbeanServers == null || mbeanServers.size() == 0) {
      System.out.println("No MBean servers found in JVM");
      return getRss(triples);
    }
    MBeanServer mbeanServer = mbeanServers.get(0);
    QueryExp query = Query.isInstanceOf(Query.value(
      ScriptAdapter.class.getName()));
    Set<ObjectName> objectNames = mbeanServer.queryNames(null, query);
    for (ObjectName objectName : objectNames) {
      try {
        StatusTriple triple = new StatusTriple();
        triple.script = objectName;
        triple.status = 
          (String) mbeanServer.getAttribute(objectName, "Status");
        triple.httpUrl = new URL("http://" + httpAdapterHostPort + 
          "/ViewObjectRes//" + 
          URLEncoder.encode(objectName.getCanonicalName(), "UTF-8"));
        triples.add(triple);
      } catch (Exception e) {
        System.out.println("Cannot invoke getStatus on " + 
          objectName.getCanonicalName());
        e.printStackTrace();
        continue;
      }
    }
    System.out.println("Reporting on " + triples.size() + " MBeans");
    return getRss(triples);
  }
  
  @SuppressWarnings("unchecked")
  private String getRss(List<StatusTriple> triples) {
    SyndFeed feed = new SyndFeedImpl();
    feed.setFeedType("rss_2.0");
    feed.setTitle("Status of Scripts running on: " + httpAdapterHostPort);
    feed.setDescription("Status of scripts running on: " + 
      httpAdapterHostPort);
    feed.setLink("http://localhost:" + port);
    for (StatusTriple triple : triples) {
      SyndEntry entry = new SyndEntryImpl();
      entry.setTitle(triple.script.getCanonicalName());
      entry.setLink(triple.httpUrl.toExternalForm());
      SyndContent description = new SyndContentImpl();
      description.setType("text/plain");
      description.setValue(triple.status);
      entry.setDescription(description);
      feed.getEntries().add(entry);
    }
    WireFeedOutput outputter = new WireFeedOutput();
    WireFeed wirefeed = feed.createWireFeed("rss_2.0");
    try {
      return outputter.outputString(wirefeed);
    } catch (FeedException e) {
      e.printStackTrace();
      System.out.println("Feed exception trying to deserialize to RSS");
      return "";
    }
  }
}

And here is how it is registered with the MBean Server in the ScriptAgent.java code. For brevity, I only show the calls to register and start this server, please refer to previous articles to see the entire code for the ScriptAdapter.java. The snippet shown below should appear right after the block where the HTTP Adapter Server gets instantiated, registered to the MBean server and started. Notice that we choose a hardcoded (aka convention :-)) port number for the Rss Adapter Server as 1000 + the port number for the HTTP Adapter server.

// Source: src/main/java/com/mycompany/myapp/ScriptAgent.java
package com.mycompany.myapp;
...
public class ScriptAgent {
  ...
  protected void init() throws Exception {
    ...
    // load RSS Adapter
    RssAdapterServer rssAdapter = new RssAdapterServer();
    rssAdapter.setPort(DEFAULT_AGENT_PORT + 1000);
    rssAdapter.setHttpAdapterHostPort("localhost:" + DEFAULT_AGENT_PORT);
    server.registerMBean(rssAdapter, new ObjectName("adapter:protocol=RSS"));
    rssAdapter.start();
  }
  ...
}

Here are some screenshots of the RSS Adapter in action:

The Agent View - notice that the RSS Adapter server appears in the List of MBeans of type "adapter".
This is the output of the RSS Adapter server for the Agent shown above. This provides us with a single high-level view of the status of the scripts in the MBean server.
The links on the script ObjectNames in the previous screenshot points to the actual MBean view of the ScriptAdapter.

I'm sure you see where this is going, right? Now that we have an RSS feed from one MBean server, it is trivial to write a feed aggregator that shows the results of all these feeds on a single web page. You could also simply use one of the many freely available feed readers, but I prefer a custom web application doing the aggregation because that way all the end-user needs is a web browser.

I do suggest making your HTTP adapter have some sort of basic authentication, since anyone can now potentially open up the MBean viewer from the RSS feed page, so the convenience for the ops guys may end up becoming a security hole if the MBean viewer is not password protected.

The only thing left (at least from my point of view) is to make this whole thing more configurable, there are too many conventions floating around in this app at the moment. I plan to use Spring for the configuration, I will describe this in a future blog post.

Saturday, April 15, 2006

Jetty setup for serving web apps

My servlet/JSP container of choice is Resin. For those that are unfamiliar with Resin, it is a fast and extremely easy to use servlet container. However, of late, I have been experimenting with using Jetty as an in-place servlet container that I can start with Ant and run my JWebUnit tests.

While there is some documentation in the Jetty site that explains how to set up Jetty to serve JSP pages from a web application, the process is not exactly straightforward. Moreover, there was not much information available on the web about my desired setup. The Jetty documentation actually recommends that one should precompile the JSPs using the Jasper compiler before deployment, implying that JSP support may be flaky. I am happy to report, however, that I was successful in serving an application that uses Spring and Hibernate for the Model and Controller layers, and JSTL in JSPs for the View layer. So I decided to write up my experience hoping that it would be helpful to someone else with similar requirements.

My desired setup was to be able to start Jetty as an Ant target in a standard Web Application project. The Jetty server would work directly on the application. No packaging into a WAR and deploying should be necessary, and neither would the server be required to explode a WAR file. To minimize problems, I made sure that my webapp worked perfectly with Resin.

Interestingly, there are at least 3 ways to start up Jetty for any specific purpose. The first approach is to use the provided start.jar with an application specific XML configuration file. The second approach is to call the org.mortbay.jetty.Server with the appropriate classpath settings from within Ant or from a shell script, and the application specific XML configuration file. The third approach is to write a class that will instantiate the Server and configure it using Java code.

The first approach is closely tied to the directory structure of the Jetty distribution, so if your application has a different directory structure, as mine was, you would need to override the start.config with your own settings. I did not want to package the Jetty JAR files along with this start.jar in my application, and I did not really want to mess with the non-standard start.config file (unless I could not do it any other way), so that eliminated the first approach.

I started off with the third approach, but had some early successes, but since I was still trying to figure out the configuration that will work for me, this approach started me on a path of too many compile-test cycles, so I ultimately gave it up in favor of the second approach.

I used the latest stable full release of Jetty at the time of writing this, which is 5.1.10. I downloaded jetty-5.1.10-all.zip which contained the demo and the sources. I started reading through the documentation and found that there is an example web application XML configuration etc/jetty.xml file which worked with a built in Ant target "run". This target uses the second approach outlined above.

I started out with a copy of jetty.xml as my starting point. To find the classes that need to be in the Java classpath for the Server class to run correctly, I ran the following command in the root directory of the Jetty download distribution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[sujit@cyclone jetty-5.1.10]$ ant -v run
Apache Ant version 1.6.5 compiled on June 2 2005
     [java] ... output snipped ...
     [java] Executing '/usr/java/jdk1.5.0_03/jre/bin/java' with arguments:
     [java] '-Djetty.home=/home/sujit/tmp/jetty-5.1.10'
     [java] '-classpath'
     [java] '/home/sujit/tmp/jetty-5.1.10/lib/org.mortbay.jetty.jar:             /home/sujit/tmp/jetty-5.1.10/lib/javax.servlet.jar:             /home/sujit/tmp/jetty-5.1.10/ext/jasper-runtime.jar:             /home/sujit/tmp/jetty-5.1.10/ext/jasper-compiler.jar:             /home/sujit/tmp/jetty-5.1.10/ext/ant.jar:             /home/sujit/tmp/jetty-5.1.10/ext/commons-el.jar:             /home/sujit/tmp/jetty-5.1.10/ext/commons-logging.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j-remote.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j-tools.jar:             /home/sujit/tmp/jetty-5.1.10/ext/mx4j.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xercesImpl.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xml-apis.jar:             /home/sujit/tmp/jetty-5.1.10/ext/xmlParserAPIs.jar'
     [java] 'org.mortbay.jetty.Server'
     [java] '/home/sujit/tmp/jetty-5.1.10/etc/admin.xml'
     [java] '/home/sujit/tmp/jetty-5.1.10/etc/jetty.xml'
     [java] ... more stuff snipped ...

Some of the JARs I already had in my WEB-INF/lib directory, the rest I copied from the Jetty distribution into my WEB-INF/lib directory of the webapp. I also made a copy of the supplied etc/jetty.xml file. The jetty.xml file is set up to start all applications under the context root, so I commented that portion and uncommented the next block which works with a single web application. I also changed the context root and the webapp name for my web application. This is the block that I changed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  <Call name="addWebApplication">
    <Arg>/prozac</Arg>
    <Arg>./webapps/prozac</Arg>
                                                                                
    <Set name="extractWAR">false</Set>
    <Set name="defaultsDescriptor">org/mortbay/jetty/servlet/webdefault.xml</Set>
    <Set name="classLoaderJava2Compliant">true</Set>
                                                                                
    <Set name="virtualHosts">
      <Array type="java.lang.String">
        <Item></Item>
        <Item>127.0.0.1</Item>
        <Item>localhost</Item>
      </Array>
    </Set>
  </Call>

I also uncommented the systemClasses and serverClasses section that prevents the webapp from reloading the classes listed under systemClasses and makes the serverClasses inaccessible from the web application. Since I was using the JSTL tag libraries, I also uncommented the TagLibConfiguration under WebApplicationConfigurationClassNames.

I also built a local "start-server" target that mimicked the "run" target of the Jetty distribution. When I ran this target, I got a log4j error, saying that log4j was not properly configured. A quick look at the log4j documentation pointed me to the answer, which was to add a system property "log4j.configuration" pointing to a file URL for the log4j.properties file.

One thing I want to mention here is that I created a specific log4j.properties file for the Jetty server, which was different from what I was using for the rest of the application. The reason for this is that I wanted to only log messages INFO and above for Jetty but DEBUG and above for the rest of the application. Setting the level to DEBUG for Jetty gives many messages which look like errors but is basically Jetty cycling through various alternatives. Also the DEBUG logging for Jetty is quite verbose and not very useful unless you are debugging Jetty.

The next roadblock I had was that Jetty complained that it could not find the class javax.servlet.jsp.jstl.fmt.LocalizationContext. I found this class in lib/jstl-11.jar of my Resin distribution, so I copied this to my local server classpath as well.

The next problem I had was that Jasper failed to compile my JSP because it could not find com.sun.javac.Main in my JDK. It gives a misleading message about possible bad setting of JAVA_HOME, but if you specifically include the tools.jar file of your Java distribution in your classpath, it is able to compile the JSP.

Another little side note. For those who are tempted to ignore the ant.jar in the original classpath, as I was, based on the understanding that ant.jar is already in the classpath since the target is being invoked by Ant, here is the reason why ant.jar is needed - Jasper uses Ant and the Java compiler javac to process and compile the JSPs in the application, and the ant.jar does need to be specifically included in the classpath.

The final problem before everything came together was the start-server target complaining that the log4j.properties file was not a zip file. This was because I had added the log4j.properties file to the classpath before I found out about the log4j.configuration system property. Removing the log4j.properties file allowed Jetty to start up without problems and serve my web application without any problems.

Here is my Ant target for starting the Jetty server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    <target name="start-server" depends="setup" description="Starts the built-in Jetty server">
        <java fork="yes" classname="org.mortbay.jetty.Server" dir="." failonerror="true">
            <classpath>
                <fileset dir="lib">
                    <include name="org.mortbay.jetty.jar" />
                    <include name="javax.servlet.jar" />
                    <include name="jasper-runtime*.jar" />
                    <include name="jasper-compiler*.jar" />
                    <include name="ant*.jar" />
                    <include name="commons-el*.jar" />
                    <include name="commons-logging*.jar" />
                    <include name="mx4j-remote*.jar" />
                    <include name="mx4j-tools*.jar" />
                    <include name="xercesImpl*.jar" />
                    <include name="xml-apis*.jar" />
                    <include name="xmlParserAPIs*.jar" />
                    <include name="log4j*.jar" />
                 </fileset>
                 <fileset dir="${env.JAVA_HOME}/lib">
                     <include name="tools.jar" />
                 </fileset>
            </classpath>
            <jvmarg line="-Djetty.home=${basedir}" />
            <arg value="WEB-INF/jetty.xml" />
            <sysproperty key="log4j.configuration" value="file://${basedir}/WEB-INF/classes/jetty-log4j.properties" />
        </java>
    </target>

And here is the contents of my jetty.xml file (commented out sections omitted for brevity), which is being passed as an argument to the org.mortbay.jetty.Server class in the "start-server" target:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd">
 
<Configure class="org.mortbay.jetty.Server">
 
  <Call name="addListener">
    <Arg>
      <New class="org.mortbay.http.SocketListener">
        <Set name="Port"><SystemProperty name="jetty.port" default="8080"/></Set>
        <Set name="PoolName">P1</Set>
        <Set name="MinThreads">20</Set>
        <Set name="MaxThreads">200</Set>
        <Set name="lowResources">50</Set>
        <Set name="MaxIdleTimeMs">30000</Set>
        <Set name="LowResourcePersistTimeMs">2000</Set>
        <Set name="acceptQueueSize">0</Set>
        <Set name="ConfidentialPort">8443</Set>
        <Set name="IntegralPort">8443</Set>
      </New>
    </Arg>
  </Call>
 
  <Set name="WebApplicationConfigurationClassNames">
    <Array type="java.lang.String">
      <Item>org.mortbay.jetty.servlet.XMLConfiguration</Item>
      <Item>org.mortbay.jetty.servlet.JettyWebConfiguration</Item>
      <Item>org.mortbay.jetty.servlet.TagLibConfiguration</Item>
    </Array>
  </Set>
 
  <Call name="addWebApplication">
    <Arg>/prozac</Arg>
    <Arg>./webapps/prozac</Arg>
 
    <Set name="extractWAR">false</Set>
    <Set name="defaultsDescriptor">org/mortbay/jetty/servlet/webdefault.xml</Set>
    <Set name="classLoaderJava2Compliant">true</Set>
 
    <Set name="virtualHosts">
      <Array type="java.lang.String">
        <Item></Item>
        <Item>127.0.0.1</Item>
        <Item>localhost</Item>
      </Array>
    </Set>
  </Call>
 
  <Set name="RequestLog">
    <New class="org.mortbay.http.NCSARequestLog">
      <Arg><SystemProperty name="jetty.home" default="."/>/logs/yyyy_mm_dd.request.log</Arg>
      <Set name="retainDays">90</Set>
      <Set name="append">true</Set>
      <Set name="extended">false</Set>
      <Set name="LogTimeZone">GMT</Set>
    </New>
  </Set>
 
  <Set name="requestsPerGC">2000</Set>
  <Set name="statsOn">false</Set>
  <Set class="org.mortbay.util.FileResource" name="checkAliases" type="boolean">true</Set>
 
  <Set name="systemClasses">
    <Array type="java.lang.String">
      <Item>java.</Item>
      <Item>javax.servlet.</Item>
      <Item>javax.xml.</Item>
      <Item>org.mortbay.</Item>
      <Item>org.xml.</Item>
      <Item>org.w3c.</Item>
      <Item>org.apache.commons.logging.</Item>
    </Array>
  </Set>
 
  <Set name="serverClasses">
    <Array type="java.lang.String">
      <Item>-org.mortbay.http.PathMap</Item>
      <Item>org.mortbay.http.</Item>
      <Item>-org.mortbay.jetty.servlet.Default</Item>
      <Item>-org.mortbay.jetty.servlet.Invoker</Item>
      <Item>-org.mortbay.jetty.servlet.JSR154Filter</Item>
      <Item>org.mortbay.jetty.</Item>
      <Item>org.mortbay.start.</Item>
      <Item>org.mortbay.stop.</Item>
    </Array>
  </Set>
 
</Configure>

No other changes were required in the application. I have seen Jetty being used before as an embedded servlet container, and I know that JBoss used Jetty as its servlet container of choice at one point, so Jetty itself is not entirely new to me. However, this is the first time I have successfully used Jetty for anything. I found Jetty to be quite nimble and light on resources. I think that along with the many uses for Jetty as a lightweight, embeddable, high performance servlet container for moderate traffic (I am told that performance degrades with extremely high traffic volumes), it can also be generally useful for the use I am putting it to, that is, to serve as an in-place servlet container for JSP unit testing.