No XML, please!

A small, flexible, and easy MVC Web framework with no XML

Some people like olives. Some don't. I don't like olives. I don't like XML either. Unfortunately, for me, all major Web frameworks rely on XML to do basically everything: setup, configuration, form handling, validation, etc. Welcome to the world of XML "programming."

Another problem for me is that I like to do things really fast. That's why I had a feeling something was wrong with C++. To do a project with C++, medium programmers end up spending more than half their time fighting with the language syntax, pointers, handlers, memory management, unbound arrays, etc., and very little time doing what they're paid to do: the project. The Java language comes to the rescue with an abstraction power that makes everything easy. Whatever pops into your head—Java can do it! You don't have to care how, you just have to use the APIs.

Unfortunately, the Web frameworks out there are not for the medium guys. They require time to master and can scare rookies with XML all over the place and little tricks they are expected to know and get away with. It is like showing a Hello World EJB example to someone that has never seen an Enterprise JavaBeans component before. The result is shock and fear.

Mentawai, an open source framework, tries to break free from this scenario by implementing a small, simple, flexible, idiot-proof MVC (Model-View-Controller) Web framework with no XML at all. Instead of XML, we can use Java code to achieve the same results, and guess what: Java code has Javadocs! You can even compile Java code to catch errors! Java code is object-oriented programming. How about XML?

The most common reply I get from people when I defend this point is: "Hey, you don't have to compile and restart your Web application with XML!" I may be losing something deeper here, but, if you change your XML, you do have to restart your Web context for most of the Web frameworks I have seen. Plus compiling is definitely not a bad thing. It is a good thing! If you could compile XML to quickly discover a wrong attribute, our debbuging time would decrease.

Now that I have argued my point, let's check what Mentawai can offer us as an alternative.

Setup

Let's begin by setting up our Web application to use the Mentawai framework. First, place the mentawai.jar file (downloadable from Resources) inside your application's /WEB-INF/lib directory and declare in your web.xml file that you want to use the Mentawai controller:

 

<!-- The Mentawai controller --> <servlet> <servlet-name>Controller</servlet-name> <servlet-class>org.mentawai.core.Controller</servlet-class> <load-on-startup>1</load-on-startup> </servlet>

<!-- You must choose an extension to indicate a mentawai action --> <servlet-mapping> <servlet-name>Controller</servlet-name> <url-pattern>*.mtw</url-pattern> </servlet-mapping>

The action paradigm

Mentawai uses the action paradigm. The main characteristics of a Mentawai action are:

  • An action has an input (org.mentawai.core.Input) and an output (org.mentawai.core.Output).
  • An action generates a result (java.lang.String) after it executes. The result is usually SUCCESS or ERROR, but you can create your own.
  • For each result, there is a consequence (org.mentawai.core.Consequence). Consequences for a Web application are usually FORWARD or REDIRECT, but you can create your own.
  • An action has access to contexts (org.mentawai.core.Context). Contexts for a Web application are usually a session context (org.mentawai.core.SessionContext) and an application context (org.mentawai.core.ApplicationContext), but you can create your own.

Let's see an example Mentawai action:

 

package examples.hello;

import org.mentawai.core.*;

public class HelloMentawai extends BaseAction {

public String execute() throws ActionException { String username = input.getStringValue("username"); if (username == null || username.trim().equals("")) { return ERROR; } output.setValue("username", username.toUpperCase()); return SUCCESS; } }

This action looks for a username input parameter and outputs it in uppercase. If it does not find the username parameter, the result is ERROR, otherwise SUCCESS. HelloMentawai inherits from BaseAction, which is a useful implementation of the Action interface. BaseAction provides, through protected data members, access to input, output, session context, application context, and more.

For each possible result, we must associate a consequence. Instead of using a boring XML file, Mentawai let's you do that with Java code by creating an ApplicationManager class that extends org.mentawai.core.ApplicationManager:

 

// No package (root package)

public class ApplicationManager extends org.mentawai.core.ApplicationManager {

public void loadActions() { ActionConfig ac = new ActionConfig("/HelloWorld", HelloMentawai.class); ac.addConsequence(HelloMentawai.SUCCESS, new Forward("/hello.jsp")); ac.addConsequence(HelloMentawai.ERROR, new Forward("/username.jsp")); addActionConfig(ac);

// TIP: you can also use this less error-prone syntax /* addActionConfig(new ActionConfig("/HelloWorld", HelloMentawai.class) .addConsequence(HelloMentawai.SUCCESS, new Forward("/hello.jsp")) .addConsequence(HelloMentawai.ERROR, new Forward("/username.jsp"))); */ } }

The consequence for a SUCCESS is a forward to /hello.jsp. The consequence for an ERROR is a forward to /username.jsp. When the container starts, the Mentawai controller calls the loadActions() method. Through the ActionConfig class, you can define different names and consequences for the same action implementation. The Mentawai controller finds actions by name, so "/HelloWorld" will be called by http://www.myapp.org/HelloWorld.mtw, and "/customers/ListCustomers" will be called by http://www.myapp.org/customers/ListCustomers.mtw. You can put your ApplicationManager class in any package, but, by default, Mentawai tries to load it from the root package.

Below is the code for the username.jsp page:

 <html>
<body>
<h1>Hello Metawai!</h1>
<form action="HelloWorld.mtw" method="post">
Type an username: <input name="username" size="25" />
<input type="submit" value="Send">
</form>
</body>
</html>

Below is the code for the hello.jsp page:

 <%@ taglib uri="/WEB-INF/lib/mentawai.jar" prefix="mtw" %>
<html>
<body>
<h3>Hello <mtw:out value="username" /> from Mentawai!</h3>
</body>
</html>

Notice the use of a Mentawai custom tag to display the action output. Mentawai provides many useful and simple tags for displaying action output. It also provides a base custom tag implementation from where you can easily create your own Mentawai tags. Mentawai also supports JSTL (JavaServer Pages Standard Tag Libraries), expression language, and Velocity for the view layer.

To test the above example, place the mentawai.jar in your classpath, compile the above classes, create the hello.jsp and username.jsp pages and set up everything in your Web application. (If you want, download the HelloWorld.war archive with all the files for this example.) Access the first page with http://www.yourapp.org/yourcontext/username.jsp. Type a username, and you should see the username in uppercase. Congratulations! You have run your first Mentawai Web application!

Best practices

Notice that through the ActionConfig class, you can reuse the same action implementation in different situations. For example, you may create two ActionConfigs with the same action implementation, but with different consequences that lead to different JSP (JavaServer Pages) pages. In other words, the same model can have different views, which is what the MVC pattern dictates.

Mentawai filters

A filter intercepts an action so it can modify its input and output, before and after the action executes. A filter is Mentawai's most important concept, and most of the framework's features are implemented as transparent filters. To learn about filters, let's code a generic filter that tries to populate a value object from the action input parameters. But, before we dive into the code, let's understand the org.mentawai.core.Filter interface:

 

package org.mentawai.core;

public interface Filter {

public String filter(InvocationChain chain) throws FilterException, ActionException;

}

An action may have more than one filter, so for each action, an InvocationChain class is created, which has two important methods:

  • getAction(): Returns the invocation chain's action.
  • invoke(): Executes the next step in the invocation chain—in other words, the next filter or the action. The action is the last step in an InvocationChain.

It is important to understand that a filter can alter an action before and after it executes. Check the code below for an example:

 

import org.mentawai.core.*;

public class MyFilter implements Filter {

public String filter(InvocationChain chain) throws FilterException, ActionException { Action action = chain.getAction(); Output output = action.getOutput(); output.setValue("before", "doing something before!"); String result = chain.invoke(); output.setValue("after", "doing something after!"); return result; } }

Now, let's code our generic filter.

The VOFilter

The org.mentawai.filter.VOFilter comes with Mentawai and proves useful for populating a value object with form parameters. For example, the simple class User.java has two attributes: username and password. We can use the VOFilter to populate a user bean before an action executes. Let's first check VOFilter's source code:

 

package org.mentawai.filter;

import java.lang.reflect.*; import java.util.*;

import org.mentawai.core.*;

public class VOFilter implements Filter { private Class klass; public VOFilter(Class klass) {

this.klass = klass; } private Object createObject() { try { return klass.newInstance(); } catch(Exception e) { e.printStackTrace(); } return null; } /* * Use reflection to set a property in the bean */ private void setValue(Object bean, String name, Object value) { try { StringBuffer sb = new StringBuffer(30); sb.append("set"); sb.append(name.substring(0,1).toUpperCase()); sb.append(name.substring(1)); Method m = klass.getMethod(sb.toString(), new Class[] { value.getClass() }); if (m != null) { m.invoke(bean, new Object[] { value }); } } catch(Exception e) { e.printStackTrace(); } } public String filter(InvocationChain chain) throws FilterException, ActionException { Action action = chain.getAction(); Input input = action.getInput(); Object bean = createObject(); if (bean != null) { Iterator iter = input.keys(); while(iter.hasNext()) { String name = (String) iter.next(); Object value = input.getValue(name); setValue(bean, name, value); } // Set the bean in the action input input.setValue(klass.getName(), bean); } return chain.invoke(); } }

Our user bean:

 

package examples.vofilter;

public class User { private String username; private String password; public User() { } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } }

The VOFilter loops through every action input parameter and tries to inject these values in the class passed to the filter's constructor. It uses the class name to place the new object in the input so the action can access it.

The code for a simple action that might use this filter is:

 

package examples.vofilter;

import java.util.*;

import org.mentawai.core.*;

public class HelloVOFilter extends BaseAction {

public String execute() throws ActionException { User user = (User) input.getValue("examples.vofilter.User"); if (user == null) return ERROR; // Do something with the User object

output.setValue("user", user); return SUCCESS; } }

Setting up the VOFilter to filter our action is easy:

 

// No package (root package)!

import org.mentawai.core.*; import org.mentawai.filter.*; import examples.vofilter.*;

public class ApplicationManager extends org.mentawai.core.ApplicationManager {

public void loadActions() {

ActionConfig ac = new ActionConfig("/HelloVOFilter", HelloVOFilter.class); ac.addConsequence(HelloVOFilter.SUCCESS, new Forward("/hello.jsp")); addActionConfig(ac); // Add a VOFilter to load a User object... ac.addFilter(new VOFilter(User.class)); } }

Through the method addFilter(), you can add filters to your actions. Notice that an action may have more than one filter, and you can also set global filters that intercept all actions with the addGlobalFilter() method.

Refer to Resources to download the complete VOFilter example as a war archive.

Best practices

Inside your actions, you should place only code related to your model layer, either through model beans or plain code. With filters, you can better separate the model layer that goes inside your action from authentication, validation, connection pooling, and any other logic that has nothing to do with your model. Filters are an elegant approach for keeping your model layer clean!

Validation

Validation is an important topic in any Web application that must guarantee only valid data is written to the database. Frameworks exist solely for this purpose, like Apache Commons Validator; however, they encourage the use of XML. Mentawai takes a different approach by using the classes org.mentawai.filter.ValidationFilter and org.mentawai.validator.Rule to obtain a similar result with plain Java code.

You should do validation for your actions through a filter. Your validation filter should inherit from org.mentawai.filter.ValidationFilter and override the initValidator() method to set up rules for each field, along with the error you want to show in case of a validation failure. Check the example below:

 

public class HelloWorldValidator extends ValidationFilter {

private static final int FIELD_REQUIRED_ERROR = 1; private static final int INVALID_USERNAME_LENGTH = 2; private static final int INVALID_AGE = 3; private static final int INVALID_PASSWORD_LENGTH = 4; private static final int PASSWORD_DOES_NOT_MATCH = 5; public void initValidator() { add("username", new RequiredFieldRule(), FIELD_REQUIRED_ERROR); add("username", new StringRule(6, 30), INVALID_USERNAME_LENGTH); add("age", new RequiredFieldRule(), FIELD_REQUIRED_ERROR);

add("age", new IntegerRule(18, 50), INVALID_AGE); add("password", new RequiredFieldRule(), FIELD_REQUIRED_ERROR); add("password", new StringRule(4, 20), INVALID_PASSWORD_LENGTH); add("password", new EqualRule("password", "passconf"), PASSWORD_DOES_NOT_MATCH); add("passconf", new RequiredFieldRule(), FIELD_REQUIRED_ERROR);

} }

Note that validation happens in a chain for each field. In the example above, the age field has two validation rules: RequiredFieldRule and IntegerRule. The second one executes only if the first one succeeds. You can add as many rules as you want for a single field.

By default, validation looks for error messages in the directory /validation, inside the document root. The i18n file inside this directory must have the same name of the validation filter, so, in our case, it is HelloWorldValidator_loc.i18n.

The contents for the file /validation/HelloWorldValidator_en_US.i18n appear below:

 

############################################### # Messages for the filter HelloWorldValidator # ###############################################

1 = Required field cannot be left blank 2 = Your username must be between %min% and %max% characters long. 3 = You must be %min% years old or older. 4 = Your password must be between %min% and %max% characters long. 5 = Passwords do not match.

What are %min% and %max% in the error messages? Each rule can place special tokens inside error messages by implementing the method getTokens(). These tokens replace the marker %token%. RequiredFieldRule and EqualRule have no tokens. StringRule and IntegerRule return the minimum and maximum values as tokens, so, if desired, you can show these values in the error messages.

A validation filter is totally decoupled from the action, so the same filter may be used to validate different actions. This avoids unnecessary code duplication and strives for better code maintenance.

Below is our ApplicationManager, which registers the validation filter for our action:

 

public class ApplicationManager extends org.mentawai.core.ApplicationManager {

public void loadActions() { ActionConfig ac = new ActionConfig("/HelloWorld", HelloWorld.class); ac.addConsequence(HelloWorld.SUCCESS, new Forward("/hello.jsp")); ac.addConsequence(HelloWorld.ERROR, new Forward("/form.jsp")); addActionConfig(ac); ac.addFilter(new HelloWorldValidator()); } public void loadLocales() { LocaleManager.add(new Locale("pt", "BR")); LocaleManager.add(new Locale("en", "US")); } }

Notice that we specify in the loadLocales() method the locales our application supports. If you don't do anything, Mentawai will have just one default locale: en_US. Therefore, it will expect to find at least *_en_US.i18n files. Since we are specifying two locales, you should have two files inside the validation directory: HelloWorldValidator_en_US.i18n and HelloWorldValidator_pt_BR.i18n. You can change your browser's default language to test these files.

In the view layer, we can use the handy <mtw:hasError> and <mtw:error field="" /> to display validation error messages. The contents of the form.jsp file appear below:

 

<%@ taglib uri="/WEB-INF/lib/mentawai.jar" prefix="mtw" %> <!-- Note: The form loses all data on each submission! Mentawai can solve this problem automatically for you through its HTML tags. --> <html> <body> <h1>Hello Validation!</h1> <form action="HelloWorld.mtw" method="post"> Your username: <input name="username" size="25" />

<mtw:hasError> <font color="red"><mtw:error field="username" /></font> </mtw:hasError>

<br>Your age: <input type="text" name="age" size="10" maxlength="3" />

<mtw:hasError> <font color="red"><mtw:error field="age" /></font> </mtw:hasError>

<br>Your password: <input type="password" name="password" size="25" />

<mtw:hasError> <font color="red"><mtw:error field="password" /></font> </mtw:hasError>

<br>Again please: <input type="password" name="passconf" size="25" />

<mtw:hasError> <font color="red"><mtw:error field="passconf" /></font> </mtw:hasError>

<input type="submit" value="Enviar"> </form> </body> </html>

Notice that the Mentawai tag <mtw:hasError> is conditional; in other words, it will only show its body contents if its conditional is true.

Refer to Resources to download the complete validation example as a war archive.

Creating more validation rules

When it comes to validation, you can create an unlimited number of validation rules. For example, you may need to have a CreditCardRule, a URLRule, an EmailRule (provided by Mentawai), or a PasswordRule. You can use one of the available Mentawai rules or create your own.

Creating your own rules in Mentawai is easy and just a matter of implementing the org.mentawai.validator.Rule interface. You can also use one of the abstract rules that come with Mentawai:

  • BasicRule: A simple rule to validate a single field (StringRule)
  • LocaleRule: A rule to validate a single field considering its locale (DateRule)
  • CrossRule: A rule to compare different fields in the same form for the validation (EqualRule)

Mentawai comes with a useful rule: org.mentawai.rule.RegexRule. With this rule, you can validate a field with a regular expression. Therefore, this rule should be subclassed to create more rules that can be performed with a regular expression. org.mentawai.rule.EmailRule is such an example. You should refer to the API documentation for more details on how to extend these classes.

Keeping the form data

You probably noticed that our form loses all its data after each submission, forcing the user to type everything again and resulting in a terrible experience for the user and also for the developer who must code a solution. However, through Mentawai's HTML tags, you can solve this problem without a single line of code. Just use the tags <mtw:input>, <mtw:select>, <mtw:checkboxes>, and <mtw:radiobuttons> to construct your HTML and the form will not lose its data anymore. These custom tags have the same attributes of normal HTML tags, so you should have no problems using them.

Conclusion

In this article, I have covered a small part of the Mentawai Web framework. You have been introduced to the framework's architecture and have seen how most of its features can be implemented as filters and, best of all, with no XML. We also examined the validation feature and saw how we can easily configure localized error messages in i18n files. With Mentawai, you don't have to deal with those boring XML files anymore. You can use the ApplicationManager class to achieve the same results with plain Java code.

Sergio Oliveira Jr. graduated from Case Western Reserve University with a degree in computer engineering in 1998. He has participated in many successful Web projects like Zipmail, Escola24horas, DatingPlace, and Gazzag. He is also the author of Lohis, a collaborative application in Java Media Framework with audio and video conferencing for distance learning, and of the open source projects Space4J and Mentawai. He can be found at GUJ discussing Java-related topics.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more