JAX-RS Application on the root context - how can it be done?

Solution 1:

It's probably not so much a bug as a limitation of the Servlet spec. The details of how a JAX-RS @ApplicationPath is handled is implementation specific, and I can't speak for all implementations, but I'd guess the typical approach is to simply use it as a servlet URL pattern. Taking a look at Jersey's ServletContainerInitializer implementation as one example, you'll find that the addServletWithApplication() method is responsible for creating the servlet and mapping to handle requests, and you can see that it does, indeed, use the path from the @ApplicationPath as the Jersey ServletContainer's mapped path.

Unfortunately, since time immemorial, the Servlet spec has allowed only a small handful of ways of mapping servlets to URL paths. The current options with Servlet 3.0, given in Section 12.2 of the spec--sadly only available as a PDF, so not linkable by section--are:

  • /.../* where the initial /... is zero or more path elements
  • *.<ext> where <ext> is some extension to match
  • the empty string, which maps only to the empty path/context root
  • /, the single slash, which indicates the "default" servlet in the context, which handles anything that doesn't match anything else
  • any other string, which is treated as a literal value to match

The same section of the spec also has specific rules for the order in which the matching rules should apply, but the short version is this: to make your resource class answer requests at the context root, you have to use either / or /* as the path. If you use /, then you're replacing the container's default servlet, which would normally be responsible for handling static resources. If you use /*, then you're making it too greedy and saying it should match everything all the time, and the default servlet will never be invoked.

So if we accept that we're inside the box determined by the limitations of servlet URL patterns, our options are fairly limited. Here are the ones I can think of:

1) Use @ApplicationPath("/"), and explicitly map your static resources by name or by extension to the container's default servlet (named "default" in Tomcat and Jetty, not sure about others). In a web.xml, it would look like

<!-- All html files at any path -->
<servlet-mapping>   
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
<!-- Specifically index.html at the root -->
<servlet-mapping>   
    <servlet-name>default</servlet-name>
    <url-pattern>/index.html</url-pattern>
</servlet-mapping>

or with a ServletContextInitializer, like

public class MyInitializer implements ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx) {
        ctx.getServletRegistration("default").addMapping("*.html");
        ctx.getServletRegistration("default").addMapping("/index.html");
    }
}

Because of the way the matching rules are written, an extension pattern wins over the default servlet, so you'd only need to add a mapping per static file extension as long as there's no overlap between those and any "extensions" that might occur in your API. This is pretty close to the undesirable option mentioned in the forum post you linked, and I just mention it for completeness and to add the ServletContextInitializer part.

2) Leave your API mapped to /rest/*, and use a Filter to identify requests for the API and forward them to that path. This way, you break out of the servlet URL pattern box and can match URLs any way you want. For example, assuming that all your REST calls are to paths that either begin with "/foo" or are exactly "/bar" and all other requests should go to static resources, then something like:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.regex.Pattern;

@WebFilter(urlPatterns = "/*")
public class PathingFilter implements Filter {
    Pattern[] restPatterns = new Pattern[] {
            Pattern.compile("/foo.*"),
            Pattern.compile("/bar"),
    };

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            String path = ((HttpServletRequest) request).getServletPath();
            for (Pattern pattern : restPatterns) {
                if (pattern.matcher(path).matches()) {
                    String newPath = "/rest/" + path;
                    request.getRequestDispatcher(newPath)
                        .forward(request, response);
                    return;
                }
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

With the above, you essentially translate requests as follows:

http://example.org/foo          -> http://example.org/rest/foo
http://example.org/foox         -> http://example.org/rest/foox
http://example.org/foo/anything -> http://example.org/rest/foo/anything
http://example.org/bar          -> http://example.org/rest/bar
http://example.org/bart         -> http://example.org/bart
http://example.org/index.html   -> http://example.org/index.html

3) Realize that the previous option is basically URL rewriting and use an existing implementation, such as Apache's mod_rewrite, the Tuckey rewrite filter, or ocpsoft Rewrite.

Solution 2:

I have found another solution that involves internal Jersey classes, I assume it's probably just not yet part of the JAX-RS spec. (based on: http://www.lucubratory.eu/simple-jerseyrest-and-jsp-based-web-application/)

web.xml

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <display-name>jersey-rest-jsp-frame-1</display-name>

  <filter>
    <filter-name>jersey</filter-name>
    <filter-class>
      com.sun.jersey.spi.container.servlet.ServletContainer
    </filter-class>
    <init-param>
      <param-name>
        com.sun.jersey.config.property.JSPTemplatesBasePath
      </param-name>
      <param-value>/WEB-INF/jsp</param-value>
    </init-param>
    <init-param>
      <param-name>
        com.sun.jersey.config.property.WebPageContentRegex
      </param-name>
      <param-value>
        (/(image|js|css)/?.*)|(/.*\.jsp)|(/WEB-INF/.*\.jsp)|
        (/WEB-INF/.*\.jspf)|(/.*\.html)|(/favicon\.ico)|
        (/robots\.txt)
      </param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>jersey</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

WEB-INF/jsp/index.jsp

<%@ page contentType="text/html; charset=UTF-8" language="java" %>

<html>
<body>
<h2>Hello ${it.foo}!</h2>
</body>
</html>

IndexModel.java

package example;

import com.sun.jersey.api.view.Viewable;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.HashMap;

@Path("/")
@Produces(MediaType.TEXT_HTML)
public class IndexModel {

    @GET
    public Response root() {
      return Response.seeOther(URI.create("/index")).build();
    }

    @GET
    @Path("index")
    public Viewable index(@Context HttpServletRequest request) {
      HashMap<String, String> model = new HashMap<String, String>();
      model.put("foo","World");
      return new Viewable("/index.jsp", model);
    }
}

This seems to work, but I wonder if it is / will be part of JAX-RS spec / implementation.

Solution 3:

You can try to look for DefaultServlet of your servlet container and add servlet-mapping for it by hands in web.xml to handle page files such as *.html, *.jsp or any other.

E.g. for Tomcat 5.5 it's described here: http://tomcat.apache.org/tomcat-5.5-doc/default-servlet.html.