When I Googled on "servlet filter with jax-rs example", this was at the top of the list. From a cursory scan at the code, I think this suits my needs.
Here is my solution (so far... see footnote caveat)
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- This web.xml file is not required when using Servlet 3.0 container,
see implementation details http://jersey.java.net/nonav/documentation/latest/jax-rs.html -->
<web-app version="2.5" 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_2_5.xsd">
<filter>
<filter-name>accept-filter</filter-name>
<filter-class>com.extratechnology.filters.AcceptFilter</filter-class>
<init-param>
<param-name>xml</param-name>
<param-value>text/xml</param-value>
</init-param>
<init-param>
<param-name>json</param-name>
<param-value>application/json</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>accept-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.extratechnology.caaews</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/webapi/*</url-pattern>
</servlet-mapping>
</web-app>
AcceptFilter.java
package com.extratechnology.filters;
import java.io.IOException;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AcceptFilter implements Filter {
private final Map<String,String> extensions = new HashMap<String,String>();
public void init(FilterConfig config) throws ServletException {
Enumeration<String> exts = config.getInitParameterNames();
while (exts.hasMoreElements()) {
String ext = exts.nextElement();
if (ext != null && !ext.isEmpty()) {
this.extensions.put(ext.toLowerCase(), config.getInitParameter(ext));
}
}
}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)request;
String uri = httpRequest.getRequestURI();
String ext = this.getExtension(uri);
String accept = this.extensions.get(ext);
if (accept == null) {
accept = httpRequest.getHeader("accept");
if (accept != null && accept.indexOf("text/html") > 0) {
// patch WebKit-style Accept headers by elevating "text/html"
accept = "text/html,"+accept;
request = new RequestWrapper(httpRequest, uri, accept);
}
} else {
// remove extension and remap the Accept header
uri = uri.substring(0, uri.length() - ext.length()-1);
request = new RequestWrapper(httpRequest, uri, accept);
}
// add "Vary: accept" to the response headers
HttpServletResponse httpResponse = (HttpServletResponse)response;
httpResponse.addHeader("Vary", "accept");
chain.doFilter(request, response);
}
private String getExtension(String path) {
String result = "";
int index = path.lastIndexOf('.');
if (!(index < 0 || path.lastIndexOf('/') > index)) {
result = path.substring(index+1).toLowerCase();
}
return result;
}
private static class RequestWrapper extends HttpServletRequestWrapper {
private final String uri;
private final String accept;
public RequestWrapper(HttpServletRequest request, String uri, String accept) {
super(request);
this.uri = uri;
this.accept = accept;
}
@Override
public String getRequestURI() {
return this.uri;
}
@Override
public Enumeration<String> getHeaders(String name) {
Enumeration<String> result;
if ("accept".equalsIgnoreCase(name)) {
Vector<String> values = new Vector<String>(1);
values.add(this.accept);
result = values.elements();
} else {
result = super.getHeaders(name);
}
return result;
}
}
}
CAAEWS.java
package com.extratechnology.caaews;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.extratechnology.caaews.model.Log;
@Path("et")
@Produces(value = {MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public class CAAEWS {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Log getJSON() {
return new Log("JSON!");
}
@GET
@Produces(MediaType.TEXT_XML)
public Log getXML() {
return new Log("XML!");
}
}
Log.java
package com.extratechnology.caaews.model;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Log {
private String log;
public Log(String log) {
this.log = log;
}
public String getLog() {
return log;
}
public void setLog(String log) {
this.log = log;
}
}
The only thing that intrigued me slightly is that HTTP has two content types for XML.
It's configurable in the web.xml, but I'd have to tweak the annotations. Why the two?
--
Footnote:
After writing this, I'm now finding I'm getting HTTP 500 errors.
The logs seem to be in some obscure folder when you run the server in Eclipse:
Documents\workspace-sts-3.8.3.RELEASE\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\logs
And I get this written to log:
0:0:0:0:0:0:0:1 - - [25/Nov/2017:16:56:00 +0000] "GET /caaews/webapi/et.xml HTTP/1.1" 500 1082
Does anyone have an idea how to get more sensible log information? Or what I need to do to trap more meaningful stack traces?
It appears the Log class needs a no argument constructor to overcome this. But I concede, the @peeskillet answer is far less cumbersome and uses built in Jersey functionality.
I'm also wondering if javax.servlet.filters don't play well with JAX-RS 2.0 after looking at examples here...
Per other related answers/comments for this question, I ended up implementing an exception handler, so you get more info on the HTTP 500 messages in Jersey..
Here's the code that helps point way to Log.java needing a no argument constructor..
ErrorMessage
package com.extratechnology.caaews.model;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class ErrorMessage {
private String errorMessage;
private String errorStackTrace;
private String cause;
private String causeStackTrace;
private int errorCode;
public ErrorMessage() {
}
public ErrorMessage(
String errorMessage,
String errorStackTrace,
String cause,
String causeStackTrace,
int errorCode
) {
this.errorMessage = errorMessage;
this.errorStackTrace = errorStackTrace;
this.cause = cause;
this.causeStackTrace = causeStackTrace;
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getErrorStackTrace() {
return errorStackTrace;
}
public void setErrorStackTrace(String errorStackTrace) {
this.errorStackTrace = errorStackTrace;
}
public String getCause() {
return cause;
}
public void setCause(String cause) {
this.cause = cause;
}
public String getCauseStackTrace() {
return causeStackTrace;
}
public void setCauseStackTrace(String causeStackTrace) {
this.causeStackTrace = causeStackTrace;
}
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
}
GenericExceptionMapper.java
package com.extratechnology.caaews.exception;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import com.extratechnology.caaews.model.ErrorMessage;
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable>{
@Override
public Response toResponse(Throwable ex) {
System.out.println("Stack Trace:");
ex.printStackTrace();
System.out.println("Cause:");
Throwable cause = ex.getCause();
if (cause != null) {
cause.printStackTrace();
}
ErrorMessage message = new ErrorMessage(
ex.getMessage(),
GenericExceptionMapper.getStackTrack(ex),
cause.getMessage(),
GenericExceptionMapper.getStackTrack(cause),
Status.INTERNAL_SERVER_ERROR.getStatusCode()
);
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.entity(message)
.build();
}
private static String getStackTrack(Throwable ex) {
StringBuilder sb = new StringBuilder();
String ls = System.lineSeparator();
if (ex != null) {
StackTraceElement[] steAll = ex.getStackTrace();
for (StackTraceElement ste : steAll) {
sb.append(ste.toString());
sb.append(ls);
}
}
return sb.toString();
}
}
The system.out.println gives console messages when debugging and you get a payload back in the web browser too on an error.
eg:
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<errorMessage>
<cause>1 counts of IllegalAnnotationExceptions</cause>
<causeStackTrace>
com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException$Builder.check(Unknown Source) com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(Unknown Source) com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.<init>(Unknown Source) com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.<init>(Unknown Source) com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(Unknown Source) com.sun.xml.internal.bind.v2.ContextFactory.createContext(Unknown Source) sun.reflect.GeneratedMethodAccessor20.invoke(Unknown Source) sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) java.lang.reflect.Method.invoke(Unknown Source) javax.xml.bind.ContextFinder.newInstance(Unknown Source) javax.xml.bind.ContextFinder.newInstance(Unknown Source) javax.xml.bind.ContextFinder.find(Unknown Source) javax.xml.bind.JAXBContext.newInstance(Unknown Source) javax.xml.bind.JAXBContext.newInstance(Unknown Source) org.glassfish.jersey.jaxb.internal.AbstractJaxbProvider.getStoredJaxbContext(AbstractJaxbProvider.java:312) org.glassfish.jersey.jaxb.internal.AbstractJaxbProvider.getJAXBContext(AbstractJaxbProvider.java:297) org.glassfish.jersey.jaxb.internal.AbstractJaxbProvider.getMarshaller(AbstractJaxbProvider.java:264) org.glassfish.jersey.jaxb.internal.AbstractJaxbProvider.getMarshaller(AbstractJaxbProvider.java:231) org.glassfish.jersey.jaxb.internal.AbstractRootElementJaxbProvider.writeTo(AbstractRootElementJaxbProvider.java:175) org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.invokeWriteTo(WriterInterceptorExecutor.java:266) org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.aroundWriteTo(WriterInterceptorExecutor.java:251) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.server.internal.JsonWithPaddingInterceptor.aroundWriteTo(JsonWithPaddingInterceptor.java:109) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundWriteTo(MappableExceptionWrapperInterceptor.java:85) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.message.internal.MessageBodyFactory.writeTo(MessageBodyFactory.java:1135) org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:662) org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:395) org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:385) org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:280) org.glassfish.jersey.internal.Errors$1.call(Errors.java:272) org.glassfish.jersey.internal.Errors$1.call(Errors.java:268) org.glassfish.jersey.internal.Errors.process(Errors.java:316) org.glassfish.jersey.internal.Errors.process(Errors.java:298) org.glassfish.jersey.internal.Errors.process(Errors.java:268) org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:289) org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:256) org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:703) org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:416) org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:370) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:389) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:342) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:229) org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291) org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:217) org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142) org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616) org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:518) org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091) org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:673) org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500) org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456) java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) java.lang.Thread.run(Unknown Source)
</causeStackTrace>
<errorCode>500</errorCode>
<errorMessage>HTTP 500 Internal Server Error</errorMessage>
<errorStackTrace>
org.glassfish.jersey.jaxb.internal.AbstractRootElementJaxbProvider.writeTo(AbstractRootElementJaxbProvider.java:183) org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.invokeWriteTo(WriterInterceptorExecutor.java:266) org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.aroundWriteTo(WriterInterceptorExecutor.java:251) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.server.internal.JsonWithPaddingInterceptor.aroundWriteTo(JsonWithPaddingInterceptor.java:109) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundWriteTo(MappableExceptionWrapperInterceptor.java:85) org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:163) org.glassfish.jersey.message.internal.MessageBodyFactory.writeTo(MessageBodyFactory.java:1135) org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:662) org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:395) org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:385) org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:280) org.glassfish.jersey.internal.Errors$1.call(Errors.java:272) org.glassfish.jersey.internal.Errors$1.call(Errors.java:268) org.glassfish.jersey.internal.Errors.process(Errors.java:316) org.glassfish.jersey.internal.Errors.process(Errors.java:298) org.glassfish.jersey.internal.Errors.process(Errors.java:268) org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:289) org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:256) org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:703) org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:416) org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:370) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:389) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:342) org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:229) org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291) org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:217) org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142) org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616) org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:518) org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091) org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:673) org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500) org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456) java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) java.lang.Thread.run(Unknown Source)
</errorStackTrace>
</errorMessage>