Wednesday, July 26, 2006

Jython Spring MVC Controllers

In my last blog I wrote about Spring transactional services written in Jython, and now I want to write about my way to use Spring MVC with Jython.
I will describe the following steps:

  • preparing the web.xml

  • Spring servlet configuration

  • Jython controller


In the web.xml, beside the ContextLoaderServlet we need a Spring Front Controller that is the entry point to all requests. This Front Controller then dispatches to the specific Task or Action or how ever you want to call it. Here is the servlet declaration and it's mapping for all URLs ending with .py

=== web.xml ===

<servlet>
<servlet-name>example</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>example</servlet-name>
<url-pattern>*.py</url-pattern>
</servlet-mapping>


For the servlet name "example" we need a configuration file in the WEB-INF folder that starts with "example" and the servlet name that always ends with "-servlet.xml". This file is finally called "example-servlet.xml" and holds the bean configurations for the Spring MVC part.

=== example-servlet.xml ===

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
<bean id="jythonController" class="com.my.view.controller.JythonController">
<property name="mappings">
<props>
<prop key="/index.*">com.my.view.controller.ForwardController(view="home", activeMenu="home")</prop>
<prop key="/login.*">com.my.view.controller.LoginController(view="login", activeMenu="login")</prop>
</props>
</property>
</bean>

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/*.py">jythonController</prop>
</props>
</property>
</bean>

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
<property name="prefix"><value>/WEB-INF/jsp/</value></property>
<property name="suffix"><value>.jsp</value></property>
</bean>
</beans>


This MVC configuration needs some detailed explanation because it wires the Spring and Jython world. The urlMapping part catches all requests that end with *.py and delegates them to the jythonController that is a native Java class. It also could be a java-compatible Jython class, but I simply chose native Java. The jythonController then delegates the specific request with its specific url to the mapped Jython controller class. This mappings are declared as a property element in the jythonController bean where the key is the url pattern like ".../index.py" or ".../login.py" and the value is a Jython expression, that will create a new instance of the Jython controller class. So while Spring initializes the Jython Controller bean, the Jython statements are executed and the resulting Jython controller instances are cached as singletons.
So the trick is simply that a native Java controller instance stores a number of Jython controller classes which will then execute the concrete controller functionality. You can see it as a babushka doll with 'controllers-within-controllers'.

The next advantage is that the Jython controller singletons can be configured declaratively. In this example I say that the LoginController should froward to the "login" view that will result in the "WEB-INF/jsp/login.jsp" page (see viewResolver bean), and that the highlighted menu (activeMenu argument) should be "login" in the shown page's navigation.
Because IMO it is so intuitive, here it is in other words.
The jython Controller executes the Jython statement


com.my.view.controller.LoginController(view="login", activeMenu="login")


The corresponding Jython code of the controller would look like the following listing.

=== LoginController.py ===

from org.springframework.web.servlet.mvc import Controller
from org.springframework.web.servlet import ModelAndView
from org.springframework.web.context.support import WebApplicationContextUtils
from java.util import HashMap
from java.lang import Boolean, System, Integer

class LoginController(Controller):
def __init__(self, view=None, activeMenu=None):
self.view = view
self.activeMainMenu = None
self.activeSubMenu = None
if activeMenu != None:
activeMenuTokens = activeMenu.split(".")
self.activeMainMenu = activeMenuTokens[0]
if len(activeMenuTokens) == 2:
self.activeSubMenu = activeMenuTokens[1]

def getApplContext(self, request):
servletContext = request.getAttribute("servletContext")
return WebApplicationContextUtils.getWebApplicationContext(servletContext)

def getLoginService(self, request):
return self.getApplContext(request).getBean("loginService");


def handleRequest(self, req, res):
"""Ensures an existing user session, sets the user's view settings
and calls completeModel().
This method should not be overridden."""
session = req.getSession(Boolean.TRUE)

model = HashMap()
model.put("activeMainMenu", self.activeMainMenu)
model.put("activeSubMenu", self.activeSubMenu)
# do the real processing
self.completeModel(req, session, model)

return ModelAndView(self.view, model)

def completeModel(self, req, session, model):
"""Handles the complete controller logic and fills the model.
The model is a java HashMap.
This method can be overridden."""

self.getTestService(req).checkLogin(req)
# ...
# ...


In the previous code the controller accesses the loginService bean, that is also a Spring bean, to get the service instance from the Spring application context. To accomplish this, the request has to hold the servletContetxt instance. This is not a very elegant solution, but better ones are wellcome.
The model is represented by a simple java.util.HashMap which is later on transformed to the request scope by Spring MVC (BTW this is the same way I handle the model in my open source project Flow4J).
You can also simply convert the LoginController in a generic BaseController of which you can subclass and override only the completeModel() method.
For the curious ones, I also support my implementation of the native Java JythonController class

=== JythonController.java ===

package de.my.view.controller;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

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

import org.python.core.PyInstance;
import org.python.core.PyJavaInstance;
import org.python.util.PythonInterpreter;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.mvc.Controller;
import org.springframework.web.util.UrlPathHelper;

public class JythonController extends AbstractController {

private UrlPathHelper urlPathHelper = new UrlPathHelper();

private Map urlMappings;

private Map controllerMappings = new HashMap();

/**
* Spring callback that creates the Jython controller instances and caches
* the singletons.
*
* @param mappings the contrller mappings
*/

public void setMappings(Properties mappings) {
this.urlMappings = mappings;

for (Iterator iter = urlMappings.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
String urlPath = (String) entry.getKey();
String expr = (String) entry.getValue();
System.out.println("register controller [urlPath: " + urlPath
+ ", expr: " + expr + "]");

Controller controller = (Controller) getPyJavaObject(expr,
Controller.class);
controllerMappings.put(urlPath, controller);
}
}

/**
* Evaluates the jython statement and returns the result casted to the given
* type. Uses the JythonInitServlet that stores an sntance of the Jython interpreter.
*
* @param stmnt jython statement
* @param clazz type to which the result should be casted
* @return the statements result casted to the given type
* @see JythonInitServlet
*/

static public Object getPyJavaObject(String stmnt, Class clazz) {
PythonInterpreter interp = JythonInitServlet.getInterpreter();
Object result = interp.eval(stmnt);
if (result instanceof PyJavaInstance) {
PyJavaInstance inst = (PyJavaInstance) result;
return inst.__tojava__(clazz);
} else if (result instanceof PyInstance) {
PyInstance inst = (PyInstance) result;
return inst.__tojava__(clazz);
}

throw new RuntimeException("Cannot evaluate statement '" + stmnt
+ "' and force result to '" + clazz.getName() + "'");
}

/**
* Returns the Jython controller singleton that is mapped to the given url.
*
* @param urlPath the url path for which the controller is looked up
* @return the Jython controller
*/

private Controller getController(String urlPath) {
for (Iterator iter = controllerMappings.entrySet().iterator(); iter
.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
String registeredPath = (String) entry.getKey();
if (PathMatcher.match(registeredPath, urlPath)) {
return (Controller) entry.getValue();
}
}
return null;
}

/**
* The Spring MVS callback for executing the controllers functionality.
* Looks up the Jython controller singleton and delegates the request to it.
*
* @return the model and view
* @see org.springframework.web.servlet.mvc.AbstractController#handleRequestInternal(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/

public ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {

request.setAttribute("servletContext", getServletContext());

String lookupPath = urlPathHelper.getLookupPathForRequest(request);
Controller controller = getController(lookupPath);
ModelAndView modelAndView = controller.handleRequest(request, response);

return modelAndView;
}
}




That is i for now, and comments on this topic are appreciated.

2 comments:

Anonymous said...

Thank you for sharing your info. I really appreciate your efforts and I will be
waiting for your further post thanks once again.
My website: what are skin tags

Anonymous said...

Hello! This post could not be written any better! Reading this
post reminds me of my previous room mate! He always kept chatting
about this. I will forward this article to him.
Pretty sure he will have a good read. Thanks for sharing!



my page: jr.scptuj.si