I want my Java web application to seamless proxy content provided by abother web server.
http://myapp.com/proxy/* -> http://other_app_to_proxy.com:9090/
My application will handle all auth-related matters, and serve other application on sub-path under same domain.
I found how to do reverse proxy: HTTP-Proxy-Servlet.
The problem now is that other application has absolute URLs like /css/style.css and when page gets open in my application, this URL is not accessible, as in my setup it should be /proxy/css/style.css.
I figured out I need some kind of URL-rewriting filter that would alter outbound response that goes to client. I tried to study Tuckey UrlRewrite but it looks like it's for different purpose - it has plenty of tools to change inbound URL and redirect requests to other locations.
Could somebody point me to some solution?
I came up to several classes that allow full response body rewriting in filter.
A couple of base classes:
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
public abstract class AbstractResponseAlteringFilter implements Filter {
public void init(FilterConfig fConfig) throws ServletException {
}
protected static class ByteArrayServletStream extends ServletOutputStream {
ByteArrayOutputStream baos;
ByteArrayServletStream(ByteArrayOutputStream baos) {
this.baos = baos;
}
public void write(int param) throws IOException {
baos.write(param);
}
}
protected static class ByteArrayPrintWriter {
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private PrintWriter pw = new PrintWriter(baos);
private ServletOutputStream sos = new ByteArrayServletStream(baos);
public PrintWriter getWriter() {
return pw;
}
public ServletOutputStream getStream() {
return sos;
}
byte[] toByteArray() {
return baos.toByteArray();
}
}
protected static class CharResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayPrintWriter output;
private boolean usingWriter;
public CharResponseWrapper(HttpServletResponse response) {
super(response);
usingWriter = false;
output = new ByteArrayPrintWriter();
}
public byte[] getByteArray() {
return output.toByteArray();
}
#Override
public ServletOutputStream getOutputStream() throws IOException {
// will error out, if in use
if (usingWriter) {
super.getOutputStream();
}
usingWriter = true;
return output.getStream();
}
#Override
public PrintWriter getWriter() throws IOException {
// will error out, if in use
if (usingWriter) {
super.getWriter();
}
usingWriter = true;
return output.getWriter();
}
public String toString() {
return output.toString();
}
}
public void destroy() {
}
}
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
public abstract class AbstractResponseBodyAlteringFilter extends AbstractResponseAlteringFilter {
private final static Logger logger = LoggerFactory.getLogger(AbstractResponseBodyAlteringFilter.class);
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
CharResponseWrapper wrappedResponse = new CharResponseWrapper(httpResponse);
chain.doFilter(request, wrappedResponse);
logger.info("wrappedResponse.getContentType() = {}", wrappedResponse.getContentType());
byte[] responseBytes = wrappedResponse.getByteArray();
if (StringUtils.containsAny(wrappedResponse.getContentType(), "text/", "javascript")) {
responseBytes = modifyResponseBody(new String(responseBytes, "UTF-8")).getBytes("UTF-8");
}
OutputStream out = httpResponse.getOutputStream();
logger.info("wrappedResponse.getStatus() = {}", wrappedResponse.getStatus());
httpResponse.setStatus(wrappedResponse.getStatus());
httpResponse.setContentType(wrappedResponse.getContentType());
httpResponse.setContentLength(responseBytes.length);
out.write(responseBytes);
out.flush();
httpResponse.flushBuffer();
}
protected abstract String modifyResponseBody(String body);
}
And here final user class:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
public class ProxyResponseBodyRewriteFilter extends AbstractResponseBodyAlteringFilter {
#Value("${proxy.filter.prefix:/proxy/}")
String prefix;
private final static Logger logger = LoggerFactory.getLogger(ProxyResponseBodyRewriteFilter.class);
protected String modifyResponseBody(String body) {
body = body.replaceAll("href\\s*=\\s*\"\\s*/", "href=\"" + prefix);
body = body.replaceAll("src\\s*=\\s*\"\\s*/", "src=\"" + prefix);
body = body.replace("</head>", "<base href=\"/proxy/\"></head>");
return body;
}
}
I'm facing a similar issue, wanting to use the referenced HttpProxyServlet but also needing to alter URLs embedded in the response. I did the following, as part of subclassing the ProxyServlet, and it works (albeit the code is still a bit slanted towards my use case and not totally generic), but I'm curious if you ever found something simpler/already written, #snowindy?
public class CustomProxyServlet extends ProxyServlet {
#Override
protected void copyResponseEntity(
HttpResponse proxyResponse, HttpServletResponse servletResponse,
HttpRequest proxyRequest, HttpServletRequest servletRequest
) throws IOException {
HttpEntity entity = proxyResponse.getEntity();
if (entity != null) {
OutputStream servletOutputStream = servletResponse.getOutputStream();
if (isRewritable(proxyResponse)) {
RewriteOutputStream rewriter = null;
try {
rewriter = new RewriteOutputStream(servletOutputStream);
entity.writeTo(rewriter);
}
finally { rewriter.flush(); }
}
else {
// parent's default behavior
entity.writeTo(servletOutputStream);
}
}
}
private boolean isRewritable(HttpResponse httpResponse) {
boolean rewriteable = false;
Header[] contentTypeHeaders = httpResponse.getHeaders("Content-Type");
for (Header header : contentTypeHeaders) {
// May need to accept other types
if (header.getValue().contains("html")) rewriteable = true;
}
return rewriteable;
}
}
where RewriteOutputStream is
public class RewriteOutputStream extends FilterOutputStream {
// Provided implementation based on BufferedOutputStream adding
// config-based string replacement before writing to output.
}
This is admittedly a workaround rather than an actual solution, but this information may be useful to those who have control over the proxied application. We were able to use Smiley's Proxy Servlet as is and avoided having to do URL-rewriting by deploying the proxied application using a structure that matches the structure under which the proxied pages are made available by the proxy servlet.
http://myapp.com/proxy/* -> http://other_app_to_proxy.com:9090/proxy/*
Try changing location /proxy to /proxy/ (trailing slash) in the config file of Your reverse proxy's server.
Refer the link if your using nginx as reverse proxy server.
https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
Related
I have a spring boot app deployed on Heroku which is also using Firebase Storage to store files. Everything works fine locally as I am able to authenticate to Firebase Storage by specifying the path to the firebase admin sdk service account key like this:
FileInputStream serviceAccount =
new FileInputStream("path/to/key.json");
StorageOptions.newBuilder()
.setProjectId(projectId)
.setCredentials(GoogleCredentials.fromStream(serviceAccount)).build();
it is not safe to add the service account key to the project which would then be committed to git. How can this be externalized such that the service key is part of Heroku's config vars when deployed to Heroku? I have tried adding the raw json content to the application.properties and reading to a temp file but I get an error when I try to set the credentials from the temp file path.
Path tempFile = createTempFile();
if (tempFile == null) throw new Exception("google storage credentials not found");
FileInputStream serviceAccount =
new FileInputStream(tempFile.toString);
StorageOptions.newBuilder()
.setProjectId(projectId)
.setCredentials(GoogleCredentials.fromStream(serviceAccount)).build();
//create temp file
private Path createTempFile() {
Path path = null;
try {
path = Files.createTempFile("serviceaccount", ".json");
System.out.println("Temp file : " + path);
//writing data
String credentials = environment.getRequiredProperty("GOOGLE_APPLICATION_CREDENTIALS");
byte[] buffer = credentials.getBytes();
Files.write(path, buffer);
} catch (IOException e) {
e.printStackTrace();
}
return path;
}
this works for me -
custom.firebase.credentials is the json i.e. just copy the contents of the json file and blindly paste it
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
#Configuration
public class FirebaseConfig {
private final String firebaseCredentials;
public FirebaseConfig(#Value("${custom.firebase.credentials}") String firebaseCredentials) {
this.firebaseCredentials = firebaseCredentials;
}
#Primary
#Bean
public FirebaseApp firebaseApp() throws IOException {
if (FirebaseApp.getApps().isEmpty()) {
InputStream credentialsStream = new ByteArrayInputStream(firebaseCredentials.getBytes());
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentialsStream)).build();
FirebaseApp.initializeApp(options);
}
return FirebaseApp.getInstance();
}
#Bean
public FirebaseAuth firebaseAuth() throws IOException {
return FirebaseAuth.getInstance(firebaseApp());
}
}
I don't have enough reputation yet to comment directly on it, but keemahs solution worked perfectly and was super easy! Just paste the entire json as a config var in the heroku console like so.
Config vars
I finally got an idea from discussions with a friend on a way to go about this. First, I had to create a class that contains fields to hold the contents of the json credentials. The class is as follows:
public class FirebaseCredential {
private String type;
private String project_id;
private String private_key_id;
private String private_key;
private String client_email;
private String client_id;
private String auth_uri;
private String token_uri;
private String auth_provider_x509_cert_url;
private String client_x509_cert_url;
public String getType() {
return type;
}
public String getProject_id() {
return project_id;
}
public String getPrivate_key_id() {
return private_key_id;
}
public String getPrivate_key() {
return private_key;
}
public String getClient_email() {
return client_email;
}
public String getClient_id() {
return client_id;
}
public String getAuth_uri() {
return auth_uri;
}
public String getToken_uri() {
return token_uri;
}
public String getAuth_provider_x509_cert_url() {
return auth_provider_x509_cert_url;
}
public String getClient_x509_cert_url() {
return client_x509_cert_url;
}
public void setType(String type) {
this.type = type;
}
public void setProject_id(String project_id) {
this.project_id = project_id;
}
public void setPrivate_key_id(String private_key_id) {
this.private_key_id = private_key_id;
}
public void setPrivate_key(String private_key) {
this.private_key = private_key;
}
public void setClient_email(String client_email) {
this.client_email = client_email;
}
public void setClient_id(String client_id) {
this.client_id = client_id;
}
public void setAuth_uri(String auth_uri) {
this.auth_uri = auth_uri;
}
public void setToken_uri(String token_uri) {
this.token_uri = token_uri;
}
public void setAuth_provider_x509_cert_url(String auth_provider_x509_cert_url) {
this.auth_provider_x509_cert_url = auth_provider_x509_cert_url;
}
public void setClient_x509_cert_url(String client_x509_cert_url) {
this.client_x509_cert_url = client_x509_cert_url;
}}
I then created the following environment properties to hold the values of the json credentials file:
FIREBASE_BUCKET_NAME=<add-the-value-from-config.json>
FIREBASE_PROJECT_ID=<add-the-value-from-config.json>
FIREBASE_TYPE=<add-the-value-from-config.json>
FIREBASE_PRIVATE_KEY_ID=<add-the-value-from-config.json>
FIREBASE_PRIVATE_KEY=<add-the-value-from-config.json>
FIREBASE_CLIENT_EMAIL=<add-the-value-from-config.json>
FIREBASE_CLIENT_ID=<add-the-value-from-config.json>
FIREBASE_AUTH_URI=<add-the-value-from-config.json>
FIREBASE_TOKEN_URI=<add-the-value-from-config.json>
FIREBASE_AUTH_PROVIDER_X509_CERT_URL=<add-the-value-from-config.json>
FIREBASE_CLIENT_X509_CERT_URL=<add-the-value-from-config.json>
With the properties set up, it is possible to read the environment values and set them in a FirebaseCredential object, serialize the object to a json string and finally convert it to an InputStream object as seen below:
private InputStream createFirebaseCredential() throws Exception {
//private key
String privateKey = environment.getRequiredProperty("FIREBASE_PRIVATE_KEY").replace("\\n", "\n");
FirebaseCredential firebaseCredential = new FirebaseCredential();
firebaseCredential.setType(environment.getRequiredProperty("FIREBASE_TYPE"));
firebaseCredential.setProject_id(projectId);
firebaseCredential.setPrivate_key_id("FIREBASE_PRIVATE_KEY_ID");
firebaseCredential.setPrivate_key(privateKey);
firebaseCredential.setClient_email(environment.getRequiredProperty("FIREBASE_CLIENT_EMAIL"));
firebaseCredential.setClient_id(environment.getRequiredProperty("FIREBASE_CLIENT_ID"));
firebaseCredential.setAuth_uri(environment.getRequiredProperty("FIREBASE_AUTH_URI"));
firebaseCredential.setToken_uri(environment.getRequiredProperty("FIREBASE_TOKEN_URI"));
firebaseCredential.setAuth_provider_x509_cert_url(environment.getRequiredProperty("FIREBASE_AUTH_PROVIDER_X509_CERT_URL"));
firebaseCredential.setClient_x509_cert_url(environment.getRequiredProperty("FIREBASE_CLIENT_X509_CERT_URL"));
//serialization of the object to json string
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(firebaseCredential);
//convert jsonString string to InputStream using Apache Commons
return IOUtils.toInputStream(jsonString);
}
The resulting InputStream object is used to initialize the Firebase Storage or Admin as the case may be.
InputStream firebaseCredentialStream = createFirebaseCredential();
StorageOptions.newBuilder()
.setProjectId(projected)
.setCredentials(GoogleCredentials.fromStream(firebaseCredentialStream))
.build();
I am trying to set some metadata with a value from the response after the rpc server call has been processed. The plan was to use server interceptor and override close method.
Something like this: https://github.com/dconnelly/grpc-error-example/blob/master/src/main/java/example/Errors.java#L38
Since the metadata value depends on the response, I need some way to pass data from rpc server call to server interceptor or access the response from interceptor
In Golang, the metadata can be set easily in the rpc call grpc.SetTrailer after processing but in java there is no way to do it in rpc call. So I am trying to use server interceptor for the same.
Can someone help?
You can use grpc-java's Contexts for that.
In the interceptor you attach a Context with a custom key containing a mutable reference. Then in the call you access that header again and extract the value from it.
public static final Context.Key<TrailerHolder> TRAILER_HOLDER_KEY = Context.key("trailerHolder");
Context context = Context.current().withValue(TRAILER_HOLDER_KEY, new TrailerHolder());
Context previousContext = context.attach();
[...]
context.detach(previousContext);
You can access the context value like this:
TrailerHolder trailerHolder = TRAILER_HOLDER_KEY.get();
You might want to implement your code similar to this method:
Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler)
EDIT:
import io.grpc.Context;
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
public class TrailerServerInterceptor implements ServerInterceptor {
public static final Context.Key<Metadata> TRAILER_HOLDER_KEY = Context.key("trailerHolder");
#Override
public <ReqT, RespT> Listener<ReqT> interceptCall(final ServerCall<ReqT, RespT> call, final Metadata headers,
final ServerCallHandler<ReqT, RespT> next) {
final TrailerCall<ReqT, RespT> call2 = new TrailerCall<>(call);
final Context context = Context.current().withValue(TRAILER_HOLDER_KEY, new Metadata());
final Context previousContext = context.attach();
try {
return new TrailerListener<>(next.startCall(call2, headers), context);
} finally {
context.detach(previousContext);
}
}
private class TrailerCall<ReqT, RespT> extends SimpleForwardingServerCall<ReqT, RespT> {
public TrailerCall(final ServerCall<ReqT, RespT> delegate) {
super(delegate);
}
#Override
public void close(final Status status, final Metadata trailers) {
trailers.merge(TRAILER_HOLDER_KEY.get());
super.close(status, trailers);
}
}
private class TrailerListener<ReqT> extends ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT> {
private final Context context;
public TrailerListener(final ServerCall.Listener<ReqT> delegate, final Context context) {
super(delegate);
this.context = context;
}
#Override
public void onMessage(final ReqT message) {
final Context previous = this.context.attach();
try {
super.onMessage(message);
} finally {
this.context.detach(previous);
}
}
#Override
public void onHalfClose() {
final Context previous = this.context.attach();
try {
super.onHalfClose();
} finally {
this.context.detach(previous);
}
}
#Override
public void onCancel() {
final Context previous = this.context.attach();
try {
super.onCancel();
} finally {
this.context.detach(previous);
}
}
#Override
public void onComplete() {
final Context previous = this.context.attach();
try {
super.onComplete();
} finally {
this.context.detach(previous);
}
}
#Override
public void onReady() {
final Context previous = this.context.attach();
try {
super.onReady();
} finally {
this.context.detach(previous);
}
}
}
}
In your grpc service method you can simply use TRAILER_HOLDER_KEY.get().put(...)
I'm using JBoss' Resteasy as our JAX-RS provider. We have a requirement to read the servlet request body for authentication purpose, the problem is once the InputStream is read in the request, it cannot be read again, hence #FormParam won't work unless I can somehow "put the content back". I've tried the following two options:
Using Resteasy's PreProcessInterceptor, I was able to read the body, but there's no way to reset the InputStream or add a wrapper type. The documentation doesn't mention anything about this. According to JBoss' issue tracker, it's not currently possible.
Using the Servlet filter + Wrapper type apporach (see example here), I was able to get the request body in #javax.ws.rs.core.Context HttpServletRequest request but all the #FormParam still return null.
Here's a snippet of the PreProcessorInterceptor:
#Provider
#ServerInterceptor
public class SomePreprocessor implements PreProcessInterceptor {
public ServerResponse preProcess(HttpRequest request, ResourceMethod method)
throws Failure, WebApplicationException {
try{
StringWriter writer = new StringWriter();
IOUtils.copy(request.getInputStream(), writer, "UTF-8");
System.out.println("Request Body: "+writer.toString());
// What can I do to reset the request body?
}
catch(Exception ex){
ex.printStackTrace();
}
return null;
}
}
Here's a snippet of the rest method:
#POST
#Path("/something")
#Produces("application/xml")
public Response doSomething(
#FormParam("name") String name,
#javax.ws.rs.core.Context HttpServletRequest request) {
// name is always null
System.out.println(name);
// prints nothing in approach 1, returns the body in approach 2
java.io.StringWriter writer = new java.io.StringWriter();
org.apache.commons.io.IOUtils.copy(request.getReader(), writer);
System.out.println(writer.toString());
}
If anyone is still interested in the answer, here's how I solved it:
Create a custom type that extends HttpServletRequestWrapper. Make sure you override
getInputStream()
getReader()
getParameter()
getParameterMap()
getParameterNames()
getParameterValues()
This is because when Resteasy tries to bind using #Form, #FormParam, and #QueryParam etc, it calls the getParameter() method on the Resteasy class, which is then delegated to the underlying request, in my case, Apache's Coyote Servlet Request. So overriding getInputStream() and getReader() alone are not enough, you must make sure that getParameter() utilize the new input stream as well.
If you want to store the body for later use, you must then construct the param map yourself by parsing the query string and url-encoded form body. It's quite straight forward to implement but it carries its own risk. I recommend reading Coyote's implementation of the same methods.
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
/**
* Wrapper class that supports repeated read of the request body and parameters.
*/
public class CustomHttpServletRequest extends HttpServletRequestWrapper {
private static final Logger logger = Logger.getLogger(CustomHttpServletRequest.class);
// A typical url encoded form is "key1=value&key2=some%20value"
public final static Pattern urlStrPattern = Pattern.compile("([^=&]+)=([^&]*)[&]?");
// Cached request body
protected ByteArrayOutputStream cachedBytes;
protected String encoding;
protected String requestBody;
// Cached form parameters
protected Map<String, String[]> paramMap = new LinkedHashMap<String, String[]>();
// Cached header names, including extra headers we injected.
protected Enumeration<?> headerNames = null;
/**
*
* #param request
*/
public CustomHttpServletRequest(HttpServletRequest request) {
super(request);
// Read the body and construct parameters
try{
encoding = (request.getCharacterEncoding()==null)?"UTF-8":request.getCharacterEncoding();
// Parameters in query strings must be added to paramMap
String queryString = request.getQueryString();
logger.debug("Extracted HTTP query string: "+queryString);
if(queryString != null && !queryString.isEmpty()){
addParameters(queryString, encoding);
}
// Parse the request body if this is a form submission. Clients must set content-type to "x-www-form-urlencoded".
requestBody = IOUtils.toString(this.getInputStream(), encoding);
if (StringUtils.isEmpty(requestBody)) {requestBody = null;}
logger.debug("Extracted HTTP request body: "+requestBody);
if(request.getContentType() != null && request.getContentType().toLowerCase().contains(MediaType.APPLICATION_FORM_URLENCODED)){
addParameters(requestBody, encoding);
}
}
catch(IOException ex){
throw new RuntimeException(ex);
}
}
/**
*
* #param requestBody
* #param encoding
* #throws IOException
*/
private void addParameters(String requestBody, String encoding) throws IOException {
if(requestBody == null){
return;
}
Matcher matcher = urlStrPattern.matcher(requestBody);
while(matcher.find()){
String decodedName = URLDecoder.decode(matcher.group(1), encoding);
// If there's no value, matcher.group(2) returns an empty string instead of null
String decodedValue = URLDecoder.decode(matcher.group(2), encoding);
addParameter(decodedName, decodedValue);
logger.debug("Parsed form parameter, name = "+decodedName+", value = "+decodedValue);
}
}
/**
*
* #param name
* #param value
*/
private void addParameter(String name, String value) {
String[] pv = paramMap.get(name);
if (pv == null) {
pv = new String[]{value};
paramMap.put(name, pv);
}
else {
String[] newValue = new String[pv.length+1];
System.arraycopy(pv, 0, newValue, 0, pv.length);
newValue[pv.length] = value;
paramMap.put(name, newValue);
}
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getInputStream()
*/
#Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null){
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
// Return a inner class that references cachedBytes
return new CachedServletInputStream();
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getReader()
*/
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
*
* #return
*/
public String getRequestBody() {
return requestBody;
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
*/
#Override
public String getParameter(String name) {
if(paramMap.containsKey(name)){
String[] value = (String[]) paramMap.get(name);
if(value == null){
return null;
}
else{
return value[0];
}
}
return null;
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getParameterMap()
*/
#Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(paramMap);
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getParameterNames()
*/
#Override
public Enumeration<?> getParameterNames() {
return Collections.enumeration(paramMap.keySet());
}
/*
* (non-Javadoc)
* #see javax.servlet.ServletRequestWrapper#getParameterValues(java.lang.String)
*/
#Override
public String[] getParameterValues(String name) {
if(paramMap.containsKey(name)){
return paramMap.get(name);
}
return null;
}
/**
* Inner class that reads from stored byte array
*/
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
#Override
public int read() throws IOException {
return input.read();
}
#Override
public int read(byte[] b) throws IOException {
return input.read(b);
}
#Override
public int read(byte[] b, int off, int len) {
return input.read(b, off, len);
}
}
}
And add a filter to wrap the original request:
public class CustomFilter implements Filter {
private static final Logger logger = Logger.getLogger(CustomFilter.class);
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(request!=null && request instanceof HttpServletRequest){
HttpServletRequest httpRequest = (HttpServletRequest) request;
logger.debug("Wrapping HTTP request");
request = new CustomHttpServletRequest(httpRequest);
}
chain.doFilter(request, response);
}
}
i'm using httpclient 4. When i use
new DecompressingHttpClient(client).execute(method)
the client acccepts gzip and decompresses if the server sends gzip.
But how can i archieve that the client sends it's data gzipped?
HttpClient 4.3 APIs:
HttpEntity entity = EntityBuilder.create()
.setText("some text")
.setContentType(ContentType.TEXT_PLAIN)
.gzipCompress()
.build();
HttpClient 4.2 APIs:
HttpEntity entity = new GzipCompressingEntity(
new StringEntity("some text", ContentType.TEXT_PLAIN));
GzipCompressingEntity implementation:
public class GzipCompressingEntity extends HttpEntityWrapper {
private static final String GZIP_CODEC = "gzip";
public GzipCompressingEntity(final HttpEntity entity) {
super(entity);
}
#Override
public Header getContentEncoding() {
return new BasicHeader(HTTP.CONTENT_ENCODING, GZIP_CODEC);
}
#Override
public long getContentLength() {
return -1;
}
#Override
public boolean isChunked() {
// force content chunking
return true;
}
#Override
public InputStream getContent() throws IOException {
throw new UnsupportedOperationException();
}
#Override
public void writeTo(final OutputStream outstream) throws IOException {
final GZIPOutputStream gzip = new GZIPOutputStream(outstream);
try {
wrappedEntity.writeTo(gzip);
} finally {
gzip.close();
}
}
}
I have a simple resource like:
#Path("/")
public class RootResource {
#Context WebConfig wc;
#PostConstruct
public void init() {
assertNotNull(wc);
}
#GET
public void String method() {
return "Hello\n";
}
}
Which I am trying to use with JerseyTest (2.x, not 1.x) and the GrizzlyTestContainerFactory.
I can't work out what I need to do in terms of config to get the WebConfig object injected.
I solved this issue by creating a subclass of GrizzlyTestContainerFactory and explicitly loading the Jersey servlet. This triggers the injection of the WebConfig object. The code looks like this:
public class ExtendedGrizzlyTestContainerFactory implements TestContainerFactory {
private static class GrizzlyTestContainer implements TestContainer {
private final URI uri;
private final ApplicationHandler appHandler;
private HttpServer server;
private static final Logger LOGGER = Logger.getLogger(GrizzlyTestContainer.class.getName());
private GrizzlyTestContainer(URI uri, ApplicationHandler appHandler) {
this.appHandler = appHandler;
this.uri = uri;
}
#Override
public ClientConfig getClientConfig() {
return null;
}
#Override
public URI getBaseUri() {
return uri;
}
#Override
public void start() {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Starting GrizzlyTestContainer...");
}
try {
this.server = GrizzlyHttpServerFactory.createHttpServer(uri, appHandler);
// Initialize and register Jersey Servlet
WebappContext context = new WebappContext("WebappContext", "");
ServletRegistration registration = context.addServlet("ServletContainer", ServletContainer.class);
registration.setInitParameter("javax.ws.rs.Application",
appHandler.getConfiguration().getApplication().getClass().getName());
// Add an init parameter - this could be loaded from a parameter in the constructor
registration.setInitParameter("myparam", "myvalue");
registration.addMapping("/*");
context.deploy(server);
} catch (ProcessingException e) {
throw new TestContainerException(e);
}
}
#Override
public void stop() {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Stopping GrizzlyTestContainer...");
}
this.server.stop();
}
}
#Override
public TestContainer create(URI baseUri, ApplicationHandler application) throws IllegalArgumentException {
return new GrizzlyTestContainer(baseUri, application);
}
Notice that the Jersey Servlet configuration is being loaded from the ApplicationHandler that is passed in as a parameter using the inner Application object's class name (ResourceConfig is a subclass of Application). Therefore, you also need to create a subclass of ResourceConfig for this approach to work. The code for this is very simple:
package com.example;
import org.glassfish.jersey.server.ResourceConfig;
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
super(MyResource.class);
}
}
This assumes the resource you are testing is MyResource. You also need to override a couple of methods in your test like this:
public class MyResourceTest extends JerseyTest {
public MyResourceTest() throws TestContainerException {
}
#Override
protected Application configure() {
return new MyResourceConfig();
}
#Override
protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
return new ExtendedGrizzlyTestContainerFactory();
}
#Test
public void testCreateSimpleBean() {
final String beanList = target("test").request().get(String.class);
Assert.assertNotNull(beanList);
}
}
Finally, for completeness, here is the code for MyResource:
#Path("test")
public class MyResource {
#Context WebConfig wc;
#PostConstruct
public void init() {
System.out.println("WebConfig: " + wc);
String url = wc.getInitParameter("myparam");
System.out.println("myparam = "+url);
}
#GET
#Produces(MediaType.APPLICATION_JSON)
public Collection<TestBean> createSimpleBean() {
Collection<TestBean> res = new ArrayList<TestBean>();
res.add(new TestBean("a", 1, 1L));
res.add(new TestBean("b", 2, 2L));
return res;
}
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public TestBean roundTrip(TestBean s) {
return s;
}
}
The output of running the test shows that the WebConfig is loaded and the init param is now available:
WebConfig: org.glassfish.jersey.servlet.WebServletConfig#107d0f44
myparam = myvalue
The solution from #ametke worked well but wasn't picking up my ExceptionMapper classes. To solve this I simplified the start() method to:
#Override
public void start() {
try {
initParams.put("jersey.config.server.provider.packages", "my.resources;my.config");
this.server = GrizzlyWebContainerFactory.create(uri, initParams);
} catch (ProcessingException | IOException e) {
throw new TestContainerException(e);
}
}
This was based on Problems running JerseyTest when dealing with HttpServletResponse