Showing posts with label Servlet. Show all posts
Showing posts with label Servlet. Show all posts
Saturday, December 22, 2012

Plain-vanilla Java Servlets and Pretty URLs

I'm writing this because I was recently asked about the growing complexity of Java-based web MVC frameworks. It made me think about the kinds of web applications a Java developer can build with just the plain-vanilla Java Servlets.

As I was pondering on the thought, I could not help ask myself about the following:

  • What happens to pretty URLs?
  • How can we add page templates like those provided by SiteMesh and Apache Tiles?

Pretty URLs and Mapping Conventions

It's common nowadays to consider question mark (?) and ampersand (&) characters in the URL as being ugly. But are they really that bad? Do they really affect SEO in a negative way? The truth is... it is just a myth. Why would Google (and other search engines) put so much weight on the URL? Fact is... Google will (and should) do a good job by looking into the contents the URL is pointing to. It should not just be looking at the URL. If it did put so much weight on URLs, then we'll all be filled with spammed links by those who just want to be on top of SERPs.

If so, how do we now map URLs to Java servlets? Below show the typical URL mapping.

MethodURLAction
GET/magazinesindex (default)
GET/magazines/{id}show
GET/magazines/{id}/editeditRender HTML form for editing magazine with given {id}
PUT/magazines/{id}updateUpdates magazine with given {id}. Redirect to GET /magazines.
GET/magazines/newnewRender HTML form for new magazine.
POST/magazinessaveSave magazine ad and generate new unique ID. Redirect to GET /magazines.
DELETE/magazines/{id}deleteRemove magazine with given {id}

Given the above URL mappings, it's impossible to implement using a web.xml. So, are we really forced to use the above conventions? Are we really forced to use a single servlet, mapped to /app/*, and have it apply a regular expression to determine which controller should handle the incoming request?

IMO, we should not be forced to do so. Assuming you'd allow URLs that contain question marks (?) and ampersands (&), we can have the following mappings.

MethodURLAction
GET/magazinesindex (default)
GET/magazines?id={id}show
GET/magazines?id={id}&editeditRender HTML form for editing magazine with given {id}
PUT/magazines?id={id}updateUpdates magazine with given {id}. Redirect to GET /magazines.
GET/magazines?newnewRender HTML form for new magazine.
POST/magazinessaveSave magazine and generate new unique ID. Redirect to GET /magazines.
DELETE/magazines?id={id}deleteRemove magazine with given {id}

The above mappings are definitely possible to implement in a web.xml. It just needs a servlet mapped to /magazines/*. It will also need a filter to support the translation of PUT and DELETE methods (as most browsers only support GET and POST). As for nested resources/namespaces, a similar approach applies. It will need another servlet mapped to /magazines/ads/*.

MethodURLAction
GET/magazines/ads?m_id={magazine_id}index (default)List ads under the magazine with the given {magazine_id}.
GET/magazines/ads?m_id={magazine_id}&id={id}showShow magazine ad with given {id}.
GET/magazines/ads?m_id={magazine_id}&id={id}&editeditRender HTML form for editing magazine ad with given {id}
PUT/magazines/ads?m_id={magazine_id}&id={id}updateUpdates magazine ad with given {id}. Redirect to GET /magazines/ads.
GET/magazines/ads?m_id={magazine_id}&newnewRender HTML form for new magazine ad.
POST/magazines/ads?m_id={magazine_id}saveSave magazine ad and generate new unique ID. Redirect to GET /magazines/ads?m_id={magazine_id}.
DELETE/magazines/ads?m_id={magazine_id}&id={id}deleteRemove magazine ad with given {id}

To extend the conventions further, each servlet acting as controller (in MVC) should forward to a view (typically implemented as a JSP). These JSPs can be located under /WEB-INF/views/magazines and /WEB-INF/views/magazines/ads to match their servlet URL mappings.

Templating Frameworks

I find it more and more common to use SiteMesh or Tiles in developing Java-based web applications. In fact, it would be uncommon to see a Java web application that does not use SiteMesh or Tiles.

After doing some research, I found a lesser known feature of Java Server Pages (JSPs) 2.0: <include-prelude> and <include-coda>. These allow developers to have templates included in the beginning and end of all JSPs that matched the URL.

An example configuration in web.xml looks like this:

  <jsp-config>
    <jsp-property-group>
      <url-pattern>*.jsp</url-pattern>
      <include-prelude>/WEB-INF/includes/prelude.jspf</include-prelude>
      <include-coda>/WEB-INF/includes/coda.jspf</include-coda>
    </jsp-property-group>
  </jsp-config>
The *.jspf is just a common filename extension for JSP fragments.

If the page layout is simple enough to allow a header and footer implemented as a prelude and coda, then using this JSP 2.0 feature should do the trick.

Conclusion

So, is it really that bad? What do you think?

IMO, it's not that bad. There's definitely more to building web apps using just plain Java Servlets and JSPs. But these alone are quite powerful. And the MVC frameworks enhance it further.

That's all for now. Happy holidays!

2019 September Update

If you'd like to know more, checkout how a Java Servlet works. It even contains a cool trivia about the people who created the Java Servlet 1.0 specification! Happy reading!

Tuesday, May 15, 2012

Decorating Error Pages with SiteMesh 2.4.x

I've encountered a problem when decorating error pages with SiteMesh 2.4.2. I thought that others may benefit from the fix that we've done. So, I'm explaining it here.

When using Jetty 7.x, and the following web.xml configuration, SiteMesh does not correctly decorate the HTTP 404 error page.

<?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">

    <!-- Sitemesh filter -->
    <filter>
        <filter-name>sitemeshFilter</filter-name>
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>sitemeshFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>

    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/pages/not-found.jsp</location>
    </error-page>

</web-app>

I'm not sure if this problem occurs in Tomcat 6.x. From my tests, when a URL to a non-existent page is used (e.g. http://localhost/…/xxx), a 404 page is expected. This should be decorated by SiteMesh. But it's not.

After further investigation, when the SiteMesh filter configuration is limited to *.jsp (see below, line 12), all non-existing URLs that do not match *.jsp do get decorated. So, xxx does get decorated (as expected). But xxx.jsp does not! This inconsistency led to some work-arounds that included using "flash"-scoped variables and a HttpServletResponse#sendRedirect() call to redirect to the 404 page.

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

    <!-- Sitemesh filter -->
    <filter>
        <filter-name>sitemeshFilter</filter-name>
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>sitemeshFilter</filter-name>
        <url-pattern>*.jsp</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>

    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/pages/not-found.jsp</location>
    </error-page>

</web-app>

This led me to look further into SiteMesh 2.4.2 code. After adding some debugging statements, I can see that the wrapped response was not used by Jetty in its HttpServletResponse#sendError() implementation. Thus, there was no content in SiteMesh's PageResponseWrapper. It was written directly to the original response, and not the wrapped response. So, in SiteMesh's point of view, there was no content to be decorated. But there is!

Given this finding, I looked for work-arounds. The result was some minor modification to the SiteMeshFilter and the following web.xml configuration.

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

    <!-- Sitemesh filter -->
    <filter>
        <filter-name>sitemeshFilter</filter-name>
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>
    <filter>
        <filter-name>errorSitemeshFilter</filter-name>
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>errorSitemeshFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>
    <filter-mapping>
        <filter-name>sitemeshFilter</filter-name>
        <url-pattern>*.jsp</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/pages/not-found.jsp</location>
    </error-page>

</web-app>

The changes to the SiteMeshFilter allowed multiple instances of the filter to co-exist and act separately. One instance handles ERROR pages (due to sendError() calls). The other instance handles non-error pages. I used the filter's name to differentiate the two instances. When a non-existent URL is encountered, the filter that handles ERROR pages decorates properly. When a non-existent URL that matches *.jsp is encountered, the "sitemeshFilter" gets to filter it. But since it is not existent, the servlet container ends up calling sendError(404). As expected, the "sitemeshFilter" could not handle this properly. The good thing is, we have another "errorSitemeshFilter" that handles this.

public class SiteMeshFilter implements Filter {

    private FilterConfig filterConfig;
    private ContainerTweaks containerTweaks;
    private static final String ALREADY_APPLIED_KEY = "com.opensymphony.sitemesh.APPLIED_ONCE";
    private String alreadyAppliedKey;

    public void init(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
        alreadyAppliedKey = ALREADY_APPLIED_KEY + "-" + this.filterConfig.getFilterName();
        containerTweaks = new ContainerTweaks();
    }

    public void destroy() {
        filterConfig = null;
        containerTweaks = null;
    }

    /**
     * Main method of the Filter.
     *
     * Checks if the Filter has been applied this request. If not, parses the page
     * and applies {@link com.opensymphony.module.sitemesh.Decorator} (if found).
     */
    public void doFilter(ServletRequest rq, ServletResponse rs, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) rq;
        HttpServletResponse response = (HttpServletResponse) rs;
        ServletContext servletContext = filterConfig.getServletContext();

        // no changes
        . . .

        try {

            Content content = obtainContent(contentProcessor, webAppContext, request, response, chain);

            if (content == null) {
                return;
            }

            Decorator decorator = decoratorSelector.selectDecorator(content, webAppContext);
            decorator.render(content, webAppContext);

        } catch (IllegalStateException e) {
            // Some containers (such as WebLogic) throw an IllegalStateException when an error page is served.
            // It may be ok to ignore this. However, for safety it is propegated if possible.
            if (!containerTweaks.shouldIgnoreIllegalStateExceptionOnErrorPage()) {
                throw e;
            }
        } catch (RuntimeException e) {
            if (containerTweaks.shouldLogUnhandledExceptions()) {
                // Some containers (such as Tomcat 4) swallow RuntimeExceptions in filters.
                servletContext.log("Unhandled exception occurred whilst decorating page", e);
            }
            throw e;
        } catch (ServletException e) {
            request.removeAttribute(alreadyAppliedKey);
            throw e;
        }

    }

    // no changes
    . . .

    private boolean filterAlreadyAppliedForRequest(HttpServletRequest request) {
        if (request.getAttribute(alreadyAppliedKey) == Boolean.TRUE) {
            return true;
        } else {
            request.setAttribute(alreadyAppliedKey, Boolean.TRUE);
            return false;
        }
    }

}

This should work for error pages other than 404.

That's all. I hope this helps. Let me know if it works for you.

UPDATE: This is related to http://jira.opensymphony.com/browse/SIM-168.

UPDATE (2013 July): Thanks to Jeremy for pointing out the use of alreadyAppliedKey instead of ALREADY_APPLIED_KEY in the doFilter() method.