Contents
- Hello World!
- Adding Methods/Pages
- Request Parameters
- Forms
- Multipart Formdata
- Streaming Content
- Cookies
- Redirects
- MIME Types
- Exception Handling
- Caching Content
- Configuration
- Session Management
- Security
- Static Content
- Groovy
- Samples
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!