I got a HttpServletRequest request in my Spring Servlet which I would like to forward AS-IS (i.e. GET or POST content) to a different server.
What would be the best way to do it using Spring Framework?
Do I need to grab all the information and build a new HTTPUrlConnection? Or there is an easier way?
Discussions of whether you should do forwarding this way aside, here's how I did it:
package com.example.servlets;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.example.servlets.GlobalConstants;
#SuppressWarnings("serial")
public class ForwardServlet extends HttpServlet {
#Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) {
forwardRequest("GET", req, resp);
}
#Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) {
forwardRequest("POST", req, resp);
}
private void forwardRequest(String method, HttpServletRequest req, HttpServletResponse resp) {
final boolean hasoutbody = (method.equals("POST"));
try {
final URL url = new URL(GlobalConstants.CLIENT_BACKEND_HTTPS // no trailing slash
+ req.getRequestURI()
+ (req.getQueryString() != null ? "?" + req.getQueryString() : ""));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
final Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
final String header = headers.nextElement();
final Enumeration<String> values = req.getHeaders(header);
while (values.hasMoreElements()) {
final String value = values.nextElement();
conn.addRequestProperty(header, value);
}
}
//conn.setFollowRedirects(false); // throws AccessDenied exception
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(hasoutbody);
conn.connect();
final byte[] buffer = new byte[16384];
while (hasoutbody) {
final int read = req.getInputStream().read(buffer);
if (read <= 0) break;
conn.getOutputStream().write(buffer, 0, read);
}
resp.setStatus(conn.getResponseCode());
for (int i = 0; ; ++i) {
final String header = conn.getHeaderFieldKey(i);
if (header == null) break;
final String value = conn.getHeaderField(i);
resp.setHeader(header, value);
}
while (true) {
final int read = conn.getInputStream().read(buffer);
if (read <= 0) break;
resp.getOutputStream().write(buffer, 0, read);
}
} catch (Exception e) {
e.printStackTrace();
// pass
}
}
}
Obviously this could use a bit of work with regard to error handling and the like but it was functional. I stopped using it, however, because it was easier in my case to make calls directly to the CLIENT_BACKEND than to deal with cookies, auth, etc. across two distinct domains.
I also needed to do the same, and after some non optimal with Spring controllers and RestTemplate, I found a better solution: Smiley's HTTP Proxy Servlet. The benefit is, it really does AS-IS proxying, just like Apache's mod_proxy, and it does it in a streaming way, without caching the full request/response in the memory.
Simply, you register a new servlet to the path you want to proxy to another server, and give this servlet the target host as an init parameter. If you are using a traditional web application with a web.xml, you can configure it like following:
<servlet>
<servlet-name>proxy</servlet-name>
<servlet-class>org.mitre.dsmiley.httpproxy.ProxyServlet</servlet-class>
<init-param>
<param-name>targetUri</param-name>
<param-value>http://target.uri/target.path</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>proxy</servlet-name>
<url-pattern>/mapping-path/*</url-pattern>
</servlet-mapping>
or, of course, you can go with the annotation config.
If you are using Spring Boot, it is even easier: You only need to create a bean of type ServletRegistrationBean, with the required configuration:
#Bean
public ServletRegistrationBean proxyServletRegistrationBean() {
ServletRegistrationBean bean = new ServletRegistrationBean(
new ProxyServlet(), "/mapping-path/*");
bean.addInitParameter("targetUri", "http://target.uri/target.path");
return bean;
}
This way, you can also use the Spring properties that are available in the environment.
You can even extend the class ProxyServlet and override its methods to customize request/response headers etc, in case you need.
Update: After using Smiley's proxy servlet for some time, we had some timeout issues, it was not working reliably. Switched to Zuul from Netflix, didn't have any problems after that. A tutorial on configuring it with Spring Boot can be found on this link.
Unfortunately there is no easy way to do this. Basically you'll have to reconstruct the request, including:
correct HTTP method
request parameters
requests headers (HTTPUrlConnection doesn't allow to set arbitrary user agent, "Java/1.*" is always appended, you'll need HttpClient)
body
That's a lot of work, not to mention it won't scale since each such proxy call will occupy one thread on your machine.
My advice: use raw sockets or netty and intercept HTTP protocol on the lowest level, just replacing some values (like Host header) on the fly. Can you provide more context, why so you need this?
#RequestMapping(value = "/**")
public ResponseEntity route(HttpServletRequest request) throws IOException {
String body = IOUtils.toString(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
try {
ResponseEntity<Object> exchange = restTemplate.exchange(firstUrl + request.getRequestURI(),
HttpMethod.valueOf(request.getMethod()),
new HttpEntity<>(body),
Object.class,
request.getParameterMap());
return exchange;
} catch (final HttpClientErrorException e) {
return new ResponseEntity<>(e.getResponseBodyAsByteArray(), e.getResponseHeaders(), e.getStatusCode());
}
}
If you are forced to use spring, please check the rest template method exchange to proxy requests to a third party service.
Here you can find a working example.
Use Spring Cloud Gateway
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-mvc</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Controller
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.mvc.ProxyExchange;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
#RestController
public class Proxy extends BaseController {
private String prefix="/proxy";
private String Base="localhost:8080/proxy"; //destination
void setHeaders(ProxyExchange<?> proxy){
proxy.header("header1", "val1"); //add additional headers
}
#GetMapping("/proxy/**")
public ResponseEntity<?> proxyPath(#RequestParam MultiValueMap<String,String> allParams, ProxyExchange<?> proxy) throws Exception {
String path = proxy.path(prefix); //extract sub path
proxy.header("Cache-Control", "no-cache");
setHeaders(proxy);
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(Base + path).queryParams(allParams).build();
return proxy.uri(uriComponents.toUri().toString()).get();
}
#PutMapping("/proxy/**")
public ResponseEntity<?> proxyPathPut(ProxyExchange<?> proxy) throws Exception {
String path = proxy.path(prefix);
setHeaders(proxy);
return proxy.uri(Base + path).put();
}
Related
How can I configure Lagom framework to work with CORS request (method request 'options').
I have enabled CORS in lagom for one of my projects in this way.
Define a method in service class to handle OPTIONS calls.
ServiceCall<NotUsed, Done> options();
Implement the method in the service-impl class.
#Override
public ServiceCall<NotUsed, Done> options() {
return request -> CompletableFuture.completedFuture(Done.getInstance());
}
Define the options call in the descriptor. As an example, assume that the actual call is,
GET /api/v0.1/user
The service descriptor should look like this:
#Override
default Descriptor descriptor() {
// #formatter:off
return named("notification").withCalls(
restCall(Method.GET, "/api/v0.1/user", this::getUser),
restCall(Method.OPTIONS, "/api/v0.1/user", this::options)
).withAutoAcl(true).withHeaderFilter(new CORSHeaderFilter());
// #formatter:on
}
Note that it has a header filter attached using,
.withHeaderFilter(new CORSHeaderFilter())
CORSHeaderFilter Class should look like this.
import com.lightbend.lagom.javadsl.api.transport.HeaderFilter;
import com.lightbend.lagom.javadsl.api.transport.Method;
import com.lightbend.lagom.javadsl.api.transport.RequestHeader;
import com.lightbend.lagom.javadsl.api.transport.ResponseHeader;
public class CORSHeaderFilter implements HeaderFilter {
#Override
public RequestHeader transformClientRequest(RequestHeader request) {
return request;
}
#Override
public RequestHeader transformServerRequest(RequestHeader request) {
return request;
}
#Override
public ResponseHeader transformServerResponse(ResponseHeader response, RequestHeader request) {
ResponseHeader modifiedResponse = response.withHeader("Access-Control-Allow-Origin", "*");
if (Method.OPTIONS.equals(request.method())) {
modifiedResponse = modifiedResponse.withStatus(204).withHeader("Access-Control-Allow-Headers",
"Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With" +
",If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range").
withHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PATCH").
withHeader("Access-Control-Max-Age", "1728000");
}
return modifiedResponse;
}
#Override
public ResponseHeader transformClientResponse(ResponseHeader response, RequestHeader request) {
ResponseHeader modifiedResponse = response.withHeader("Access-Control-Allow-Origin", "*");
if (Method.OPTIONS.equals(request.method())) {
modifiedResponse = modifiedResponse.withStatus(204).withHeader("Access-Control-Allow-Headers",
"Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With" +
",If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range").
withHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PATCH").
withHeader("Access-Control-Max-Age", "1728000");
}
return modifiedResponse;
}
}
Whenever you add a new endpoint, make sure to add the OPTIONS version of it as well.
To allow a Lagom service written in Java to work with CORS, you'll need to implement a CORS filter per Play:
package example.service.impl
import play.filters.cors.CORSFilter;
import play.http.DefaultHttpFilters;
import javax.inject.Inject;
// See https://playframework.com/documentation/2.5.x/CorsFilter
public class MyCORSFilter extends DefaultHttpFilters {
#Inject
public MyCORSFilter(CORSFilter corsFilter) {
super(corsFilter);
}
}
and then in your application.conf, you'll need to add the filter:
play.http.filters = "example.service.impl.MyCORSFilter"
// To properly setup the CORSFilter, please refer to https://playframework.com/documentation/2.5.x/CorsFilter
// This example is only meant to show what's required for Lagom to use CORS.
play.filters.cors {
// review the values of all these settings to fulfill your needs. These values are not meant for production.
pathPrefixes = ["/api"]
allowedOrigins = null
allowedHttpMethods = null
allowedHttpHeaders = null
exposedHeaders = []
supportsCredentials = false
preflightMaxAge = 6 hour
}
For more info, see the example CORS service and the Play docs.
I have a Glassfish 4 webserver (running locally atm for testing) with a servlet, need to be able to request a string from a different domain using Javascript.
Searching for a solution, people suggest enabling CORS on my web server, by creating a Filter class (implementing ContainerRequestFilter and ContainerResponseFilter).
I've tried creating such a class, but the client still gets the same error; "No 'Access-Control-Allow-Origin' header is present on the requested resource".
What am I doing wrong?
Code:
class: CrossDomainFilter.java
package server;
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
#Provider
#CORSBinding
public class CrossDomainFilter implements ContainerRequestFilter, ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext request) throws IOException {
System.out.println("filter(request) called !!");
request.getHeaders().add("Access-Control-Allow-Origin", "*");
request.getHeaders().add("Access-Control-Allow-Headers",
"Authorization");
if (request.getMethod().equals("OPTIONS")) {
System.out.println("OPTIONS is requested!!!!!!!!!!!!!");
}
if (request.getMethod().equals("GET")) {
System.out.println("GET is requested!!!!!!!!!!!!!");
}
if (request.getMethod().equals("POST")) {
System.out.println("POST is requested!!!!!!!!!!!!!");
}
if (request.getMethod().equals("DELETE")) {
System.out.println("DELETE is requested!!!!!!!!!!!!!");
}
if (request.getMethod().equals("PUT")) {
System.out.println("PUT is requested!!!!!!!!!!!!!");
}
}
#Override
public void filter(ContainerRequestContext arg0,
ContainerResponseContext arg1) throws IOException {
System.out.println("filter(request, response) called !!");
}
}
Javascript function making the request
function httpRequest(url) {
var request = new XMLHttpRequest();
request.open("GET", url, false);
request.setRequestHeader('Access-Control-Allow-Origin', '*');
request.send(null);
return (request.status == 404) ? url + " not found!" : request.responseText;
}
EDIT:
Ended up using this: https://bitbucket.org/thetransactioncompany/cors-filter
I can now make javascript requests to my server cross-domain. But I would still like to understand what's wrong with my previous attempts...
You are adding headers to the Request instead of the Response. Request is the data from the client to the server while the Response is the data from the server to the client. The headers need to be set by the server to tell the browser that the resource is allowed (or not) be access by a particular domain.
With your example, you're adding the headers to the response before dispatch by the servlet. You also don't need the header in the javascript request.
Here's a pretty good resource with more detail on the different headers and use cases. http://www.html5rocks.com/en/tutorials/cors/
Hello I've been trying to figure out generic way to log http requests in my application, so far no luck, here is how I handle the logging right now i.e:
#RequestMapping(value="register", method = RequestMethod.POST)
#ResponseBody
public String register(#RequestParam(value="param1",required=false) String param1, #RequestParam("param2") String param2, #RequestParam("param3") String param3, HttpServletRequest request){
long start = System.currentTimeMillis();
logger.info("!--REQUEST START--!");
logger.info("Request URL: " + request.getRequestURL().toString());
List<String> requestParameterNames = Collections.list((Enumeration<String>)request.getParameterNames());
logger.info("Parameter number: " + requestParameterNames.size());
for (String parameterName : requestParameterNames){
logger.info("Parameter name: " + parameterName + " - Parameter value: " + request.getParameter(parameterName));
}
//Some processing logic, call to the various services/methods with different parameters, response is always String(Json)
String response = service.callSomeServiceMethods(param1,param2,param3);
logger.info("Response is: " + response);
long end = System.currentTimeMillis();
logger.info("Requested completed in: " + (end-start) + "ms");
logger.info("!--REQUEST END--!");
return response;
}
So what I do right now for different controllers/methods is copy everything from beginning of the inside of the method until the processing logic which differs from method to method and then copy everything from below of that as showed in above template.
It is kind of messy, and there is a lot of code repetition(which I don't like). But I need to log everything.
Does anyone have more experience with this kinds of logging, can anyone shed some light on this?
EDIT: Also, see #membersound's comment on this answer, which improves this answer.
Spring supports this. See CommonsRequestLoggingFilter. If using Spring Boot, just register a bean of that type and Boot will apply it to the filter chain. Like:
#Bean
public Filter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(5120);
return filter;
}
Also, this logging filter requires the log level be set to DEBUG. E.g. do this in a logback.xml with:
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>
Use an interceptor:
extend HandlerInterceptorAdapter and override preHandle
define it with <mvc:interceptors> in dispatcher-servlet.xml
It will run for every request.
The main issue with reading request is that as soon as the input stream is consumed its gone whoof... and cannot be read again. So the input stream has to be cached. Instead of writing your own classes for caching (which can be found at several places on web), Spring provides a couple of useful classes i.e. ContentCachingRequestWrapper and ContentCachingResponseWrapper. These classes can be utilized very effectively, for example, in filters for logging purposes.
Define a filter in web.xml:
<filter>
<filter-name>loggingFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>loggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Since the filter is declared as DelegatingFilterProxy, it can be declared as a bean using #Component or #Bean annotations. In the loggingFilter's doFilter method, wrap the request and response with spring provided classes before passing it to the filter chain:
HttpServletRequest requestToCache = new ContentCachingRequestWrapper(request);
HttpServletResponse responseToCache = new ContentCachingResponseWrapper(response);
chain.doFilter(requestToCache, responseToCache);
String requestData = getRequestData(requestToCache);
String responseData = getResponseData(responseToCache);
The input stream will be cached in the wrapped request as soon as the input stream is consumed after chain.doFilter(). Then it can be accessed as below:
public static String getRequestData(final HttpServletRequest request) throws UnsupportedEncodingException {
String payload = null;
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
}
}
return payload;
}
However, things are a bit different for response. Since the response was also wrapped before passing it to the filter chain, it will also be cached to the output stream as soon as it is written on its way back. But since the output stream will also be consumed so you have to copy the response back to the output stream using wrapper.copyBodyToResponse(). See below:
public static String getResponseData(final HttpServletResponse response) throws IOException {
String payload = null;
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
wrapper.copyBodyToResponse();
}
}
return payload;
}
Hope it helps!
Here's a small library I wrote you can use: spring-mvc-logger
I made it available via maven central:
<dependency>
<groupId>com.github.isrsal</groupId>
<artifactId>spring-mvc-logger</artifactId>
<version>0.2</version>
</dependency>
Adding to what #B.Ali has answered. If you are using this in a spring asynchronous request (serlvet 3.0 or greater) handling scenario, then the following code is what worked for me.
public class OncePerRequestLoggingFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
HttpServletResponse responseToUse = response;
// The below check is critical and if not there, then the request/response gets corrupted.
// Probably because in async case the filter is invoked multiple times.
if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request);
}
if (isFirstRequest && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(requestToUse, responseToUse);
if (!isAsyncStarted(request)) {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
responseWrapper.copyBodyToResponse(); // IMPORTANT to copy it back to response
}
}
#Override
protected boolean shouldNotFilterAsyncDispatch() {
return false; // IMPORTANT this is true by default and wont work in async scenario.
}
}
As any tech answer ... it depends ..
on the tech stack you are using and what your requirements are.
for example the more generic you want to make your logging, the further upfront you would want to do it. in your case, you are logging only requests which are logging enabled and being handled in the spring context. So you could be "missing" other requests.
I would look at the container or the web server you are using to run your app. That will remove this dependency on Spring. Plus containers provide you the flexibility of plugging in a logging provider and then configuring the format of the log outside code.
For example, if you are using Apache Web server, use Apache web server logging to log all HTTP requests in the access logging layer. But be careful, some of the logging options have performance penalties. Log only what you seriously need for an access pattern monitoring perspective.
If you are using tomcat, then tomcat also will allow you to log stuff. Search for Access Valve in the tomcat documentation for the tomcat you are using. That will open up a world of possibilities.
More extensive logging should be the domain of the exception strategy ie the kind of detail you want to see when a problem occurs in the system.
I am not using JSON or anything like that. I have a simple form to upload a file and I want to read the parameters of the form. The code below is not working as expected. It will not show any parameters.
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Path("{appNum}/{docId}/file")
public Response uploadDocFile(
#PathParam("appNum") String appNum,
#PathParam("docId") String docId,
#Context HttpServletRequest req)
{
try {
log.info("POST Parameters:");
Enumeration e = req.getParameterNames();
while(e.hasMoreElements())
{
Object key = e.nextElement();
log.info("Key: " + key);
log.info("Val: " + req.getParameter(key.toString()));
}
} catch (Exception e) {
e.printStackTrace();
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(new StatusResponse(e)).build();
}
return Response.ok().build();
}
FYI, You need to use #FormParam. Also make sure INPUT HTML types are using name= not id=.
I have the same problem. Using #FormParam annotation for individual parameters works, but reading them from HttpServletRequest injected through #Context doesn't. I also tried to get the request object/parameters through Guice using Provider<HttpServletRequest> and #RequestParameters<Map<String, String[]>>. In both cases there were no post parameters.
However, it is possible to get a map of parameters by adding a MultivaluedMap<String, String> parameter to resource method. Example:
#POST
public void doSomething(MultivaluedMap<String, String> formParams) {
//...
}
If you are using Jersey RESTful API in JAVA you can look for Parameter Annotations (#*Param)
Example:
Dependency:
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.8</version>
</dependency>
Code:
package yourpack;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
#Path("/path_to_data")
public class DataResource {
#GET
#Path("/{param}")
public Response getMsg(#PathParam("param") String urlparam) {
int ok = 200;
String result = "Jersey Data resource: " + urlparam;
return Response.status(ok).entity(result ).build();
}
}
List of annotations: #MatrixParam, #HeaderParam, #CookieParam, #FormParam, #QueryParam, #PathParam
At some point of time Jersey ContainerServlet (or other Jersey object during request processing) calls request.getInputStream() or request.getReader() which set 'usingInputStream' or 'usingReader' to TRUE. This state prevents populating of parameters map inside the request object. Something like this:
parseParameters() {
if (usingInputStream || usingReader) {
return;
} else {
parametersMap.putAll({actual parameters parsing from stream})
}
}
Map getParametersMap() {
return parametersMap;
}
Try putting a break point at the very first entry point (beginning of Jersey ServletContainer.service() method) of your application and evaluate request.getParametersMap() call. You'll get your parameters.
Title say about my issue. I need wrap DTO in to a javascript method callback. Currently I return on request JSON. But problem with using this in Ajax because I send GET to other domain. and of course security police.
I have idea to create addition provide. Have you any example, links or suggestion how can do this.
There's no explicit support for JSONP in RESTEasy, however one easy way to enable JSONP in your application is to write a Servlet Filter.
Here's a few links that can help you write a filter:
jsonp-java: server side filter wraps any response into a jsonp callback
Serving up JSONP from your JAX-RS Web Services
Implementing a Servlet Filter for JSONP callback with Spring’s DelegatingFilterProxy (if you're using Spring)
When I had this requirement I ended up writing my own since none of the examples I found seemed to quite nail it. Here's my advice for writing your own filter:
only wrap the response if a callback parameter is specified (obviously)
only wrap the response if the response content type is application/json (or if you want to support a wider selection of variants, only wrap if the response content type is application/json or application/*+json)
use an HttpServletResponseWrapper so that you can invoke the forward chain (chain.doFilter) without writing any data to the real response. Once the forward chain is complete you can then check the content type, make sure you want to wrap the response as JSONP, then write the captured data into the real response, along with the JSONP prefix and suffix.
when you do decide to wrap the response as JSONP, make sure you change the response content type to text/javascript
If you haven't done much with Java EE Filters before, you may want to read the relevant section of the Java EE tutorial first: Filtering Requests and Responses.
I make draft workaround for this problem. Try it. This solution takes data via http get parameters and translate to virtual POST request.
JQuery:
function call(){
var val = '{"routes":[{"arrivalAddress":{"fullAddress":"DME"},"destinationAddress":{"fullAddress":"SVO"}}],"carsCount":"1"}';
var jHandler = "doMap";
$.getJSON("http://xxx:yyy/app-0.0.0.1/rest/requestPrice?callback=" + jHandler + "&json=" + encodeURIComponent(val)+"&jsoncallback=?", null, null, "json");
}
function doMap(obj){
alert(obj);
}
Declaration in Service interface
#POST
#Path("requestPrice")
#Produces("application/json")
#Consumes("application/json")
PriceResponse requestPrice(PriceRequest request) throws ServiceException;
Filter class:
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class JSONPRequestFilter implements Filter {
private String callbackParameter;
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("This filter can " +
" only process HttpServletRequest requests");
}
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
if (isJSONPRequest(httpRequest)) {
RequestWrapper requestWrapper = new RequestWrapper(httpRequest);
requestWrapper.setContentType("application/json; charset=UTF-8");
requestWrapper.setHeader("cache-control", "no-cache");
requestWrapper.setHeader("accept", "application/json");
requestWrapper.setCharacterEncoding("UTF-8");
requestWrapper.setBody(httpRequest.getParameter("json"));
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(httpResponse) {
#Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
#Override
public void write(int b) throws IOException {
baos.write(b);
}
};
}
#Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(baos);
}
public String getData() {
return baos.toString();
}
};
chain.doFilter(requestWrapper, responseWrapper);
response.getOutputStream().write((getCallbackParameter(httpRequest) + "(").getBytes());
response.getOutputStream().write(baos.toByteArray());
response.getOutputStream().write(");".getBytes());
response.setContentType("text/javascript");
} else {
chain.doFilter(request, response);
}
}
private String getCallbackMethod(HttpServletRequest httpRequest) {
return httpRequest.getParameter(callbackParameter);
}
private boolean isJSONPRequest(HttpServletRequest httpRequest) {
String callbackMethod = getCallbackMethod(httpRequest);
return (callbackMethod != null && callbackMethod.length() > 0);
}
private String getCallbackParameter(HttpServletRequest request) {
return request.getParameter(callbackParameter);
}
public void init(FilterConfig filterConfig) throws ServletException {
callbackParameter = filterConfig.getInitParameter("callbackParameter");
}
public void destroy() {
}
void printRequest(HttpServletRequest request) throws IOException {
{
System.out.println("--------------Headers---------------");
Enumeration en = request.getHeaderNames();
while (en.hasMoreElements()) {
String val = en.nextElement().toString();
System.out.println(val + " :");
Enumeration en1 = request.getHeaders(val);
while (en1.hasMoreElements()) {
System.out.println("\t" + en1.nextElement());
}
}
}
{
System.out.println("------------Parameters--------------");
Enumeration en = request.getParameterNames();
while (en.hasMoreElements()) {
String val = en.nextElement().toString();
System.out.println(val + " :");
String[] en1 = request.getParameterValues(val);
for (String val1 : en1) {
System.out.println("\t" + val1);
}
}
}
System.out.println("---------------BODY--------------");
BufferedReader is = request.getReader();
String line;
while ((line = is.readLine()) != null) {
System.out.println(line);
}
System.out.println("---------------------------------");
System.out.println("ContentType: " + request.getContentType());
System.out.println("ContentLength: " + request.getContentLength());
System.out.println("characterEncodings: " + request.getCharacterEncoding());
System.out.println("AuthType: " + request.getAuthType());
System.out.println("ContextPath: " + request.getContextPath());
System.out.println("Method: " + request.getMethod());
}
public static class RequestWrapper extends HttpServletRequestWrapper {
Map<String, String> headers = new HashMap<String, String>();
int contentLength;
BufferedReader reader;
public RequestWrapper(HttpServletRequest request) {
super(request);
}
public void setHeader(String key, String value) {
headers.put(key, value);
}
ByteArrayInputStream bais;
public void setBody(String body) {
bais = new ByteArrayInputStream(body.getBytes());
contentLength = body.length();
headers.put("content-length", Integer.toString(contentLength));
}
#Override
public BufferedReader getReader() throws IOException {
reader = new BufferedReader(new InputStreamReader(bais));
return reader;
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
#Override
public int read() throws IOException {
return bais.read();
}
};
}
#Override
public String getMethod() {
return "POST";
}
private String contentType;
public void setContentType(String contentType) {
this.contentType = contentType;
headers.put("content-type", contentType);
}
#Override
public String getContentType() {
return contentType;
}
#Override
public int getContentLength() {
return contentLength;
}
#Override
public String getHeader(String name) {
String val = headers.get(name);
if (val != null) {
return val;
}
return super.getHeader(name); //To change body of overridden methods use File | Settings | File Templates.
}
#Override
public Enumeration getHeaders(final String name) {
return super.getHeaders(name);
}
#Override
public Enumeration getHeaderNames() {
final Enumeration en1 = super.getHeaderNames();
final Iterator it = headers.keySet().iterator();
return new Enumeration() {
public boolean hasMoreElements() {
return en1.hasMoreElements() || it.hasNext();
}
public Object nextElement() {
return en1.hasMoreElements() ? en1.nextElement() : (it.hasNext() ? it.next() : null);
}
};
}
#Override
public int getIntHeader(String name) {
String val = headers.get(name);
if (val == null) {
return super.getIntHeader(name);
} else {
return Integer.parseInt(val);
}
}
}
}
web.xml
<filter>
<filter-name>JSONPRequestFilter</filter-name>
<filter-class>xxxxx.JSONPRequestFilter</filter-class>
<init-param>
<param-name>callbackParameter</param-name>
<param-value>callback</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>JSONPRequestFilter</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
An enhancement to support JSONP is scheduled to be released in RESTEasy 2.3.6 Final/3.0-beta-4 (https://issues.jboss.org/browse/RESTEASY-342). I was able to "backport" it my project which uses RESTEasy 2.3.5 by simply copying their code from GitHub.
RESTEasy automatically picks up the new provider based on the annotation. It works automatically by wrapping your results in a js callback once it sees a query parameter named "callback" in the url. This is compatible with what JQuery sends to the server for JSONP requests.
To follow on from #talawahdotnet, I'm using RestEasy 3.0.9.Final and there is support for JSONP, once enabled, any request with a "callback" query parameter will be wrapped as JSONP. I'm using JBoss so the full docs are here for other containers. Here's the steps I had to do:
In your web.xml add:
<context-param>
<param-name>resteasy.providers</param-name>
<param-value>org.jboss.resteasy.plugins.providers.jackson.JacksonJsonpInterceptor</param-value>
</context-param>
Make sure you have a WEB-INF/jboss-deployment-structure.xml with:
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import" annotations="true"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
Make sure you have a resteasy-jackson-provider dependency in your pom.xml, something like:
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson-provider</artifactId>
<scope>provided</scope>
</dependency>
Resteasy claims to support JSONP out of the box in 3.x version:
If you're using Jackson, Resteasy has JSONP that you can turn on by
adding the provider
org.jboss.resteasy.plugins.providers.jackson.JacksonJsonpInterceptor
(Jackson2JsonpInterceptor if you're using the Jackson2 provider) to
your deployments. If the media type of the response is json and a
callback query parameter is given, the response will be a javascript
snippet with a method call of the method defined by the callback
parameter. For example:
GET /resources/stuff?callback=processStuffResponse will produce this
response:
processStuffResponse() This supports the default
behavior of jQuery.
You can change the name of the callback parameter by setting the
callbackQueryParameter property.
However, it seems that it is broken due to RESTEASY-1168: Jackson2JsonpInterceptor does not render closing bracket
So
foo({"foo":"bar"}
is rendered instead of
foo({"foo":"bar"})
And that causes "Uncaught SyntaxError: Unexpected Identifier" error
I have submitted a pull-request with a fix and hopefully it should get into next release 3.0.12
I know that this question is pretty old, but it is shown on the first page of Google when you search for resteasy jsonp problems, so I decided to update it