/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.kernel.rest;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.regex.Pattern;
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.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Adds padding to json responses when the 'jsonp' parameter is specified.
*/
public class JsonpFilter implements Filter {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(JsonpFilter.class);
/** The content type HTTP header name */
public static final String CONTENT_TYPE_HEADER = "Content-Type";
/** The querystring parameter that indicates the response should be padded */
public static final String CALLBACK_PARAM = "jsonp";
/** The regular expression to ensure that the callback is safe for display to a browser */
public static final Pattern SAFE_PATTERN = Pattern.compile("[a-zA-Z0-9\\.\\_]+");
/** The content type for jsonp is "application/x-javascript", not "application/json". */
public static final String JS_CONTENT_TYPE = "application/x-javascript";
/** The character encoding. */
public static final String CHARACTER_ENCODING = "UTF-8";
/** The default padding to use if the specified padding contains invalid characters */
public static final String DEFAULT_CALLBACK = "handleMatterhornData";
/** The '(' constant. */
public static final String OPEN_PARENS = "(";
/** The post padding, which is always ');' no matter what the pre-padding looks like */
public static final String POST_PADDING = ");";
/**
* {@inheritDoc}
*
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig config) throws ServletException {
}
/**
* {@inheritDoc}
*
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy() {
}
/**
* {@inheritDoc}
*
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
* javax.servlet.FilterChain)
*/
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
ServletException {
// Cast the request and response to HTTP versions
HttpServletRequest request = (HttpServletRequest) req;
// Determine whether the response must be wrapped
String callbackValue = request.getParameter(CALLBACK_PARAM);
if (callbackValue == null || callbackValue.isEmpty()) {
logger.debug("No json padding requested from {}", request);
chain.doFilter(request, resp);
} else {
logger.debug("Json padding '{}' requested from {}", callbackValue, request);
// Ensure the callback value contains only safe characters
if (!SAFE_PATTERN.matcher(callbackValue).matches()) {
callbackValue = DEFAULT_CALLBACK;
}
// Write the padded response
HttpServletResponse originalResponse = (HttpServletResponse) resp;
HttpServletResponseContentWrapper wrapper = new HttpServletResponseContentWrapper(originalResponse, callbackValue);
chain.doFilter(request, wrapper);
wrapper.flushWrapper();
}
}
/**
* A response wrapper that allows for json padding.
*/
static class HttpServletResponseContentWrapper extends HttpServletResponseWrapper {
protected ByteArrayServletOutputStream buffer;
protected PrintWriter bufferWriter;
protected boolean committed = false;
protected boolean enableWrapping = false;
protected String preWrapper;
/**
* Construct a response wrapper.
*
* @param response
* the response
* @param callbackValue
* the jsonp callback value
*/
HttpServletResponseContentWrapper(HttpServletResponse response, String callbackValue) {
super(response);
this.preWrapper = callbackValue + OPEN_PARENS;
this.buffer = new ByteArrayServletOutputStream();
}
/**
* Flush the buffer for this response wrapper.
*
* @throws IOException
*/
public void flushWrapper() throws IOException {
if (enableWrapping) {
if (bufferWriter != null)
bufferWriter.close();
if (buffer != null)
buffer.close();
getResponse().setContentType(JS_CONTENT_TYPE);
getResponse().setContentLength(
preWrapper.getBytes(CHARACTER_ENCODING).length + buffer.size() + POST_PADDING.getBytes().length);
getResponse().setCharacterEncoding(CHARACTER_ENCODING);
getResponse().getOutputStream().write(preWrapper.getBytes(CHARACTER_ENCODING));
getResponse().getOutputStream().write(buffer.toByteArray());
getResponse().getOutputStream().write(POST_PADDING.getBytes());
getResponse().flushBuffer();
committed = true;
}
}
/**
* If we set a {@link javax.ws.rs.core.MediaType#APPLICATION_JSON} {@link JsonpFilter#CONTENT_TYPE_HEADER} header,
* enable padding.
*
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#setHeader(java.lang.String, java.lang.String)
*/
@Override
public void setHeader(String name, String value) {
if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name) && APPLICATION_JSON.equals(value)) {
enableWrapping = true;
}
super.setHeader(name, value);
}
/**
* If we add a {@link javax.ws.rs.core.MediaType#APPLICATION_JSON} {@link JsonpFilter#CONTENT_TYPE_HEADER} header,
* enable padding.
*
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#addHeader(java.lang.String, java.lang.String)
*/
@Override
public void addHeader(String name, String value) {
if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name) && APPLICATION_JSON.equals(value)) {
enableWrapping = true;
}
super.addHeader(name, value);
}
/**
* Returns the content type. If we are wrapping json with padding, return {@link JsonpFilter#JS_CONTENT_TYPE}.
*
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#getContentType()
*/
@Override
public String getContentType() {
return enableWrapping ? JS_CONTENT_TYPE : getResponse().getContentType();
}
/**
* If the content type is set to JSON, we enable wrapping. Otherwise, we leave it disabled.
*
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#setContentType(java.lang.String)
*/
@Override
public void setContentType(String type) {
enableWrapping = APPLICATION_JSON.equals(type);
super.setContentType(type);
}
/**
* If we are wrapping json with padding, , return the wrapped buffer. Otherwise, return the original outputstream.
*
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#getOutputStream()
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return enableWrapping ? buffer : getResponse().getOutputStream();
}
/**
* If we are wrapping json with padding, , return the wrapped writer. Otherwise, return the original writer.
*
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#getWriter()
*/
@Override
public PrintWriter getWriter() throws IOException {
if (enableWrapping) {
if (bufferWriter == null) {
bufferWriter = new PrintWriter(new OutputStreamWriter(buffer, this.getCharacterEncoding()));
}
return bufferWriter;
} else {
return getResponse().getWriter();
}
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#setBufferSize(int)
*/
@Override
public void setBufferSize(int size) {
if (enableWrapping) {
buffer.enlarge(size);
} else {
getResponse().setBufferSize(size);
}
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#getBufferSize()
*/
@Override
public int getBufferSize() {
return enableWrapping ? buffer.size() : getResponse().getBufferSize();
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#flushBuffer()
*/
@Override
public void flushBuffer() throws IOException {
if (!enableWrapping)
getResponse().flushBuffer();
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#isCommitted()
*/
@Override
public boolean isCommitted() {
return enableWrapping ? committed : getResponse().isCommitted();
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#reset()
*/
@Override
public void reset() {
getResponse().reset();
buffer.reset();
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#resetBuffer()
*/
@Override
public void resetBuffer() {
getResponse().resetBuffer();
buffer.reset();
}
}
/**
* A buffered output stream for jsonp padding.
*/
static class ByteArrayServletOutputStream extends ServletOutputStream {
/** The buffer */
protected byte[] buf;
/** The current write count */
protected int count;
/**
* Creates a new buffered stream with the default size (32).
*/
ByteArrayServletOutputStream() {
this(32);
}
/**
* Creates a new buffered stream with the specified size.
*
* @param size
* the buffer size
*/
ByteArrayServletOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: " + size);
}
buf = new byte[size];
}
/**
* Returns a copy of the buffer as an array.
*
* @return the buffer as a byte array
*/
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
/**
* Resets the stream.
*/
public synchronized void reset() {
count = 0;
}
/**
* Gets the size of the stream
*
* @return the stream size
*/
public synchronized int size() {
return count;
}
/**
* Expands the size of the stream.
*
* @param size
* the new size of the buffer
*/
public void enlarge(int size) {
if (size > buf.length) {
buf = Arrays.copyOf(buf, Math.max(buf.length << 1, size));
}
}
@Override
public synchronized void write(int b) throws IOException {
int newcount = count + 1;
enlarge(newcount);
buf[count] = (byte) b;
count = newcount;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
}
}
}