/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.wink.server.internal.servlet.contentencode;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ext.RuntimeDelegate;
import javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate;
import org.apache.wink.common.internal.http.AcceptEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A servlet filter which changes the HttpServletResponse to
* automatically deflate or GZIP encode an outgoing response if the incoming
* request has an appropriate Accept-Encoding request header value. Add to your
* web.xml like: <br/>
* <code>
* <filter><br/>
<filter-name>ContentEncodingResponseFilter</filter-name><br/>
<filter-class>org.apache.wink.server.internal.servlet.contentencode.ContentEncodingResponseFilter</filter-class><br/>
</filter><br/>
<br/>
<filter-mapping><br/>
<filter-name>ContentEncodingResponseFilter</filter-name><br/>
<url-pattern>/*</url-pattern><br/>
</filter-mapping><br/>
* </code>
*/
public class ContentEncodingResponseFilter implements Filter {
private final static Logger logger =
LoggerFactory
.getLogger(ContentEncodingResponseFilter.class);
private final static HeaderDelegate<AcceptEncoding> acceptEncodingHeaderDelegate =
RuntimeDelegate
.getInstance()
.createHeaderDelegate(AcceptEncoding.class);
public void init(FilterConfig arg0) throws ServletException {
logger.trace("init({}) entry", arg0); //$NON-NLS-1$
/* do nothing */
logger.trace("init() exit"); //$NON-NLS-1$
}
public void destroy() {
logger.trace("destroy() entry"); //$NON-NLS-1$
/* do nothing */
logger.trace("destroy() exit"); //$NON-NLS-1$
}
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain chain) throws IOException, ServletException {
if (logger.isTraceEnabled()) {
logger.trace("doFilter({}, {}, {}) entry", new Object[] {servletRequest, //$NON-NLS-1$
servletResponse, chain});
}
/*
* wraps the servlet response if necessary
*/
if (servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
final AcceptEncoding acceptEncoding = getAcceptEncodingHeader(httpServletRequest);
logger.trace("AcceptEncoding header was {}", acceptEncoding); //$NON-NLS-1$
if (acceptEncoding != null && (acceptEncoding.isAnyEncodingAllowed() || acceptEncoding
.getAcceptableEncodings().size() > 0)) {
logger.trace("AcceptEncoding header was set so wrapping HttpServletResponse"); //$NON-NLS-1$
HttpServletResponseContentEncodingWrapperImpl wrappedServletResponse =
new HttpServletResponseContentEncodingWrapperImpl(
(HttpServletResponse)servletResponse,
acceptEncoding);
logger.trace("Passing on request and response down the filter chain"); //$NON-NLS-1$
chain.doFilter(servletRequest, wrappedServletResponse);
logger.trace("Finished filter chain"); //$NON-NLS-1$
EncodedOutputStream encodedOutputStream =
wrappedServletResponse.getEncodedOutputStream();
if (encodedOutputStream != null) {
logger.trace("Calling encodedOutputStream finish"); //$NON-NLS-1$
encodedOutputStream.finish();
}
logger.trace("doFilter exit()"); //$NON-NLS-1$
return;
}
}
logger.trace("AcceptEncoding header not found so processing like normal request"); //$NON-NLS-1$
chain.doFilter(servletRequest, servletResponse);
logger.trace("doFilter exit()"); //$NON-NLS-1$
}
/**
* Returns an AcceptEncoding object if there is an Accept Encoding header.
*
* @param httpServletRequest
* @return
*/
static AcceptEncoding getAcceptEncodingHeader(HttpServletRequest httpServletRequest) {
logger.trace("getAcceptEncodingHeader({}) entry", httpServletRequest); //$NON-NLS-1$
Enumeration<String> acceptEncodingEnum =
httpServletRequest.getHeaders(HttpHeaders.ACCEPT_ENCODING);
StringBuilder sb = new StringBuilder();
if (acceptEncodingEnum.hasMoreElements()) {
sb.append(acceptEncodingEnum.nextElement());
while (acceptEncodingEnum.hasMoreElements()) {
sb.append(","); //$NON-NLS-1$
sb.append(acceptEncodingEnum.nextElement());
}
String acceptEncodingHeader = sb.toString();
logger.trace("acceptEncodingHeader is {} so returning as AcceptEncodingHeader", //$NON-NLS-1$
acceptEncodingHeader);
return acceptEncodingHeaderDelegate.fromString(acceptEncodingHeader);
}
logger.trace("No Accept-Encoding header"); //$NON-NLS-1$
logger.trace("getAcceptEncodingHeader() exit - returning null"); //$NON-NLS-1$
return null;
}
static abstract class EncodedOutputStream extends ServletOutputStream {
private boolean isWritten = false;
private DeflaterOutputStream outputStream;
public EncodedOutputStream(DeflaterOutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void write(int b) throws IOException {
if (!isWritten) {
isFirstWrite();
isWritten = true;
}
outputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (!isWritten) {
isFirstWrite();
isWritten = true;
}
outputStream.write(b, off, len);
}
@Override
public void write(byte[] b) throws IOException {
if (!isWritten) {
isFirstWrite();
isWritten = true;
}
outputStream.write(b);
}
@Override
public void flush() throws IOException {
if (!isWritten) {
isFirstWrite();
isWritten = true;
}
outputStream.flush();
}
@Override
public void close() throws IOException {
outputStream.finish();
outputStream.close();
}
public void finish() throws IOException {
outputStream.finish();
}
public abstract void isFirstWrite();
}
static class GzipEncoderOutputStream extends EncodedOutputStream {
final private HttpServletResponse response;
public GzipEncoderOutputStream(OutputStream outputStream, HttpServletResponse response)
throws IOException {
super(new GZIPOutputStream(outputStream));
this.response = response;
}
@Override
public void isFirstWrite() {
response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); //$NON-NLS-1$
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
}
}
static class DeflaterContentEncodedOutputStream extends EncodedOutputStream {
final private HttpServletResponse response;
public DeflaterContentEncodedOutputStream(OutputStream outputStream,
HttpServletResponse response) throws IOException {
super(new DeflaterOutputStream(outputStream));
this.response = response;
}
@Override
public void isFirstWrite() {
response.addHeader(HttpHeaders.CONTENT_ENCODING, "deflate"); //$NON-NLS-1$
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
}
}
static class HttpServletResponseContentEncodingWrapperImpl extends HttpServletResponseWrapper {
private final static Logger logger =
LoggerFactory
.getLogger(HttpServletResponseContentEncodingWrapperImpl.class);
final private AcceptEncoding acceptEncoding;
private ServletOutputStream outputStream;
private EncodedOutputStream encodedOutputStream;
private int varyHeaderCount = 0;
public EncodedOutputStream getEncodedOutputStream() {
return encodedOutputStream;
}
public HttpServletResponseContentEncodingWrapperImpl(HttpServletResponse response,
AcceptEncoding acceptEncoding) {
super(response);
this.acceptEncoding = acceptEncoding;
}
private boolean containsAcceptEncoding(String value) {
String[] str = value.split(","); //$NON-NLS-1$
for (String s : str) {
if (HttpHeaders.ACCEPT_ENCODING.equalsIgnoreCase(s.trim())) {
return true;
}
}
return false;
}
@Override
public void addHeader(String name, String value) {
logger.trace("addHeader({}, {}) entry", name, value); //$NON-NLS-1$
/*
* this logic is added to append Accept-Encoding to the first Vary
* header value.
*/
if (HttpHeaders.VARY.equalsIgnoreCase(name)) {
++varyHeaderCount;
logger.trace("Vary header count is now {}", varyHeaderCount); //$NON-NLS-1$
if (varyHeaderCount == 1) {
// add the Accept-Encoding value to the Vary header
if (!"*".equals(value) && !containsAcceptEncoding(value)) { //$NON-NLS-1$
logger
.trace("Vary header did not contain Accept-Encoding so appending to Vary header value"); //$NON-NLS-1$
super.addHeader(HttpHeaders.VARY, value + ", " //$NON-NLS-1$
+ HttpHeaders.ACCEPT_ENCODING);
return;
}
} else if (HttpHeaders.ACCEPT_ENCODING.equals(value)) {
logger
.trace("Skipping Vary header that was only Accept-Encoding since it was already appended to a previous Vary header value"); //$NON-NLS-1$
// skip this addition since it has already been appended to
// the first Vary value by the "if true" block above
return;
}
}
super.addHeader(name, value);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
logger.trace("getOutputStream() entry"); //$NON-NLS-1$
if (outputStream == null) {
logger.trace("output stream was null"); //$NON-NLS-1$
this.outputStream = super.getOutputStream();
List<String> acceptableEncodings = acceptEncoding.getAcceptableEncodings();
logger.trace("acceptableEncodings is {}", acceptableEncodings); //$NON-NLS-1$
for (String encoding : acceptableEncodings) {
logger.trace("encoding under test is {}", encoding); //$NON-NLS-1$
if ("gzip".equalsIgnoreCase(encoding)) { //$NON-NLS-1$
logger.trace("going to use gzip encoding"); //$NON-NLS-1$
this.encodedOutputStream = new GzipEncoderOutputStream(outputStream, this);
this.outputStream = encodedOutputStream;
logger.trace("getOutputStream() exit - returning gzipped encode stream"); //$NON-NLS-1$
return outputStream;
} else if ("deflate".equalsIgnoreCase(encoding)) { //$NON-NLS-1$
logger.trace("going to use deflate encoding"); //$NON-NLS-1$
this.encodedOutputStream =
new DeflaterContentEncodedOutputStream(outputStream, this);
this.outputStream = encodedOutputStream;
logger.trace("getOutputStream() exit - returning deflate encode stream"); //$NON-NLS-1$
return outputStream;
}
}
if (acceptEncoding.isAnyEncodingAllowed() && !acceptEncoding.getBannedEncodings()
.contains("gzip")) { //$NON-NLS-1$
logger.trace("going to use gzip encoding because any encoding is allowed"); //$NON-NLS-1$
this.encodedOutputStream = new GzipEncoderOutputStream(outputStream, this);
this.outputStream = encodedOutputStream;
logger.trace("getOutputStream() exit - returning gzipped encode stream"); //$NON-NLS-1$
return outputStream;
}
}
logger.trace("getOutputStream() exit - returning output stream"); //$NON-NLS-1$
return outputStream;
}
}
}