Wednesday, January 25, 2012

Simple Java MVC for Google App Engine

Using heavy Java frameworks on Google Application Engine (GAE)  is painful: after some time, when you website gets no incoming requests, GAE stops Java Virtual Machine (JVM). After that to processing new request JVM has to start again and has to start the Framework. Unfortunately Java EE frameworks have doing some initialization process, that takes much time. I tried to use great Play!Framework and Spring Framework but slow "cold start" and slow working forced me to find another solution for implementing MVC for my projects.
I'm going to describe how to write simple MVC framework using plain Java HttpServlets and FreeMarker that can run fast in GAE, doesn't have slow "cold start" and much CPU time consuming.
For my project I used Eclipse Java EE IDE (Indigo Release). I had installed Google Plugin for Eclipse
to make GAE applications development faster and easier.


Layouts 
From Google Development Tools helper create new Web Application Project with name SimpleMVC and package com.blogspot.shulgadim. I used appengine-java-sdk-1.6.0.
To create view templates I used FreeMarker library due to its power and flexibility.
Let's configure FreeMarker create views folder and layout files.
1. Download and copy to war/WEB-INF/lib folder freemarker-gae-2.3.18.jar archive.
2. In Project Properties -> Java Build Path -> Add External Jars... choose freemarker-gae-2.3.18.jar.
3. In war/WEB-INF directory create views folder.
4. In views folder create layout folder to store website layout, home and about folders to store our custom views.

We are going to create two sections in our website: "Home" and "About". Each section will be managed by controllers. (So we will have HomeController and AboutController - I  will write about it later). Each section will have subsections (Each subsection will be managed by action in controller). The names of subsections (actions in controllers) let be: index, pageOne and pageTwo and actually what you want to add ass actions and views.
In layout  folder you have to create following files:

war/WEB-INF/views/layout/header.ftl
<center>
    Header
</center>

war/WEB-INF/views/layout/footer.ftl
<center>
    Footer
</center>

war/WEB-INF/views/layout/menubar.ftl
<a href="/home/index">Home</a>
<a href="/about/index">About</a>

war/WEB-INF/views/layout/homeSidebar.ftl
<a href="/home/pageOne">PageOne</a><br/>
<a href="/home/pageTwo">PageTwo</a><br/>

war/WEB-INF/views/layout/homeLayout.ftl
<#macro mainLayout>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <title>SimpleMVC</title>                    
        </head>  
        
        <body >        
            <table width="400" border="1" cellpadding="0" cellspacing="0" >        
                <tr >    
                    <td colspan="2" style="background-color: rgb(250,250,200);">                                                            
                        <#include "header.ftl"/>
                    </td>                                                                    
                </tr>
                <tr >
                    <td colspan="2" style="background-color: rgb(200,250,200);">
                        <#include "menubar.ftl"/>
                    </td>
                </tr>
                <tr>
                    <td width="50" style="background-color: rgb(250,200,200);">                            
                        <#include "homeSidebar.ftl"/>                                                                                        
                                                                                                                                                        
                    </td>    
                    <td style="background-color: rgb(200,200,250);">
                        <center>                                            
                            <b><#nested/></b>
                        </center>                        
                    </td>                                
                </tr>
                <tr>
                    <td colspan="2" style="background-color: rgb(250,250,200);">
                        <#include "footer.ftl"/>
                    </td>
                </tr>
            </table>
        </body>
    </html>
</#macro>

war/WEB-INF/views/layout/aboutSidebar.ftl
<a href="/about/pageOne">PageOne</a><br/>
<a href="/about/pageTwo">PageTwo</a><br/>

war/WEB-INF/views/layout/aboutLayout.ftl
<#macro mainLayout>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <title>SimpleMVC</title>                    
        </head>  
        
        <body >        
            <table width="400" border="1" cellpadding="0" cellspacing="0" >        
                <tr >    
                    <td colspan="2" style="background-color: rgb(240,240,200);">                                                            
                        <#include "header.ftl"/>
                    </td>                                                                    
                </tr>
                <tr >
                    <td colspan="2" style="background-color: rgb(200,240,200);">
                        <#include "menubar.ftl"/>
                    </td>
                </tr>
                <tr>
                    <td width="50" style="background-color: rgb(240,200,200);">                            
                        <#include "aboutSidebar.ftl"/>                                                                                        
                                                                                                                                                        
                    </td>    
                    <td style="background-color: rgb(200,200,240);">
                        <center>                                            
                            <b><#nested/></b>
                        </center>                        
                    </td>                                
                </tr>
                <tr>
                    <td colspan="2" style="background-color: rgb(240,240,200);">
                        <#include "footer.ftl"/>
                    </td>
                </tr>
            </table>
        </body>
    </html>
</#macro>

I don't write much about using FreeMarker because it's not a topic of this article. The most important thing that we used to create out layouts is macros to implement simple inheritance of layouts.
Lest create out views:

Views

war/WEB-INF/views/errorpage.ftl
<#import "/layout/homeLayout.ftl" as layout>

<@layout.mainLayout>
    <div style="color: rgb(250,10,10);">
        ${message}
    </div>        
</@layout.mainLayout>
The error page appears when unsupported request is sent to Controllers.

war/WEB-INF/views/home/index.ftl ... pageOne.ftl ... pageTwo.ftl
<#import "/layout/homeLayout.ftl" as layout>

<@layout.mainLayout>
    ${message}    
</@layout.mainLayout>
The ${message} just a variable to display some text message that is defined in actions.

war/WEB-INF/views/about/index.ftl ... pageOne.ftl ... pageTwo.ftl
<#import "/layout/aboutLayout.ftl" as layout>

<@layout.mainLayout>
    ${message}
</@layout.mainLayout>

war/WEB-INF/index.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>SimpleMVC</title>
            <meta http-equiv="refresh" content="0; 
            url=http://localhost:8888/home/index">
    </head>
    <body>
    </body>
</html>
It's out start page, "entry point" to website. It redirects us to home/index page.

Controllers
The most interested thing is how to implement controllers. We have one controller superclass called Controller. Another controllers will be inherited from it. In the init() method of Controller class the FreeMarker initialization is performed. We set "WEB-INF/views" folder to find layouts and views, set encoding and so on. The doGet() method parses requests and by means of Java reflection called appropriate class (Controller) and it's method (Action).  If no class or method is found the errorpage appears.

src/com.blogspot.shulgadim.controllers.Controller
package com.blogspot.shulgadim.controllers;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import freemarker.template.*;


@SuppressWarnings("serial")
public class Controller extends HttpServlet {
    
    protected Configuration cfg;    
    protected HttpServletRequest req;
    protected HttpServletResponse resp;
    
    public void init() { 
        //FreeMarker configuration initialization;         
        cfg = new Configuration();//create a FreeMarker configuration object
        // set a "views" directory         
        cfg.setServletContextForTemplateLoading(getServletContext(), 
                                                "WEB-INF/views"); 
        cfg.setDefaultEncoding("UTF-8");
        cfg.setNumberFormat("0.######");        
    }
    
    public void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        doGet(req,resp);
    }
    
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        
        this.req = req;
        this.resp = resp;
                
        System.out.println( "Controller: " + req.getServletPath());        
                        
        String s = req.getPathInfo();
        if(s!=null){
            String[] s1 = s.split("/");        
            if(s1.length>1){
                System.out.println( "Action: " + s1[1]);                        
                try {
                    java.lang.reflect.Method method = this.getClass().
                                                           getMethod(s1[1]);
                
                    try {
                        method.invoke(this, new Object[] {});
                    } catch (IllegalArgumentException e) {                            
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {                            
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {                            
                            e.printStackTrace();
                    }
                
                } catch (SecurityException e1) {                
                        e1.printStackTrace();
                } catch (NoSuchMethodException e1) {
                    try {
                        this.errorpage("Page not found!");
                    } catch (ServletException e) {                            
                        e.printStackTrace();
                    }
                                
                }
            }
        }                   
    }
        
    public void errorpage(String message) 
            throws IOException, ServletException{
        Map<String,String> root = new HashMap<String,String>(); 
        root.put("message", message);         
        TemplateHelper.callTemplate(cfg,resp,"/errorpage.ftl",root);
    }
    
}

The TemplateHelper class calls the view by its name and path. It sets content type, charset and  passes parameters to view from action. The parameters are contained in Map collection called "root".

src/com.blogspot.shulgadim.controllers.TemplateHelper 

package com.blogspot.shulgadim.controllers;

import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

public class TemplateHelper {
    @SuppressWarnings("rawtypes")
    public static void callTemplate(Configuration cfg, 
                                    HttpServletResponse resp, 
                                    String template, 
                                    Map root)
            throws IOException, ServletException
    {        
        Template t = cfg.getTemplate(template); // Get the template object              
        resp.setContentType("text/html; charset=utf-8");                
        Writer out = resp.getWriter();                 
        try { 
            t.process(root, out); // Merge the data-model and the template 
        } catch (TemplateException e) { 
        throw new ServletException( 
            "Error while processing FreeMarker template", e); 
        }
    }
    
}

Here is our custom controller that is inherited from Controller superclass. HomeController contains index(), pageOne(), pageTwo() or what we want methods that can be called by url request. In web.xml file (I'll show it later ) the servlet mapping is performed, so by /home name in url request servlet HomeController is called, by /home/index request the index() method of HomeController is called according to the doGet() method in Controller superclass. Inside index() method we put message "Home/index" in root object and send it to /home/index.ftl view by means of TemplateHelper. The working of another methods is the same as described.

src/com.blogspot.shulgadim.controllers.HomeController 

package com.blogspot.shulgadim.controllers;

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

import javax.servlet.ServletException;

@SuppressWarnings("serial")
public class HomeController extends Controller {
    
    public static final String ctrlName = "/home";
    
    public void index() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","Home/index");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/index.ftl",root);        
    }
    
    public void pageOne() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","Home/pageOne");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/pageOne.ftl",root);        
    }
    
    public void pageTwo() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","Home/pageTwo");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/pageTwo.ftl",root);        
    }
    
}


src/com.blogspot.shulgadim.controllers.AboutController 

package com.blogspot.shulgadim.controllers;

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

import javax.servlet.ServletException;

@SuppressWarnings("serial")
public class AboutController extends Controller {
    
    public static final String ctrlName = "/about";
    
    public void index() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","About/index");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/index.ftl",root);        
    }
    
    public void pageOne() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","About/pageOne");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/pageOne.ftl",root);        
    }
    
    public void pageTwo() throws IOException, ServletException
    {
        Map<String, Object> root = new HashMap<String, Object>();
        root.put("message","About/pageTwo");                                                                                                                        
        TemplateHelper.callTemplate(cfg,resp, ctrlName + "/pageTwo.ftl",root);        
    }
}

Deployment descriptor file performs the servlet mapping and sets welcome file.

war/WEB-INF/web.xml
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

      <servlet>
        <servlet-name>HomeController</servlet-name>
        <servlet-class>
            com.blogspot.shulgadim.controllers.HomeController
        </servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HomeController</servlet-name>
        <url-pattern>/home/*</url-pattern>
    </servlet-mapping>
    
    <servlet>
        <servlet-name>AboutController</servlet-name>
        <servlet-class>
            com.blogspot.shulgadim.controllers.AboutController
        </servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AboutController</servlet-name>
        <url-pattern>/about/*</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

File appengine-web.xml stays as default.

Ran you SimpleMVC project as WebApplication in Eclipce IDE. Then go to the http://localhost:8888/home/index in you web browser. I will see the Home/index page (Fig. 1).
Fig. 1
Go to http://localhost:8888/home/pageOne :

Fig. 2
http://localhost:8888/about/index
Fig. 3
 http://localhost:8888/about/pageTwo
Fig. 4




 Where is Models?
I don't describe how to write Models classes because it depends of what you want you website or webservice does. It it actually you business-logic, core of website. It can interact with database or does some clever calculations... Models objects should be called inside actions, doing some work and return some data to pass in view. All in your hands! I just show how you can build lightweight framework that interconnects Controller and View together. 

2 comments:

  1. Man tag "CENTER" is very old. WTF?

    ReplyDelete
  2. Yes, I know it. But the HTML makeup isn't a topic of article and for simplicity I used "CENTER" tag. Actually even www.google.com index page uses "CENTER" tag widely :)

    ReplyDelete