this project has kindly been sponsored by UPCO - The Ultimate People Company, supporters of Open Source sponsored by UPCO


Contents

Hello World!

import net.sf.ooweb.http.Server;
import net.sf.ooweb.http.pygmy.OowebServer;
import net.sf.ooweb.objectmapping.Controller;

@Controller("/")
public class HelloWorld {

    public static void main(String[] args) {
        Server s = new OowebServer();
        s.addController(new HelloWorld());
        s.start();
    }

    public String index() {
        return "<p>Hello World @ " + new Date().toString() + "</p>";
    }
}       

This is the code for the usual "Hello World" application with OOWeb. The main() method creates an OOWeb server, adds our HelloWorld object and then waits for requests. The @Controller annotation specifies the path to attach the object to ("/") which means that the HelloWorld instance will be mapped to the server root.

The index method is similar to the index page of a webserver - it is called on the object when no specific page is requested. As you can see, it simply returns some HTML with the Hello World text.

Compile and run this, making sure you have the ooweb-[version].jar, pygmy-core-ooweb.jar and pygmy-handlers.jar files on your classpath. Then point your browser at http://localhost:8080/ to test.

Adding methods/pages

The next step is to add another page, let's add a doStuff() method to the HelloWorld class. This means that when you go to http://localhost:8080/doStuff, that method will be used to serve the page.

public String doStuff() {
    return "<h1>I'm doin' it!</h1>";
}

Normally, methods need to be public and return either a String or a ResponseState (which we'll come on to) for OOWeb to treat it as a web method. So you can define private methods on your objects without worrying that the server will try to serve them to the browser clients

/** will be served as http://localhost:8080/calculate */
public String calculate() {
    return "The result is: " + complexCalculation();
}

/** will not be visible in browser */
private long complexCalculation() {
    return ((3+5+7+9)^2)*5;
}

Sometimes you will want to exclude a method that has a valid signature from being visible in a browser. Typical examples might include your getter methods for fields on your controller object. To exclude any method from being served to a client, just add the @Exclude tag.

/** has a valid web method signature, but will not show up in browser */
@Exclude
public String getConfigValue() {
    return configValue;
}

Request Parameters

To take advantage of query string name/value pairs, you simply have your methods accept a RequestState and use its getRequestArgs() method.

public String formget(RequestState state) {
    StringBuffer sb = new StringBuffer();
    sb.append("Found the following data in the request:<br><ul>");
    
    for (Map.Entry<String, Object> me : state.getRequestArgs().entrySet())
        sb.append("<li>").append(me.toString()).append("</li>");    
    }	
    
    sb.append("</ul><br><br>Try changing the request params in the address bar.");
    return sb.toString();
}

Try http://localhost:8080/formget?foo=bar&ooweb=good

Forms

Form posts are easy. Simply create your form in one method, setting the action property to the method that will receive the POSTed values.

public String form() {
    return "<html><body>" +
           "<form method='post' action='formpost'>" +
           "<p>Your Name: <input type='text' name='name'/>" +
           "<input type='submit' value='submit'/></p></form>" +
           "</body></html>";
}

public String formpost(RequestState state) {
    Map m = state.getRequestArgs();
    return "Nice to meet you " + params.get("name") + "!";
}

Go to http://localhost:8080/form and submit your name.

Multipart Formdata

OOWeb can also handle multipart form-data, including file uploads. To accept a file, you can extract it from the request.

/*
 *  Presents a form allowing the user to upload a file.
 */
public String upload() {
    return "<html><body><form name=\"upload\" " + 
        "  enctype=\"multipart/form-data\" action=\"uploadFile\" method=\"post\">" +
        "<p>Pick a file to upload.</p><p><input type=\"file\" name=\"somefile\"/></p>" +
        "<p><input type=\"hidden\" value=\"nothing\" name=\"other\">" +
        "<input type=\"submit\" value=\"submit\"/></p>" +
        "</form></body></html>";
}

/*
 *  Tells the user some info about the file uploaded
 */
public String uploadFile(RequestState state) {
    FormEncodedFile f = (FormEncodedFile) state.getRequestArgs().get("somefile");
    return "<html><body><p>Info about the file you uploaded:</p>" +
        "<p>Filename: " + f.getFilename() + "<br/>" +
        "Size: " + f.getData().length + " bytes<br/>" +
        "MIMEType: " + f.getMimeType() + "</p>" +
        "<p><a href=\"/\">Back</a></p></body></html>";
}

Streaming Content

Creating and returning String content is OK for lots of simple, small to medium sized pages, but it's going to cause you real problems if you want to send the content of a large file for example. Since version 0.8.0, you can now make several calls to the ResponseState.setBody(Object a) method, and OOWeb will send those parts in the order you add them. Importantly, you can now call that method with parameters of type java.io.File or java.io.InputStream :)

/**
 * Streams a file and tops and tails the file content with two String parts
 * @throws IOException 
 */
public ResponseState mixStringAndStreams() throws IOException {
    File target = new File("/some/big/file.txt");
    ResponseState resp = new ResponseState();
    resp.setBody("<h1>This is a String header</h1>");
    resp.setBody(target); // <--- would also work with any InputStream
    resp.setBody("<hr/>And this is a String footer!");
    return resp;
}

Cookies

In order to retrieve cookies from the request, call RequestState.getCookies() which returns a Map<String, String>

If you want to add new cookies to the outgoing response you use the ResponseState class which we introduce here. Your OOWeb methods to date have always returned a String which forms the body of the response, and when you do that, OOWeb will wrap that body in a ResponseState object. For more control over the other aspects of the response, you can return an instance of ResponseState yourself

public ResponseState cookie1() {
    ResponseState resp = new ResponseState();
    resp.addCookie("test2", "53");
    resp.setBody("<html><body>" +
       "<p>I just set the cookie 'test2' with a  value of '53' in " +
       "this response. Go <a href='cookie2'>here</a> to read it back.</p>" +
       "</body></html>");
    return resp;
}
    
public String cookie2(RequestState state) {
    Map<String, String> cookies = state.getCookies();
    return "<html><body>" +
       "<p>For the cookie 'test2', I get a value of '" + 
       cookies.get("test2") + "'.</p>" +
       "</body></html>";
}

Redirects

Use the ResponseState once more to send the browser to another page/URL.

public ResponseState redirect() {
    ResponseState resp = new ResponseState();
    resp.sendRedirect("http://www.bbc.co.uk");
    return resp;
}

Mime Types

Your method can set its own Mime Type for the response by calling RequestState.setMimeType("mime/type"). If your method doesn't set a mime type, it defaults to text/html.

public ResponseState xmlTest() {
    ResponseState state = new ResponseState();
    state.setMimeType("text/xml");
    state.setBody("<hello><world>this</world><world>is</world><world>xml</world></hello>");
    return state;
}

This sort of thing is pretty handy for RSS/ATOM feeds, XMLRPC servers and so on

Exception Handling

OOWeb handles exceptions thrown from your pages (methods). The root cause Exception that you throw will be logged to the server log, and the message in the exception will be sent back to the client.

public String alwaysCausesSomeProblem() throws Exception {
    throw new Exception("It's that problem method again!");
}

To provide custom error pages, implement the net.sf.ooweb.objectmapping.ErrorHandler interface and register it as a controller (you don't need to add the @Controller annotation). Any exceptions thrown from your web methods will result in a call to this method to generate the error page, passing the thrown object as a parameter.

As the interface is a single method, you could even implement it as an anonymous class or just have one of your existing controllers do it.

public class HelloWorld implements ErrorHandler {    
    public String onError(Throwable ex) {
        return "<h1>Oops!</h1>We caught an exception. Message was '"
		    + ex.getMessage() + "'";
    }
}

-or-

Server s = new OowebServer();
s.addController(new ErrorHandler() {
    public String onError(Throwable ex) {
           return "<h1>Oops!</h1>We caught an exception. Message was '"
		    + ex.getMessage() + "'";
	}
});

Caching Content

Individual methods, or entire controllers can be marked as Cacheable. This annotation denotes that the response can be cached by the server, and how long, in seconds, it can be cached for.

    @Cacheable(30)
    public String index() {
        return "<p>Hello World @ " + new Date().toString() + "</p>";
    }

In the above example, a copy of our first Hello World sample, the method has been marked as cacheable for 30 seconds. That means after the first request, you will - for the next 30 seconds - see the same system time in your browser as the cached response is returned. After 30 seconds, you will get a new system date and time shown. A value of 0 specifies that the content may never expire from cache.

By default, you get a simple memory based implementation of the ooweb Cache interface. It has no distribution or replication functionality and is not configurable with sizes, disk stores and so on. If you need serious use of caching, then create your own implementation of this interface (perhaps backed with ehcache, swarmcache or similar) and use that.

Configuration

The OOWeb server will start with sane defaults when you instantiate the Server object with no additional configuration - we've done quite a lot without any already :) However you can optionally configure your OOWeb application by supplying a reference to a properties file too.

Values that you specify in your properties file will either override defaults or provide additional configuration for your installation. The example below shows changing the port OOWeb listens on for requests.

#
# ooweb.properties
#
# example config file for an ooweb application
#

# specify a different port to run the server on
http.port=1234

# session timeout 900s = 15mins
ooweb.sessiontimeout=900

In order to use your properties, supply them as a File argument to the server

Server s = new OowebServer(new File("/path/to/ooweb.properties"));
s.start();

Session Management

OOWeb provides a session handler and automatically takes care of session cookies.

public String session1(RequestState state) {
    state.getSession().put("test1", "58");
    return "<html><body>" +
        "<p>I just added the value pair test1=58 to your session. " +
        "Go <a href='session2'>here</a> to read it back.</p>" +
        "</body></html>";
}
   
public String session2(RequestState state) {
    return "<html><body>" +
        "<p>For the key 'test1' in your session, I " +
        "got the value '" + state.getSession().get("test1") + "' back.</p>" +
        "<p>Try <a href='session3'>invalidating the session</a> if you like</p>" + 
        "</body></html>";
}
    
public ResponseState session3() {
    ResponseState resp = new ResponseState();
    resp.invalidateSession();
    resp.setBody("<html><body>" +
        "<p>Session invalidated.  Try <a href='session2'>reading</a> " + 
        "the previous value in the session</p>" + 
        "</body></html>");
    return resp;
}

Access the session by calling RequestState.getSession() in your code. The return type is a java.util.Map of the session values.

By calling the SessionManager.setSessionStrategy() method and passing SessionManager.Strategy.REPLICATED (or alternatively setting the ooweb.sessionstrategy=replicated property in the configuration file), you can use OOWeb's built in distributed hashtable for session management. It will autodiscover other OOWeb nodes on the same network via UDP broadcast and automatically propagate sessions to the other nodes. Bonus!

Security

OOWeb methods can be secured with roles based authorization, and authentication using either HTTP BASIC (the one where your browser normally throws up a login box for the name/password) or form based authentication where you define your own custom login form.

The OOWeb framework implements the correct redirects on your behalf in order to protect secure methods, but it's up to you to provide your own authentication mechanisms. This means you have to do a bit of work, but it also means you have ultimate flexibility on how to authenticate your users. You can look up the details in a database, LDAP server, properties file or anything you want.

Let's look at authentication methods first. Whether you want to use BASIC or form based authentication, you have to provide a class that will actually do the lookups of the name/password combination that the user inputs. This is achieved by implementing the net.sf.ooweb.objectmapping.Authenticator interface. It's simple and has just one method...

public User authenticate(String username, String password) throws Exception;

You register the authenticator the same way as you do a controller..

Server s = new OowebServer();
s.addController(new SampleAuthenticator());
s.addController(etc...);

The framework will pass the username and password properties gathered either from the BASIC dialog your browser throws up, or a form that you define (see later on) to this Authenticator. What you need to do is determine whether those credentials are any good and if so return a valid net.sf.ooweb.objectmapping.User object with all of the roles that the user has specific to your application. If the credentials are no good, you can return null or throw an exception from the method. Any exception will be taken by OOWeb to mean "Not Authenticated".

Here's an example implementation that queries a database and returns a valid User object to represent the subject.

public class MyAuthenticator implements Authenticator {
    public void User authenticate(String username, String password) 
    throws Exception {

        try {
            // get database connection/statement (code omitted)

            // Prevent sql injection attacks
            username = username.replace(';',' '); username = username.replace('\'',' ');
            password = password.replace(';',' '); password = password.replace('\'',' ');

            ResultSet rs = stmt.executeQuery(
                "SELECT password FROM credentials WHERE username='" + username + "'");
            if (password.equals(rs.getString("password"))) {
                // login ok
                WebUser u = new WebUser();
                // in reality you'd look the roles up too..
                u.addRole("manager");
                u.setUserName(username);
                return u;
            }
            else
                // login not ok!
                throw new Exception("Don't know you");
			
        } finally {
            // clean up db resources (omitted)
        }
    }
}

The WebUser class used above is part of OOWeb and is included for convenience. It has the minimal amount of functionality you might want from a User implementation. If you need no more, great. If you do, simply implement your own version of the net.sf.ooweb.objectmapping.User interface.

Form based login

If you don't specify a LoginForm implementation, you'll automatically get HTTP BASIC authentication. If that's what you're after, you need specify no more information for the authentication config. To get a nice looking form however, simply implement LoginForm and register it with the OOWeb server..

public String loginForm(String action, String usernameField, String passwordField)
throws Exception;

Make sure you DON'T protect the login form itself (see below)

Method protection

OK, so authentication is all well and good, but you need something to protect in order that OOWeb actually asks your users for a username and password in the first place! It's another simple annotation for this.

@Secure({"manager", "admin"})
public String securePage() {
    return "You must be a manager or an admin to see this!";
}

Adding the @Secure annotation to the method protects it. An authenticated user must have one of the roles specified to be able to access the method. You can also use this annotation at the class level to protect all the methods in the class.

Static Content

Static content is dealt with entirely by the underlying HTTP implementation that OOWeb uses - pygmy. In order to serve up files from your disk, you will need to add additional handler configurations which means if you weren't using a properties file before, you need one now :)

chain.chain=ooweb,file

file.class=pygmy.handlers.FileHandler
file.url-prefix=/doc
file.root=/usr/share/doc/

The example above configures a file handler that will map the /doc URL to the /usr/share/doc directory on your file system. Note that no directory indexing has been configured yet so if you go to http://localhost:8080/doc/ you'll get an HTTP 404 (Not Found) error.

Note the chain.chain property: this tells pygmy in what order to process handlers. Here, the OOWeb handler takes precedence, so if you registered a controller at the /doc URL too, that is what would be used first. The list of names in the chain.chain property also relates to the other config items, so as you can see from the bolded property names above, file in the chain list relates to the file.whatever properties below.

To add more file locations, you could do this:

chain.chain=ooweb,mypages,docs

mypages.class=pygmy.handlers.FileHandler
mypages.url-prefix=/~darren
mypages.root=/home/darren/public_html

docs.class=pygmy.handlers.FileHandler
docs.url-prefix=/doc
docs.root=/usr/share/doc/

Directory Indeces

Adding in automatic directory index pages is similar to configuring files.

chain.chain=ooweb,file,directory

file.class=pygmy.handlers.FileHandler
file.url-prefix=/doc
file.root=/usr/share/doc/

directory.class=pygmy.handlers.DirectoryHandler
directory.url-prefix=${file.url-prefix}
directory.root=${file.root}

Note the convenient reuse in the config file of property values that have already been configured such as ${file.root}.

Groovy

The latest version of Groovy (version 1.1-beta1 at the time of writing) now supports Java5 annotations and is the first JVM scripting language to do so. That's awesome (as is Groovy itself) - because that means your dynamic web applications just got even easier! What would you say to a web application that displays database content driven by a user query and ALL implemented in less than 30 lines of code with no config??

Here it is..

#!/usr/bin/env groovy

import net.sf.ooweb.http.pygmy.OowebServer
import net.sf.ooweb.objectmapping.Controller
import groovy.sql.Sql

def s = new OowebServer()
s.addController(new WorldApp())
s.start()


@Controller(["/"])
class WorldApp {

  def sql = Sql.newInstance("jdbc:mysql://localhost/world", "darren", "", "com.mysql.jdbc.Driver")

  def index() {
    def writer = new StringWriter()
    def html = new groovy.xml.MarkupBuilder(writer)
    html.html() {
      h1("Choose Country"){}
      form(action:'showcities', method:'POST') {
        select(name:'code', onchange:'this.form.submit()'){
          sql.eachRow("SELECT Code, Name FROM Country ORDER BY Name") { row->
            option(value: row.Code, row.Name)
          }
        }
      }
    }
    writer
  }

  def showcities(state) {
    def ccode = state.requestArgs.code
    def writer = new StringWriter()
    def html = new groovy.xml.MarkupBuilder(writer) 
    html.html() {
      h1("City Populations"){}
      table() { 
        tr() { 
          th("City"){} 
          th("Population") {} 
        }
        sql.eachRow(
        "SELECT Name, Population FROM City WHERE CountryCode = ${ccode}") { row ->
          tr() { 
            td(row.Name){} 
            td(row.Population){}
          }
        }
      }
      a(href:'/', "Again"){}
    }
    writer
  }
}

The sample is using the 'world' database available from mysql here. It also takes advantage of Groovy's MarkupBuilder syntax to keep the String concatenation and angled brackets out of the code.

Please note the complete absence of any input validation that might lead to SQL injection attacks in a less trivial implementation!

Samples

OOWeb sample code in the /samples folder can be built using their own build files in the root of the sample application directory. Two sample applications exist, one showing basic OOWeb functionality and the other displaying a more realistic application with database integration and a more robust templating strategy for the view tier.

Easy!

Go play!