Project 4: Java on Rails

COMP 150-SEN, Spring 2019

Test case due: Wed, Apr 17 @ 11:59pm

Main project due: Fri, Apr 26 @ 11:59pm

In this project you will implement a web server that uses a model-view-controller architecture. We'll call the system jrails, because it uses ideas from Ruby on Rails. However, because Java is, well, Java, the framework will be noticeably cruftier than Rails. On the other hand we'll only implement a tiny fraction of what goes into a real web server framework.

The code for this project is structured a bit differently than for previous projects. The MVC framework source code, which is what you'll be editing, is in the jrails/ directory. The code in that directory will be compiled to jrails.jar, which can then be added to the classpath when compiling a web app that uses the framework.

To make this process easier, we've given you a build.xml file for the Apache Ant build system. Ant is widely available as a package for many different package managers, e.g., there are Ubuntu packages, for OSX you can install it with Homebrew, etc. Once you have ant installed, you can just run ant build to build the project. Among other things, this will compile the jrails code and put it in jrails.jar.

To help you more easily understand the structure of jrails, we've included an example app spread across four files in the top-level directory: Book.java, BookController.java, BookView.java, and Main.java. Once you implement jrails, this particular app will let users keep track of a list of books. The book project is also built when you run ant build. When everything is successfully compiled, you can run the book app with

   java -cp jrails.jar:. Main

When you do this, the server will start up on port 8000 on the local machine. You can connect to it by firing up a web browser and going to

   http://localhost:8000/test

This should display a simple web page in your browser, and the console should show that the request was received. If you want to play around a bit more with the server, if you go to any other URL that's not routed (see below), the web server will dump some information about the request onto the console. For example, I noticed that when I go to a web page on this server, my web browser tries to get /favicon.ico in addition to the page I requested. It's fine for the server to just drop such requests on the floor.

Like most MVC web app frameworks, jrails includes models that represent the database. More specifically, a model is any class that subclasses jrails.Model, which uses reflection to provide primitive database functionality. For example, here is the model from the book app:

import jrails.*;
public class Book extends Model {
    @Column public String title;
    @Column public String author;
    @Column public int num_copies;
}

Because Book extends Models, conceptually there is a database table corresponding to the Book class. That table has three columns, title, author, and num_copies, with the corresponding types.

Here is some code that uses the above model, with inline comments to explain what's happening:

Book b = new Book();
b.title = "Programming Languages: Build, Prove, and Compare";
b.author = "Norman Ramsey";
b.num_copies = 999;
// The book b exists in memory but isn't saved to the db
b.save(); // now the book is in the db
b.num_copies = 42; // the book in the db still has 999 copies
b.save(); // now the book in the db has 42 copies
Book b2 = new Book();
b2.title = "Programming Languages: Build, Prove, and Compare";
b2.author = "Norman Ramsey";
b2.num_copies = 999; // hm, same as other book
b2.save(); // a second record is added to the database
assert(b.id() != b2.id()); // every row has a globally unique id (int) column,  so we can tell them apart
Book b3 = Model.find(Book.class, 3); // finds the book with id 3 in the db, if any
List bs = Model.all(Book.class); // returns all books in the db
b.destroy(); // remove book b from db

Your job for this part of the project is to implement the Model class in jrails to achieve this functionality. Here are the details:

The views for jrails are HTML pages that are sent back to the client web browser. Ruby on Rails uses HTML with embedded Ruby code, but developing such a system is a bit too complex for this project. Instead, we will create HTML by invoking methods in Java. More specifically, here is an example view from the book project:

    public static Html show(Book b) {
	return p(strong(t("Title:")).t(b.title)).
	    p(strong(t("Author:")).t(b.author)).
	    p(strong(t("Copies:")).t(b.num_copies)).
	    t(link_to("Edit", "/edit")).t(" | ").
	    t(link_to("Back", "/"));
    }

Here the show method takes a Book and returns an Html, which is a data structure representation of HTML. The View superclass provides a variety of methods for constructing HTML, e.g., the p(...) method constructs HTML with paragraph tags <p>...</p>, where the child ... is more HTML that is created by calling some other methods inherited from View. Moreover, an Html object has as instance methods all the same tag methods as View, which are used to sequence HTML. For example, calling p(a).p(b) returns in <p>a</p><p>b</p> for some a and b.

Your next task is to implement View and Html such that calling toString on an Html should provide HTML of the corresponding format. Here are the tags you need to support:

For example, the following code:

Book b = new Book();
b.title = "Programming Languages: Build, Prove, and Compare";
b.author = "Norman Ramsey";
b.num_copies = 999;
String s = BookView.show(b);

results in the following HTML (spaces and line breaks added for clarity):

<p><strong>Title:</strong>Programming Languages: Build, Prove, and Compare</p>
<p><strong>Author:</strong>Norman Ramsey</p>
<p><strong>Copies:</strong>999</p>
<a href="/edit">Edit</a> | <a href="/">Back</a>

When a web request comes it, it's eventually passed to a controller, which handles the request and returns an HTML page in response. For example, here is a controller from our example project:

    public static Html show(Map<String, String> params) {
	int id = Integer.parseInt(params.get("id"));
	Book b = (Book) Book.find(id);
	return BookView.show(b);
    }

The input to all controller methods is a hash of parameter names to values, both of which are strings. In this particular case, the router (discussed next), will map the URL http://localhost:8000/show?id=42 to a call to show where id is mapped to "42". Then the body of show does some computation, in this case retrieving a book from the database, and returns the corresponding HTML page by calling one of the view methods.

If you look through the controller methods for the example app, you'll see that they support a basic CRUD interface, i.e., create, read, update, and delete.

There's actually no code you need to implement to support controllers. There is a superclass, Controller, but for purposes of this project it can be empty. (For a more full-featured web framework, we'd probably want to add some functionality to it.)

As we just saw, different HTTP requests are handled by different controllers. Among other things, each HTTP request has a verb (e.g., GET, POST) and a path (e.g., /show from the URL localhost:8000/show?id=42). The job of the router is to map such requests to controller methods.

The router has to be configured on a per-application basis, which is done with a series of calls to addRoute. For example, here is the routing for the example application:

        JRouter r = new JRouter(); 
        r.addRoute("GET", "/", "BookController", "index"); 
        r.addRoute("GET", "/show", "BookController", "show"); 
        r.addRoute("GET", "/new", "BookController", "new_book"); 
        r.addRoute("GET", "/edit", "BookController", "edit"); 
        r.addRoute("POST", "/create", "BookController", "create"); 
        r.addRoute("POST", "/update", "BookController", "update"); 
        r.addRoute("GET", "/destroy", "BookController", "destroy"); 

For example, the second call to addRoute tells the routers to map a GET request for show to the BookController's show method.

You need to implement the following behavior for JRouter:

Finally, jrails include a class JServer with a method void start(JRouter r) that starts up an HTTP server on port 8000, listens for requests, and routes any requests received through r, sending the result back to the web browser. We've written this class for you, and you shouldn't need to modify it.

Then each application sets up its routes and calls start to launch the web server. We've put all the necessary code for the example app in a class called Main, which you can run as described above.

In general, when web servers encounter errors, they often send some nice web page back to the web browser indicating something went wrong. But for this project, we're not going to do that. Instead, your web server is just going to crash with an exception. That's okay for this project, though it wouldn't be great for a real-world system.

Although on the surface this project seems hard to test—e.g., you might think you have to write tests that connect over the network to a web server—in fact all the model/view/router code is designed to be tested locally. You'll notice that the only code involving networking is in JServer, which you didn't write and therefore you don't have to test.

Thus, as part of this project, you must write at least one test case and post it on Piazza to share with the class. Since there are several different parts of the project, we'll use the following rules to split up the tests:

Last digit of Tufts IDClass for your test
0-3Model.java
4-7View.java and Html.java
8-9Router.java

Put all your code in the code skeleton we have supplied, and upload your solution using Gradescope.