Fixed JavaDoc errors to enable Java 8 build.

This commit is contained in:
Harald Kuhr
2019-08-10 00:41:36 +02:00
parent 7d2c692663
commit 9e23413456
168 changed files with 34586 additions and 34396 deletions

View File

@@ -1,384 +1,394 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.*;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
/**
* Defines a generic, protocol-independent filter.
* <P/>
* {@code GenericFilter} is inspired by {@link GenericServlet}, and
* implements the {@code Filter} and {@code FilterConfig} interfaces.
* <P/>
* {@code GenericFilter} makes writing filters easier. It provides simple
* versions of the lifecycle methods {@code init} and {@code destroy}
* and of the methods in the {@code FilterConfig} interface.
* {@code GenericFilter} also implements the {@code log} methods,
* declared in the {@code ServletContext} interface.
* <p/
* {@code GenericFilter} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
* <p/>
* To write a generic filter, you need only override the abstract
* {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: GenericFilter.java#1 $
*
* @see Filter
* @see FilterConfig
*/
public abstract class GenericFilter implements Filter, FilterConfig, Serializable {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* The filter config.
*/
private transient FilterConfig filterConfig = null;
/**
* Makes sure the filter runs once per request
* <p/>
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_VALUE
* @see #oncePerRequest
*/
private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED";
/**
* Makes sure the filter runs once per request.
* Must be configured through init method, as the filter name is not
* available before we have a {@code FilterConfig} object.
* <p/>
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_VALUE
* @see #oncePerRequest
*/
private String attribRunOnce = null;
/**
* Makes sure the filter runs once per request
* <p/>
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_EXT
* @see #oncePerRequest
*/
private static final Object ATTRIB_RUN_ONCE_VALUE = new Object();
/**
* Indicates if this filter should run once per request ({@code true}),
* or for each forward/include resource ({@code false}).
* <p/>
* Set this variable to true, to make sure the filter runs once per request.
*
* <em>NOTE: As of Servlet 2.4, this field
* should always be left to it's default value ({@code false}).
* <br/>
* To run the filter once per request, the {@code filter-mapping} element
* of the web-descriptor should include a {@code dispatcher} element:
* <pre>&lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;</pre>
* </em>
*/
protected boolean oncePerRequest = false;
/**
* Does nothing.
*/
public GenericFilter() {}
/**
* Called by the web container to indicate to a filter that it is being
* placed into service.
* <p/>
* This implementation stores the {@code FilterConfig} object it
* receives from the servlet container for later use.
* Generally, there's no reason to override this method, override the
* no-argument {@code init} instead. However, <em>if</em> you are
* overriding this form of the method,
* always call {@code super.init(config)}.
* <p/>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
*
* @param pConfig the filter config
* @throws ServletException if an error occurs during init
*
* @see Filter#init(javax.servlet.FilterConfig)
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
public void init(final FilterConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("filter config == null");
}
// Store filter config
filterConfig = pConfig;
// Configure this
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getFilterName(), e.getCause());
}
// Create run-once attribute name
attribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT;
log("init (oncePerRequest=" + oncePerRequest + ", attribRunOnce=" + attribRunOnce + ")");
init();
}
/**
* A convenience method which can be overridden so that there's no need to
* call {@code super.init(config)}.
*
* @see #init(FilterConfig)
*
* @throws ServletException if an error occurs during init
*/
public void init() throws ServletException {}
/**
* The {@code doFilter} method of the Filter is called by the container
* each time a request/response pair is passed through the chain due to a
* client request for a resource at the end of the chain.
* <p/>
* Subclasses <em>should not override this method</em>, but rather the
* abstract {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method.
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pFilterChain the filter chain
*
* @throws IOException
* @throws ServletException
*
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
* @see #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilterImpl
*/
public final void doFilter(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pFilterChain) throws IOException, ServletException {
// If request filter and already run, continue chain and return fast
if (oncePerRequest && isRunOnce(pRequest)) {
pFilterChain.doFilter(pRequest, pResponse);
return;
}
// Do real filter
doFilterImpl(pRequest, pResponse, pFilterChain);
}
/**
* If request is filtered, returns true, otherwise marks request as filtered
* and returns false.
* A return value of false, indicates that the filter has not yet run.
* A return value of true, indicates that the filter has run for this
* request, and processing should not continue.
* <P/>
* Note that the method will mark the request as filtered on first
* invocation.
* <p/>
* @see #ATTRIB_RUN_ONCE_EXT
* @see #ATTRIB_RUN_ONCE_VALUE
*
* @param pRequest the servlet request
* @return {@code true} if the request is already filtered, otherwise
* {@code false}.
*/
private boolean isRunOnce(final ServletRequest pRequest) {
// If request already filtered, return true (skip)
if (pRequest.getAttribute(attribRunOnce) == ATTRIB_RUN_ONCE_VALUE) {
return true;
}
// Set attribute and return false (continue)
pRequest.setAttribute(attribRunOnce, ATTRIB_RUN_ONCE_VALUE);
return false;
}
/**
* Invoked once, or each time a request/response pair is passed through the
* chain, depending on the {@link #oncePerRequest} member variable.
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pChain the filter chain
*
* @throws IOException if an I/O error occurs
* @throws ServletException if an exception occurs during the filter process
*
* @see #oncePerRequest
* @see #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
*/
protected abstract void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain)
throws IOException, ServletException;
/**
* Called by the web container to indicate to a filter that it is being
* taken out of service.
*
* @see Filter#destroy
*/
public void destroy() {
log("destroy");
filterConfig = null;
}
/**
* Returns the filter-name of this filter as defined in the deployment
* descriptor.
*
* @return the filter-name
* @see FilterConfig#getFilterName
*/
public String getFilterName() {
return filterConfig.getFilterName();
}
/**
* Returns a reference to the {@link ServletContext} in which the caller is
* executing.
*
* @return the {@code ServletContext} object, used by the caller to
* interact with its servlet container
* @see FilterConfig#getServletContext
* @see ServletContext
*/
public ServletContext getServletContext() {
return filterConfig.getServletContext();
}
/**
* Returns a {@code String} containing the value of the named
* initialization parameter, or null if the parameter does not exist.
*
* @param pKey a {@code String} specifying the name of the
* initialization parameter
* @return a {@code String} containing the value of the initialization
* parameter
*/
public String getInitParameter(final String pKey) {
return filterConfig.getInitParameter(pKey);
}
/**
* Returns the names of the servlet's initialization parameters as an
* {@code Enumeration} of {@code String} objects, or an empty
* {@code Enumeration} if the servlet has no initialization parameters.
*
* @return an {@code Enumeration} of {@code String} objects
* containing the mNames of the servlet's initialization parameters
*/
public Enumeration getInitParameterNames() {
return filterConfig.getInitParameterNames();
}
/**
* Writes the specified message to a servlet log file, prepended by the
* filter's name.
*
* @param pMessage the log message
* @see ServletContext#log(String)
*/
protected void log(final String pMessage) {
getServletContext().log(getFilterName() + ": " + pMessage);
}
/**
* Writes an explanatory message and a stack trace for a given
* {@code Throwable} to the servlet log file, prepended by the
* filter's name.
*
* @param pMessage the log message
* @param pThrowable the exception
* @see ServletContext#log(String,Throwable)
*/
protected void log(final String pMessage, final Throwable pThrowable) {
getServletContext().log(getFilterName() + ": " + pMessage, pThrowable);
}
/**
* Initializes the filter.
*
* @param pFilterConfig the filter config
* @see #init init
*
* @deprecated For compatibility only, use {@link #init init} instead.
*/
@SuppressWarnings("UnusedDeclaration")
public void setFilterConfig(final FilterConfig pFilterConfig) {
try {
init(pFilterConfig);
}
catch (ServletException e) {
log("Error in init(), see stack trace for details.", e);
}
}
/**
* Gets the {@code FilterConfig} for this filter.
*
* @return the {@code FilterConfig} for this filter
* @see FilterConfig
*/
public FilterConfig getFilterConfig() {
return filterConfig;
}
/**
* Specifies if this filter should run once per request ({@code true}),
* or for each forward/include resource ({@code false}).
* Called automatically from the {@code init}-method, with settings
* from web.xml.
*
* @param pOncePerRequest {@code true} if the filter should run only
* once per request
* @see #oncePerRequest
*/
@InitParam(name = "once-per-request")
public void setOncePerRequest(final boolean pOncePerRequest) {
oncePerRequest = pOncePerRequest;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.*;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
/**
* Defines a generic, protocol-independent filter.
* <p>
* {@code GenericFilter} is inspired by {@link GenericServlet}, and
* implements the {@code Filter} and {@code FilterConfig} interfaces.
* </p>
* <p>
* {@code GenericFilter} makes writing filters easier. It provides simple
* versions of the lifecycle methods {@code init} and {@code destroy}
* and of the methods in the {@code FilterConfig} interface.
* {@code GenericFilter} also implements the {@code log} methods,
* declared in the {@code ServletContext} interface.
* </p>
* <p>
* {@code GenericFilter} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
* </p>
* <p>
* To write a generic filter, you need only override the abstract
* {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: GenericFilter.java#1 $
*
* @see Filter
* @see FilterConfig
*/
public abstract class GenericFilter implements Filter, FilterConfig, Serializable {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* The filter config.
*/
private transient FilterConfig filterConfig = null;
/**
* Makes sure the filter runs once per request
*
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_VALUE
* @see #oncePerRequest
*/
private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED";
/**
* Makes sure the filter runs once per request.
* Must be configured through init method, as the filter name is not
* available before we have a {@code FilterConfig} object.
*
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_VALUE
* @see #oncePerRequest
*/
private String attribRunOnce = null;
/**
* Makes sure the filter runs once per request
*
* @see #isRunOnce
* @see #ATTRIB_RUN_ONCE_EXT
* @see #oncePerRequest
*/
private static final Object ATTRIB_RUN_ONCE_VALUE = new Object();
/**
* Indicates if this filter should run once per request ({@code true}),
* or for each forward/include resource ({@code false}).
* <p>
* Set this variable to true, to make sure the filter runs once per request.
* </p>
* <p>
* <em>NOTE: As of Servlet 2.4, this field
* should always be left to it's default value ({@code false}).
* <br>
* To run the filter once per request, the {@code filter-mapping} element
* of the web-descriptor should include a {@code dispatcher} element:
* </em>
* </p>
* <pre>&lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;</pre>
*/
protected boolean oncePerRequest = false;
/**
* Does nothing.
*/
public GenericFilter() {}
/**
* Called by the web container to indicate to a filter that it is being
* placed into service.
* <p>
* This implementation stores the {@code FilterConfig} object it
* receives from the servlet container for later use.
* Generally, there's no reason to override this method, override the
* no-argument {@code init} instead. However, <em>if</em> you are
* overriding this form of the method,
* always call {@code super.init(config)}.
* </p>
* <p>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
* </p>
*
* @param pConfig the filter config
* @throws ServletException if an error occurs during init
*
* @see Filter#init(javax.servlet.FilterConfig)
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
public void init(final FilterConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("filter config == null");
}
// Store filter config
filterConfig = pConfig;
// Configure this
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getFilterName(), e.getCause());
}
// Create run-once attribute name
attribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT;
log("init (oncePerRequest=" + oncePerRequest + ", attribRunOnce=" + attribRunOnce + ")");
init();
}
/**
* A convenience method which can be overridden so that there's no need to
* call {@code super.init(config)}.
*
* @see #init(FilterConfig)
*
* @throws ServletException if an error occurs during init
*/
public void init() throws ServletException {}
/**
* The {@code doFilter} method of the Filter is called by the container
* each time a request/response pair is passed through the chain due to a
* client request for a resource at the end of the chain.
* <p>
* Subclasses <em>should not override this method</em>, but rather the
* abstract {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method.
* </p>
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pFilterChain the filter chain
*
* @throws IOException
* @throws ServletException
*
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
* @see #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilterImpl
*/
public final void doFilter(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pFilterChain) throws IOException, ServletException {
// If request filter and already run, continue chain and return fast
if (oncePerRequest && isRunOnce(pRequest)) {
pFilterChain.doFilter(pRequest, pResponse);
return;
}
// Do real filter
doFilterImpl(pRequest, pResponse, pFilterChain);
}
/**
* If request is filtered, returns true, otherwise marks request as filtered
* and returns false.
* A return value of false, indicates that the filter has not yet run.
* A return value of true, indicates that the filter has run for this
* request, and processing should not continue.
* <p>
* Note that the method will mark the request as filtered on first
* invocation.
* </p>
*
* @see #ATTRIB_RUN_ONCE_EXT
* @see #ATTRIB_RUN_ONCE_VALUE
*
* @param pRequest the servlet request
* @return {@code true} if the request is already filtered, otherwise
* {@code false}.
*/
private boolean isRunOnce(final ServletRequest pRequest) {
// If request already filtered, return true (skip)
if (pRequest.getAttribute(attribRunOnce) == ATTRIB_RUN_ONCE_VALUE) {
return true;
}
// Set attribute and return false (continue)
pRequest.setAttribute(attribRunOnce, ATTRIB_RUN_ONCE_VALUE);
return false;
}
/**
* Invoked once, or each time a request/response pair is passed through the
* chain, depending on the {@link #oncePerRequest} member variable.
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pChain the filter chain
*
* @throws IOException if an I/O error occurs
* @throws ServletException if an exception occurs during the filter process
*
* @see #oncePerRequest
* @see #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
*/
protected abstract void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain)
throws IOException, ServletException;
/**
* Called by the web container to indicate to a filter that it is being
* taken out of service.
*
* @see Filter#destroy
*/
public void destroy() {
log("destroy");
filterConfig = null;
}
/**
* Returns the filter-name of this filter as defined in the deployment
* descriptor.
*
* @return the filter-name
* @see FilterConfig#getFilterName
*/
public String getFilterName() {
return filterConfig.getFilterName();
}
/**
* Returns a reference to the {@link ServletContext} in which the caller is
* executing.
*
* @return the {@code ServletContext} object, used by the caller to
* interact with its servlet container
* @see FilterConfig#getServletContext
* @see ServletContext
*/
public ServletContext getServletContext() {
return filterConfig.getServletContext();
}
/**
* Returns a {@code String} containing the value of the named
* initialization parameter, or null if the parameter does not exist.
*
* @param pKey a {@code String} specifying the name of the
* initialization parameter
* @return a {@code String} containing the value of the initialization
* parameter
*/
public String getInitParameter(final String pKey) {
return filterConfig.getInitParameter(pKey);
}
/**
* Returns the names of the servlet's initialization parameters as an
* {@code Enumeration} of {@code String} objects, or an empty
* {@code Enumeration} if the servlet has no initialization parameters.
*
* @return an {@code Enumeration} of {@code String} objects
* containing the mNames of the servlet's initialization parameters
*/
public Enumeration getInitParameterNames() {
return filterConfig.getInitParameterNames();
}
/**
* Writes the specified message to a servlet log file, prepended by the
* filter's name.
*
* @param pMessage the log message
* @see ServletContext#log(String)
*/
protected void log(final String pMessage) {
getServletContext().log(getFilterName() + ": " + pMessage);
}
/**
* Writes an explanatory message and a stack trace for a given
* {@code Throwable} to the servlet log file, prepended by the
* filter's name.
*
* @param pMessage the log message
* @param pThrowable the exception
* @see ServletContext#log(String,Throwable)
*/
protected void log(final String pMessage, final Throwable pThrowable) {
getServletContext().log(getFilterName() + ": " + pMessage, pThrowable);
}
/**
* Initializes the filter.
*
* @param pFilterConfig the filter config
* @see #init init
*
* @deprecated For compatibility only, use {@link #init init} instead.
*/
@SuppressWarnings("UnusedDeclaration")
public void setFilterConfig(final FilterConfig pFilterConfig) {
try {
init(pFilterConfig);
}
catch (ServletException e) {
log("Error in init(), see stack trace for details.", e);
}
}
/**
* Gets the {@code FilterConfig} for this filter.
*
* @return the {@code FilterConfig} for this filter
* @see FilterConfig
*/
public FilterConfig getFilterConfig() {
return filterConfig;
}
/**
* Specifies if this filter should run once per request ({@code true}),
* or for each forward/include resource ({@code false}).
* Called automatically from the {@code init}-method, with settings
* from web.xml.
*
* @param pOncePerRequest {@code true} if the filter should run only
* once per request
* @see #oncePerRequest
*/
@InitParam(name = "once-per-request")
public void setOncePerRequest(final boolean pOncePerRequest) {
oncePerRequest = pOncePerRequest;
}
}

View File

@@ -1,90 +1,93 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.lang.reflect.InvocationTargetException;
/**
* Defines a generic, protocol-independent servlet.
* <p/>
* {@code GenericServlet} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: GenericServlet.java#1 $
*/
public abstract class GenericServlet extends javax.servlet.GenericServlet {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* Called by the web container to indicate to a servlet that it is being
* placed into service.
* <p/>
* This implementation stores the {@code ServletConfig} object it
* receives from the servlet container for later use. When overriding this
* form of the method, call {@code super.init(config)}.
* <p/>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
*
* @param pConfig the servlet config
* @throws ServletException
*
* @see javax.servlet.GenericServlet#init
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
@Override
public void init(final ServletConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("servlet config == null");
}
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getServletName(), e.getCause());
}
super.init(pConfig);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.lang.reflect.InvocationTargetException;
/**
* Defines a generic, protocol-independent servlet.
* <p>
* {@code GenericServlet} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: GenericServlet.java#1 $
*/
public abstract class GenericServlet extends javax.servlet.GenericServlet {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* Called by the web container to indicate to a servlet that it is being
* placed into service.
* <p>
* This implementation stores the {@code ServletConfig} object it
* receives from the servlet container for later use. When overriding this
* form of the method, call {@code super.init(config)}.
* </p>
* <p>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
* </p>
*
* @param pConfig the servlet config
* @throws ServletException if the servlet could not be initialized.
*
* @see javax.servlet.GenericServlet#init
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
@Override
public void init(final ServletConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("servlet config == null");
}
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getServletName(), e.getCause());
}
super.init(pConfig);
}
}

View File

@@ -1,90 +1,93 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.lang.reflect.InvocationTargetException;
/**
* Defines a generic, HTTP specific servlet.
* <p/>
* {@code HttpServlet} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: HttpServlet.java#1 $
*/
public abstract class HttpServlet extends javax.servlet.http.HttpServlet {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* Called by the web container to indicate to a servlet that it is being
* placed into service.
* <p/>
* This implementation stores the {@code ServletConfig} object it
* receives from the servlet container for later use. When overriding this
* form of the method, call {@code super.init(config)}.
* <p/>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
*
* @param pConfig the servlet config
* @throws ServletException if an error occurred during init
*
* @see javax.servlet.GenericServlet#init
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
@Override
public void init(ServletConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("servlet config == null");
}
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getServletName(), e.getCause());
}
super.init(pConfig);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.BeanUtil;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.lang.reflect.InvocationTargetException;
/**
* Defines a generic, HTTP specific servlet.
* <p>
* {@code HttpServlet} has an auto-init system, that automatically invokes
* the method matching the signature {@code void setX(&lt;Type&gt;)},
* for every init-parameter {@code x}. Both camelCase and lisp-style parameter
* naming is supported, lisp-style names will be converted to camelCase.
* Parameter values are automatically converted from string representation to
* most basic types, if necessary.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: HttpServlet.java#1 $
*/
public abstract class HttpServlet extends javax.servlet.http.HttpServlet {
// TODO: Rewrite to use ServletConfigurator instead of BeanUtil
/**
* Called by the web container to indicate to a servlet that it is being
* placed into service.
* <p>
* This implementation stores the {@code ServletConfig} object it
* receives from the servlet container for later use. When overriding this
* form of the method, call {@code super.init(config)}.
* </p>
* <p>
* This implementation will also set all configured key/value pairs, that
* have a matching setter method annotated with {@link InitParam}.
* </p>
*
* @param pConfig the servlet config
* @throws ServletException if an error occurred during init
*
* @see javax.servlet.GenericServlet#init
* @see #init() init
* @see BeanUtil#configure(Object, java.util.Map, boolean)
*/
@Override
public void init(ServletConfig pConfig) throws ServletException {
if (pConfig == null) {
throw new ServletConfigException("servlet config == null");
}
try {
BeanUtil.configure(this, ServletUtil.asMap(pConfig), true);
}
catch (InvocationTargetException e) {
throw new ServletConfigException("Could not configure " + getServletName(), e.getCause());
}
super.init(pConfig);
}
}

View File

@@ -1,122 +1,125 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.Validate;
import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A {@code ServletOutputStream} implementation backed by a
* {@link java.io.OutputStream}. For filters that need to buffer the
* response and do post filtering, it may be used like this:<pre>
* ByteArrayOutputStream buffer = new ByteArraOutputStream();
* ServletOutputStream adapter = new OutputStreamAdapter(buffer);
* </pre>
* <p/>
* As a {@code ServletOutputStream} is itself an {@code OutputStream}, this
* class may also be used as a superclass for wrappers of other
* {@code ServletOutputStream}s, like this:<pre>
* class FilterServletOutputStream extends OutputStreamAdapter {
* public FilterServletOutputStream(ServletOutputStream out) {
* super(out);
* }
*
* public void write(int abyte) {
* // do filtering...
* super.write(...);
* }
* }
*
* ...
*
* ServletOutputStream original = response.getOutputStream();
* ServletOutputStream wrapper = new FilterServletOutputStream(original);
* </pre>
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author $Author: haku $
* @version $Id: OutputStreamAdapter.java#1 $
*
*/
public class OutputStreamAdapter extends ServletOutputStream {
/** The wrapped {@code OutputStream}. */
protected final OutputStream out;
/**
* Creates an {@code OutputStreamAdapter}.
*
* @param pOut the wrapped {@code OutputStream}
*
* @throws IllegalArgumentException if {@code pOut} is {@code null}.
*/
public OutputStreamAdapter(final OutputStream pOut) {
Validate.notNull(pOut, "out");
out = pOut;
}
/**
* Returns the wrapped {@code OutputStream}.
*
* @return the wrapped {@code OutputStream}.
*/
public OutputStream getOutputStream() {
return out;
}
@Override
public String toString() {
return "ServletOutputStream adapted from " + out.toString();
}
/**
* Writes a byte to the underlying stream.
*
* @param pByte the byte to write.
*
* @throws IOException if an error occurs during writing
*/
public void write(final int pByte) throws IOException {
out.write(pByte);
}
// Overide for efficiency
public void write(final byte pBytes[]) throws IOException {
out.write(pBytes);
}
// Overide for efficiency
public void write(final byte pBytes[], final int pOff, final int pLen) throws IOException {
out.write(pBytes, pOff, pLen);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.Validate;
import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A {@code ServletOutputStream} implementation backed by a
* {@link java.io.OutputStream}. For filters that need to buffer the
* response and do post filtering, it may be used like this:<pre>
* ByteArrayOutputStream buffer = new ByteArraOutputStream();
* ServletOutputStream adapter = new OutputStreamAdapter(buffer);
* </pre>
* <p>
* As a {@code ServletOutputStream} is itself an {@code OutputStream}, this
* class may also be used as a superclass for wrappers of other
* {@code ServletOutputStream}s, like this:
* </p>
* <pre>
* class FilterServletOutputStream extends OutputStreamAdapter {
* public FilterServletOutputStream(ServletOutputStream out) {
* super(out);
* }
*
* public void write(int abyte) {
* // do filtering...
* super.write(...);
* }
* }
*
* ...
*
* ServletOutputStream original = response.getOutputStream();
* ServletOutputStream wrapper = new FilterServletOutputStream(original);
* </pre>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author $Author: haku $
* @version $Id: OutputStreamAdapter.java#1 $
*
*/
public class OutputStreamAdapter extends ServletOutputStream {
/** The wrapped {@code OutputStream}. */
protected final OutputStream out;
/**
* Creates an {@code OutputStreamAdapter}.
*
* @param pOut the wrapped {@code OutputStream}
*
* @throws IllegalArgumentException if {@code pOut} is {@code null}.
*/
public OutputStreamAdapter(final OutputStream pOut) {
Validate.notNull(pOut, "out");
out = pOut;
}
/**
* Returns the wrapped {@code OutputStream}.
*
* @return the wrapped {@code OutputStream}.
*/
public OutputStream getOutputStream() {
return out;
}
@Override
public String toString() {
return "ServletOutputStream adapted from " + out.toString();
}
/**
* Writes a byte to the underlying stream.
*
* @param pByte the byte to write.
*
* @throws IOException if an error occurs during writing
*/
public void write(final int pByte) throws IOException {
out.write(pByte);
}
// Overide for efficiency
public void write(final byte pBytes[]) throws IOException {
out.write(pBytes);
}
// Overide for efficiency
public void write(final byte pBytes[], final int pOff, final int pLen) throws IOException {
out.write(pBytes, pOff, pLen);
}
}

View File

@@ -1,437 +1,440 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;
/**
* A simple proxy servlet implementation. Supports HTTP and HTTPS.
* <p/>
* Note: The servlet is not a true HTTP proxy as described in
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">RFC 2616</a>,
* instead it passes on all incoming HTTP requests to the configured remote
* server.
* Useful for bypassing firewalls or to avoid exposing internal network
* infrastructure to external clients.
* <p/>
* At the moment, no caching of content is implemented.
* <p/>
* If the {@code remoteServer} init parameter is not set, the servlet will
* respond by sending a {@code 500 Internal Server Error} response to the client.
* If the configured remote server is down, or unreachable, the servlet will
* respond by sending a {@code 502 Bad Gateway} response to the client.
* Otherwise, the response from the remote server will be tunneled unmodified
* to the client.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: ProxyServlet.java#1 $
*/
public class ProxyServlet extends GenericServlet {
/** Remote server host name or IP address */
protected String remoteServer = null;
/** Remote server port */
protected int remotePort = 80;
/** Remote server "mount" path */
protected String remotePath = "";
private static final String HTTP_REQUEST_HEADER_HOST = "host";
private static final String HTTP_RESPONSE_HEADER_SERVER = "server";
private static final String MESSAGE_REMOTE_SERVER_NOT_CONFIGURED = "Remote server not configured.";
/**
* Called by {@code init} to set the remote server. Must be a valid host
* name or IP address. No default.
*
* @param pRemoteServer
*/
public void setRemoteServer(String pRemoteServer) {
remoteServer = pRemoteServer;
}
/**
* Called by {@code init} to set the remote port. Must be a number.
* Default is {@code 80}.
*
* @param pRemotePort
*/
public void setRemotePort(String pRemotePort) {
try {
remotePort = Integer.parseInt(pRemotePort);
}
catch (NumberFormatException e) {
log("RemotePort must be a number!", e);
}
}
/**
* Called by {@code init} to set the remote path. May be an empty string
* for the root path, or any other valid path on the remote server.
* Default is {@code ""}.
*
* @param pRemotePath
*/
public void setRemotePath(String pRemotePath) {
if (StringUtil.isEmpty(pRemotePath)) {
pRemotePath = "";
}
else if (pRemotePath.charAt(0) != '/') {
pRemotePath = "/" + pRemotePath;
}
remotePath = pRemotePath;
}
/**
* Override {@code service} to use HTTP specifics.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see #service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException {
service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse);
}
/**
* Services a single request.
* Supports HTTP and HTTPS.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see ProxyServlet Class descrition
*/
protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException {
// Sanity check configuration
if (remoteServer == null) {
log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
return;
}
HttpURLConnection remoteConnection = null;
try {
// Recreate request URI for remote request
String requestURI = createRemoteRequestURI(pRequest);
URL remoteURL = new URL(pRequest.getScheme(), remoteServer, remotePort, requestURI);
// Get connection, with method from original request
// NOTE: The actual connection is not done before we ask for streams...
// NOTE: The HttpURLConnection is supposed to handle multiple
// requests to the same server internally
String method = pRequest.getMethod();
remoteConnection = (HttpURLConnection) remoteURL.openConnection();
remoteConnection.setRequestMethod(method);
// Copy header fields
copyHeadersFromClient(pRequest, remoteConnection);
// Do proxy specifc stuff?
// TODO: Read up the specs from RFC 2616 (HTTP) on proxy behaviour
// TODO: RFC 2616 says "[a] proxy server MUST NOT establish an HTTP/1.1
// persistent connection with an HTTP/1.0 client"
// Copy message body from client to remote server
copyBodyFromClient(pRequest, remoteConnection);
// Set response status code from remote server to client
int responseCode = remoteConnection.getResponseCode();
pResponse.setStatus(responseCode);
//System.out.println("Response is: " + responseCode + " " + remoteConnection.getResponseMessage());
// Copy header fields back
copyHeadersToClient(remoteConnection, pResponse);
// More proxy specific stuff?
// Copy message body from remote server to client
copyBodyToClient(remoteConnection, pResponse);
}
catch (ConnectException e) {
// In case we could not connecto to the remote server
log("Could not connect to remote server.", e);
pResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage());
}
finally {
// Disconnect from server
// TODO: Should we actually do this?
if (remoteConnection != null) {
remoteConnection.disconnect();
}
}
}
/**
* Copies the message body from the remote server to the client (outgoing
* {@code HttpServletResponse}).
*
* @param pRemoteConnection
* @param pResponse
*
* @throws IOException
*/
private void copyBodyToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) throws IOException {
InputStream fromRemote = null;
OutputStream toClient = null;
try {
// Get either input or error stream
try {
fromRemote = pRemoteConnection.getInputStream();
}
catch (IOException e) {
// If exception, use errorStream instead
fromRemote = pRemoteConnection.getErrorStream();
}
// I guess the stream might be null if there is no response other
// than headers (Continue, No Content, etc).
if (fromRemote != null) {
toClient = pResponse.getOutputStream();
FileUtil.copy(fromRemote, toClient);
}
}
finally {
if (fromRemote != null) {
try {
fromRemote.close();
}
catch (IOException e) {
log("Stream from remote could not be closed.", e);
}
}
if (toClient != null) {
try {
toClient.close();
}
catch (IOException e) {
log("Stream to client could not be closed.", e);
}
}
}
}
/**
* Copies the message body from the client (incomming
* {@code HttpServletRequest}) to the remote server if the request method
* is {@code POST} or <tt>PUT<tt>.
* Otherwise this method does nothing.
*
* @param pRequest
* @param pRemoteConnection
*
* @throws java.io.IOException
*/
private void copyBodyFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) throws IOException {
// If this is a POST or PUT, copy message body from client remote server
if (!("POST".equals(pRequest.getMethod()) || "PUT".equals(pRequest.getMethod()))) {
return;
}
// NOTE: Setting doOutput to true, will make it a POST request (why?)...
pRemoteConnection.setDoOutput(true);
// Get streams and do the copying
InputStream fromClient = null;
OutputStream toRemote = null;
try {
fromClient = pRequest.getInputStream();
toRemote = pRemoteConnection.getOutputStream();
FileUtil.copy(fromClient, toRemote);
}
finally {
if (fromClient != null) {
try {
fromClient.close();
}
catch (IOException e) {
log("Stream from client could not be closed.", e);
}
}
if (toRemote != null) {
try {
toRemote.close();
}
catch (IOException e) {
log("Stream to remote could not be closed.", e);
}
}
}
}
/**
* Creates the remote request URI based on the incoming request.
* The URI will include any query strings etc.
*
* @param pRequest
*
* @return a {@code String} representing the remote request URI
*/
private String createRemoteRequestURI(HttpServletRequest pRequest) {
StringBuilder requestURI = new StringBuilder(remotePath);
requestURI.append(pRequest.getPathInfo());
if (!StringUtil.isEmpty(pRequest.getQueryString())) {
requestURI.append("?");
requestURI.append(pRequest.getQueryString());
}
return requestURI.toString();
}
/**
* Copies headers from the remote connection back to the client
* (the outgoing HttpServletResponse). All headers except the "Server"
* header are copied.
*
* @param pRemoteConnection
* @param pResponse
*/
private void copyHeadersToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) {
// NOTE: There is no getHeaderFieldCount method or similar...
// Also, the getHeaderFields() method was introduced in J2SE 1.4, and
// we want to be 1.2 compatible.
// So, just try to loop until there are no more headers.
int i = 0;
while (true) {
String key = pRemoteConnection.getHeaderFieldKey(i);
// NOTE: getHeaderField(String) returns only the last value
String value = pRemoteConnection.getHeaderField(i);
// If the key is not null, life is simple, and Sun is shining
// However, the default implementations includes the HTTP response
// code ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
// In addition, we want to skip the original "Server" header
if (key != null && !HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
else if (value == null) {
// If BOTH key and value is null, there are no more header fields
break;
}
i++;
}
/* 1.4+ version below....
Map headers = pRemoteConnection.getHeaderFields();
for (Iterator iterator = headers.entrySet().iterator(); iterator.hasNext();) {
Map.Entry header = (Map.Entry) iterator.next();
List values = (List) header.getValue();
for (Iterator valueIter = values.iterator(); valueIter.hasNext();) {
String value = (String) valueIter.next();
String key = (String) header.getKey();
// Skip the server header
if (HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
key = null;
}
// The default implementations includes the HTTP response code
// ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
if (key != null) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
}
}
*/
}
/**
* Copies headers from the client (the incoming {@code HttpServletRequest})
* to the outgoing connection.
* All headers except the "Host" header are copied.
*
* @param pRequest
* @param pRemoteConnection
*/
private void copyHeadersFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) {
Enumeration headerNames = pRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
Enumeration headerValues = pRequest.getHeaders(headerName);
// Skip the "host" header, as we want something else
if (HTTP_REQUEST_HEADER_HOST.equalsIgnoreCase(headerName)) {
// Skip this header
headerName = null;
}
// Set the the header to the remoteConnection
if (headerName != null) {
// Convert from multiple line to single line, comma separated, as
// there seems to be a shortcoming in the URLConneciton API...
StringBuilder headerValue = new StringBuilder();
while (headerValues.hasMoreElements()) {
String value = (String) headerValues.nextElement();
headerValue.append(value);
if (headerValues.hasMoreElements()) {
headerValue.append(", ");
}
}
//System.out.println("client -->>> remote: " + headerName + ": " + headerValue);
pRemoteConnection.setRequestProperty(headerName, headerValue.toString());
}
}
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;
/**
* A simple proxy servlet implementation. Supports HTTP and HTTPS.
* <p>
* Note: The servlet is not a true HTTP proxy as described in
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">RFC 2616</a>,
* instead it passes on all incoming HTTP requests to the configured remote
* server.
* Useful for bypassing firewalls or to avoid exposing internal network
* infrastructure to external clients.
* </p>
* <p>
* At the moment, no caching of content is implemented.
* </p>
* <p>
* If the {@code remoteServer} init parameter is not set, the servlet will
* respond by sending a {@code 500 Internal Server Error} response to the client.
* If the configured remote server is down, or unreachable, the servlet will
* respond by sending a {@code 502 Bad Gateway} response to the client.
* Otherwise, the response from the remote server will be tunneled unmodified
* to the client.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
*
* @version $Id: ProxyServlet.java#1 $
*/
public class ProxyServlet extends GenericServlet {
/** Remote server host name or IP address */
protected String remoteServer = null;
/** Remote server port */
protected int remotePort = 80;
/** Remote server "mount" path */
protected String remotePath = "";
private static final String HTTP_REQUEST_HEADER_HOST = "host";
private static final String HTTP_RESPONSE_HEADER_SERVER = "server";
private static final String MESSAGE_REMOTE_SERVER_NOT_CONFIGURED = "Remote server not configured.";
/**
* Called by {@code init} to set the remote server. Must be a valid host
* name or IP address. No default.
*
* @param pRemoteServer
*/
public void setRemoteServer(String pRemoteServer) {
remoteServer = pRemoteServer;
}
/**
* Called by {@code init} to set the remote port. Must be a number.
* Default is {@code 80}.
*
* @param pRemotePort
*/
public void setRemotePort(String pRemotePort) {
try {
remotePort = Integer.parseInt(pRemotePort);
}
catch (NumberFormatException e) {
log("RemotePort must be a number!", e);
}
}
/**
* Called by {@code init} to set the remote path. May be an empty string
* for the root path, or any other valid path on the remote server.
* Default is {@code ""}.
*
* @param pRemotePath
*/
public void setRemotePath(String pRemotePath) {
if (StringUtil.isEmpty(pRemotePath)) {
pRemotePath = "";
}
else if (pRemotePath.charAt(0) != '/') {
pRemotePath = "/" + pRemotePath;
}
remotePath = pRemotePath;
}
/**
* Override {@code service} to use HTTP specifics.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see #service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException {
service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse);
}
/**
* Services a single request.
* Supports HTTP and HTTPS.
*
* @param pRequest
* @param pResponse
*
* @throws ServletException
* @throws IOException
*
* @see ProxyServlet Class descrition
*/
protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException {
// Sanity check configuration
if (remoteServer == null) {
log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
MESSAGE_REMOTE_SERVER_NOT_CONFIGURED);
return;
}
HttpURLConnection remoteConnection = null;
try {
// Recreate request URI for remote request
String requestURI = createRemoteRequestURI(pRequest);
URL remoteURL = new URL(pRequest.getScheme(), remoteServer, remotePort, requestURI);
// Get connection, with method from original request
// NOTE: The actual connection is not done before we ask for streams...
// NOTE: The HttpURLConnection is supposed to handle multiple
// requests to the same server internally
String method = pRequest.getMethod();
remoteConnection = (HttpURLConnection) remoteURL.openConnection();
remoteConnection.setRequestMethod(method);
// Copy header fields
copyHeadersFromClient(pRequest, remoteConnection);
// Do proxy specifc stuff?
// TODO: Read up the specs from RFC 2616 (HTTP) on proxy behaviour
// TODO: RFC 2616 says "[a] proxy server MUST NOT establish an HTTP/1.1
// persistent connection with an HTTP/1.0 client"
// Copy message body from client to remote server
copyBodyFromClient(pRequest, remoteConnection);
// Set response status code from remote server to client
int responseCode = remoteConnection.getResponseCode();
pResponse.setStatus(responseCode);
//System.out.println("Response is: " + responseCode + " " + remoteConnection.getResponseMessage());
// Copy header fields back
copyHeadersToClient(remoteConnection, pResponse);
// More proxy specific stuff?
// Copy message body from remote server to client
copyBodyToClient(remoteConnection, pResponse);
}
catch (ConnectException e) {
// In case we could not connecto to the remote server
log("Could not connect to remote server.", e);
pResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage());
}
finally {
// Disconnect from server
// TODO: Should we actually do this?
if (remoteConnection != null) {
remoteConnection.disconnect();
}
}
}
/**
* Copies the message body from the remote server to the client (outgoing
* {@code HttpServletResponse}).
*
* @param pRemoteConnection
* @param pResponse
*
* @throws IOException
*/
private void copyBodyToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) throws IOException {
InputStream fromRemote = null;
OutputStream toClient = null;
try {
// Get either input or error stream
try {
fromRemote = pRemoteConnection.getInputStream();
}
catch (IOException e) {
// If exception, use errorStream instead
fromRemote = pRemoteConnection.getErrorStream();
}
// I guess the stream might be null if there is no response other
// than headers (Continue, No Content, etc).
if (fromRemote != null) {
toClient = pResponse.getOutputStream();
FileUtil.copy(fromRemote, toClient);
}
}
finally {
if (fromRemote != null) {
try {
fromRemote.close();
}
catch (IOException e) {
log("Stream from remote could not be closed.", e);
}
}
if (toClient != null) {
try {
toClient.close();
}
catch (IOException e) {
log("Stream to client could not be closed.", e);
}
}
}
}
/**
* Copies the message body from the client (incomming
* {@code HttpServletRequest}) to the remote server if the request method
* is {@code POST} or <tt>PUT<tt>.
* Otherwise this method does nothing.
*
* @param pRequest
* @param pRemoteConnection
*
* @throws java.io.IOException
*/
private void copyBodyFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) throws IOException {
// If this is a POST or PUT, copy message body from client remote server
if (!("POST".equals(pRequest.getMethod()) || "PUT".equals(pRequest.getMethod()))) {
return;
}
// NOTE: Setting doOutput to true, will make it a POST request (why?)...
pRemoteConnection.setDoOutput(true);
// Get streams and do the copying
InputStream fromClient = null;
OutputStream toRemote = null;
try {
fromClient = pRequest.getInputStream();
toRemote = pRemoteConnection.getOutputStream();
FileUtil.copy(fromClient, toRemote);
}
finally {
if (fromClient != null) {
try {
fromClient.close();
}
catch (IOException e) {
log("Stream from client could not be closed.", e);
}
}
if (toRemote != null) {
try {
toRemote.close();
}
catch (IOException e) {
log("Stream to remote could not be closed.", e);
}
}
}
}
/**
* Creates the remote request URI based on the incoming request.
* The URI will include any query strings etc.
*
* @param pRequest
*
* @return a {@code String} representing the remote request URI
*/
private String createRemoteRequestURI(HttpServletRequest pRequest) {
StringBuilder requestURI = new StringBuilder(remotePath);
requestURI.append(pRequest.getPathInfo());
if (!StringUtil.isEmpty(pRequest.getQueryString())) {
requestURI.append("?");
requestURI.append(pRequest.getQueryString());
}
return requestURI.toString();
}
/**
* Copies headers from the remote connection back to the client
* (the outgoing HttpServletResponse). All headers except the "Server"
* header are copied.
*
* @param pRemoteConnection
* @param pResponse
*/
private void copyHeadersToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) {
// NOTE: There is no getHeaderFieldCount method or similar...
// Also, the getHeaderFields() method was introduced in J2SE 1.4, and
// we want to be 1.2 compatible.
// So, just try to loop until there are no more headers.
int i = 0;
while (true) {
String key = pRemoteConnection.getHeaderFieldKey(i);
// NOTE: getHeaderField(String) returns only the last value
String value = pRemoteConnection.getHeaderField(i);
// If the key is not null, life is simple, and Sun is shining
// However, the default implementations includes the HTTP response
// code ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
// In addition, we want to skip the original "Server" header
if (key != null && !HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
else if (value == null) {
// If BOTH key and value is null, there are no more header fields
break;
}
i++;
}
/* 1.4+ version below....
Map headers = pRemoteConnection.getHeaderFields();
for (Iterator iterator = headers.entrySet().iterator(); iterator.hasNext();) {
Map.Entry header = (Map.Entry) iterator.next();
List values = (List) header.getValue();
for (Iterator valueIter = values.iterator(); valueIter.hasNext();) {
String value = (String) valueIter.next();
String key = (String) header.getKey();
// Skip the server header
if (HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) {
key = null;
}
// The default implementations includes the HTTP response code
// ("HTTP/1.1 200 Ok" or similar) as a header field with
// key "null" (why..?)...
if (key != null) {
//System.out.println("client <<<-- remote: " + key + ": " + value);
pResponse.setHeader(key, value);
}
}
}
*/
}
/**
* Copies headers from the client (the incoming {@code HttpServletRequest})
* to the outgoing connection.
* All headers except the "Host" header are copied.
*
* @param pRequest
* @param pRemoteConnection
*/
private void copyHeadersFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) {
Enumeration headerNames = pRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
Enumeration headerValues = pRequest.getHeaders(headerName);
// Skip the "host" header, as we want something else
if (HTTP_REQUEST_HEADER_HOST.equalsIgnoreCase(headerName)) {
// Skip this header
headerName = null;
}
// Set the the header to the remoteConnection
if (headerName != null) {
// Convert from multiple line to single line, comma separated, as
// there seems to be a shortcoming in the URLConneciton API...
StringBuilder headerValue = new StringBuilder();
while (headerValues.hasMoreElements()) {
String value = (String) headerValues.nextElement();
headerValue.append(value);
if (headerValues.hasMoreElements()) {
headerValue.append(", ");
}
}
//System.out.println("client -->>> remote: " + headerName + ": " + headerValue);
pRemoteConnection.setRequestProperty(headerName, headerValue.toString());
}
}
}
}

View File

@@ -1,284 +1,284 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.lang.Validate;
import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import java.io.Serializable;
import java.util.*;
/**
* {@code ServletConfig} or {@code FilterConfig} adapter, that implements
* the {@code Map} interface for interoperability with collection-based API's.
* <p/>
* This {@code Map} is not synchronized.
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ServletConfigMapAdapter.java#2 $
*/
class ServletConfigMapAdapter extends AbstractMap<String, String> implements Map<String, String>, Serializable, Cloneable {
enum ConfigType {
ServletConfig, FilterConfig, ServletContext
}
private final ConfigType type;
private final ServletConfig servletConfig;
private final FilterConfig filterConfig;
private final ServletContext servletContext;
// Cache the entry set
private transient Set<Entry<String, String>> entrySet;
public ServletConfigMapAdapter(final ServletConfig pConfig) {
this(pConfig, ConfigType.ServletConfig);
}
public ServletConfigMapAdapter(final FilterConfig pConfig) {
this(pConfig, ConfigType.FilterConfig);
}
public ServletConfigMapAdapter(final ServletContext pContext) {
this(pContext, ConfigType.ServletContext);
}
private ServletConfigMapAdapter(final Object pConfig, final ConfigType pType) {
// Could happen if client code invokes with null reference
Validate.notNull(pConfig, "config");
type = pType;
switch (type) {
case ServletConfig:
servletConfig = (ServletConfig) pConfig;
filterConfig = null;
servletContext = null;
break;
case FilterConfig:
servletConfig = null;
filterConfig = (FilterConfig) pConfig;
servletContext = null;
break;
case ServletContext:
servletConfig = null;
filterConfig = null;
servletContext = (ServletContext) pConfig;
break;
default:
throw new IllegalArgumentException("Wrong type: " + pType);
}
}
/**
* Gets the servlet or filter name from the config.
*
* @return the servlet or filter name
*/
public final String getName() {
switch (type) {
case ServletConfig:
return servletConfig.getServletName();
case FilterConfig:
return filterConfig.getFilterName();
case ServletContext:
return servletContext.getServletContextName();
default:
throw new IllegalStateException();
}
}
/**
* Gets the servlet context from the config.
*
* @return the servlet context
*/
public final ServletContext getServletContext() {
switch (type) {
case ServletConfig:
return servletConfig.getServletContext();
case FilterConfig:
return filterConfig.getServletContext();
case ServletContext:
return servletContext;
default:
throw new IllegalStateException();
}
}
public final Enumeration getInitParameterNames() {
switch (type) {
case ServletConfig:
return servletConfig.getInitParameterNames();
case FilterConfig:
return filterConfig.getInitParameterNames();
case ServletContext:
return servletContext.getInitParameterNames();
default:
throw new IllegalStateException();
}
}
public final String getInitParameter(final String pName) {
switch (type) {
case ServletConfig:
return servletConfig.getInitParameter(pName);
case FilterConfig:
return filterConfig.getInitParameter(pName);
case ServletContext:
return servletContext.getInitParameter(pName);
default:
throw new IllegalStateException();
}
}
public Set<Entry<String, String>> entrySet() {
if (entrySet == null) {
entrySet = createEntrySet();
}
return entrySet;
}
private Set<Entry<String, String>> createEntrySet() {
return new AbstractSet<Entry<String, String>>() {
// Cache size, if requested, -1 means not calculated
private int size = -1;
public Iterator<Entry<String, String>> iterator() {
return new Iterator<Entry<String, String>>() {
// Iterator is backed by initParameterNames enumeration
final Enumeration names = getInitParameterNames();
public boolean hasNext() {
return names.hasMoreElements();
}
public Entry<String, String> next() {
final String key = (String) names.nextElement();
return new Entry<String, String>() {
public String getKey() {
return key;
}
public String getValue() {
return get(key);
}
public String setValue(String pValue) {
throw new UnsupportedOperationException();
}
// NOTE: Override equals
public boolean equals(Object pOther) {
if (!(pOther instanceof Map.Entry)) {
return false;
}
Map.Entry e = (Map.Entry) pOther;
Object value = get(key);
Object rKey = e.getKey();
Object rValue = e.getValue();
return (key == null ? rKey == null : key.equals(rKey))
&& (value == null ? rValue == null : value.equals(rValue));
}
// NOTE: Override hashCode to keep the map's
// hashCode constant and compatible
public int hashCode() {
Object value = get(key);
return ((key == null) ? 0 : key.hashCode()) ^
((value == null) ? 0 : value.hashCode());
}
public String toString() {
return key + "=" + get(key);
}
};
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public int size() {
if (size < 0) {
size = calculateSize();
}
return size;
}
private int calculateSize() {
final Enumeration names = getInitParameterNames();
int size = 0;
while (names.hasMoreElements()) {
size++;
names.nextElement();
}
return size;
}
};
}
public String get(Object pKey) {
return getInitParameter(StringUtil.valueOf(pKey));
}
/// Unsupported Map methods
@Override
public String put(String pKey, String pValue) {
throw new UnsupportedOperationException();
}
@Override
public String remove(Object pKey) {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map pMap) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.lang.Validate;
import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import java.io.Serializable;
import java.util.*;
/**
* {@code ServletConfig} or {@code FilterConfig} adapter, that implements
* the {@code Map} interface for interoperability with collection-based API's.
* <p>
* This {@code Map} is not synchronized.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ServletConfigMapAdapter.java#2 $
*/
class ServletConfigMapAdapter extends AbstractMap<String, String> implements Map<String, String>, Serializable, Cloneable {
enum ConfigType {
ServletConfig, FilterConfig, ServletContext
}
private final ConfigType type;
private final ServletConfig servletConfig;
private final FilterConfig filterConfig;
private final ServletContext servletContext;
// Cache the entry set
private transient Set<Entry<String, String>> entrySet;
public ServletConfigMapAdapter(final ServletConfig pConfig) {
this(pConfig, ConfigType.ServletConfig);
}
public ServletConfigMapAdapter(final FilterConfig pConfig) {
this(pConfig, ConfigType.FilterConfig);
}
public ServletConfigMapAdapter(final ServletContext pContext) {
this(pContext, ConfigType.ServletContext);
}
private ServletConfigMapAdapter(final Object pConfig, final ConfigType pType) {
// Could happen if client code invokes with null reference
Validate.notNull(pConfig, "config");
type = pType;
switch (type) {
case ServletConfig:
servletConfig = (ServletConfig) pConfig;
filterConfig = null;
servletContext = null;
break;
case FilterConfig:
servletConfig = null;
filterConfig = (FilterConfig) pConfig;
servletContext = null;
break;
case ServletContext:
servletConfig = null;
filterConfig = null;
servletContext = (ServletContext) pConfig;
break;
default:
throw new IllegalArgumentException("Wrong type: " + pType);
}
}
/**
* Gets the servlet or filter name from the config.
*
* @return the servlet or filter name
*/
public final String getName() {
switch (type) {
case ServletConfig:
return servletConfig.getServletName();
case FilterConfig:
return filterConfig.getFilterName();
case ServletContext:
return servletContext.getServletContextName();
default:
throw new IllegalStateException();
}
}
/**
* Gets the servlet context from the config.
*
* @return the servlet context
*/
public final ServletContext getServletContext() {
switch (type) {
case ServletConfig:
return servletConfig.getServletContext();
case FilterConfig:
return filterConfig.getServletContext();
case ServletContext:
return servletContext;
default:
throw new IllegalStateException();
}
}
public final Enumeration getInitParameterNames() {
switch (type) {
case ServletConfig:
return servletConfig.getInitParameterNames();
case FilterConfig:
return filterConfig.getInitParameterNames();
case ServletContext:
return servletContext.getInitParameterNames();
default:
throw new IllegalStateException();
}
}
public final String getInitParameter(final String pName) {
switch (type) {
case ServletConfig:
return servletConfig.getInitParameter(pName);
case FilterConfig:
return filterConfig.getInitParameter(pName);
case ServletContext:
return servletContext.getInitParameter(pName);
default:
throw new IllegalStateException();
}
}
public Set<Entry<String, String>> entrySet() {
if (entrySet == null) {
entrySet = createEntrySet();
}
return entrySet;
}
private Set<Entry<String, String>> createEntrySet() {
return new AbstractSet<Entry<String, String>>() {
// Cache size, if requested, -1 means not calculated
private int size = -1;
public Iterator<Entry<String, String>> iterator() {
return new Iterator<Entry<String, String>>() {
// Iterator is backed by initParameterNames enumeration
final Enumeration names = getInitParameterNames();
public boolean hasNext() {
return names.hasMoreElements();
}
public Entry<String, String> next() {
final String key = (String) names.nextElement();
return new Entry<String, String>() {
public String getKey() {
return key;
}
public String getValue() {
return get(key);
}
public String setValue(String pValue) {
throw new UnsupportedOperationException();
}
// NOTE: Override equals
public boolean equals(Object pOther) {
if (!(pOther instanceof Map.Entry)) {
return false;
}
Map.Entry e = (Map.Entry) pOther;
Object value = get(key);
Object rKey = e.getKey();
Object rValue = e.getValue();
return (key == null ? rKey == null : key.equals(rKey))
&& (value == null ? rValue == null : value.equals(rValue));
}
// NOTE: Override hashCode to keep the map's
// hashCode constant and compatible
public int hashCode() {
Object value = get(key);
return ((key == null) ? 0 : key.hashCode()) ^
((value == null) ? 0 : value.hashCode());
}
public String toString() {
return key + "=" + get(key);
}
};
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public int size() {
if (size < 0) {
size = calculateSize();
}
return size;
}
private int calculateSize() {
final Enumeration names = getInitParameterNames();
int size = 0;
while (names.hasMoreElements()) {
size++;
names.nextElement();
}
return size;
}
};
}
public String get(Object pKey) {
return getInitParameter(StringUtil.valueOf(pKey));
}
/// Unsupported Map methods
@Override
public String put(String pKey, String pValue) {
throw new UnsupportedOperationException();
}
@Override
public String remove(Object pKey) {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map pMap) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,116 +1,118 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* A delegate for handling stream support in wrapped servlet responses.
* <p/>
* Client code should delegate {@code getOutputStream}, {@code getWriter},
* {@code flushBuffer} and {@code resetBuffer} methods from the servlet response.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ServletResponseStreamDelegate.java#2 $
*/
public class ServletResponseStreamDelegate {
private Object out = null;
protected final ServletResponse response;
public ServletResponseStreamDelegate(final ServletResponse pResponse) {
response = notNull(pResponse, "response");
}
// NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY.
public final ServletOutputStream getOutputStream() throws IOException {
if (out == null) {
OutputStream out = createOutputStream();
this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out);
}
else if (out instanceof PrintWriter) {
throw new IllegalStateException("getWriter() already called.");
}
return (ServletOutputStream) out;
}
// NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY.
public final PrintWriter getWriter() throws IOException {
if (out == null) {
// NOTE: getCharacterEncoding may/should not return null
OutputStream out = createOutputStream();
String charEncoding = response.getCharacterEncoding();
this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out));
}
else if (out instanceof ServletOutputStream) {
throw new IllegalStateException("getOutputStream() already called.");
}
return (PrintWriter) out;
}
/**
* Returns the {@code OutputStream}.
* Subclasses should override this method to provide a decorated output stream.
* This method is guaranteed to be invoked only once for a request/response
* (unless {@code resetBuffer} is invoked).
* <P/>
* This implementation simply returns the output stream from the wrapped
* response.
*
* @return the {@code OutputStream} to use for the response
* @throws IOException if an I/O exception occurs
*/
protected OutputStream createOutputStream() throws IOException {
return response.getOutputStream();
}
public void flushBuffer() throws IOException {
if (out instanceof ServletOutputStream) {
((ServletOutputStream) out).flush();
}
else if (out != null) {
((PrintWriter) out).flush();
}
}
public void resetBuffer() {
out = null;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* A delegate for handling stream support in wrapped servlet responses.
* <p>
* Client code should delegate {@code getOutputStream}, {@code getWriter},
* {@code flushBuffer} and {@code resetBuffer} methods from the servlet response.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ServletResponseStreamDelegate.java#2 $
*/
public class ServletResponseStreamDelegate {
private Object out = null;
protected final ServletResponse response;
public ServletResponseStreamDelegate(final ServletResponse pResponse) {
response = notNull(pResponse, "response");
}
// NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY.
public final ServletOutputStream getOutputStream() throws IOException {
if (out == null) {
OutputStream out = createOutputStream();
this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out);
}
else if (out instanceof PrintWriter) {
throw new IllegalStateException("getWriter() already called.");
}
return (ServletOutputStream) out;
}
// NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY.
public final PrintWriter getWriter() throws IOException {
if (out == null) {
// NOTE: getCharacterEncoding may/should not return null
OutputStream out = createOutputStream();
String charEncoding = response.getCharacterEncoding();
this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out));
}
else if (out instanceof ServletOutputStream) {
throw new IllegalStateException("getOutputStream() already called.");
}
return (PrintWriter) out;
}
/**
* Returns the {@code OutputStream}.
* Subclasses should override this method to provide a decorated output stream.
* This method is guaranteed to be invoked only once for a request/response
* (unless {@code resetBuffer} is invoked).
* <p>
* This implementation simply returns the output stream from the wrapped
* response.
* </p>
*
* @return the {@code OutputStream} to use for the response
* @throws IOException if an I/O exception occurs
*/
protected OutputStream createOutputStream() throws IOException {
return response.getOutputStream();
}
public void flushBuffer() throws IOException {
if (out instanceof ServletOutputStream) {
((ServletOutputStream) out).flush();
}
else if (out != null) {
((PrintWriter) out).flush();
}
}
public void resetBuffer() {
out = null;
}
}

View File

@@ -1,308 +1,309 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ThrottleFilter, a filter for easing server during heavy load.
* <p/>
* Intercepts requests, and returns HTTP response code {@code 503 (Service Unavailable)},
* if there are more than a given number of concurrent
* requests, to avoid large backlogs. The number of concurrent requests and the
* response messages sent to the user agent, is configurable from the web
* descriptor.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ThrottleFilter.java#1 $
* @see #setMaxConcurrentThreadCount
* @see #setResponseMessages
*/
public class ThrottleFilter extends GenericFilter {
/**
* Minimum free thread count, defaults to {@code 10}
*/
protected int maxConcurrentThreadCount = 10;
/**
* The number of running request threads
*/
private int runningThreads = 0;
private final Object runningThreadsLock = new Object();
/**
* Default response message sent to user agents, if the request is rejected
*/
protected final static String DEFUALT_RESPONSE_MESSAGE =
"Service temporarily unavailable, please try again later.";
/**
* Default response content type
*/
protected static final String DEFAULT_TYPE = "text/html";
/**
* The reposne message sent to user agenta, if the request is rejected
*/
private Map<String, String> responseMessageNames = new HashMap<String, String>(10);
/**
* The reposne message sent to user agents, if the request is rejected
*/
private String[] responseMessageTypes = null;
/**
* Cache for response messages
*/
private Map<String, CacheEntry> responseCache = new HashMap<String, CacheEntry>(10);
/**
* Sets the minimum free thread count.
*
* @param pMaxConcurrentThreadCount
*/
public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) {
if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) {
try {
maxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount);
}
catch (NumberFormatException nfe) {
// Use default
}
}
}
/**
* Sets the response message sent to the user agent, if the request is
* rejected.
* <BR/>
* The format is {@code &lt;mime-type&gt;=&lt;filename&gt;,
* &lt;mime-type&gt;=&lt;filename&gt;}.
* <BR/>
* Example: {@code &lt;text/vnd.wap.wmlgt;=&lt;/errors/503.wml&gt;,
* &lt;text/html&gt;=&lt;/errors/503.html&gt;}
*
* @param pResponseMessages
*/
public void setResponseMessages(String pResponseMessages) {
// Split string in type=filename pairs
String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t");
List<String> types = new ArrayList<String>();
for (String pair : mappings) {
// Split pairs on '='
String[] mapping = StringUtil.toStringArray(pair, "= ");
// Test for wrong mapping
if ((mapping == null) || (mapping.length < 2)) {
log("Error in init param \"responseMessages\": " + pResponseMessages);
continue;
}
types.add(mapping[0]);
responseMessageNames.put(mapping[0], mapping[1]);
}
// Create arrays
responseMessageTypes = types.toArray(new String[types.size()]);
}
/**
* @param pRequest
* @param pResponse
* @param pChain
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
try {
if (beginRequest()) {
// Continue request
pChain.doFilter(pRequest, pResponse);
}
else {
// Send error and end request
// Get HTTP specific versions
HttpServletRequest request = (HttpServletRequest) pRequest;
HttpServletResponse response = (HttpServletResponse) pResponse;
// Get content type
String contentType = getContentType(request);
// Note: This is not the way the spec says you should do it.
// However, we handle error response this way for preformace reasons.
// The "correct" way would be to use sendError() and register a servlet
// that does the content negotiation as errorpage in the web descriptor.
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
response.setContentType(contentType);
response.getWriter().println(getMessage(contentType));
// Log warning, as this shouldn't happen too often
log("Request denied, no more available threads for requestURI=" + request.getRequestURI());
}
}
finally {
doneRequest();
}
}
/**
* Marks the beginning of a request
*
* @return {@code true} if the request should be handled.
*/
private boolean beginRequest() {
synchronized (runningThreadsLock) {
runningThreads++;
}
return (runningThreads <= maxConcurrentThreadCount);
}
/**
* Marks the end of the request
*/
private void doneRequest() {
synchronized (runningThreadsLock) {
runningThreads--;
}
}
/**
* Gets the content type for the response, suitable for the requesting user agent.
*
* @param pRequest
* @return the content type
*/
private String getContentType(HttpServletRequest pRequest) {
if (responseMessageTypes != null) {
String accept = pRequest.getHeader("Accept");
for (String type : responseMessageTypes) {
// Note: This is not 100% correct way of doing content negotiation
// But we just want a compatible result, quick, so this is okay
if (StringUtil.contains(accept, type)) {
return type;
}
}
}
// If none found, return default
return DEFAULT_TYPE;
}
/**
* Gets the response message for the given content type.
*
* @param pContentType
* @return the message
*/
private String getMessage(String pContentType) {
String fileName = responseMessageNames.get(pContentType);
// Get cached value
CacheEntry entry = responseCache.get(fileName);
if ((entry == null) || entry.isExpired()) {
// Create and add or replace cached value
entry = new CacheEntry(readMessage(fileName));
responseCache.put(fileName, entry);
}
// Return value
return (entry.getValue() != null)
? (String) entry.getValue()
: DEFUALT_RESPONSE_MESSAGE;
}
/**
* Reads the response message from a file in the current web app.
*
* @param pFileName
* @return the message
*/
private String readMessage(String pFileName) {
try {
// Read resource from web app
InputStream is = getServletContext().getResourceAsStream(pFileName);
if (is != null) {
return new String(FileUtil.read(is));
}
else {
log("File not found: " + pFileName);
}
}
catch (IOException ioe) {
log("Error reading file: " + pFileName + " (" + ioe.getMessage() + ")");
}
return null;
}
/**
* Keeps track of Cached objects
*/
private static class CacheEntry {
private Object value;
private long timestamp = -1;
CacheEntry(Object pValue) {
value = pValue;
timestamp = System.currentTimeMillis();
}
Object getValue() {
return value;
}
boolean isExpired() {
return (System.currentTimeMillis() - timestamp) > 60000; // Cache 1 minute
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ThrottleFilter, a filter for easing server during heavy load.
* <p>
* Intercepts requests, and returns HTTP response code {@code 503 (Service Unavailable)},
* if there are more than a given number of concurrent
* requests, to avoid large backlogs. The number of concurrent requests and the
* response messages sent to the user agent, is configurable from the web
* descriptor.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ThrottleFilter.java#1 $
* @see #setMaxConcurrentThreadCount
* @see #setResponseMessages
*/
public class ThrottleFilter extends GenericFilter {
/**
* Minimum free thread count, defaults to {@code 10}
*/
protected int maxConcurrentThreadCount = 10;
/**
* The number of running request threads
*/
private int runningThreads = 0;
private final Object runningThreadsLock = new Object();
/**
* Default response message sent to user agents, if the request is rejected
*/
protected final static String DEFUALT_RESPONSE_MESSAGE =
"Service temporarily unavailable, please try again later.";
/**
* Default response content type
*/
protected static final String DEFAULT_TYPE = "text/html";
/**
* The reposne message sent to user agenta, if the request is rejected
*/
private Map<String, String> responseMessageNames = new HashMap<String, String>(10);
/**
* The reposne message sent to user agents, if the request is rejected
*/
private String[] responseMessageTypes = null;
/**
* Cache for response messages
*/
private Map<String, CacheEntry> responseCache = new HashMap<String, CacheEntry>(10);
/**
* Sets the minimum free thread count.
*
* @param pMaxConcurrentThreadCount
*/
public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) {
if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) {
try {
maxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount);
}
catch (NumberFormatException nfe) {
// Use default
}
}
}
/**
* Sets the response message sent to the user agent, if the request is
* rejected.
* <br>
* The format is {@code &lt;mime-type&gt;=&lt;filename&gt;,
* &lt;mime-type&gt;=&lt;filename&gt;}.
* <br>
* Example: {@code &lt;text/vnd.wap.wmlgt;=&lt;/errors/503.wml&gt;,
* &lt;text/html&gt;=&lt;/errors/503.html&gt;}
*
* @param pResponseMessages
*/
public void setResponseMessages(String pResponseMessages) {
// Split string in type=filename pairs
String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t");
List<String> types = new ArrayList<String>();
for (String pair : mappings) {
// Split pairs on '='
String[] mapping = StringUtil.toStringArray(pair, "= ");
// Test for wrong mapping
if ((mapping == null) || (mapping.length < 2)) {
log("Error in init param \"responseMessages\": " + pResponseMessages);
continue;
}
types.add(mapping[0]);
responseMessageNames.put(mapping[0], mapping[1]);
}
// Create arrays
responseMessageTypes = types.toArray(new String[types.size()]);
}
/**
* @param pRequest
* @param pResponse
* @param pChain
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
try {
if (beginRequest()) {
// Continue request
pChain.doFilter(pRequest, pResponse);
}
else {
// Send error and end request
// Get HTTP specific versions
HttpServletRequest request = (HttpServletRequest) pRequest;
HttpServletResponse response = (HttpServletResponse) pResponse;
// Get content type
String contentType = getContentType(request);
// Note: This is not the way the spec says you should do it.
// However, we handle error response this way for preformace reasons.
// The "correct" way would be to use sendError() and register a servlet
// that does the content negotiation as errorpage in the web descriptor.
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
response.setContentType(contentType);
response.getWriter().println(getMessage(contentType));
// Log warning, as this shouldn't happen too often
log("Request denied, no more available threads for requestURI=" + request.getRequestURI());
}
}
finally {
doneRequest();
}
}
/**
* Marks the beginning of a request
*
* @return {@code true} if the request should be handled.
*/
private boolean beginRequest() {
synchronized (runningThreadsLock) {
runningThreads++;
}
return (runningThreads <= maxConcurrentThreadCount);
}
/**
* Marks the end of the request
*/
private void doneRequest() {
synchronized (runningThreadsLock) {
runningThreads--;
}
}
/**
* Gets the content type for the response, suitable for the requesting user agent.
*
* @param pRequest
* @return the content type
*/
private String getContentType(HttpServletRequest pRequest) {
if (responseMessageTypes != null) {
String accept = pRequest.getHeader("Accept");
for (String type : responseMessageTypes) {
// Note: This is not 100% correct way of doing content negotiation
// But we just want a compatible result, quick, so this is okay
if (StringUtil.contains(accept, type)) {
return type;
}
}
}
// If none found, return default
return DEFAULT_TYPE;
}
/**
* Gets the response message for the given content type.
*
* @param pContentType
* @return the message
*/
private String getMessage(String pContentType) {
String fileName = responseMessageNames.get(pContentType);
// Get cached value
CacheEntry entry = responseCache.get(fileName);
if ((entry == null) || entry.isExpired()) {
// Create and add or replace cached value
entry = new CacheEntry(readMessage(fileName));
responseCache.put(fileName, entry);
}
// Return value
return (entry.getValue() != null)
? (String) entry.getValue()
: DEFUALT_RESPONSE_MESSAGE;
}
/**
* Reads the response message from a file in the current web app.
*
* @param pFileName
* @return the message
*/
private String readMessage(String pFileName) {
try {
// Read resource from web app
InputStream is = getServletContext().getResourceAsStream(pFileName);
if (is != null) {
return new String(FileUtil.read(is));
}
else {
log("File not found: " + pFileName);
}
}
catch (IOException ioe) {
log("Error reading file: " + pFileName + " (" + ioe.getMessage() + ")");
}
return null;
}
/**
* Keeps track of Cached objects
*/
private static class CacheEntry {
private Object value;
private long timestamp = -1;
CacheEntry(Object pValue) {
value = pValue;
timestamp = System.currentTimeMillis();
}
Object getValue() {
return value;
}
boolean isExpired() {
return (System.currentTimeMillis() - timestamp) > 60000; // Cache 1 minute
}
}
}

View File

@@ -1,235 +1,239 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import javax.servlet.*;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
* Removes extra unneccessary white space from a servlet response.
* White space is defined as per {@link Character#isWhitespace(char)}.
* <p/>
* This filter has no understanding of the content in the reponse, and will
* remove repeated white space anywhere in the stream. It is intended for
* removing white space from HTML or XML streams, but this limitation makes it
* less suited for filtering HTML/XHTML with embedded CSS or JavaScript,
* in case white space should be significant here. It is strongly reccommended
* you keep CSS and JavaScript in separate files (this will have the added
* benefit of further reducing the ammount of data communicated between
* server and client).
* <p/>
* <em>At the moment this filter has no concept of encoding</em>.
* This means, that if some multi-byte escape sequence contains one or more
* bytes that <em>individually</em> is treated as a white space, these bytes
* may be skipped.
* As <a href="http://en.wikipedia.org/wiki/UTF-8" title="UTF-8">UTF-8</a>
* guarantees that no bytes are repeated in this way, this filter can safely
* filter UTF-8.
* Simple 8 bit character encodings, like the
* <a href="http://en.wikipedia.org/wiki/ISO/IEC_8859"
* title="ISO/IEC 8859">ISO/IEC 8859</a> standard, or
* <a href="http://en.wikipedia.org/wiki/Windows-1252" title="Windows-1252">
* are always safe.
* <p/>
* <b>Configuration</b><br/>
* To use {@code TrimWhiteSpaceFilter} in your web-application, you simply need
* to add it to your web descriptor ({@code web.xml}).
* If using a servlet container that supports the Servlet 2.4 spec, the new
* {@code dispatcher} element should be used, and set to
* {@code REQUEST/FORWARD}, to make sure the filter is invoked only once for
* requests.
* If using an older web descriptor, set the {@code init-param}
* {@code "once-per-request"} to {@code "true"} (this will have the same effect,
* but might perform slightly worse than the 2.4 version).
* Please see the examples below.
* <p/>
* <b>Servlet 2.4 version, filter section:</b><br/>
* <pre>
* &lt;!-- TrimWS Filter Configuration --&gt;
* &lt;filter&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;filter-class&gt;com.twelvemonkeys.servlet.TrimWhiteSpaceFilter&lt;/filter-class&gt;
* &lt;!-- auto-flush=true is the default, may be omitted --&gt;
* &lt;init-param&gt;
* &lt;param-name&gt;auto-flush&lt;/param-name&gt;
* &lt;param-value&gt;true&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;/filter&gt;
* </pre>
* <b>Filter-mapping section:</b><br/>
* <pre>
* &lt;!-- TimWS Filter Mapping --&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.jsp&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: TrimWhiteSpaceFilter.java#2 $
*/
public class TrimWhiteSpaceFilter extends GenericFilter {
private boolean autoFlush = true;
@InitParam
public void setAutoFlush(final boolean pAutoFlush) {
autoFlush = pAutoFlush;
}
public void init() throws ServletException {
super.init();
log("Automatic flushing is " + (autoFlush ? "enabled" : "disabled"));
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
ServletResponseWrapper wrapped = new TrimWSServletResponseWrapper(pResponse);
pChain.doFilter(pRequest, ServletUtil.createWrapper(wrapped));
if (autoFlush) {
wrapped.flushBuffer();
}
}
static final class TrimWSFilterOutputStream extends FilterOutputStream {
boolean lastWasWS = true; // Avoids leading WS by init to true
public TrimWSFilterOutputStream(OutputStream pOut) {
super(pOut);
}
// Override this, in case the wrapped outputstream overrides...
public final void write(byte pBytes[]) throws IOException {
write(pBytes, 0, pBytes.length);
}
// Override this, in case the wrapped outputstream overrides...
public final void write(byte pBytes[], int pOff, int pLen) throws IOException {
if (pBytes == null) {
throw new NullPointerException("bytes == null");
}
else if (pOff < 0 || pLen < 0 || (pOff + pLen > pBytes.length)) {
throw new IndexOutOfBoundsException("Bytes: " + pBytes.length + " Offset: " + pOff + " Length: " + pLen);
}
for (int i = 0; i < pLen ; i++) {
write(pBytes[pOff + i]);
}
}
public void write(int pByte) throws IOException {
// TODO: Is this good enough for multi-byte encodings like UTF-16?
// Consider writing through a Writer that does that for us, and
// also buffer whitespace, so we write a linefeed every time there's
// one in the original...
// According to http://en.wikipedia.org/wiki/UTF-8:
// "[...] US-ASCII octet values do not appear otherwise in a UTF-8
// encoded character stream. This provides compatibility with file
// systems or other software (e.g., the printf() function in
// C libraries) that parse based on US-ASCII values but are
// transparent to other values."
if (!Character.isWhitespace((char) pByte)) {
// If char is not WS, just store
super.write(pByte);
lastWasWS = false;
}
else {
// TODO: Consider writing only 0x0a (LF) and 0x20 (space)
// Else, if char is WS, store first, skip the rest
if (!lastWasWS) {
if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a
super.write(0x0a);
}
else {
super.write(pByte);
}
}
lastWasWS = true;
}
}
}
private static class TrimWSStreamDelegate extends ServletResponseStreamDelegate {
public TrimWSStreamDelegate(ServletResponse pResponse) {
super(pResponse);
}
protected OutputStream createOutputStream() throws IOException {
return new TrimWSFilterOutputStream(response.getOutputStream());
}
}
static class TrimWSServletResponseWrapper extends ServletResponseWrapper {
private final ServletResponseStreamDelegate streamDelegate = new TrimWSStreamDelegate(getResponse());
public TrimWSServletResponseWrapper(ServletResponse pResponse) {
super(pResponse);
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public void setContentLength(int pLength) {
// Will be changed by filter, so don't set.
}
@Override
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
@Override
public void resetBuffer() {
streamDelegate.resetBuffer();
}
// TODO: Consider picking up content-type/encoding, as we can only
// filter US-ASCII, UTF-8 and other compatible encodings?
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet;
import javax.servlet.*;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
* Removes extra unneccessary white space from a servlet response.
* White space is defined as per {@link Character#isWhitespace(char)}.
* <p>
* This filter has no understanding of the content in the reponse, and will
* remove repeated white space anywhere in the stream. It is intended for
* removing white space from HTML or XML streams, but this limitation makes it
* less suited for filtering HTML/XHTML with embedded CSS or JavaScript,
* in case white space should be significant here. It is strongly reccommended
* you keep CSS and JavaScript in separate files (this will have the added
* benefit of further reducing the ammount of data communicated between
* server and client).
* </p>
* <p>
* <em>At the moment this filter has no concept of encoding</em>.
* This means, that if some multi-byte escape sequence contains one or more
* bytes that <em>individually</em> is treated as a white space, these bytes
* may be skipped.
* As <a href="http://en.wikipedia.org/wiki/UTF-8">UTF-8</a>
* guarantees that no bytes are repeated in this way, this filter can safely
* filter UTF-8.
* Simple 8 bit character encodings, like the
* <a href="http://en.wikipedia.org/wiki/ISO/IEC_8859">ISO/IEC 8859</a> standard, or
* <a href="http://en.wikipedia.org/wiki/Windows-1252">Windows-1252"</a>
* are always safe.
* </p>
* <p>
* <b>Configuration</b>
* <br>
* To use {@code TrimWhiteSpaceFilter} in your web-application, you simply need
* to add it to your web descriptor ({@code web.xml}).
* If using a servlet container that supports the Servlet 2.4 spec, the new
* {@code dispatcher} element should be used, and set to
* {@code REQUEST/FORWARD}, to make sure the filter is invoked only once for
* requests.
* If using an older web descriptor, set the {@code init-param}
* {@code "once-per-request"} to {@code "true"} (this will have the same effect,
* but might perform slightly worse than the 2.4 version).
* Please see the examples below.
* </p>
* <p>
* <b>Servlet 2.4 version, filter section:</b>
* </p>
* <pre>
* &lt;!-- TrimWS Filter Configuration --&gt;
* &lt;filter&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;filter-class&gt;com.twelvemonkeys.servlet.TrimWhiteSpaceFilter&lt;/filter-class&gt;
* &lt;!-- auto-flush=true is the default, may be omitted --&gt;
* &lt;init-param&gt;
* &lt;param-name&gt;auto-flush&lt;/param-name&gt;
* &lt;param-value&gt;true&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;/filter&gt;
* </pre>
* <b>Filter-mapping section:</b><br>
* <pre>
* &lt;!-- TimWS Filter Mapping --&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;trimws&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.jsp&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* </pre>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: TrimWhiteSpaceFilter.java#2 $
*/
public class TrimWhiteSpaceFilter extends GenericFilter {
private boolean autoFlush = true;
@InitParam
public void setAutoFlush(final boolean pAutoFlush) {
autoFlush = pAutoFlush;
}
public void init() throws ServletException {
super.init();
log("Automatic flushing is " + (autoFlush ? "enabled" : "disabled"));
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
ServletResponseWrapper wrapped = new TrimWSServletResponseWrapper(pResponse);
pChain.doFilter(pRequest, ServletUtil.createWrapper(wrapped));
if (autoFlush) {
wrapped.flushBuffer();
}
}
static final class TrimWSFilterOutputStream extends FilterOutputStream {
boolean lastWasWS = true; // Avoids leading WS by init to true
public TrimWSFilterOutputStream(OutputStream pOut) {
super(pOut);
}
// Override this, in case the wrapped outputstream overrides...
public final void write(byte pBytes[]) throws IOException {
write(pBytes, 0, pBytes.length);
}
// Override this, in case the wrapped outputstream overrides...
public final void write(byte pBytes[], int pOff, int pLen) throws IOException {
if (pBytes == null) {
throw new NullPointerException("bytes == null");
}
else if (pOff < 0 || pLen < 0 || (pOff + pLen > pBytes.length)) {
throw new IndexOutOfBoundsException("Bytes: " + pBytes.length + " Offset: " + pOff + " Length: " + pLen);
}
for (int i = 0; i < pLen ; i++) {
write(pBytes[pOff + i]);
}
}
public void write(int pByte) throws IOException {
// TODO: Is this good enough for multi-byte encodings like UTF-16?
// Consider writing through a Writer that does that for us, and
// also buffer whitespace, so we write a linefeed every time there's
// one in the original...
// According to http://en.wikipedia.org/wiki/UTF-8:
// "[...] US-ASCII octet values do not appear otherwise in a UTF-8
// encoded character stream. This provides compatibility with file
// systems or other software (e.g., the printf() function in
// C libraries) that parse based on US-ASCII values but are
// transparent to other values."
if (!Character.isWhitespace((char) pByte)) {
// If char is not WS, just store
super.write(pByte);
lastWasWS = false;
}
else {
// TODO: Consider writing only 0x0a (LF) and 0x20 (space)
// Else, if char is WS, store first, skip the rest
if (!lastWasWS) {
if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a
super.write(0x0a);
}
else {
super.write(pByte);
}
}
lastWasWS = true;
}
}
}
private static class TrimWSStreamDelegate extends ServletResponseStreamDelegate {
public TrimWSStreamDelegate(ServletResponse pResponse) {
super(pResponse);
}
protected OutputStream createOutputStream() throws IOException {
return new TrimWSFilterOutputStream(response.getOutputStream());
}
}
static class TrimWSServletResponseWrapper extends ServletResponseWrapper {
private final ServletResponseStreamDelegate streamDelegate = new TrimWSStreamDelegate(getResponse());
public TrimWSServletResponseWrapper(ServletResponse pResponse) {
super(pResponse);
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public void setContentLength(int pLength) {
// Will be changed by filter, so don't set.
}
@Override
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
@Override
public void resetBuffer() {
streamDelegate.resetBuffer();
}
// TODO: Consider picking up content-type/encoding, as we can only
// filter US-ASCII, UTF-8 and other compatible encodings?
}
}

View File

@@ -1,202 +1,203 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.GenericFilter;
import com.twelvemonkeys.servlet.ServletConfigException;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Filter that provides response caching, for HTTP {@code GET} requests.
* <p/>
* Originally based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CacheFilter.java#4 $
*
*/
public class CacheFilter extends GenericFilter {
HTTPCache cache;
/**
* Initializes the filter
*
* @throws javax.servlet.ServletException
*/
public void init() throws ServletException {
FilterConfig config = getFilterConfig();
// Default don't delete cache files on exit (persistent cache)
boolean deleteCacheOnExit = "TRUE".equalsIgnoreCase(config.getInitParameter("deleteCacheOnExit"));
// Default expiry time 10 minutes
int expiryTime = 10 * 60 * 1000;
String expiryTimeStr = config.getInitParameter("expiryTime");
if (!StringUtil.isEmpty(expiryTimeStr)) {
try {
// TODO: This is insane.. :-P Let the expiry time be in minutes or seconds..
expiryTime = Integer.parseInt(expiryTimeStr);
}
catch (NumberFormatException e) {
throw new ServletConfigException("Could not parse expiryTime: " + e.toString(), e);
}
}
// Default max mem cache size 10 MB
int memCacheSize = 10;
String memCacheSizeStr = config.getInitParameter("memCacheSize");
if (!StringUtil.isEmpty(memCacheSizeStr)) {
try {
memCacheSize = Integer.parseInt(memCacheSizeStr);
}
catch (NumberFormatException e) {
throw new ServletConfigException("Could not parse memCacheSize: " + e.toString(), e);
}
}
int maxCachedEntites = 10000;
try {
cache = new HTTPCache(
getTempFolder(),
expiryTime,
memCacheSize * 1024 * 1024,
maxCachedEntites,
deleteCacheOnExit,
new ServletContextLoggerAdapter(getFilterName(), getServletContext())
) {
@Override
protected File getRealFile(CacheRequest pRequest) {
String contextRelativeURI = ServletUtil.getContextRelativeURI(((ServletCacheRequest) pRequest).getRequest());
String path = getServletContext().getRealPath(contextRelativeURI);
if (path != null) {
return new File(path);
}
return null;
}
};
log("Created cache: " + cache);
}
catch (IllegalArgumentException e) {
throw new ServletConfigException("Could not create cache: " + e.toString(), e);
}
}
private File getTempFolder() {
File tempRoot = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
if (tempRoot == null) {
throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\"");
}
return new File(tempRoot, getFilterName());
}
public void destroy() {
log("Destroying cache: " + cache);
cache = null;
super.destroy();
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// We can only cache HTTP GET/HEAD requests
if (!(pRequest instanceof HttpServletRequest
&& pResponse instanceof HttpServletResponse
&& isCachable((HttpServletRequest) pRequest))) {
pChain.doFilter(pRequest, pResponse); // Continue chain
}
else {
ServletCacheRequest cacheRequest = new ServletCacheRequest((HttpServletRequest) pRequest);
ServletCacheResponse cacheResponse = new ServletCacheResponse((HttpServletResponse) pResponse);
ServletResponseResolver resolver = new ServletResponseResolver(cacheRequest, cacheResponse, pChain);
// Render fast
try {
cache.doCached(cacheRequest, cacheResponse, resolver);
}
catch (CacheException e) {
if (e.getCause() instanceof ServletException) {
throw (ServletException) e.getCause();
}
else {
throw new ServletException(e);
}
}
finally {
pResponse.flushBuffer();
}
}
}
private boolean isCachable(HttpServletRequest pRequest) {
// TODO: Get Cache-Control: no-cache/max-age=0 and Pragma: no-cache from REQUEST too?
return "GET".equals(pRequest.getMethod()) || "HEAD".equals(pRequest.getMethod());
}
// TODO: Extract, complete and document this class, might be useful in other cases
// Maybe add it to the ServletUtil class
static class ServletContextLoggerAdapter extends Logger {
private final ServletContext context;
public ServletContextLoggerAdapter(String pName, ServletContext pContext) {
super(pName, null);
context = pContext;
}
@Override
public void log(Level pLevel, String pMessage) {
context.log(pMessage);
}
@Override
public void log(Level pLevel, String pMessage, Throwable pThrowable) {
context.log(pMessage, pThrowable);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.GenericFilter;
import com.twelvemonkeys.servlet.ServletConfigException;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Filter that provides response caching, for HTTP {@code GET} requests.
* <p>
* Originally based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* </p>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CacheFilter.java#4 $
*
*/
public class CacheFilter extends GenericFilter {
HTTPCache cache;
/**
* Initializes the filter
*
* @throws javax.servlet.ServletException
*/
public void init() throws ServletException {
FilterConfig config = getFilterConfig();
// Default don't delete cache files on exit (persistent cache)
boolean deleteCacheOnExit = "TRUE".equalsIgnoreCase(config.getInitParameter("deleteCacheOnExit"));
// Default expiry time 10 minutes
int expiryTime = 10 * 60 * 1000;
String expiryTimeStr = config.getInitParameter("expiryTime");
if (!StringUtil.isEmpty(expiryTimeStr)) {
try {
// TODO: This is insane.. :-P Let the expiry time be in minutes or seconds..
expiryTime = Integer.parseInt(expiryTimeStr);
}
catch (NumberFormatException e) {
throw new ServletConfigException("Could not parse expiryTime: " + e.toString(), e);
}
}
// Default max mem cache size 10 MB
int memCacheSize = 10;
String memCacheSizeStr = config.getInitParameter("memCacheSize");
if (!StringUtil.isEmpty(memCacheSizeStr)) {
try {
memCacheSize = Integer.parseInt(memCacheSizeStr);
}
catch (NumberFormatException e) {
throw new ServletConfigException("Could not parse memCacheSize: " + e.toString(), e);
}
}
int maxCachedEntites = 10000;
try {
cache = new HTTPCache(
getTempFolder(),
expiryTime,
memCacheSize * 1024 * 1024,
maxCachedEntites,
deleteCacheOnExit,
new ServletContextLoggerAdapter(getFilterName(), getServletContext())
) {
@Override
protected File getRealFile(CacheRequest pRequest) {
String contextRelativeURI = ServletUtil.getContextRelativeURI(((ServletCacheRequest) pRequest).getRequest());
String path = getServletContext().getRealPath(contextRelativeURI);
if (path != null) {
return new File(path);
}
return null;
}
};
log("Created cache: " + cache);
}
catch (IllegalArgumentException e) {
throw new ServletConfigException("Could not create cache: " + e.toString(), e);
}
}
private File getTempFolder() {
File tempRoot = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
if (tempRoot == null) {
throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\"");
}
return new File(tempRoot, getFilterName());
}
public void destroy() {
log("Destroying cache: " + cache);
cache = null;
super.destroy();
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// We can only cache HTTP GET/HEAD requests
if (!(pRequest instanceof HttpServletRequest
&& pResponse instanceof HttpServletResponse
&& isCachable((HttpServletRequest) pRequest))) {
pChain.doFilter(pRequest, pResponse); // Continue chain
}
else {
ServletCacheRequest cacheRequest = new ServletCacheRequest((HttpServletRequest) pRequest);
ServletCacheResponse cacheResponse = new ServletCacheResponse((HttpServletResponse) pResponse);
ServletResponseResolver resolver = new ServletResponseResolver(cacheRequest, cacheResponse, pChain);
// Render fast
try {
cache.doCached(cacheRequest, cacheResponse, resolver);
}
catch (CacheException e) {
if (e.getCause() instanceof ServletException) {
throw (ServletException) e.getCause();
}
else {
throw new ServletException(e);
}
}
finally {
pResponse.flushBuffer();
}
}
}
private boolean isCachable(HttpServletRequest pRequest) {
// TODO: Get Cache-Control: no-cache/max-age=0 and Pragma: no-cache from REQUEST too?
return "GET".equals(pRequest.getMethod()) || "HEAD".equals(pRequest.getMethod());
}
// TODO: Extract, complete and document this class, might be useful in other cases
// Maybe add it to the ServletUtil class
static class ServletContextLoggerAdapter extends Logger {
private final ServletContext context;
public ServletContextLoggerAdapter(String pName, ServletContext pContext) {
super(pName, null);
context = pContext;
}
@Override
public void log(Level pLevel, String pMessage) {
context.log(pMessage);
}
@Override
public void log(Level pLevel, String pMessage, Throwable pThrowable) {
context.log(pMessage, pThrowable);
}
}
}

View File

@@ -1,263 +1,264 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.net.HTTPUtil;
import com.twelvemonkeys.servlet.ServletResponseStreamDelegate;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
* CacheResponseWrapper class description.
* <p/>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CacheResponseWrapper.java#3 $
*/
class CacheResponseWrapper extends HttpServletResponseWrapper {
private ServletResponseStreamDelegate streamDelegate;
private CacheResponse response;
private CachedEntity cached;
private WritableCachedResponse cachedResponse;
private Boolean cacheable;
private int status;
public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) {
super(pResponse.getResponse());
response = pResponse;
cached = pCached;
init();
}
/*
NOTE: This class defers determining if a response is cacheable until the
output stream is needed.
This it the reason for the somewhat complicated logic in the add/setHeader
methods below.
*/
private void init() {
cacheable = null;
status = SC_OK;
cachedResponse = cached.createCachedResponse();
streamDelegate = new ServletResponseStreamDelegate(this) {
protected OutputStream createOutputStream() throws IOException {
// Test if this request is really cacheable, otherwise,
// just write through to underlying response, and don't cache
if (isCacheable()) {
return cachedResponse.getOutputStream();
}
else {
cachedResponse.setStatus(status);
cachedResponse.writeHeadersTo(CacheResponseWrapper.this.response);
return super.getOutputStream();
}
}
};
}
CachedResponse getCachedResponse() {
return cachedResponse.getCachedResponse();
}
public boolean isCacheable() {
// NOTE: Intentionally not synchronized
if (cacheable == null) {
cacheable = isCacheableImpl();
}
return cacheable;
}
private boolean isCacheableImpl() {
if (status != SC_OK) {
return false;
}
// Vary: *
String[] values = cachedResponse.getHeaderValues(HTTPCache.HEADER_VARY);
if (values != null) {
for (String value : values) {
if ("*".equals(value)) {
return false;
}
}
}
// Cache-Control: no-cache, no-store, must-revalidate
values = cachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")
|| StringUtil.contains(value, "no-store")
|| StringUtil.contains(value, "must-revalidate")) {
return false;
}
}
}
// Pragma: no-cache
values = cachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")) {
return false;
}
}
}
return true;
}
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
public void resetBuffer() {
// Servlet 2.3
streamDelegate.resetBuffer();
}
public void reset() {
if (Boolean.FALSE.equals(cacheable)) {
super.reset();
}
// No else, might be cacheable after all..
init();
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public boolean containsHeader(String name) {
return cachedResponse.getHeaderValues(name) != null;
}
public void sendError(int pStatusCode, String msg) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode, msg);
}
public void sendError(int pStatusCode) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode);
}
public void setStatus(int pStatusCode, String sm) {
// NOTE: This method is deprecated
setStatus(pStatusCode);
}
public void setStatus(int pStatusCode) {
// NOT cacheable unless pStatusCode == 200 (or a FEW others?)
if (pStatusCode != SC_OK) {
status = pStatusCode;
super.setStatus(pStatusCode);
}
}
public void sendRedirect(String pLocation) throws IOException {
// NOT cacheable
status = SC_MOVED_TEMPORARILY;
super.sendRedirect(pLocation);
}
public void setDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setDateHeader(pName, pValue);
}
cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void addDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addDateHeader(pName, pValue);
}
cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void setHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setHeader(pName, pValue);
}
cachedResponse.setHeader(pName, pValue);
}
public void addHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addHeader(pName, pValue);
}
cachedResponse.addHeader(pName, pValue);
}
public void setIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setIntHeader(pName, pValue);
}
cachedResponse.setHeader(pName, String.valueOf(pValue));
}
public void addIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addIntHeader(pName, pValue);
}
cachedResponse.addHeader(pName, String.valueOf(pValue));
}
public final void setContentType(String type) {
setHeader(HTTPCache.HEADER_CONTENT_TYPE, type);
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.net.HTTPUtil;
import com.twelvemonkeys.servlet.ServletResponseStreamDelegate;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
/**
* CacheResponseWrapper class description.
* <p>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* </p>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CacheResponseWrapper.java#3 $
*/
class CacheResponseWrapper extends HttpServletResponseWrapper {
private ServletResponseStreamDelegate streamDelegate;
private CacheResponse response;
private CachedEntity cached;
private WritableCachedResponse cachedResponse;
private Boolean cacheable;
private int status;
public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) {
super(pResponse.getResponse());
response = pResponse;
cached = pCached;
init();
}
/*
NOTE: This class defers determining if a response is cacheable until the
output stream is needed.
This it the reason for the somewhat complicated logic in the add/setHeader
methods below.
*/
private void init() {
cacheable = null;
status = SC_OK;
cachedResponse = cached.createCachedResponse();
streamDelegate = new ServletResponseStreamDelegate(this) {
protected OutputStream createOutputStream() throws IOException {
// Test if this request is really cacheable, otherwise,
// just write through to underlying response, and don't cache
if (isCacheable()) {
return cachedResponse.getOutputStream();
}
else {
cachedResponse.setStatus(status);
cachedResponse.writeHeadersTo(CacheResponseWrapper.this.response);
return super.getOutputStream();
}
}
};
}
CachedResponse getCachedResponse() {
return cachedResponse.getCachedResponse();
}
public boolean isCacheable() {
// NOTE: Intentionally not synchronized
if (cacheable == null) {
cacheable = isCacheableImpl();
}
return cacheable;
}
private boolean isCacheableImpl() {
if (status != SC_OK) {
return false;
}
// Vary: *
String[] values = cachedResponse.getHeaderValues(HTTPCache.HEADER_VARY);
if (values != null) {
for (String value : values) {
if ("*".equals(value)) {
return false;
}
}
}
// Cache-Control: no-cache, no-store, must-revalidate
values = cachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")
|| StringUtil.contains(value, "no-store")
|| StringUtil.contains(value, "must-revalidate")) {
return false;
}
}
}
// Pragma: no-cache
values = cachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")) {
return false;
}
}
}
return true;
}
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
public void resetBuffer() {
// Servlet 2.3
streamDelegate.resetBuffer();
}
public void reset() {
if (Boolean.FALSE.equals(cacheable)) {
super.reset();
}
// No else, might be cacheable after all..
init();
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public boolean containsHeader(String name) {
return cachedResponse.getHeaderValues(name) != null;
}
public void sendError(int pStatusCode, String msg) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode, msg);
}
public void sendError(int pStatusCode) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode);
}
public void setStatus(int pStatusCode, String sm) {
// NOTE: This method is deprecated
setStatus(pStatusCode);
}
public void setStatus(int pStatusCode) {
// NOT cacheable unless pStatusCode == 200 (or a FEW others?)
if (pStatusCode != SC_OK) {
status = pStatusCode;
super.setStatus(pStatusCode);
}
}
public void sendRedirect(String pLocation) throws IOException {
// NOT cacheable
status = SC_MOVED_TEMPORARILY;
super.sendRedirect(pLocation);
}
public void setDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setDateHeader(pName, pValue);
}
cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void addDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addDateHeader(pName, pValue);
}
cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void setHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setHeader(pName, pValue);
}
cachedResponse.setHeader(pName, pValue);
}
public void addHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addHeader(pName, pValue);
}
cachedResponse.addHeader(pName, pValue);
}
public void setIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setIntHeader(pName, pValue);
}
cachedResponse.setHeader(pName, String.valueOf(pValue));
}
public void addIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addIntHeader(pName, pValue);
}
cachedResponse.addHeader(pName, String.valueOf(pValue));
}
public final void setContentType(String type) {
setHeader(HTTPCache.HEADER_CONTENT_TYPE, type);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,275 +1,276 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.net.HTTPUtil;
import com.twelvemonkeys.servlet.ServletResponseStreamDelegate;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
/**
* CacheResponseWrapper class description.
* <p/>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: SerlvetCacheResponseWrapper.java#2 $
*/
class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper {
private ServletResponseStreamDelegate streamDelegate;
private CacheResponse cacheResponse;
private Boolean cacheable;
private int status;
public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) {
super(pServletResponse);
cacheResponse = pResponse;
init();
}
/*
NOTE: This class defers determining if a response is cacheable until the
output stream is needed.
This it the reason for the somewhat complicated logic in the add/setHeader
methods below.
*/
private void init() {
cacheable = null;
status = SC_OK;
streamDelegate = new ServletResponseStreamDelegate(this) {
protected OutputStream createOutputStream() throws IOException {
// Test if this request is really cacheable, otherwise,
// just write through to underlying response, and don't cache
if (isCacheable()) {
return cacheResponse.getOutputStream();
}
else {
// TODO: We need to tell the cache about this, somehow...
writeHeaders(cacheResponse, (HttpServletResponse) getResponse());
return super.getOutputStream();
}
}
};
}
private void writeHeaders(final CacheResponse pResponse, final HttpServletResponse pServletResponse) {
Map<String,List<String>> headers = pResponse.getHeaders();
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
for (int i = 0; i < header.getValue().size(); i++) {
String value = header.getValue().get(i);
if (i == 0) {
pServletResponse.setHeader(header.getKey(), value);
}
else {
pServletResponse.addHeader(header.getKey(), value);
}
}
}
}
public boolean isCacheable() {
// NOTE: Intentionally not synchronized
if (cacheable == null) {
cacheable = isCacheableImpl();
}
return cacheable;
}
private boolean isCacheableImpl() {
// TODO: This code is duped in the cache...
if (status != SC_OK) {
return false;
}
// Vary: *
List<String> values = cacheResponse.getHeaders().get(HTTPCache.HEADER_VARY);
if (values != null) {
for (String value : values) {
if ("*".equals(value)) {
return false;
}
}
}
// Cache-Control: no-cache, no-store, must-revalidate
values = cacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")
|| StringUtil.contains(value, "no-store")
|| StringUtil.contains(value, "must-revalidate")) {
return false;
}
}
}
// Pragma: no-cache
values = cacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")) {
return false;
}
}
}
return true;
}
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
public void resetBuffer() {
// Servlet 2.3
streamDelegate.resetBuffer();
}
public void reset() {
if (Boolean.FALSE.equals(cacheable)) {
super.reset();
}
// No else, might be cacheable after all..
init();
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public boolean containsHeader(String name) {
return cacheResponse.getHeaders().get(name) != null;
}
public void sendError(int pStatusCode, String msg) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode, msg);
}
public void sendError(int pStatusCode) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode);
}
public void setStatus(int pStatusCode, String sm) {
// NOTE: This method is deprecated
setStatus(pStatusCode);
}
public void setStatus(int pStatusCode) {
// NOT cacheable unless pStatusCode == 200 (or a FEW others?)
if (pStatusCode != SC_OK) {
status = pStatusCode;
super.setStatus(pStatusCode);
}
}
public void sendRedirect(String pLocation) throws IOException {
// NOT cacheable
status = SC_MOVED_TEMPORARILY;
super.sendRedirect(pLocation);
}
public void setDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setDateHeader(pName, pValue);
}
cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void addDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addDateHeader(pName, pValue);
}
cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void setHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setHeader(pName, pValue);
}
cacheResponse.setHeader(pName, pValue);
}
public void addHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addHeader(pName, pValue);
}
cacheResponse.addHeader(pName, pValue);
}
public void setIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setIntHeader(pName, pValue);
}
cacheResponse.setHeader(pName, String.valueOf(pValue));
}
public void addIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addIntHeader(pName, pValue);
}
cacheResponse.addHeader(pName, String.valueOf(pValue));
}
public final void setContentType(String type) {
setHeader(HTTPCache.HEADER_CONTENT_TYPE, type);
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.net.HTTPUtil;
import com.twelvemonkeys.servlet.ServletResponseStreamDelegate;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
/**
* CacheResponseWrapper class description.
* <p>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* </p>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: SerlvetCacheResponseWrapper.java#2 $
*/
class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper {
private ServletResponseStreamDelegate streamDelegate;
private CacheResponse cacheResponse;
private Boolean cacheable;
private int status;
public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) {
super(pServletResponse);
cacheResponse = pResponse;
init();
}
/*
NOTE: This class defers determining if a response is cacheable until the
output stream is needed.
This it the reason for the somewhat complicated logic in the add/setHeader
methods below.
*/
private void init() {
cacheable = null;
status = SC_OK;
streamDelegate = new ServletResponseStreamDelegate(this) {
protected OutputStream createOutputStream() throws IOException {
// Test if this request is really cacheable, otherwise,
// just write through to underlying response, and don't cache
if (isCacheable()) {
return cacheResponse.getOutputStream();
}
else {
// TODO: We need to tell the cache about this, somehow...
writeHeaders(cacheResponse, (HttpServletResponse) getResponse());
return super.getOutputStream();
}
}
};
}
private void writeHeaders(final CacheResponse pResponse, final HttpServletResponse pServletResponse) {
Map<String,List<String>> headers = pResponse.getHeaders();
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
for (int i = 0; i < header.getValue().size(); i++) {
String value = header.getValue().get(i);
if (i == 0) {
pServletResponse.setHeader(header.getKey(), value);
}
else {
pServletResponse.addHeader(header.getKey(), value);
}
}
}
}
public boolean isCacheable() {
// NOTE: Intentionally not synchronized
if (cacheable == null) {
cacheable = isCacheableImpl();
}
return cacheable;
}
private boolean isCacheableImpl() {
// TODO: This code is duped in the cache...
if (status != SC_OK) {
return false;
}
// Vary: *
List<String> values = cacheResponse.getHeaders().get(HTTPCache.HEADER_VARY);
if (values != null) {
for (String value : values) {
if ("*".equals(value)) {
return false;
}
}
}
// Cache-Control: no-cache, no-store, must-revalidate
values = cacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")
|| StringUtil.contains(value, "no-store")
|| StringUtil.contains(value, "must-revalidate")) {
return false;
}
}
}
// Pragma: no-cache
values = cacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")) {
return false;
}
}
}
return true;
}
public void flushBuffer() throws IOException {
streamDelegate.flushBuffer();
}
public void resetBuffer() {
// Servlet 2.3
streamDelegate.resetBuffer();
}
public void reset() {
if (Boolean.FALSE.equals(cacheable)) {
super.reset();
}
// No else, might be cacheable after all..
init();
}
public ServletOutputStream getOutputStream() throws IOException {
return streamDelegate.getOutputStream();
}
public PrintWriter getWriter() throws IOException {
return streamDelegate.getWriter();
}
public boolean containsHeader(String name) {
return cacheResponse.getHeaders().get(name) != null;
}
public void sendError(int pStatusCode, String msg) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode, msg);
}
public void sendError(int pStatusCode) throws IOException {
// NOT cacheable
status = pStatusCode;
super.sendError(pStatusCode);
}
public void setStatus(int pStatusCode, String sm) {
// NOTE: This method is deprecated
setStatus(pStatusCode);
}
public void setStatus(int pStatusCode) {
// NOT cacheable unless pStatusCode == 200 (or a FEW others?)
if (pStatusCode != SC_OK) {
status = pStatusCode;
super.setStatus(pStatusCode);
}
}
public void sendRedirect(String pLocation) throws IOException {
// NOT cacheable
status = SC_MOVED_TEMPORARILY;
super.sendRedirect(pLocation);
}
public void setDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setDateHeader(pName, pValue);
}
cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void addDateHeader(String pName, long pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addDateHeader(pName, pValue);
}
cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue));
}
public void setHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setHeader(pName, pValue);
}
cacheResponse.setHeader(pName, pValue);
}
public void addHeader(String pName, String pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addHeader(pName, pValue);
}
cacheResponse.addHeader(pName, pValue);
}
public void setIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.setIntHeader(pName, pValue);
}
cacheResponse.setHeader(pName, String.valueOf(pValue));
}
public void addIntHeader(String pName, int pValue) {
// If in write-trough-mode, set headers directly
if (Boolean.FALSE.equals(cacheable)) {
super.addIntHeader(pName, pValue);
}
cacheResponse.addHeader(pName, String.valueOf(pValue));
}
public final void setContentType(String type) {
setHeader(HTTPCache.HEADER_CONTENT_TYPE, type);
}
}

View File

@@ -1,44 +1,43 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.fileupload;
/**
* FileSizeExceededException
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: FileSizeExceededException.java#1 $
*/
public class FileSizeExceededException extends FileUploadException {
public FileSizeExceededException(Throwable pCause) {
super(pCause.getMessage(), pCause);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.fileupload;
/**
* FileSizeExceededException
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: FileSizeExceededException.java#1 $
*/
public class FileSizeExceededException extends FileUploadException {
public FileSizeExceededException(Throwable pCause) {
super(pCause.getMessage(), pCause);
}
}

View File

@@ -1,54 +1,53 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.fileupload;
import javax.servlet.ServletException;
/**
* FileUploadException
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: FileUploadException.java#1 $
*/
public class FileUploadException extends ServletException {
public FileUploadException(String pMessage) {
super(pMessage);
}
public FileUploadException(String pMessage, Throwable pCause) {
super(pMessage, pCause);
}
public FileUploadException(Throwable pCause) {
super(pCause.getMessage(), pCause);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.fileupload;
import javax.servlet.ServletException;
/**
* FileUploadException
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: FileUploadException.java#1 $
*/
public class FileUploadException extends ServletException {
public FileUploadException(String pMessage) {
super(pMessage);
}
public FileUploadException(String pMessage, Throwable pCause) {
super(pMessage, pCause);
}
public FileUploadException(Throwable pCause) {
super(pCause.getMessage(), pCause);
}
}

View File

@@ -1,143 +1,149 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.gzip;
import com.twelvemonkeys.servlet.GenericFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* A filter to reduce the output size of web resources.
* <p/>
* The HTTP protocol supports compression of the content to reduce network
* bandwidth. The important headers involved, are the {@code Accept-Encoding}
* request header, and the {@code Content-Encoding} response header.
* This feature can be used to further reduce the number of bytes transferred
* over the network, at the cost of some extra processing time at both endpoints.
* Most modern browsers supports compression in GZIP format, which is fairly
* efficient in cost/compression ratio.
* <p/>
* The filter tests for the presence of an {@code Accept-Encoding} header with a
* value of {@code "gzip"} (several different encoding header values are
* possible in one header). If not present, the filter simply passes the
* request/response pair through, leaving it untouched. If present, the
* {@code Content-Encoding} header is set, with the value {@code "gzip"},
* and the response is wrapped.
* The response output stream is wrapped in a
* {@link java.util.zip.GZIPOutputStream} which performs the GZIP encoding.
* For efficiency, the filter does not buffer the response, but writes through
* the gzipped output stream.
* <p/>
* <b>Configuration</b><br/>
* To use {@code GZIPFilter} in your web-application, you simply need to add it
* to your web descriptor ({@code web.xml}). If using a servlet container that
* supports the Servlet 2.4 spec, the new {@code dispatcher} element should be
* used, and set to {@code REQUEST/FORWARD}, to make sure the filter is invoked
* only once for requests.
* If using an older web descriptor, set the {@code init-param}
* {@code "once-per-request"} to {@code "true"} (this will have the same effect,
* but might perform slightly worse than the 2.4 version).
* Please see the examples below.
* <b>Servlet 2.4 version, filter section:</b><br/>
* <pre>
* &lt;!-- GZIP Filter Configuration --&gt;
* &lt;filter&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;filter-class&gt;com.twelvemonkeys.servlet.GZIPFilter&lt;/filter-class&gt;
* &lt;/filter&gt;
* </pre>
* <b>Filter-mapping section:</b><br/>
* <pre>
* &lt;!-- GZIP Filter Mapping --&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.jsp&lt; /url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* </pre>
* <p/>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* <p/>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: GZIPFilter.java#1 $
*/
public class GZIPFilter extends GenericFilter {
{
oncePerRequest = true;
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// Can only filter HTTP responses
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
HttpServletResponse response = (HttpServletResponse) pResponse;
// If GZIP is supported, use compression
String accept = request.getHeader("Accept-Encoding");
if (accept != null && accept.contains("gzip")) {
//System.out.println("GZIP supported, compressing.");
GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response);
try {
pChain.doFilter(pRequest, wrapped);
}
finally {
wrapped.flushResponse();
}
return;
}
}
// Else, continue chain
pChain.doFilter(pRequest, pResponse);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.gzip;
import com.twelvemonkeys.servlet.GenericFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* A filter to reduce the output size of web resources.
* <p>
* The HTTP protocol supports compression of the content to reduce network
* bandwidth. The important headers involved, are the {@code Accept-Encoding}
* request header, and the {@code Content-Encoding} response header.
* This feature can be used to further reduce the number of bytes transferred
* over the network, at the cost of some extra processing time at both endpoints.
* Most modern browsers supports compression in GZIP format, which is fairly
* efficient in cost/compression ratio.
* </p>
* <p>
* The filter tests for the presence of an {@code Accept-Encoding} header with a
* value of {@code "gzip"} (several different encoding header values are
* possible in one header). If not present, the filter simply passes the
* request/response pair through, leaving it untouched. If present, the
* {@code Content-Encoding} header is set, with the value {@code "gzip"},
* and the response is wrapped.
* The response output stream is wrapped in a
* {@link java.util.zip.GZIPOutputStream} which performs the GZIP encoding.
* For efficiency, the filter does not buffer the response, but writes through
* the gzipped output stream.
* </p>
* <p>
* <b>Configuration</b>
* <br>
* To use {@code GZIPFilter} in your web-application, you simply need to add it
* to your web descriptor ({@code web.xml}). If using a servlet container that
* supports the Servlet 2.4 spec, the new {@code dispatcher} element should be
* used, and set to {@code REQUEST/FORWARD}, to make sure the filter is invoked
* only once for requests.
* If using an older web descriptor, set the {@code init-param}
* {@code "once-per-request"} to {@code "true"} (this will have the same effect,
* but might perform slightly worse than the 2.4 version).
* Please see the examples below.
* <b>Servlet 2.4 version, filter section:</b>
* </p>
* <br>
* <pre>
* &lt;!-- GZIP Filter Configuration --&gt;
* &lt;filter&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;filter-class&gt;com.twelvemonkeys.servlet.GZIPFilter&lt;/filter-class&gt;
* &lt;/filter&gt;
* </pre>
* <b>Filter-mapping section:</b>
* <br>
* <pre>
* &lt;!-- GZIP Filter Mapping --&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;gzip&lt;/filter-name&gt;
* &lt;url-pattern&gt;*.jsp&lt; /url-pattern&gt;
* &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
* &lt;dispatcher&gt;FORWARD&lt;/dispatcher&gt;
* &lt;/filter-mapping&gt;
* </pre>
* <p>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two
* Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* </p>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: GZIPFilter.java#1 $
*/
public class GZIPFilter extends GenericFilter {
{
oncePerRequest = true;
}
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// Can only filter HTTP responses
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
HttpServletResponse response = (HttpServletResponse) pResponse;
// If GZIP is supported, use compression
String accept = request.getHeader("Accept-Encoding");
if (accept != null && accept.contains("gzip")) {
//System.out.println("GZIP supported, compressing.");
GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response);
try {
pChain.doFilter(pRequest, wrapped);
}
finally {
wrapped.flushResponse();
}
return;
}
}
// Else, continue chain
pChain.doFilter(pRequest, pResponse);
}
}

View File

@@ -1,149 +1,150 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.gzip;
import com.twelvemonkeys.servlet.OutputStreamAdapter;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
/**
* GZIPResponseWrapper class description.
* <p/>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: GZIPResponseWrapper.java#1 $
*/
public class GZIPResponseWrapper extends HttpServletResponseWrapper {
// TODO: Remove/update ETags if needed? Read the spec (RFC 2616) on Vary/ETag for caching
protected ServletOutputStream out;
protected PrintWriter writer;
protected GZIPOutputStream gzipOut;
protected int contentLength = -1;
public GZIPResponseWrapper(final HttpServletResponse response) {
super(response);
response.addHeader("Content-Encoding", "gzip");
response.addHeader("Vary", "Accept");
}
public ServletOutputStream createOutputStream() throws IOException {
// FIX: Write directly to servlet output stream, for faster responses.
// Relies on chunked streams, or buffering in the servlet engine.
if (contentLength >= 0) {
gzipOut = new GZIPOutputStream(getResponse().getOutputStream(), contentLength);
}
else {
gzipOut = new GZIPOutputStream(getResponse().getOutputStream());
}
// Wrap in ServletOutputStream and return
return new OutputStreamAdapter(gzipOut);
}
// TODO: Move this to flushbuffer or something? Hmmm..
public void flushResponse() throws IOException {
try {
// Finish GZIP encodig
if (gzipOut != null) {
gzipOut.finish();
}
flushBuffer();
}
finally {
// Close stream
if (writer != null) {
writer.close();
}
else {
if (out != null) {
out.close();
}
}
}
}
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
}
else if (out != null) {
out.flush();
}
}
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called!");
}
if (out == null) {
out = createOutputStream();
}
return out;
}
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return (writer);
}
if (out != null) {
throw new IllegalStateException("getOutputStream() has already been called!");
}
out = createOutputStream();
// TODO: This is wrong. Should use getCharacterEncoding() or "ISO-8859-1" if getCE returns null.
writer = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
return writer;
}
public void setContentLength(int pLength) {
// NOTE: Do not call super, as we will shrink the size.
contentLength = pLength;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.gzip;
import com.twelvemonkeys.servlet.OutputStreamAdapter;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
/**
* GZIPResponseWrapper class description.
* <p>
* Based on ideas and code found in the ONJava article
* <a href="http://www.onjava.com/pub/a/onjava/2003/11/19/filters.html">Two Servlet Filters Every Web Application Should Have</a>
* by Jayson Falkner.
* </p>
*
* @author Jayson Falkner
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: GZIPResponseWrapper.java#1 $
*/
public class GZIPResponseWrapper extends HttpServletResponseWrapper {
// TODO: Remove/update ETags if needed? Read the spec (RFC 2616) on Vary/ETag for caching
protected ServletOutputStream out;
protected PrintWriter writer;
protected GZIPOutputStream gzipOut;
protected int contentLength = -1;
public GZIPResponseWrapper(final HttpServletResponse response) {
super(response);
response.addHeader("Content-Encoding", "gzip");
response.addHeader("Vary", "Accept");
}
public ServletOutputStream createOutputStream() throws IOException {
// FIX: Write directly to servlet output stream, for faster responses.
// Relies on chunked streams, or buffering in the servlet engine.
if (contentLength >= 0) {
gzipOut = new GZIPOutputStream(getResponse().getOutputStream(), contentLength);
}
else {
gzipOut = new GZIPOutputStream(getResponse().getOutputStream());
}
// Wrap in ServletOutputStream and return
return new OutputStreamAdapter(gzipOut);
}
// TODO: Move this to flushbuffer or something? Hmmm..
public void flushResponse() throws IOException {
try {
// Finish GZIP encodig
if (gzipOut != null) {
gzipOut.finish();
}
flushBuffer();
}
finally {
// Close stream
if (writer != null) {
writer.close();
}
else {
if (out != null) {
out.close();
}
}
}
}
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
}
else if (out != null) {
out.flush();
}
}
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called!");
}
if (out == null) {
out = createOutputStream();
}
return out;
}
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return (writer);
}
if (out != null) {
throw new IllegalStateException("getOutputStream() has already been called!");
}
out = createOutputStream();
// TODO: This is wrong. Should use getCharacterEncoding() or "ISO-8859-1" if getCE returns null.
writer = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
return writer;
}
public void setContentLength(int pLength) {
// NOTE: Do not call super, as we will shrink the size.
contentLength = pLength;
}
}

View File

@@ -1,213 +1,214 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.zip.CRC32;
/**
* Creates a minimal 1 x 1 pixel PNG image, in a color specified by the
* {@code "color"} parameter. The color is HTML-style #RRGGBB, with two
* digits hex number for red, green and blue (the hash, '#', is optional).
* <p/>
* The class does only byte manipulation, there is no server-side image
* processing involving AWT ({@code Toolkit} class) of any kind.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ColorServlet.java#2 $
*/
public class ColorServlet extends GenericServlet {
private final static String RGB_PARAME = "color";
// A minimal, one color indexed PNG
private final static byte[] PNG_IMG = new byte[]{
(byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', // PNG signature (8 bytes)
0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, // IHDR length (13)
(byte) 'I', (byte) 'H', (byte) 'D', (byte) 'R', // Image header
0x00, 0x00, 0x00, 0x01, // width
0x00, 0x00, 0x00, 0x01, // height
0x01, 0x03, 0x00, 0x00, 0x00, // bits, color type, compression, filter, interlace
0x25, (byte) 0xdb, 0x56, (byte) 0xca, // IHDR CRC
0x00, 0x00, 0x00, 0x03, // PLTE length (3)
(byte) 'P', (byte) 'L', (byte) 'T', (byte) 'E', // Palette
0x00, 0x00, (byte) 0xff, // red, green, blue (updated by this servlet)
(byte) 0x8a, (byte) 0x78, (byte) 0xd2, 0x57, // PLTE CRC
0x00, 0x00, 0x00, 0x0a, // IDAT length (10)
(byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T', // Image data
0x78, (byte) 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
(byte) 0xe5, 0x27, (byte) 0xde, (byte) 0xfc, // IDAT CRC
0x00, 0x00, 0x00, 0x00, // IEND length (0)
(byte) 'I', (byte) 'E', (byte) 'N', (byte) 'D', // Image end
(byte) 0xae, (byte) 0x42, (byte) 0x60, (byte) 0x82 // IEND CRC
};
private final static int PLTE_CHUNK_START = 37; // after chunk length
private final static int PLTE_CHUNK_LENGTH = 7; // chunk name & data
private final static int RED_IDX = 4;
private final static int GREEN_IDX = RED_IDX + 1;
private final static int BLUE_IDX = GREEN_IDX + 1;
private final CRC32 crc = new CRC32();
/**
* Creates a ColorDroplet.
*/
public ColorServlet() {
super();
}
/**
* Renders the 1 x 1 single color PNG to the response.
*
* @see ColorServlet class description
*
* @param pRequest the request
* @param pResponse the response
*
* @throws IOException
* @throws ServletException
*/
public void service(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException {
int red = 0;
int green = 0;
int blue = 0;
// Get color parameter and parse color
String rgb = pRequest.getParameter(RGB_PARAME);
if (rgb != null && rgb.length() >= 6 && rgb.length() <= 7) {
int index = 0;
// If the hash ('#') character is included, skip it.
if (rgb.length() == 7) {
index++;
}
try {
// Two digit hex for each color
String r = rgb.substring(index, index += 2);
red = Integer.parseInt(r, 0x10);
String g = rgb.substring(index, index += 2);
green = Integer.parseInt(g, 0x10);
String b = rgb.substring(index, index += 2);
blue = Integer.parseInt(b, 0x10);
}
catch (NumberFormatException nfe) {
log("Wrong color format for ColorDroplet: " + rgb + ". Must be RRGGBB.");
}
}
// Set MIME type for PNG
pResponse.setContentType("image/png");
ServletOutputStream out = pResponse.getOutputStream();
try {
// Write header (and palette chunk length)
out.write(PNG_IMG, 0, PLTE_CHUNK_START);
// Create palette chunk, excl lenght, and write
byte[] palette = makePalette(red, green, blue);
out.write(palette);
// Write image data until end
int pos = PLTE_CHUNK_START + PLTE_CHUNK_LENGTH + 4;
out.write(PNG_IMG, pos, PNG_IMG.length - pos);
}
finally {
out.flush();
}
}
/**
* Updates the CRC for a byte array. Note that the byte array must be at
* least {@code pOff + pLen + 4} bytes long, as the CRC is stored in the
* 4 last bytes.
*
* @param pBytes the bytes to create CRC for
* @param pOff the offset into the byte array to create CRC for
* @param pLen the length of the byte array to create CRC for
*/
private void updateCRC(byte[] pBytes, int pOff, int pLen) {
int value;
synchronized (crc) {
crc.reset();
crc.update(pBytes, pOff, pLen);
value = (int) crc.getValue();
}
pBytes[pOff + pLen ] = (byte) ((value >> 24) & 0xff);
pBytes[pOff + pLen + 1] = (byte) ((value >> 16) & 0xff);
pBytes[pOff + pLen + 2] = (byte) ((value >> 8) & 0xff);
pBytes[pOff + pLen + 3] = (byte) ( value & 0xff);
}
/**
* Creates a PNG palette (PLTE) chunk with one color.
* The palette chunk data is always 3 bytes in length (one byte per color
* component).
* The returned byte array is then {@code 4 + 3 + 4 = 11} bytes,
* including chunk header, data and CRC.
*
* @param pRed the red component
* @param pGreen the reen component
* @param pBlue the blue component
*
* @return the bytes for the PLTE chunk, including CRC (but not length)
*/
private byte[] makePalette(int pRed, int pGreen, int pBlue) {
byte[] palette = new byte[PLTE_CHUNK_LENGTH + 4];
System.arraycopy(PNG_IMG, PLTE_CHUNK_START, palette, 0, PLTE_CHUNK_LENGTH);
palette[RED_IDX] = (byte) pRed;
palette[GREEN_IDX] = (byte) pGreen;
palette[BLUE_IDX] = (byte) pBlue;
updateCRC(palette, 0, PLTE_CHUNK_LENGTH);
return palette;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.zip.CRC32;
/**
* Creates a minimal 1 x 1 pixel PNG image, in a color specified by the
* {@code "color"} parameter. The color is HTML-style #RRGGBB, with two
* digits hex number for red, green and blue (the hash, '#', is optional).
* <p>
* The class does only byte manipulation, there is no server-side image
* processing involving AWT ({@code Toolkit} class) of any kind.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ColorServlet.java#2 $
*/
public class ColorServlet extends GenericServlet {
private final static String RGB_PARAME = "color";
// A minimal, one color indexed PNG
private final static byte[] PNG_IMG = new byte[]{
(byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', // PNG signature (8 bytes)
0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, // IHDR length (13)
(byte) 'I', (byte) 'H', (byte) 'D', (byte) 'R', // Image header
0x00, 0x00, 0x00, 0x01, // width
0x00, 0x00, 0x00, 0x01, // height
0x01, 0x03, 0x00, 0x00, 0x00, // bits, color type, compression, filter, interlace
0x25, (byte) 0xdb, 0x56, (byte) 0xca, // IHDR CRC
0x00, 0x00, 0x00, 0x03, // PLTE length (3)
(byte) 'P', (byte) 'L', (byte) 'T', (byte) 'E', // Palette
0x00, 0x00, (byte) 0xff, // red, green, blue (updated by this servlet)
(byte) 0x8a, (byte) 0x78, (byte) 0xd2, 0x57, // PLTE CRC
0x00, 0x00, 0x00, 0x0a, // IDAT length (10)
(byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T', // Image data
0x78, (byte) 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
(byte) 0xe5, 0x27, (byte) 0xde, (byte) 0xfc, // IDAT CRC
0x00, 0x00, 0x00, 0x00, // IEND length (0)
(byte) 'I', (byte) 'E', (byte) 'N', (byte) 'D', // Image end
(byte) 0xae, (byte) 0x42, (byte) 0x60, (byte) 0x82 // IEND CRC
};
private final static int PLTE_CHUNK_START = 37; // after chunk length
private final static int PLTE_CHUNK_LENGTH = 7; // chunk name & data
private final static int RED_IDX = 4;
private final static int GREEN_IDX = RED_IDX + 1;
private final static int BLUE_IDX = GREEN_IDX + 1;
private final CRC32 crc = new CRC32();
/**
* Creates a ColorDroplet.
*/
public ColorServlet() {
super();
}
/**
* Renders the 1 x 1 single color PNG to the response.
*
* @see ColorServlet class description
*
* @param pRequest the request
* @param pResponse the response
*
* @throws IOException
* @throws ServletException
*/
public void service(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException {
int red = 0;
int green = 0;
int blue = 0;
// Get color parameter and parse color
String rgb = pRequest.getParameter(RGB_PARAME);
if (rgb != null && rgb.length() >= 6 && rgb.length() <= 7) {
int index = 0;
// If the hash ('#') character is included, skip it.
if (rgb.length() == 7) {
index++;
}
try {
// Two digit hex for each color
String r = rgb.substring(index, index += 2);
red = Integer.parseInt(r, 0x10);
String g = rgb.substring(index, index += 2);
green = Integer.parseInt(g, 0x10);
String b = rgb.substring(index, index += 2);
blue = Integer.parseInt(b, 0x10);
}
catch (NumberFormatException nfe) {
log("Wrong color format for ColorDroplet: " + rgb + ". Must be RRGGBB.");
}
}
// Set MIME type for PNG
pResponse.setContentType("image/png");
ServletOutputStream out = pResponse.getOutputStream();
try {
// Write header (and palette chunk length)
out.write(PNG_IMG, 0, PLTE_CHUNK_START);
// Create palette chunk, excl lenght, and write
byte[] palette = makePalette(red, green, blue);
out.write(palette);
// Write image data until end
int pos = PLTE_CHUNK_START + PLTE_CHUNK_LENGTH + 4;
out.write(PNG_IMG, pos, PNG_IMG.length - pos);
}
finally {
out.flush();
}
}
/**
* Updates the CRC for a byte array. Note that the byte array must be at
* least {@code pOff + pLen + 4} bytes long, as the CRC is stored in the
* 4 last bytes.
*
* @param pBytes the bytes to create CRC for
* @param pOff the offset into the byte array to create CRC for
* @param pLen the length of the byte array to create CRC for
*/
private void updateCRC(byte[] pBytes, int pOff, int pLen) {
int value;
synchronized (crc) {
crc.reset();
crc.update(pBytes, pOff, pLen);
value = (int) crc.getValue();
}
pBytes[pOff + pLen ] = (byte) ((value >> 24) & 0xff);
pBytes[pOff + pLen + 1] = (byte) ((value >> 16) & 0xff);
pBytes[pOff + pLen + 2] = (byte) ((value >> 8) & 0xff);
pBytes[pOff + pLen + 3] = (byte) ( value & 0xff);
}
/**
* Creates a PNG palette (PLTE) chunk with one color.
* The palette chunk data is always 3 bytes in length (one byte per color
* component).
* The returned byte array is then {@code 4 + 3 + 4 = 11} bytes,
* including chunk header, data and CRC.
*
* @param pRed the red component
* @param pGreen the reen component
* @param pBlue the blue component
*
* @return the bytes for the PLTE chunk, including CRC (but not length)
*/
private byte[] makePalette(int pRed, int pGreen, int pBlue) {
byte[] palette = new byte[PLTE_CHUNK_LENGTH + 4];
System.arraycopy(PNG_IMG, PLTE_CHUNK_START, palette, 0, PLTE_CHUNK_LENGTH);
palette[RED_IDX] = (byte) pRed;
palette[GREEN_IDX] = (byte) pGreen;
palette[BLUE_IDX] = (byte) pBlue;
updateCRC(palette, 0, PLTE_CHUNK_LENGTH);
return palette;
}
}

View File

@@ -1,61 +1,60 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import javax.servlet.ServletRequest;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* ComposeFilter
* <p/>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ComposeFilter.java#1 $
*/
public class ComposeFilter extends ImageFilter {
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException {
// 1. Load different image, locally (using ServletContext.getResource)
// - Allow loading other filtered sources, or servlets? For example to
// apply filename or timestamps?
// - Allow applying text directly? Variables?
// 2. Apply transformations from config
// - Relative positioning
// - Relative scaling
// - Repeat (fill-pattern)?
// - Rotation?
// - Transparency?
// - Background or foreground (layers)?
// 3. Apply loaded image to original image (or vice versa?).
return pImage;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import javax.servlet.ServletRequest;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* ComposeFilter
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ComposeFilter.java#1 $
*/
public class ComposeFilter extends ImageFilter {
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException {
// 1. Load different image, locally (using ServletContext.getResource)
// - Allow loading other filtered sources, or servlets? For example to
// apply filename or timestamps?
// - Allow applying text directly? Variables?
// 2. Apply transformations from config
// - Relative positioning
// - Relative scaling
// - Repeat (fill-pattern)?
// - Rotation?
// - Transparency?
// - Background or foreground (layers)?
// 3. Apply loaded image to original image (or vice versa?).
return pImage;
}
}

View File

@@ -1,442 +1,441 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.ImageIO;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
/**
* This filter implements server side content negotiation and transcoding for
* images.
*
* @todo Add support for automatic recognition of known browsers, to avoid
* unneccessary conversion (as IE supports PNG, the latests FireFox supports
* JPEG and GIF, etc. even though they both don't explicitly list these formats
* in their Accept headers).
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ContentNegotiationFilter.java#1 $
*/
public class ContentNegotiationFilter extends ImageFilter {
private final static String MIME_TYPE_IMAGE_PREFIX = "image/";
private static final String MIME_TYPE_IMAGE_ANY = MIME_TYPE_IMAGE_PREFIX + "*";
private static final String MIME_TYPE_ANY = "*/*";
private static final String HTTP_HEADER_ACCEPT = "Accept";
private static final String HTTP_HEADER_VARY = "Vary";
protected static final String HTTP_HEADER_USER_AGENT = "User-Agent";
private static final String FORMAT_JPEG = "image/jpeg";
private static final String FORMAT_WBMP = "image/wbmp";
private static final String FORMAT_GIF = "image/gif";
private static final String FORMAT_PNG = "image/png";
private final static String[] sKnownFormats = new String[] {
FORMAT_JPEG, FORMAT_PNG, FORMAT_GIF, FORMAT_WBMP
};
private float[] knownFormatQuality = new float[] {
1f, 1f, 0.99f, 0.5f
};
private HashMap<String, Float> formatQuality; // HashMap, as I need to clone this for each request
private final Object lock = new Object();
/*
private Pattern[] mKnownAgentPatterns;
private String[] mKnownAgentAccpets;
*/
{
// Hack: Make sure the filter don't trigger all the time
// See: super.trigger(ServletRequest)
triggerParams = new String[] {};
}
/*
public void setAcceptMappings(String pPropertiesFile) {
// NOTE: Supposed to be:
// <agent-name>=<reg-exp>
// <agent-name>.accept=<http-accept-header>
Properties mappings = new Properties();
try {
mappings.load(getServletContext().getResourceAsStream(pPropertiesFile));
List patterns = new ArrayList();
List accepts = new ArrayList();
for (Iterator iterator = mappings.keySet().iterator(); iterator.hasNext();) {
String agent = (String) iterator.next();
if (agent.endsWith(".accept")) {
continue;
}
try {
patterns.add(Pattern.compile((String) mappings.get(agent)));
// TODO: Consider preparsing ACCEPT header??
accepts.add(mappings.get(agent + ".accept"));
}
catch (PatternSyntaxException e) {
log("Could not parse User-Agent identification for " + agent, e);
}
mKnownAgentPatterns = (Pattern[]) patterns.toArray(new Pattern[patterns.size()]);
mKnownAgentAccpets = (String[]) accepts.toArray(new String[accepts.size()]);
}
}
catch (IOException e) {
log("Could not read accetp-mappings properties file: " + pPropertiesFile, e);
}
}
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// NOTE: super invokes trigger() and image specific doFilter() if needed
super.doFilterImpl(pRequest, pResponse, pChain);
if (pResponse instanceof HttpServletResponse) {
// Update the Vary HTTP header field
((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT);
//((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_USER_AGENT);
}
}
/**
* Makes sure the filter triggers for unknown file formats.
*
* @param pRequest the request
* @return {@code true} if the filter should execute, {@code false}
* otherwise
*/
protected boolean trigger(ServletRequest pRequest) {
boolean trigger = false;
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
String accept = getAcceptedFormats(request);
String originalFormat = getServletContext().getMimeType(request.getRequestURI());
//System.out.println("Accept: " + accept);
//System.out.println("Original format: " + originalFormat);
// Only override original format if it is not accpeted by the client
// Note: Only explicit matches are okay, */* or image/* is not.
if (!StringUtil.contains(accept, originalFormat)) {
trigger = true;
}
}
// Call super, to allow content negotiation even though format is supported
return trigger || super.trigger(pRequest);
}
private String getAcceptedFormats(HttpServletRequest pRequest) {
return pRequest.getHeader(HTTP_HEADER_ACCEPT);
}
/*
private String getAcceptedFormats(HttpServletRequest pRequest) {
String accept = pRequest.getHeader(HTTP_HEADER_ACCEPT);
// Check if User-Agent is in list of known agents
if (mKnownAgentPatterns != null) {
String agent = pRequest.getHeader(HTTP_HEADER_USER_AGENT);
for (int i = 0; i < mKnownAgentPatterns.length; i++) {
Pattern pattern = mKnownAgentPatterns[i];
if (pattern.matcher(agent).matches()) {
// Merge known with real accpet, in case plugins add extra capabilities
accept = mergeAccept(mKnownAgentAccpets[i], accept);
System.out.println("--> User-Agent: " + agent + " accepts: " + accept);
return accept;
}
}
}
System.out.println("No agent match, defaulting to Accept header: " + accept);
return accept;
}
private String mergeAccept(String pKnown, String pAccept) {
// TODO: Make sure there are no duplicates...
return pKnown + ", " + pAccept;
}
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException {
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
Map<String, Float> formatQuality = getFormatQualityMapping();
// TODO: Consider adding original format, and use as fallback in worst case?
// TODO: Original format should have some boost, to avoid unneccesary convertsion?
// Update source quality settings from image properties
adjustQualityFromImage(formatQuality, pImage);
//System.out.println("Source quality mapping: " + formatQuality);
adjustQualityFromAccept(formatQuality, request);
//System.out.println("Final media scores: " + formatQuality);
// Find the formats with the highest quality factor, and use the first (predictable)
String acceptable = findBestFormat(formatQuality);
//System.out.println("Acceptable: " + acceptable);
// Send HTTP 406 Not Acceptable
if (acceptable == null) {
if (pResponse instanceof HttpServletResponse) {
((HttpServletResponse) pResponse).sendError(HttpURLConnection.HTTP_NOT_ACCEPTABLE);
}
return null;
}
else {
// TODO: Only if the format was changed!
// Let other filters/caches/proxies know we changed the image
}
// Set format
pResponse.setOutputContentType(acceptable);
//System.out.println("Set format: " + acceptable);
}
return pImage;
}
private Map<String, Float> getFormatQualityMapping() {
synchronized(lock) {
if (formatQuality == null) {
formatQuality = new HashMap<String, Float>();
// Use ImageIO to find formats we can actually write
String[] formats = ImageIO.getWriterMIMETypes();
// All known formats qs are initially 1.0
// Others should be 0.1 or something like that...
for (String format : formats) {
formatQuality.put(format, getKnownFormatQuality(format));
}
}
}
//noinspection unchecked
return (Map<String, Float>) formatQuality.clone();
}
/**
* Finds the best available format.
*
* @param pFormatQuality the format to quality mapping
* @return the mime type of the best available format
*/
private static String findBestFormat(Map<String, Float> pFormatQuality) {
String acceptable = null;
float acceptQuality = 0.0f;
for (Map.Entry<String, Float> entry : pFormatQuality.entrySet()) {
float qValue = entry.getValue();
if (qValue > acceptQuality) {
acceptQuality = qValue;
acceptable = entry.getKey();
}
}
//System.out.println("Accepted format: " + acceptable);
//System.out.println("Accepted quality: " + acceptQuality);
return acceptable;
}
/**
* Adjust quality from HTTP Accept header
*
* @param pFormatQuality the format to quality mapping
* @param pRequest the request
*/
private void adjustQualityFromAccept(Map<String, Float> pFormatQuality, HttpServletRequest pRequest) {
// Multiply all q factors with qs factors
// No q=.. should be interpreted as q=1.0
// Apache does some extras; if both explicit types and wildcards
// (without qaulity factor) are present, */* is interpreted as
// */*;q=0.01 and image/* is interpreted as image/*;q=0.02
// See: http://httpd.apache.org/docs-2.0/content-negotiation.html
String accept = getAcceptedFormats(pRequest);
//System.out.println("Accept: " + accept);
float anyImageFactor = getQualityFactor(accept, MIME_TYPE_IMAGE_ANY);
anyImageFactor = (anyImageFactor == 1) ? 0.02f : anyImageFactor;
float anyFactor = getQualityFactor(accept, MIME_TYPE_ANY);
anyFactor = (anyFactor == 1) ? 0.01f : anyFactor;
for (String format : pFormatQuality.keySet()) {
//System.out.println("Trying format: " + format);
String formatMIME = MIME_TYPE_IMAGE_PREFIX + format;
float qFactor = getQualityFactor(accept, formatMIME);
qFactor = (qFactor == 0f) ? Math.max(anyFactor, anyImageFactor) : qFactor;
adjustQuality(pFormatQuality, format, qFactor);
}
}
/**
*
* @param pAccept the accpet header value
* @param pContentType the content type to get the quality factor for
* @return the q factor of the given format, according to the accept header
*/
private static float getQualityFactor(String pAccept, String pContentType) {
float qFactor = 0;
int foundIndex = pAccept.indexOf(pContentType);
if (foundIndex >= 0) {
int startQIndex = foundIndex + pContentType.length();
if (startQIndex < pAccept.length() && pAccept.charAt(startQIndex) == ';') {
while (startQIndex < pAccept.length() && pAccept.charAt(startQIndex++) == ' ') {
// Skip over whitespace
}
if (pAccept.charAt(startQIndex++) == 'q' && pAccept.charAt(startQIndex++) == '=') {
int endQIndex = pAccept.indexOf(',', startQIndex);
if (endQIndex < 0) {
endQIndex = pAccept.length();
}
try {
qFactor = Float.parseFloat(pAccept.substring(startQIndex, endQIndex));
//System.out.println("Found qFactor " + qFactor);
}
catch (NumberFormatException e) {
// TODO: Determine what to do here.. Maybe use a very low value?
// Ahem.. The specs don't say anything about how to interpret a wrong q factor..
//System.out.println("Unparseable q setting; " + e.getMessage());
}
}
// TODO: Determine what to do here.. Maybe use a very low value?
// Unparseable q value, use 0
}
else {
// Else, assume quality is 1.0
qFactor = 1;
}
}
return qFactor;
}
/**
* Adjusts source quality settings from image properties.
*
* @param pFormatQuality the format to quality mapping
* @param pImage the image
*/
private static void adjustQualityFromImage(Map<String, Float> pFormatQuality, BufferedImage pImage) {
// NOTE: The values are all made-up. May need tuning.
// If pImage.getColorModel() instanceof IndexColorModel
// JPEG qs*=0.6
// If NOT binary or 2 color index
// WBMP qs*=0.5
// Else
// GIF qs*=0.02
// PNG qs*=0.9 // JPEG is smaller/faster
if (pImage.getColorModel() instanceof IndexColorModel) {
adjustQuality(pFormatQuality, FORMAT_JPEG, 0.6f);
if (pImage.getType() != BufferedImage.TYPE_BYTE_BINARY || ((IndexColorModel) pImage.getColorModel()).getMapSize() != 2) {
adjustQuality(pFormatQuality, FORMAT_WBMP, 0.5f);
}
}
else {
adjustQuality(pFormatQuality, FORMAT_GIF, 0.01f);
adjustQuality(pFormatQuality, FORMAT_PNG, 0.99f); // JPEG is smaller/faster
}
// If pImage.getColorModel().hasTransparentPixels()
// JPEG qs*=0.05
// WBMP qs*=0.05
// If NOT transparency == BITMASK
// GIF qs*=0.8
if (ImageUtil.hasTransparentPixels(pImage, true)) {
adjustQuality(pFormatQuality, FORMAT_JPEG, 0.009f);
adjustQuality(pFormatQuality, FORMAT_WBMP, 0.009f);
if (pImage.getColorModel().getTransparency() != Transparency.BITMASK) {
adjustQuality(pFormatQuality, FORMAT_GIF, 0.8f);
}
}
}
/**
* Updates the quality in the map.
*
* @param pFormatQuality Map<String,Float>
* @param pFormat the format
* @param pFactor the quality factor
*/
private static void adjustQuality(Map<String, Float> pFormatQuality, String pFormat, float pFactor) {
Float oldValue = pFormatQuality.get(pFormat);
if (oldValue != null) {
pFormatQuality.put(pFormat, oldValue * pFactor);
//System.out.println("New vallue after multiplying with " + pFactor + " is " + pFormatQuality.get(pFormat));
}
}
/**
* Gets the initial quality if this is a known format, otherwise 0.1
*
* @param pFormat the format name
* @return the q factor of the given format
*/
private float getKnownFormatQuality(String pFormat) {
for (int i = 0; i < sKnownFormats.length; i++) {
if (pFormat.equals(sKnownFormats[i])) {
return knownFormatQuality[i];
}
}
return 0.1f;
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.ImageIO;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
/**
* This filter implements server side content negotiation and transcoding for
* images.
**
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ContentNegotiationFilter.java#1 $
*/
// TODO: Add support for automatic recognition of known browsers, to avoid
// unneccessary conversion (as IE supports PNG, the latests FireFox supports
// JPEG and GIF, etc. even though they both don't explicitly list these formats
// in their Accept headers).
public class ContentNegotiationFilter extends ImageFilter {
private final static String MIME_TYPE_IMAGE_PREFIX = "image/";
private static final String MIME_TYPE_IMAGE_ANY = MIME_TYPE_IMAGE_PREFIX + "*";
private static final String MIME_TYPE_ANY = "*/*";
private static final String HTTP_HEADER_ACCEPT = "Accept";
private static final String HTTP_HEADER_VARY = "Vary";
protected static final String HTTP_HEADER_USER_AGENT = "User-Agent";
private static final String FORMAT_JPEG = "image/jpeg";
private static final String FORMAT_WBMP = "image/wbmp";
private static final String FORMAT_GIF = "image/gif";
private static final String FORMAT_PNG = "image/png";
private final static String[] sKnownFormats = new String[] {
FORMAT_JPEG, FORMAT_PNG, FORMAT_GIF, FORMAT_WBMP
};
private float[] knownFormatQuality = new float[] {
1f, 1f, 0.99f, 0.5f
};
private HashMap<String, Float> formatQuality; // HashMap, as I need to clone this for each request
private final Object lock = new Object();
/*
private Pattern[] mKnownAgentPatterns;
private String[] mKnownAgentAccpets;
*/
{
// Hack: Make sure the filter don't trigger all the time
// See: super.trigger(ServletRequest)
triggerParams = new String[] {};
}
/*
public void setAcceptMappings(String pPropertiesFile) {
// NOTE: Supposed to be:
// <agent-name>=<reg-exp>
// <agent-name>.accept=<http-accept-header>
Properties mappings = new Properties();
try {
mappings.load(getServletContext().getResourceAsStream(pPropertiesFile));
List patterns = new ArrayList();
List accepts = new ArrayList();
for (Iterator iterator = mappings.keySet().iterator(); iterator.hasNext();) {
String agent = (String) iterator.next();
if (agent.endsWith(".accept")) {
continue;
}
try {
patterns.add(Pattern.compile((String) mappings.get(agent)));
// TODO: Consider preparsing ACCEPT header??
accepts.add(mappings.get(agent + ".accept"));
}
catch (PatternSyntaxException e) {
log("Could not parse User-Agent identification for " + agent, e);
}
mKnownAgentPatterns = (Pattern[]) patterns.toArray(new Pattern[patterns.size()]);
mKnownAgentAccpets = (String[]) accepts.toArray(new String[accepts.size()]);
}
}
catch (IOException e) {
log("Could not read accetp-mappings properties file: " + pPropertiesFile, e);
}
}
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// NOTE: super invokes trigger() and image specific doFilter() if needed
super.doFilterImpl(pRequest, pResponse, pChain);
if (pResponse instanceof HttpServletResponse) {
// Update the Vary HTTP header field
((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT);
//((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_USER_AGENT);
}
}
/**
* Makes sure the filter triggers for unknown file formats.
*
* @param pRequest the request
* @return {@code true} if the filter should execute, {@code false}
* otherwise
*/
protected boolean trigger(ServletRequest pRequest) {
boolean trigger = false;
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
String accept = getAcceptedFormats(request);
String originalFormat = getServletContext().getMimeType(request.getRequestURI());
//System.out.println("Accept: " + accept);
//System.out.println("Original format: " + originalFormat);
// Only override original format if it is not accpeted by the client
// Note: Only explicit matches are okay, */* or image/* is not.
if (!StringUtil.contains(accept, originalFormat)) {
trigger = true;
}
}
// Call super, to allow content negotiation even though format is supported
return trigger || super.trigger(pRequest);
}
private String getAcceptedFormats(HttpServletRequest pRequest) {
return pRequest.getHeader(HTTP_HEADER_ACCEPT);
}
/*
private String getAcceptedFormats(HttpServletRequest pRequest) {
String accept = pRequest.getHeader(HTTP_HEADER_ACCEPT);
// Check if User-Agent is in list of known agents
if (mKnownAgentPatterns != null) {
String agent = pRequest.getHeader(HTTP_HEADER_USER_AGENT);
for (int i = 0; i < mKnownAgentPatterns.length; i++) {
Pattern pattern = mKnownAgentPatterns[i];
if (pattern.matcher(agent).matches()) {
// Merge known with real accpet, in case plugins add extra capabilities
accept = mergeAccept(mKnownAgentAccpets[i], accept);
System.out.println("--> User-Agent: " + agent + " accepts: " + accept);
return accept;
}
}
}
System.out.println("No agent match, defaulting to Accept header: " + accept);
return accept;
}
private String mergeAccept(String pKnown, String pAccept) {
// TODO: Make sure there are no duplicates...
return pKnown + ", " + pAccept;
}
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException {
if (pRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) pRequest;
Map<String, Float> formatQuality = getFormatQualityMapping();
// TODO: Consider adding original format, and use as fallback in worst case?
// TODO: Original format should have some boost, to avoid unneccesary convertsion?
// Update source quality settings from image properties
adjustQualityFromImage(formatQuality, pImage);
//System.out.println("Source quality mapping: " + formatQuality);
adjustQualityFromAccept(formatQuality, request);
//System.out.println("Final media scores: " + formatQuality);
// Find the formats with the highest quality factor, and use the first (predictable)
String acceptable = findBestFormat(formatQuality);
//System.out.println("Acceptable: " + acceptable);
// Send HTTP 406 Not Acceptable
if (acceptable == null) {
if (pResponse instanceof HttpServletResponse) {
((HttpServletResponse) pResponse).sendError(HttpURLConnection.HTTP_NOT_ACCEPTABLE);
}
return null;
}
else {
// TODO: Only if the format was changed!
// Let other filters/caches/proxies know we changed the image
}
// Set format
pResponse.setOutputContentType(acceptable);
//System.out.println("Set format: " + acceptable);
}
return pImage;
}
private Map<String, Float> getFormatQualityMapping() {
synchronized(lock) {
if (formatQuality == null) {
formatQuality = new HashMap<String, Float>();
// Use ImageIO to find formats we can actually write
String[] formats = ImageIO.getWriterMIMETypes();
// All known formats qs are initially 1.0
// Others should be 0.1 or something like that...
for (String format : formats) {
formatQuality.put(format, getKnownFormatQuality(format));
}
}
}
//noinspection unchecked
return (Map<String, Float>) formatQuality.clone();
}
/**
* Finds the best available format.
*
* @param pFormatQuality the format to quality mapping
* @return the mime type of the best available format
*/
private static String findBestFormat(Map<String, Float> pFormatQuality) {
String acceptable = null;
float acceptQuality = 0.0f;
for (Map.Entry<String, Float> entry : pFormatQuality.entrySet()) {
float qValue = entry.getValue();
if (qValue > acceptQuality) {
acceptQuality = qValue;
acceptable = entry.getKey();
}
}
//System.out.println("Accepted format: " + acceptable);
//System.out.println("Accepted quality: " + acceptQuality);
return acceptable;
}
/**
* Adjust quality from HTTP Accept header
*
* @param pFormatQuality the format to quality mapping
* @param pRequest the request
*/
private void adjustQualityFromAccept(Map<String, Float> pFormatQuality, HttpServletRequest pRequest) {
// Multiply all q factors with qs factors
// No q=.. should be interpreted as q=1.0
// Apache does some extras; if both explicit types and wildcards
// (without qaulity factor) are present, */* is interpreted as
// */*;q=0.01 and image/* is interpreted as image/*;q=0.02
// See: http://httpd.apache.org/docs-2.0/content-negotiation.html
String accept = getAcceptedFormats(pRequest);
//System.out.println("Accept: " + accept);
float anyImageFactor = getQualityFactor(accept, MIME_TYPE_IMAGE_ANY);
anyImageFactor = (anyImageFactor == 1) ? 0.02f : anyImageFactor;
float anyFactor = getQualityFactor(accept, MIME_TYPE_ANY);
anyFactor = (anyFactor == 1) ? 0.01f : anyFactor;
for (String format : pFormatQuality.keySet()) {
//System.out.println("Trying format: " + format);
String formatMIME = MIME_TYPE_IMAGE_PREFIX + format;
float qFactor = getQualityFactor(accept, formatMIME);
qFactor = (qFactor == 0f) ? Math.max(anyFactor, anyImageFactor) : qFactor;
adjustQuality(pFormatQuality, format, qFactor);
}
}
/**
*
* @param pAccept the accpet header value
* @param pContentType the content type to get the quality factor for
* @return the q factor of the given format, according to the accept header
*/
private static float getQualityFactor(String pAccept, String pContentType) {
float qFactor = 0;
int foundIndex = pAccept.indexOf(pContentType);
if (foundIndex >= 0) {
int startQIndex = foundIndex + pContentType.length();
if (startQIndex < pAccept.length() && pAccept.charAt(startQIndex) == ';') {
while (startQIndex < pAccept.length() && pAccept.charAt(startQIndex++) == ' ') {
// Skip over whitespace
}
if (pAccept.charAt(startQIndex++) == 'q' && pAccept.charAt(startQIndex++) == '=') {
int endQIndex = pAccept.indexOf(',', startQIndex);
if (endQIndex < 0) {
endQIndex = pAccept.length();
}
try {
qFactor = Float.parseFloat(pAccept.substring(startQIndex, endQIndex));
//System.out.println("Found qFactor " + qFactor);
}
catch (NumberFormatException e) {
// TODO: Determine what to do here.. Maybe use a very low value?
// Ahem.. The specs don't say anything about how to interpret a wrong q factor..
//System.out.println("Unparseable q setting; " + e.getMessage());
}
}
// TODO: Determine what to do here.. Maybe use a very low value?
// Unparseable q value, use 0
}
else {
// Else, assume quality is 1.0
qFactor = 1;
}
}
return qFactor;
}
/**
* Adjusts source quality settings from image properties.
*
* @param pFormatQuality the format to quality mapping
* @param pImage the image
*/
private static void adjustQualityFromImage(Map<String, Float> pFormatQuality, BufferedImage pImage) {
// NOTE: The values are all made-up. May need tuning.
// If pImage.getColorModel() instanceof IndexColorModel
// JPEG qs*=0.6
// If NOT binary or 2 color index
// WBMP qs*=0.5
// Else
// GIF qs*=0.02
// PNG qs*=0.9 // JPEG is smaller/faster
if (pImage.getColorModel() instanceof IndexColorModel) {
adjustQuality(pFormatQuality, FORMAT_JPEG, 0.6f);
if (pImage.getType() != BufferedImage.TYPE_BYTE_BINARY || ((IndexColorModel) pImage.getColorModel()).getMapSize() != 2) {
adjustQuality(pFormatQuality, FORMAT_WBMP, 0.5f);
}
}
else {
adjustQuality(pFormatQuality, FORMAT_GIF, 0.01f);
adjustQuality(pFormatQuality, FORMAT_PNG, 0.99f); // JPEG is smaller/faster
}
// If pImage.getColorModel().hasTransparentPixels()
// JPEG qs*=0.05
// WBMP qs*=0.05
// If NOT transparency == BITMASK
// GIF qs*=0.8
if (ImageUtil.hasTransparentPixels(pImage, true)) {
adjustQuality(pFormatQuality, FORMAT_JPEG, 0.009f);
adjustQuality(pFormatQuality, FORMAT_WBMP, 0.009f);
if (pImage.getColorModel().getTransparency() != Transparency.BITMASK) {
adjustQuality(pFormatQuality, FORMAT_GIF, 0.8f);
}
}
}
/**
* Updates the quality in the map.
*
* @param pFormatQuality Map<String,Float>
* @param pFormat the format
* @param pFactor the quality factor
*/
private static void adjustQuality(Map<String, Float> pFormatQuality, String pFormat, float pFactor) {
Float oldValue = pFormatQuality.get(pFormat);
if (oldValue != null) {
pFormatQuality.put(pFormat, oldValue * pFactor);
//System.out.println("New vallue after multiplying with " + pFactor + " is " + pFormatQuality.get(pFormat));
}
}
/**
* Gets the initial quality if this is a known format, otherwise 0.1
*
* @param pFormat the format name
* @return the q factor of the given format
*/
private float getKnownFormatQuality(String pFormat) {
for (int i = 0; i < sKnownFormats.length; i++) {
if (pFormat.equals(sKnownFormats[i])) {
return knownFormatQuality[i];
}
}
return 0.1f;
}
}

View File

@@ -1,234 +1,235 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
/**
* This Servlet is able to render a cropped part of an image.
*
* <P><HR><P>
*
* <A name="parameters"></A><STRONG>Parameters:</STRONG><BR>
* <DL>
* <DT>{@code cropX}</DT>
* <DD>integer, the new left edge of the image.
* <DT>{@code cropY}</DT>
* <DD>integer, the new top of the image.
* <DT>{@code cropWidth}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code cropHeight}</DT>
* <DD>integer, the new height of the image.
* <DT>{@code cropUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code cropUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
*
* <!-- inherited from ScaleImage below: -->
*
* <DT>{@code image}</DT>
* <DD>string, the URL of the image to scale.
*
* <DT>{@code scaleX}</DT>
* <DD>integer, the new width of the image.
*
* <DT>{@code scaleY}</DT>
* <DD>integer, the new height of the image.
*
* <DT>{@code scaleUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
*
* <DT>{@code scaleUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
*
* <DT>{@code scaleQuality}</DT>
* <DD>string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST},
* {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}.
* {@code SCALE_DEFAULT} is default.
*
* </DL>
*
* @example
* &lt;IMG src="/crop/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=500&cropUniform=true"&gt;
*
* @example
* &lt;IMG src="/crop/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=50&cropUnits=PERCENT"&gt;
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CropFilter.java#1 $
*/
public class CropFilter extends ScaleFilter {
/** {@code cropX}*/
protected final static String PARAM_CROP_X = "cropX";
/** {@code cropY}*/
protected final static String PARAM_CROP_Y = "cropY";
/** {@code cropWidth}*/
protected final static String PARAM_CROP_WIDTH = "cropWidth";
/** {@code cropHeight}*/
protected final static String PARAM_CROP_HEIGHT = "cropHeight";
/** {@code cropUniform}*/
protected final static String PARAM_CROP_UNIFORM = "cropUniform";
/** {@code cropUnits}*/
protected final static String PARAM_CROP_UNITS = "cropUnits";
/**
* Reads the image from the requested URL, scales it, crops it, and returns
* it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get crop coordinates
int x = ServletUtil.getIntParameter(pRequest, PARAM_CROP_X, -1);
int y = ServletUtil.getIntParameter(pRequest, PARAM_CROP_Y, -1);
int width = ServletUtil.getIntParameter(pRequest, PARAM_CROP_WIDTH, -1);
int height = ServletUtil.getIntParameter(pRequest, PARAM_CROP_HEIGHT, -1);
boolean uniform =
ServletUtil.getBooleanParameter(pRequest, PARAM_CROP_UNIFORM, false);
int units = getUnits(ServletUtil.getParameter(pRequest, PARAM_CROP_UNITS, null));
// Get crop bounds
Rectangle bounds =
getBounds(x, y, width, height, units, uniform, pImage);
// Return cropped version
return pImage.getSubimage((int) bounds.getX(), (int) bounds.getY(),
(int) bounds.getWidth(),
(int) bounds.getHeight());
//return scaled.getSubimage(x, y, width, height);
}
protected Rectangle getBounds(int pX, int pY, int pWidth, int pHeight,
int pUnits, boolean pUniform,
BufferedImage pImg) {
// Algoritm:
// Try to get x and y (default 0,0).
// Try to get width and height (default width-x, height-y)
//
// If percent, get ratio
//
// If uniform
//
int oldWidth = pImg.getWidth();
int oldHeight = pImg.getHeight();
float ratio;
if (pUnits == UNITS_PERCENT) {
if (pWidth >= 0 && pHeight >= 0) {
// Non-uniform
pWidth = (int) ((float) oldWidth * (float) pWidth / 100f);
pHeight = (int) ((float) oldHeight * (float) pHeight / 100f);
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
// Else: No crop
}
//else if (UNITS_PIXELS.equalsIgnoreCase(pUnits)) {
else if (pUnits == UNITS_PIXELS) {
// Uniform
if (pUniform) {
if (pWidth >= 0 && pHeight >= 0) {
// Compute both ratios
ratio = (float) pWidth / (float) oldWidth;
float heightRatio = (float) pHeight / (float) oldHeight;
// Find the largest ratio, and use that for both
if (heightRatio < ratio) {
ratio = heightRatio;
pWidth = (int) ((float) oldWidth * ratio);
}
else {
pHeight = (int) ((float) oldHeight * ratio);
}
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / (float) oldWidth;
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / (float) oldHeight;
pWidth = (int) ((float) oldWidth * ratio);
}
// Else: No crop
}
}
// Else: No crop
// Not specified, or outside bounds: Use original dimensions
if (pWidth < 0 || (pX < 0 && pWidth > oldWidth)
|| (pX >= 0 && (pX + pWidth) > oldWidth)) {
pWidth = (pX >= 0 ? oldWidth - pX : oldWidth);
}
if (pHeight < 0 || (pY < 0 && pHeight > oldHeight)
|| (pY >= 0 && (pY + pHeight) > oldHeight)) {
pHeight = (pY >= 0 ? oldHeight - pY : oldHeight);
}
// Center
if (pX < 0) {
pX = (pImg.getWidth() - pWidth) / 2;
}
if (pY < 0) {
pY = (pImg.getHeight() - pHeight) / 2;
}
//System.out.println("x: " + pX + " y: " + pY
// + " w: " + pWidth + " h " + pHeight);
return new Rectangle(pX, pY, pWidth, pHeight);
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
/**
* This Servlet is able to render a cropped part of an image.
*
* <HR>
*
* <A name="parameters"></A><STRONG>Parameters:</STRONG><BR>
* <DL>
* <DT>{@code cropX}</DT>
* <DD>integer, the new left edge of the image.
* <DT>{@code cropY}</DT>
* <DD>integer, the new top of the image.
* <DT>{@code cropWidth}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code cropHeight}</DT>
* <DD>integer, the new height of the image.
* <DT>{@code cropUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code cropUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
*
* <!-- inherited from ScaleImage below: -->
*
* <DT>{@code image}</DT>
* <DD>string, the URL of the image to scale.
*
* <DT>{@code scaleX}</DT>
* <DD>integer, the new width of the image.
*
* <DT>{@code scaleY}</DT>
* <DD>integer, the new height of the image.
*
* <DT>{@code scaleUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
*
* <DT>{@code scaleUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
*
* <DT>{@code scaleQuality}</DT>
* <DD>string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST},
* {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}.
* {@code SCALE_DEFAULT} is default.
*
* </DL>
* <p>
* Examples:
* <br>
* &lt;IMG src="/crop/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&amp;cropWidth=500&amp;cropUniform=true"&gt;
* <br>
* &lt;IMG src="/crop/test.png?cache=false&amp;image=http://www.iconmedialab.com/images/random/home_image_12.jpg&amp;cropWidth=50&amp;cropUnits=PERCENT"&gt;
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: CropFilter.java#1 $
*/
public class CropFilter extends ScaleFilter {
/** {@code cropX}*/
protected final static String PARAM_CROP_X = "cropX";
/** {@code cropY}*/
protected final static String PARAM_CROP_Y = "cropY";
/** {@code cropWidth}*/
protected final static String PARAM_CROP_WIDTH = "cropWidth";
/** {@code cropHeight}*/
protected final static String PARAM_CROP_HEIGHT = "cropHeight";
/** {@code cropUniform}*/
protected final static String PARAM_CROP_UNIFORM = "cropUniform";
/** {@code cropUnits}*/
protected final static String PARAM_CROP_UNITS = "cropUnits";
/**
* Reads the image from the requested URL, scales it, crops it, and returns
* it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get crop coordinates
int x = ServletUtil.getIntParameter(pRequest, PARAM_CROP_X, -1);
int y = ServletUtil.getIntParameter(pRequest, PARAM_CROP_Y, -1);
int width = ServletUtil.getIntParameter(pRequest, PARAM_CROP_WIDTH, -1);
int height = ServletUtil.getIntParameter(pRequest, PARAM_CROP_HEIGHT, -1);
boolean uniform =
ServletUtil.getBooleanParameter(pRequest, PARAM_CROP_UNIFORM, false);
int units = getUnits(ServletUtil.getParameter(pRequest, PARAM_CROP_UNITS, null));
// Get crop bounds
Rectangle bounds =
getBounds(x, y, width, height, units, uniform, pImage);
// Return cropped version
return pImage.getSubimage((int) bounds.getX(), (int) bounds.getY(),
(int) bounds.getWidth(),
(int) bounds.getHeight());
//return scaled.getSubimage(x, y, width, height);
}
protected Rectangle getBounds(int pX, int pY, int pWidth, int pHeight,
int pUnits, boolean pUniform,
BufferedImage pImg) {
// Algoritm:
// Try to get x and y (default 0,0).
// Try to get width and height (default width-x, height-y)
//
// If percent, get ratio
//
// If uniform
//
int oldWidth = pImg.getWidth();
int oldHeight = pImg.getHeight();
float ratio;
if (pUnits == UNITS_PERCENT) {
if (pWidth >= 0 && pHeight >= 0) {
// Non-uniform
pWidth = (int) ((float) oldWidth * (float) pWidth / 100f);
pHeight = (int) ((float) oldHeight * (float) pHeight / 100f);
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
// Else: No crop
}
//else if (UNITS_PIXELS.equalsIgnoreCase(pUnits)) {
else if (pUnits == UNITS_PIXELS) {
// Uniform
if (pUniform) {
if (pWidth >= 0 && pHeight >= 0) {
// Compute both ratios
ratio = (float) pWidth / (float) oldWidth;
float heightRatio = (float) pHeight / (float) oldHeight;
// Find the largest ratio, and use that for both
if (heightRatio < ratio) {
ratio = heightRatio;
pWidth = (int) ((float) oldWidth * ratio);
}
else {
pHeight = (int) ((float) oldHeight * ratio);
}
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / (float) oldWidth;
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / (float) oldHeight;
pWidth = (int) ((float) oldWidth * ratio);
}
// Else: No crop
}
}
// Else: No crop
// Not specified, or outside bounds: Use original dimensions
if (pWidth < 0 || (pX < 0 && pWidth > oldWidth)
|| (pX >= 0 && (pX + pWidth) > oldWidth)) {
pWidth = (pX >= 0 ? oldWidth - pX : oldWidth);
}
if (pHeight < 0 || (pY < 0 && pHeight > oldHeight)
|| (pY >= 0 && (pY + pHeight) > oldHeight)) {
pHeight = (pY >= 0 ? oldHeight - pY : oldHeight);
}
// Center
if (pX < 0) {
pX = (pImg.getWidth() - pWidth) / 2;
}
if (pY < 0) {
pY = (pImg.getHeight() - pHeight) / 2;
}
//System.out.println("x: " + pX + " y: " + pY
// + " w: " + pWidth + " h " + pHeight);
return new Rectangle(pX, pY, pWidth, pHeight);
}
}

View File

@@ -41,11 +41,12 @@ import java.util.List;
/**
* Takes care of registering and de-registering local ImageIO plugins (service providers) for the servlet context.
* <p/>
* <p>
* Registers all available plugins on {@code contextInitialized} event, using {@code ImageIO.scanForPlugins()}, to make
* sure they are available to the current servlet context.
* De-registers all plugins which have the {@link Thread#getContextClassLoader() current thread's context class loader}
* as its class loader on {@code contextDestroyed} event, to avoid class/resource leak.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$

View File

@@ -1,219 +1,219 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.GenericFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* Abstract base class for image filters. Automatically decoding and encoding of
* the image is handled in the {@code doFilterImpl} method.
*
* @see #doFilter(java.awt.image.BufferedImage,javax.servlet.ServletRequest,ImageServletResponse)
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ImageFilter.java#2 $
*
*/
public abstract class ImageFilter extends GenericFilter {
// TODO: Take the design back to the drawing board (see ImageServletResponseImpl)
// - Allow multiple filters to set size attribute
// - Allow a later filter to reset, to get pass-through given certain criteria...
// - Or better yet, allow a filter to decide if it wants to decode, based on image metadata on the original image (ie: width/height)
protected String[] triggerParams = null;
/**
* The {@code doFilterImpl} method is called once, or each time a
* request/response pair is passed through the chain, depending on the
* {@link #oncePerRequest} member variable.
*
* @see #oncePerRequest
* @see com.twelvemonkeys.servlet.GenericFilter#doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pChain the filter chain
*
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pChain)
throws IOException, ServletException {
//System.out.println("Starting filtering...");
// Test for trigger params
if (!trigger(pRequest)) {
//System.out.println("Passing request on to next in chain (skipping " + getFilterName() + ")...");
// Pass the request on
pChain.doFilter(pRequest, pResponse);
}
else {
// If already wrapped, the image will be encoded later in the chain
// Or, if this is first filter in chain, we must encode when done
boolean encode = !(pResponse instanceof ImageServletResponse);
// For images, we do post filtering only and need to wrap the response
ImageServletResponse imageResponse = createImageServletResponse(pRequest, pResponse);
//System.out.println("Passing request on to next in chain...");
// Pass the request on
pChain.doFilter(pRequest, imageResponse);
//System.out.println("Post filtering...");
// Get image
//System.out.println("Getting image from ImageServletResponse...");
// Get the image from the wrapped response
RenderedImage image = imageResponse.getImage();
//System.out.println("Got image: " + image);
// Note: Image will be null if this is a HEAD request, the
// If-Modified-Since header is present, or similar.
if (image != null) {
// Do the image filtering
//System.out.println("Filtering image (" + getFilterName() + ")...");
image = doFilter(ImageUtil.toBuffered(image), pRequest, imageResponse);
//System.out.println("Done filtering.");
//System.out.println("Making image available...");
// Make image available to other filters (avoid unnecessary serializing/deserializing)
imageResponse.setImage(image);
//System.out.println("Done.");
}
if (encode) {
//System.out.println("Encoding image...");
// Encode image to original response
if (image != null) {
// TODO: Be smarter than this...
// TODO: Make sure ETag is same, if image content is the same...
// Use ETag of original response (or derived from)
// Use last modified of original response? Or keep original resource's, don't set at all?
// TODO: Why weak ETag?
String etag = "W/\"" + Integer.toHexString(hashCode()) + "-" + Integer.toHexString(image.hashCode()) + "\"";
// TODO: This breaks for wrapped instances, need to either unwrap or test for HttpSR...
((HttpServletResponse) pResponse).setHeader("ETag", etag);
((HttpServletResponse) pResponse).setDateHeader("Last-Modified", (System.currentTimeMillis() / 1000) * 1000);
}
imageResponse.flush();
//System.out.println("Done encoding.");
}
}
//System.out.println("Filtering done.");
}
/**
* Creates the image servlet response for this response.
*
* @param pResponse the original response
* @param pRequest the original request
* @return the new response, or {@code pResponse} if the response is already wrapped
*
* @see com.twelvemonkeys.servlet.image.ImageServletResponseWrapper
*/
private ImageServletResponse createImageServletResponse(final ServletRequest pRequest, final ServletResponse pResponse) {
if (pResponse instanceof ImageServletResponseImpl) {
ImageServletResponseImpl response = (ImageServletResponseImpl) pResponse;
// response.setRequest(pRequest);
return response;
}
return new ImageServletResponseImpl(pRequest, pResponse, getServletContext());
}
/**
* Tests if the filter should do image filtering/processing.
* <P/>
* This default implementation uses {@link #triggerParams} to test if:
* <dl>
* <dt>{@code mTriggerParams == null}</dt>
* <dd>{@code return true}</dd>
* <dt>{@code mTriggerParams != null}, loop through parameters, and test
* if {@code pRequest} contains the parameter. If match</dt>
* <dd>{@code return true}</dd>
* <dt>Otherwise</dt>
* <dd>{@code return false}</dd>
* </dl>
*
*
* @param pRequest the servlet request
* @return {@code true} if the filter should do image filtering
*/
protected boolean trigger(final ServletRequest pRequest) {
// If triggerParams not set, assume always trigger
if (triggerParams == null) {
return true;
}
// Trigger only for certain request parameters
for (String triggerParam : triggerParams) {
if (pRequest.getParameter(triggerParam) != null) {
return true;
}
}
// Didn't trigger
return false;
}
/**
* Sets the trigger parameters.
* The parameter is supposed to be a comma-separated string of parameter
* names.
*
* @param pTriggerParams a comma-separated string of parameter names.
*/
// TODO: Make it an @InitParam, and make sure we may set String[]/Collection<String> as parameter?
public void setTriggerParams(final String pTriggerParams) {
triggerParams = StringUtil.toStringArray(pTriggerParams);
}
/**
* Filters the image for this request.
*
* @param pImage the image to filter
* @param pRequest the servlet request
* @param pResponse the servlet response
*
* @return the filtered image
* @throws java.io.IOException if an I/O error occurs during filtering
*/
protected abstract RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException;
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.GenericFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* Abstract base class for image filters. Automatically decoding and encoding of
* the image is handled in the {@code doFilterImpl} method.
*
* @see #doFilter(java.awt.image.BufferedImage,javax.servlet.ServletRequest,ImageServletResponse)
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ImageFilter.java#2 $
*
*/
public abstract class ImageFilter extends GenericFilter {
// TODO: Take the design back to the drawing board (see ImageServletResponseImpl)
// - Allow multiple filters to set size attribute
// - Allow a later filter to reset, to get pass-through given certain criteria...
// - Or better yet, allow a filter to decide if it wants to decode, based on image metadata on the original image (ie: width/height)
protected String[] triggerParams = null;
/**
* The {@code doFilterImpl} method is called once, or each time a
* request/response pair is passed through the chain, depending on the
* {@link #oncePerRequest} member variable.
*
* @see #oncePerRequest
* @see com.twelvemonkeys.servlet.GenericFilter#doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) doFilter
* @see Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) Filter.doFilter
*
* @param pRequest the servlet request
* @param pResponse the servlet response
* @param pChain the filter chain
*
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pChain)
throws IOException, ServletException {
//System.out.println("Starting filtering...");
// Test for trigger params
if (!trigger(pRequest)) {
//System.out.println("Passing request on to next in chain (skipping " + getFilterName() + ")...");
// Pass the request on
pChain.doFilter(pRequest, pResponse);
}
else {
// If already wrapped, the image will be encoded later in the chain
// Or, if this is first filter in chain, we must encode when done
boolean encode = !(pResponse instanceof ImageServletResponse);
// For images, we do post filtering only and need to wrap the response
ImageServletResponse imageResponse = createImageServletResponse(pRequest, pResponse);
//System.out.println("Passing request on to next in chain...");
// Pass the request on
pChain.doFilter(pRequest, imageResponse);
//System.out.println("Post filtering...");
// Get image
//System.out.println("Getting image from ImageServletResponse...");
// Get the image from the wrapped response
RenderedImage image = imageResponse.getImage();
//System.out.println("Got image: " + image);
// Note: Image will be null if this is a HEAD request, the
// If-Modified-Since header is present, or similar.
if (image != null) {
// Do the image filtering
//System.out.println("Filtering image (" + getFilterName() + ")...");
image = doFilter(ImageUtil.toBuffered(image), pRequest, imageResponse);
//System.out.println("Done filtering.");
//System.out.println("Making image available...");
// Make image available to other filters (avoid unnecessary serializing/deserializing)
imageResponse.setImage(image);
//System.out.println("Done.");
}
if (encode) {
//System.out.println("Encoding image...");
// Encode image to original response
if (image != null) {
// TODO: Be smarter than this...
// TODO: Make sure ETag is same, if image content is the same...
// Use ETag of original response (or derived from)
// Use last modified of original response? Or keep original resource's, don't set at all?
// TODO: Why weak ETag?
String etag = "W/\"" + Integer.toHexString(hashCode()) + "-" + Integer.toHexString(image.hashCode()) + "\"";
// TODO: This breaks for wrapped instances, need to either unwrap or test for HttpSR...
((HttpServletResponse) pResponse).setHeader("ETag", etag);
((HttpServletResponse) pResponse).setDateHeader("Last-Modified", (System.currentTimeMillis() / 1000) * 1000);
}
imageResponse.flush();
//System.out.println("Done encoding.");
}
}
//System.out.println("Filtering done.");
}
/**
* Creates the image servlet response for this response.
*
* @param pResponse the original response
* @param pRequest the original request
* @return the new response, or {@code pResponse} if the response is already wrapped
*
* @see com.twelvemonkeys.servlet.image.ImageServletResponseWrapper
*/
private ImageServletResponse createImageServletResponse(final ServletRequest pRequest, final ServletResponse pResponse) {
if (pResponse instanceof ImageServletResponseImpl) {
ImageServletResponseImpl response = (ImageServletResponseImpl) pResponse;
// response.setRequest(pRequest);
return response;
}
return new ImageServletResponseImpl(pRequest, pResponse, getServletContext());
}
/**
* Tests if the filter should do image filtering/processing.
* <p>
* This default implementation uses {@link #triggerParams} to test if:
* </p>
* <dl>
* <dt>{@code mTriggerParams == null}</dt>
* <dd>{@code return true}</dd>
* <dt>{@code mTriggerParams != null}, loop through parameters, and test
* if {@code pRequest} contains the parameter. If match</dt>
* <dd>{@code return true}</dd>
* <dt>Otherwise</dt>
* <dd>{@code return false}</dd>
* </dl>
*
* @param pRequest the servlet request
* @return {@code true} if the filter should do image filtering
*/
protected boolean trigger(final ServletRequest pRequest) {
// If triggerParams not set, assume always trigger
if (triggerParams == null) {
return true;
}
// Trigger only for certain request parameters
for (String triggerParam : triggerParams) {
if (pRequest.getParameter(triggerParam) != null) {
return true;
}
}
// Didn't trigger
return false;
}
/**
* Sets the trigger parameters.
* The parameter is supposed to be a comma-separated string of parameter
* names.
*
* @param pTriggerParams a comma-separated string of parameter names.
*/
// TODO: Make it an @InitParam, and make sure we may set String[]/Collection<String> as parameter?
public void setTriggerParams(final String pTriggerParams) {
triggerParams = StringUtil.toStringArray(pTriggerParams);
}
/**
* Filters the image for this request.
*
* @param pImage the image to filter
* @param pRequest the servlet request
* @param pResponse the servlet response
*
* @return the filtered image
* @throws java.io.IOException if an I/O error occurs during filtering
*/
protected abstract RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException;
}

View File

@@ -1,195 +1,209 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import javax.servlet.ServletResponse;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* ImageServletResponse.
* <p/>
* The request attributes regarding image size and source region (AOI) are used
* in the decoding process, and must be set before the first invocation of
* {@link #getImage()} to have any effect.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ImageServletResponse.java#4 $
*/
public interface ImageServletResponse extends ServletResponse {
/**
* Request attribute of type {@link java.awt.Dimension} controlling image
* size.
* If either {@code width} or {@code height} is negative, the size is
* computed, using uniform scaling.
* Else, if {@code SIZE_UNIFORM} is {@code true}, the size will be
* computed to the largest possible area (with correct aspect ratio)
* fitting inside the target area.
* Otherwise, the image is scaled to the given size, with no regard to
* aspect ratio.
* <p/>
* Defaults to {@code null} (original image size).
*/
String ATTRIB_SIZE = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE";
/**
* Request attribute of type {@link Boolean} controlling image sizing.
* <p/>
* Defaults to {@code Boolean.TRUE}.
*/
String ATTRIB_SIZE_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_UNIFORM";
/**
* Request attribute of type {@link Boolean} controlling image sizing.
* <p/>
* Defaults to {@code Boolean.FALSE}.
*/
String ATTRIB_SIZE_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_PERCENT";
/**
* Request attribute of type {@link java.awt.Rectangle} controlling image
* source region (area of interest).
* <p/>
* Defaults to {@code null} (the entire image).
*/
String ATTRIB_AOI = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI";
/**
* Request attribute of type {@link Boolean} controlling image AOI.
* <p/>
* Defaults to {@code Boolean.FALSE}.
*/
String ATTRIB_AOI_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_UNIFORM";
/**
* Request attribute of type {@link Boolean} controlling image AOI.
* <p/>
* Defaults to {@code Boolean.FALSE}.
*/
String ATTRIB_AOI_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_PERCENT";
/**
* Request attribute of type {@link java.awt.Color} controlling background
* color for any transparent/translucent areas of the image.
* <p/>
* Defaults to {@code null} (keeps the transparent areas transparent).
*/
String ATTRIB_BG_COLOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.BG_COLOR";
/**
* Request attribute of type {@link Float} controlling image output compression/quality.
* Used for formats that accepts compression or quality settings,
* like JPEG (quality), PNG (compression only) etc.
* <p/>
* Defaults to {@code 0.8f} for JPEG.
*/
String ATTRIB_OUTPUT_QUALITY = "com.twelvemonkeys.servlet.image.ImageServletResponse.OUTPUT_QUALITY";
/**
* Request attribute of type {@link Double} controlling image read
* subsampling factor. Controls the maximum sample pixels in each direction,
* that is read per pixel in the output image, if the result will be
* downscaled.
* Larger values will result in better quality, at the expense of higher
* memory consumption and CPU usage.
* However, using values above {@code 3.0} will usually not improve image
* quality.
* Legal values are in the range {@code [1.0 .. positive infinity&gt;}.
* <p/>
* Defaults to {@code 2.0}.
*/
String ATTRIB_READ_SUBSAMPLING_FACTOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.READ_SUBSAMPLING_FACTOR";
/**
* Request attribute of type {@link Integer} controlling image resample
* algorithm.
* Legal values are {@link java.awt.Image#SCALE_DEFAULT SCALE_DEFAULT},
* {@link java.awt.Image#SCALE_FAST SCALE_FAST} or
* {@link java.awt.Image#SCALE_SMOOTH SCALE_SMOOTH}.
* <p/>
* Note: When using a value of {@code SCALE_FAST}, you should also use a
* subsampling factor of {@code 1.0}, for fast read/scale.
* Otherwise, use a subsampling factor of {@code 2.0} for better quality.
* <p/>
* Defaults to {@code SCALE_DEFAULT}.
*/
String ATTRIB_IMAGE_RESAMPLE_ALGORITHM = "com.twelvemonkeys.servlet.image.ImageServletResponse.IMAGE_RESAMPLE_ALGORITHM";
/**
* Gets the image format for this response, such as "image/gif" or "image/jpeg".
* If not set, the default format is that of the original image.
*
* @return the image format for this response.
* @see #setOutputContentType(String)
*/
String getOutputContentType();
/**
* Sets the image format for this response, such as "image/gif" or "image/jpeg".
* <p/>
* As an example, a custom filter could do content negotiation based on the
* request header fields and write the image back in an appropriate format.
* <p/>
* If not set, the default format is that of the original image.
*
* @param pImageFormat the image format for this response.
*/
void setOutputContentType(String pImageFormat);
//TODO: ?? void setCompressionQuality(float pQualityFactor);
//TODO: ?? float getCompressionQuality();
/**
* Writes the image to the original {@code ServletOutputStream}.
* If no format is {@linkplain #setOutputContentType(String) set} in this response,
* the image is encoded in the same format as the original image.
*
* @throws java.io.IOException if an I/O exception occurs during writing
*/
void flush() throws IOException;
/**
* Gets the decoded image from the response.
*
* @return a {@code BufferedImage} or {@code null} if the image could not be read.
*
* @throws java.io.IOException if an I/O exception occurs during reading
*/
BufferedImage getImage() throws IOException;
/**
* Sets the image for this response.
*
* @param pImage the new response image.
*/
void setImage(RenderedImage pImage);
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import javax.servlet.ServletResponse;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* ImageServletResponse.
* <p>
* The request attributes regarding image size and source region (AOI) are used
* in the decoding process, and must be set before the first invocation of
* {@link #getImage()} to have any effect.
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: ImageServletResponse.java#4 $
*/
public interface ImageServletResponse extends ServletResponse {
/**
* Request attribute of type {@link java.awt.Dimension} controlling image
* size.
* If either {@code width} or {@code height} is negative, the size is
* computed, using uniform scaling.
* Else, if {@code SIZE_UNIFORM} is {@code true}, the size will be
* computed to the largest possible area (with correct aspect ratio)
* fitting inside the target area.
* Otherwise, the image is scaled to the given size, with no regard to
* aspect ratio.
* <p>
* Defaults to {@code null} (original image size).
* </p>
*/
String ATTRIB_SIZE = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE";
/**
* Request attribute of type {@link Boolean} controlling image sizing.
* <p>
* Defaults to {@code Boolean.TRUE}.
* </p>
*/
String ATTRIB_SIZE_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_UNIFORM";
/**
* Request attribute of type {@link Boolean} controlling image sizing.
* <p>
* Defaults to {@code Boolean.FALSE}.
* </p>
*/
String ATTRIB_SIZE_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_PERCENT";
/**
* Request attribute of type {@link java.awt.Rectangle} controlling image
* source region (area of interest).
* <p>
* Defaults to {@code null} (the entire image).
* </p>
*/
String ATTRIB_AOI = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI";
/**
* Request attribute of type {@link Boolean} controlling image AOI.
* <p>
* Defaults to {@code Boolean.FALSE}.
* </p>
*/
String ATTRIB_AOI_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_UNIFORM";
/**
* Request attribute of type {@link Boolean} controlling image AOI.
* <p>
* Defaults to {@code Boolean.FALSE}.
* </p>
*/
String ATTRIB_AOI_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_PERCENT";
/**
* Request attribute of type {@link java.awt.Color} controlling background
* color for any transparent/translucent areas of the image.
* <p>
* Defaults to {@code null} (keeps the transparent areas transparent).
* </p>
*/
String ATTRIB_BG_COLOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.BG_COLOR";
/**
* Request attribute of type {@link Float} controlling image output compression/quality.
* Used for formats that accepts compression or quality settings,
* like JPEG (quality), PNG (compression only) etc.
* <p>
* Defaults to {@code 0.8f} for JPEG.
* </p>
*/
String ATTRIB_OUTPUT_QUALITY = "com.twelvemonkeys.servlet.image.ImageServletResponse.OUTPUT_QUALITY";
/**
* Request attribute of type {@link Double} controlling image read
* subsampling factor. Controls the maximum sample pixels in each direction,
* that is read per pixel in the output image, if the result will be
* downscaled.
* Larger values will result in better quality, at the expense of higher
* memory consumption and CPU usage.
* However, using values above {@code 3.0} will usually not improve image
* quality.
* Legal values are in the range {@code [1.0 .. positive infinity&gt;}.
* <p>
* Defaults to {@code 2.0}.
* </p>
*/
String ATTRIB_READ_SUBSAMPLING_FACTOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.READ_SUBSAMPLING_FACTOR";
/**
* Request attribute of type {@link Integer} controlling image resample
* algorithm.
* Legal values are {@link java.awt.Image#SCALE_DEFAULT SCALE_DEFAULT},
* {@link java.awt.Image#SCALE_FAST SCALE_FAST} or
* {@link java.awt.Image#SCALE_SMOOTH SCALE_SMOOTH}.
* <p>
* Note: When using a value of {@code SCALE_FAST}, you should also use a
* subsampling factor of {@code 1.0}, for fast read/scale.
* Otherwise, use a subsampling factor of {@code 2.0} for better quality.
* </p>
* <p>
* Defaults to {@code SCALE_DEFAULT}.
* </p>
*/
String ATTRIB_IMAGE_RESAMPLE_ALGORITHM = "com.twelvemonkeys.servlet.image.ImageServletResponse.IMAGE_RESAMPLE_ALGORITHM";
/**
* Gets the image format for this response, such as "image/gif" or "image/jpeg".
* If not set, the default format is that of the original image.
*
* @return the image format for this response.
* @see #setOutputContentType(String)
*/
String getOutputContentType();
/**
* Sets the image format for this response, such as "image/gif" or "image/jpeg".
* <p>
* As an example, a custom filter could do content negotiation based on the
* request header fields and write the image back in an appropriate format.
* </p>
* <p>
* If not set, the default format is that of the original image.
* </p>
*
* @param pImageFormat the image format for this response.
*/
void setOutputContentType(String pImageFormat);
//TODO: ?? void setCompressionQuality(float pQualityFactor);
//TODO: ?? float getCompressionQuality();
/**
* Writes the image to the original {@code ServletOutputStream}.
* If no format is {@linkplain #setOutputContentType(String) set} in this response,
* the image is encoded in the same format as the original image.
*
* @throws java.io.IOException if an I/O exception occurs during writing
*/
void flush() throws IOException;
/**
* Gets the decoded image from the response.
*
* @return a {@code BufferedImage} or {@code null} if the image could not be read.
*
* @throws java.io.IOException if an I/O exception occurs during reading
*/
BufferedImage getImage() throws IOException;
/**
* Sets the image for this response.
*
* @param pImage the new response image.
*/
void setImage(RenderedImage pImage);
}

View File

@@ -1,204 +1,203 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
/**
* This Servlet is able to render a cropped part of an image.
*
* <P><HR><P>
*
* <A name="parameters"></A><STRONG>Parameters:</STRONG><BR>
* <DL>
* <DT>{@code cropX}</DT>
* <DD>integer, the new left edge of the image.
* <DT>{@code cropY}</DT>
* <DD>integer, the new top of the image.
* <DT>{@code cropWidth}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code cropHeight}</DT>
* <DD>integer, the new height of the image.
* <!--
* <DT>{@code cropUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code cropUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default. -->
*
*
* </DL>
*
* @example
* JPEG:
* &lt;IMG src="/scale/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=500&uniform=true"&gt;
*
* PNG:
* &lt;IMG src="/scale/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=50&units=PERCENT"&gt;
*
* @todo Correct rounding errors, resulting in black borders when rotating 90
* degrees, and one of width or height is odd length...
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: RotateFilter.java#1 $
*/
public class RotateFilter extends ImageFilter {
/** {@code angle}*/
protected final static String PARAM_ANGLE = "angle";
/** {@code angleUnits (RADIANS|DEGREES)}*/
protected final static String PARAM_ANGLE_UNITS = "angleUnits";
/** {@code crop}*/
protected final static String PARAM_CROP = "rotateCrop";
/** {@code bgcolor}*/
protected final static String PARAM_BGCOLOR = "rotateBgcolor";
/** {@code degrees}*/
private final static String ANGLE_DEGREES = "degrees";
/** {@code radians}*/
//private final static String ANGLE_RADIANS = "radians";
/**
* Reads the image from the requested URL, rotates it, and returns
* it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get angle
double ang = getAngle(pRequest);
// Get bounds
Rectangle2D rect = getBounds(pRequest, pImage, ang);
int width = (int) rect.getWidth();
int height = (int) rect.getHeight();
// Create result image
BufferedImage res = ImageUtil.createTransparent(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = res.createGraphics();
// Get background color and clear
String str = pRequest.getParameter(PARAM_BGCOLOR);
if (!StringUtil.isEmpty(str)) {
Color bgcolor = StringUtil.toColor(str);
g.setBackground(bgcolor);
g.clearRect(0, 0, width, height);
}
// Set mHints (why do I always get jagged edgdes?)
RenderingHints hints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
g.setRenderingHints(hints);
// Rotate around center
AffineTransform at = AffineTransform
.getRotateInstance(ang, width / 2.0, height / 2.0);
// Move to center
at.translate(width / 2.0 - pImage.getWidth() / 2.0,
height / 2.0 - pImage.getHeight() / 2.0);
// Draw it, centered
g.drawImage(pImage, at, null);
return res;
}
/**
* Gets the angle of rotation.
*/
private double getAngle(ServletRequest pReq) {
double angle = 0.0;
String str = pReq.getParameter(PARAM_ANGLE);
if (!StringUtil.isEmpty(str)) {
angle = Double.parseDouble(str);
// Convert to radians, if needed
str = pReq.getParameter(PARAM_ANGLE_UNITS);
if (!StringUtil.isEmpty(str)
&& ANGLE_DEGREES.equalsIgnoreCase(str)) {
angle = Math.toRadians(angle);
}
}
return angle;
}
/**
* Get the bounding rectangle of the rotated image.
*/
private Rectangle2D getBounds(ServletRequest pReq, BufferedImage pImage,
double pAng) {
// Get dimensions of original image
int width = pImage.getWidth(); // loads the image
int height = pImage.getHeight();
// Test if we want to crop image (default)
// if true
// - find the largest bounding box INSIDE the rotated image,
// that matches the original proportions (nearest 90deg)
// (scale up to fit dimensions?)
// else
// - find the smallest bounding box OUTSIDE the rotated image.
// - that matches the original proportions (nearest 90deg) ?
// (scale down to fit dimensions?)
AffineTransform at =
AffineTransform.getRotateInstance(pAng, width / 2.0, height / 2.0);
Rectangle2D orig = new Rectangle(width, height);
Shape rotated = at.createTransformedShape(orig);
if (ServletUtil.getBooleanParameter(pReq, PARAM_CROP, false)) {
// TODO: Inside box
return rotated.getBounds2D();
}
else {
return rotated.getBounds2D();
}
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
/**
* This Servlet is able to render a cropped part of an image.
*
* <hr>
*
* <a name="parameters"></a><strong>Parameters:</strong><br>
* <DL>
* <DT>{@code cropX}</DT>
* <DD>integer, the new left edge of the image.
* <DT>{@code cropY}</DT>
* <DD>integer, the new top of the image.
* <DT>{@code cropWidth}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code cropHeight}</DT>
* <DD>integer, the new height of the image.
* <!--
* <DT>{@code cropUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code cropUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default. -->
* </DL>
*
* <p>
* Examples:
* <br>
* JPEG:
* &lt;IMG src="/scale/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&amp;width=500&amp;uniform=true"&gt;
* <br>
* PNG:
* &lt;IMG src="/scale/test.png?cache=false&amp;image=http://www.iconmedialab.com/images/random/home_image_12.jpg&amp;width=50&amp;units=PERCENT"&gt;
* </p>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: RotateFilter.java#1 $
*/
// TODO: Correct rounding errors, resulting in black borders when rotating 90
// degrees, and one of width or height is odd length...
public class RotateFilter extends ImageFilter {
/** {@code angle}*/
protected final static String PARAM_ANGLE = "angle";
/** {@code angleUnits (RADIANS|DEGREES)}*/
protected final static String PARAM_ANGLE_UNITS = "angleUnits";
/** {@code crop}*/
protected final static String PARAM_CROP = "rotateCrop";
/** {@code bgcolor}*/
protected final static String PARAM_BGCOLOR = "rotateBgcolor";
/** {@code degrees}*/
private final static String ANGLE_DEGREES = "degrees";
/** {@code radians}*/
//private final static String ANGLE_RADIANS = "radians";
/**
* Reads the image from the requested URL, rotates it, and returns
* it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get angle
double ang = getAngle(pRequest);
// Get bounds
Rectangle2D rect = getBounds(pRequest, pImage, ang);
int width = (int) rect.getWidth();
int height = (int) rect.getHeight();
// Create result image
BufferedImage res = ImageUtil.createTransparent(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = res.createGraphics();
// Get background color and clear
String str = pRequest.getParameter(PARAM_BGCOLOR);
if (!StringUtil.isEmpty(str)) {
Color bgcolor = StringUtil.toColor(str);
g.setBackground(bgcolor);
g.clearRect(0, 0, width, height);
}
// Set mHints (why do I always get jagged edgdes?)
RenderingHints hints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
g.setRenderingHints(hints);
// Rotate around center
AffineTransform at = AffineTransform
.getRotateInstance(ang, width / 2.0, height / 2.0);
// Move to center
at.translate(width / 2.0 - pImage.getWidth() / 2.0,
height / 2.0 - pImage.getHeight() / 2.0);
// Draw it, centered
g.drawImage(pImage, at, null);
return res;
}
/**
* Gets the angle of rotation.
*/
private double getAngle(ServletRequest pReq) {
double angle = 0.0;
String str = pReq.getParameter(PARAM_ANGLE);
if (!StringUtil.isEmpty(str)) {
angle = Double.parseDouble(str);
// Convert to radians, if needed
str = pReq.getParameter(PARAM_ANGLE_UNITS);
if (!StringUtil.isEmpty(str)
&& ANGLE_DEGREES.equalsIgnoreCase(str)) {
angle = Math.toRadians(angle);
}
}
return angle;
}
/**
* Get the bounding rectangle of the rotated image.
*/
private Rectangle2D getBounds(ServletRequest pReq, BufferedImage pImage,
double pAng) {
// Get dimensions of original image
int width = pImage.getWidth(); // loads the image
int height = pImage.getHeight();
// Test if we want to crop image (default)
// if true
// - find the largest bounding box INSIDE the rotated image,
// that matches the original proportions (nearest 90deg)
// (scale up to fit dimensions?)
// else
// - find the smallest bounding box OUTSIDE the rotated image.
// - that matches the original proportions (nearest 90deg) ?
// (scale down to fit dimensions?)
AffineTransform at =
AffineTransform.getRotateInstance(pAng, width / 2.0, height / 2.0);
Rectangle2D orig = new Rectangle(width, height);
Shape rotated = at.createTransformedShape(orig);
if (ServletUtil.getBooleanParameter(pReq, PARAM_CROP, false)) {
// TODO: Inside box
return rotated.getBounds2D();
}
else {
return rotated.getBounds2D();
}
}
}

View File

@@ -1,324 +1,331 @@
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.lang.reflect.Field;
/**
* This filter renders a scaled version of an image read from a
* given URL. The image can be output as a GIF, JPEG or PNG image
* or similar<!--,
* with optional caching of the rendered image files-->.
* <P>
* <P><HR><P>
* <p/>
* <A name="parameters"></A><STRONG>Parameters:</STRONG><BR>
* <DL>
* <DT>{@code scaleX}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code scaleY}</DT>
* <DD>integer, the new height of the image.
* <DT>{@code scaleUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code scaleUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
* <DT>{@code scaleQuality}</DT>
* <DD>string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST},
* {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}.
* {@code SCALE_DEFAULT} is default (see
* {@link java.awt.Image#getScaledInstance(int,int,int)}, {@link java.awt.Image}
* for more details).
* </DL>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ScaleFilter.java#1 $
*
* @example &lt;IMG src="/scale/test.jpg?scaleX=500&scaleUniform=false"&gt;
* @example &lt;IMG src="/scale/test.png?scaleY=50&scaleUnits=PERCENT"&gt;
*/
public class ScaleFilter extends ImageFilter {
/**
* Width and height are absolute pixels. The default.
*/
public static final int UNITS_PIXELS = 1;
/**
* Width and height are percentage of original width and height.
*/
public static final int UNITS_PERCENT = 5;
/**
* Ahh, good choice!
*/
//private static final int UNITS_METRIC = 42;
/**
* The root of all evil...
*/
//private static final int UNITS_INCHES = 666;
/**
* Unknown units. <!-- Oops, what now? -->
*/
public static final int UNITS_UNKNOWN = 0;
/**
* {@code scaleQuality}
*/
protected final static String PARAM_SCALE_QUALITY = "scaleQuality";
/**
* {@code scaleUnits}
*/
protected final static String PARAM_SCALE_UNITS = "scaleUnits";
/**
* {@code scaleUniform}
*/
protected final static String PARAM_SCALE_UNIFORM = "scaleUniform";
/**
* {@code scaleX}
*/
protected final static String PARAM_SCALE_X = "scaleX";
/**
* {@code scaleY}
*/
protected final static String PARAM_SCALE_Y = "scaleY";
/**
* {@code image}
*/
protected final static String PARAM_IMAGE = "image";
/** */
protected int defaultScaleQuality = Image.SCALE_DEFAULT;
/**
* Reads the image from the requested URL, scales it, and returns it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get quality setting
// SMOOTH | FAST | REPLICATE | DEFAULT | AREA_AVERAGING
// See Image (mHints)
int quality = getQuality(pRequest.getParameter(PARAM_SCALE_QUALITY));
// Get units, default is pixels
// PIXELS | PERCENT | METRIC | INCHES
int units = getUnits(pRequest.getParameter(PARAM_SCALE_UNITS));
if (units == UNITS_UNKNOWN) {
log("Unknown units for scale, returning original.");
return pImage;
}
// Use uniform scaling? Default is true
boolean uniformScale = ServletUtil.getBooleanParameter(pRequest, PARAM_SCALE_UNIFORM, true);
// Get dimensions
int width = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_X, -1);
int height = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_Y, -1);
// Get dimensions for scaled image
Dimension dim = getDimensions(pImage, width, height, units, uniformScale);
width = (int) dim.getWidth();
height = (int) dim.getHeight();
// Return scaled instance directly
return ImageUtil.createScaled(pImage, width, height, quality);
}
/**
* Gets the quality constant for the scaling, from the string argument.
*
* @param pQualityStr The string representation of the scale quality
* constant.
* @return The matching quality constant, or the default quality if none
* was found.
* @see java.awt.Image
* @see java.awt.Image#getScaledInstance(int,int,int)
*/
protected int getQuality(String pQualityStr) {
if (!StringUtil.isEmpty(pQualityStr)) {
try {
// Get quality constant from Image using reflection
Class cl = Image.class;
Field field = cl.getField(pQualityStr.toUpperCase());
return field.getInt(null);
}
catch (IllegalAccessException ia) {
log("Unable to get quality.", ia);
}
catch (NoSuchFieldException nsf) {
log("Unable to get quality.", nsf);
}
}
return defaultScaleQuality;
}
public void setDefaultScaleQuality(String pDefaultScaleQuality) {
defaultScaleQuality = getQuality(pDefaultScaleQuality);
}
/**
* Gets the units constant for the width and height arguments, from the
* given string argument.
*
* @param pUnitStr The string representation of the units constant,
* can be one of "PIXELS" or "PERCENT".
* @return The mathcing units constant, or UNITS_UNKNOWN if none was found.
*/
protected int getUnits(String pUnitStr) {
if (StringUtil.isEmpty(pUnitStr)
|| pUnitStr.equalsIgnoreCase("PIXELS")) {
return UNITS_PIXELS;
}
else if (pUnitStr.equalsIgnoreCase("PERCENT")) {
return UNITS_PERCENT;
}
else {
return UNITS_UNKNOWN;
}
}
/**
* Gets the dimensions (height and width) of the scaled image. The
* dimensions are computed based on the old image's dimensions, the units
* used for specifying new dimensions and whether or not uniform scaling
* should be used (se algorithm below).
*
* @param pImage the image to be scaled
* @param pWidth the new width of the image, or -1 if unknown
* @param pHeight the new height of the image, or -1 if unknown
* @param pUnits the constant specifying units for width and height
* parameter (UNITS_PIXELS or UNITS_PERCENT)
* @param pUniformScale boolean specifying uniform scale or not
* @return a Dimension object, with the correct width and heigth
* in pixels, for the scaled version of the image.
*/
protected Dimension getDimensions(Image pImage, int pWidth, int pHeight,
int pUnits, boolean pUniformScale) {
// If uniform, make sure width and height are scaled the same ammount
// (use ONLY height or ONLY width).
//
// Algoritm:
// if uniform
// if newHeight not set
// find ratio newWidth / oldWidth
// oldHeight *= ratio
// else if newWidth not set
// find ratio newWidth / oldWidth
// oldHeight *= ratio
// else
// find both ratios and use the smallest one
// (this will be the largest version of the image that fits
// inside the rectangle given)
// (if PERCENT, just use smallest percentage).
//
// If units is percent, we only need old height and width
int oldWidth = ImageUtil.getWidth(pImage);
int oldHeight = ImageUtil.getHeight(pImage);
float ratio;
if (pUnits == UNITS_PERCENT) {
if (pWidth >= 0 && pHeight >= 0) {
// Non-uniform
pWidth = (int) ((float) oldWidth * (float) pWidth / 100f);
pHeight = (int) ((float) oldHeight * (float) pHeight / 100f);
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
// Else: No scale
}
else if (pUnits == UNITS_PIXELS) {
if (pUniformScale) {
if (pWidth >= 0 && pHeight >= 0) {
// Compute both ratios
ratio = (float) pWidth / (float) oldWidth;
float heightRatio = (float) pHeight / (float) oldHeight;
// Find the largest ratio, and use that for both
if (heightRatio < ratio) {
ratio = heightRatio;
pWidth = (int) ((float) oldWidth * ratio);
}
else {
pHeight = (int) ((float) oldHeight * ratio);
}
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / (float) oldWidth;
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / (float) oldHeight;
pWidth = (int) ((float) oldWidth * ratio);
}
// Else: No scale
}
}
// Default is no scale, just work as a proxy
if (pWidth < 0) {
pWidth = oldWidth;
}
if (pHeight < 0) {
pHeight = oldHeight;
}
// Create new Dimension object and return
return new Dimension(pWidth, pHeight);
}
}
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.ServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.lang.reflect.Field;
/**
* This filter renders a scaled version of an image read from a
* given URL. The image can be output as a GIF, JPEG or PNG image
* or similar<!--,
* with optional caching of the rendered image files-->.
*
* <hr>
*
* <p>
* <A name="parameters"></A><STRONG>Parameters:</STRONG><BR>
* </p>
* <DL>
* <DT>{@code scaleX}</DT>
* <DD>integer, the new width of the image.
* <DT>{@code scaleY}</DT>
* <DD>integer, the new height of the image.
* <DT>{@code scaleUniform}</DT>
* <DD>boolean, wether or not uniform scalnig should be used. Default is
* {@code true}.
* <DT>{@code scaleUnits}</DT>
* <DD>string, one of {@code PIXELS}, {@code PERCENT}.
* {@code PIXELS} is default.
* <DT>{@code scaleQuality}</DT>
* <DD>string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST},
* {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}.
* {@code SCALE_DEFAULT} is default (see
* {@link java.awt.Image#getScaledInstance(int,int,int)}, {@link java.awt.Image}
* for more details).
* </DL>
* <p>
* Examples:
* </p>
* <pre>
* &lt;IMG src="/scale/test.jpg?scaleX=500&amp;scaleUniform=false"&gt;
* &lt;IMG src="/scale/test.png?scaleY=50&amp;scaleUnits=PERCENT"&gt;
* </pre>
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haku $
* @version $Id: ScaleFilter.java#1 $
*
*/
public class ScaleFilter extends ImageFilter {
/**
* Width and height are absolute pixels. The default.
*/
public static final int UNITS_PIXELS = 1;
/**
* Width and height are percentage of original width and height.
*/
public static final int UNITS_PERCENT = 5;
/**
* Ahh, good choice!
*/
//private static final int UNITS_METRIC = 42;
/**
* The root of all evil...
*/
//private static final int UNITS_INCHES = 666;
/**
* Unknown units. <!-- Oops, what now? -->
*/
public static final int UNITS_UNKNOWN = 0;
/**
* {@code scaleQuality}
*/
protected final static String PARAM_SCALE_QUALITY = "scaleQuality";
/**
* {@code scaleUnits}
*/
protected final static String PARAM_SCALE_UNITS = "scaleUnits";
/**
* {@code scaleUniform}
*/
protected final static String PARAM_SCALE_UNIFORM = "scaleUniform";
/**
* {@code scaleX}
*/
protected final static String PARAM_SCALE_X = "scaleX";
/**
* {@code scaleY}
*/
protected final static String PARAM_SCALE_Y = "scaleY";
/**
* {@code image}
*/
protected final static String PARAM_IMAGE = "image";
/** */
protected int defaultScaleQuality = Image.SCALE_DEFAULT;
/**
* Reads the image from the requested URL, scales it, and returns it in the
* Servlet stream. See above for details on parameters.
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
// Get quality setting
// SMOOTH | FAST | REPLICATE | DEFAULT | AREA_AVERAGING
// See Image (mHints)
int quality = getQuality(pRequest.getParameter(PARAM_SCALE_QUALITY));
// Get units, default is pixels
// PIXELS | PERCENT | METRIC | INCHES
int units = getUnits(pRequest.getParameter(PARAM_SCALE_UNITS));
if (units == UNITS_UNKNOWN) {
log("Unknown units for scale, returning original.");
return pImage;
}
// Use uniform scaling? Default is true
boolean uniformScale = ServletUtil.getBooleanParameter(pRequest, PARAM_SCALE_UNIFORM, true);
// Get dimensions
int width = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_X, -1);
int height = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_Y, -1);
// Get dimensions for scaled image
Dimension dim = getDimensions(pImage, width, height, units, uniformScale);
width = (int) dim.getWidth();
height = (int) dim.getHeight();
// Return scaled instance directly
return ImageUtil.createScaled(pImage, width, height, quality);
}
/**
* Gets the quality constant for the scaling, from the string argument.
*
* @param pQualityStr The string representation of the scale quality
* constant.
* @return The matching quality constant, or the default quality if none
* was found.
* @see java.awt.Image
* @see java.awt.Image#getScaledInstance(int,int,int)
*/
protected int getQuality(String pQualityStr) {
if (!StringUtil.isEmpty(pQualityStr)) {
try {
// Get quality constant from Image using reflection
Class cl = Image.class;
Field field = cl.getField(pQualityStr.toUpperCase());
return field.getInt(null);
}
catch (IllegalAccessException ia) {
log("Unable to get quality.", ia);
}
catch (NoSuchFieldException nsf) {
log("Unable to get quality.", nsf);
}
}
return defaultScaleQuality;
}
public void setDefaultScaleQuality(String pDefaultScaleQuality) {
defaultScaleQuality = getQuality(pDefaultScaleQuality);
}
/**
* Gets the units constant for the width and height arguments, from the
* given string argument.
*
* @param pUnitStr The string representation of the units constant,
* can be one of "PIXELS" or "PERCENT".
* @return The mathcing units constant, or UNITS_UNKNOWN if none was found.
*/
protected int getUnits(String pUnitStr) {
if (StringUtil.isEmpty(pUnitStr)
|| pUnitStr.equalsIgnoreCase("PIXELS")) {
return UNITS_PIXELS;
}
else if (pUnitStr.equalsIgnoreCase("PERCENT")) {
return UNITS_PERCENT;
}
else {
return UNITS_UNKNOWN;
}
}
/**
* Gets the dimensions (height and width) of the scaled image. The
* dimensions are computed based on the old image's dimensions, the units
* used for specifying new dimensions and whether or not uniform scaling
* should be used (se algorithm below).
*
* @param pImage the image to be scaled
* @param pWidth the new width of the image, or -1 if unknown
* @param pHeight the new height of the image, or -1 if unknown
* @param pUnits the constant specifying units for width and height
* parameter (UNITS_PIXELS or UNITS_PERCENT)
* @param pUniformScale boolean specifying uniform scale or not
* @return a Dimension object, with the correct width and heigth
* in pixels, for the scaled version of the image.
*/
protected Dimension getDimensions(Image pImage, int pWidth, int pHeight,
int pUnits, boolean pUniformScale) {
// If uniform, make sure width and height are scaled the same ammount
// (use ONLY height or ONLY width).
//
// Algoritm:
// if uniform
// if newHeight not set
// find ratio newWidth / oldWidth
// oldHeight *= ratio
// else if newWidth not set
// find ratio newWidth / oldWidth
// oldHeight *= ratio
// else
// find both ratios and use the smallest one
// (this will be the largest version of the image that fits
// inside the rectangle given)
// (if PERCENT, just use smallest percentage).
//
// If units is percent, we only need old height and width
int oldWidth = ImageUtil.getWidth(pImage);
int oldHeight = ImageUtil.getHeight(pImage);
float ratio;
if (pUnits == UNITS_PERCENT) {
if (pWidth >= 0 && pHeight >= 0) {
// Non-uniform
pWidth = (int) ((float) oldWidth * (float) pWidth / 100f);
pHeight = (int) ((float) oldHeight * (float) pHeight / 100f);
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / 100f;
pWidth = (int) ((float) oldWidth * ratio);
pHeight = (int) ((float) oldHeight * ratio);
}
// Else: No scale
}
else if (pUnits == UNITS_PIXELS) {
if (pUniformScale) {
if (pWidth >= 0 && pHeight >= 0) {
// Compute both ratios
ratio = (float) pWidth / (float) oldWidth;
float heightRatio = (float) pHeight / (float) oldHeight;
// Find the largest ratio, and use that for both
if (heightRatio < ratio) {
ratio = heightRatio;
pWidth = (int) ((float) oldWidth * ratio);
}
else {
pHeight = (int) ((float) oldHeight * ratio);
}
}
else if (pWidth >= 0) {
// Find ratio from pWidth
ratio = (float) pWidth / (float) oldWidth;
pHeight = (int) ((float) oldHeight * ratio);
}
else if (pHeight >= 0) {
// Find ratio from pHeight
ratio = (float) pHeight / (float) oldHeight;
pWidth = (int) ((float) oldWidth * ratio);
}
// Else: No scale
}
}
// Default is no scale, just work as a proxy
if (pWidth < 0) {
pWidth = oldWidth;
}
if (pHeight < 0) {
pHeight = oldHeight;
}
// Create new Dimension object and return
return new Dimension(pWidth, pHeight);
}
}

View File

@@ -1,184 +1,184 @@
/*
* Copyright (c) 2009, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* A {@link javax.servlet.Filter} that extracts request parameters, and sets the
* corresponding request attributes from {@link ImageServletResponse}.
* Only affects how the image is decoded, and must be applied before any
* other image filters in the chain.
* <p/>
* @see ImageServletResponse#ATTRIB_SIZE
* @see ImageServletResponse#ATTRIB_AOI
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: SourceRenderFilter.java#1 $
*/
public class SourceRenderFilter extends ImageFilter {
private String sizeWidthParam = "size.w";
private String sizeHeightParam = "size.h";
private String sizePercentParam = "size.percent";
private String sizeUniformParam = "size.uniform";
private String regionWidthParam = "aoi.w";
private String regionHeightParam = "aoi.h";
private String regionLeftParam = "aoi.x";
private String regionTopParam = "aoi.y";
private String regionPercentParam = "aoi.percent";
private String regionUniformParam = "aoi.uniform";
public void setRegionHeightParam(String pRegionHeightParam) {
regionHeightParam = pRegionHeightParam;
}
public void setRegionWidthParam(String pRegionWidthParam) {
regionWidthParam = pRegionWidthParam;
}
public void setRegionLeftParam(String pRegionLeftParam) {
regionLeftParam = pRegionLeftParam;
}
public void setRegionTopParam(String pRegionTopParam) {
regionTopParam = pRegionTopParam;
}
public void setSizeHeightParam(String pSizeHeightParam) {
sizeHeightParam = pSizeHeightParam;
}
public void setSizeWidthParam(String pSizeWidthParam) {
sizeWidthParam = pSizeWidthParam;
}
public void setRegionPercentParam(String pRegionPercentParam) {
regionPercentParam = pRegionPercentParam;
}
public void setRegionUniformParam(String pRegionUniformParam) {
regionUniformParam = pRegionUniformParam;
}
public void setSizePercentParam(String pSizePercentParam) {
sizePercentParam = pSizePercentParam;
}
public void setSizeUniformParam(String pSizeUniformParam) {
sizeUniformParam = pSizeUniformParam;
}
public void init() throws ServletException {
if (triggerParams == null) {
// Add all params as triggers
triggerParams = new String[]{sizeWidthParam, sizeHeightParam,
sizeUniformParam, sizePercentParam,
regionLeftParam, regionTopParam,
regionWidthParam, regionHeightParam,
regionUniformParam, regionPercentParam};
}
}
/**
* Extracts request parameters, and sets the corresponding request
* attributes if specified.
*
* @param pRequest
* @param pResponse
* @param pChain
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// TODO: Max size configuration, to avoid DOS attacks? OutOfMemory
// Size parameters
int width = ServletUtil.getIntParameter(pRequest, sizeWidthParam, -1);
int height = ServletUtil.getIntParameter(pRequest, sizeHeightParam, -1);
if (width > 0 || height > 0) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE, new Dimension(width, height));
}
// Size uniform/percent
boolean uniform = ServletUtil.getBooleanParameter(pRequest, sizeUniformParam, true);
if (!uniform) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.FALSE);
}
boolean percent = ServletUtil.getBooleanParameter(pRequest, sizePercentParam, false);
if (percent) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE);
}
// Area of interest parameters
int x = ServletUtil.getIntParameter(pRequest, regionLeftParam, -1); // Default is center
int y = ServletUtil.getIntParameter(pRequest, regionTopParam, -1); // Default is center
width = ServletUtil.getIntParameter(pRequest, regionWidthParam, -1);
height = ServletUtil.getIntParameter(pRequest, regionHeightParam, -1);
if (width > 0 || height > 0) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_AOI, new Rectangle(x, y, width, height));
}
// AOI uniform/percent
uniform = ServletUtil.getBooleanParameter(pRequest, regionUniformParam, false);
if (uniform) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE);
}
percent = ServletUtil.getBooleanParameter(pRequest, regionPercentParam, false);
if (percent) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE);
}
super.doFilterImpl(pRequest, pResponse, pChain);
}
/**
* This implementation does no filtering, and simply returns the image
* passed in.
*
* @param pImage
* @param pRequest
* @param pResponse
* @return {@code pImage}
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
return pImage;
}
/*
* Copyright (c) 2009, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.image;
import com.twelvemonkeys.servlet.ServletUtil;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.IOException;
/**
* A {@link javax.servlet.Filter} that extracts request parameters, and sets the
* corresponding request attributes from {@link ImageServletResponse}.
* Only affects how the image is decoded, and must be applied before any
* other image filters in the chain.
*
* @see ImageServletResponse#ATTRIB_SIZE
* @see ImageServletResponse#ATTRIB_AOI
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @version $Id: SourceRenderFilter.java#1 $
*/
public class SourceRenderFilter extends ImageFilter {
private String sizeWidthParam = "size.w";
private String sizeHeightParam = "size.h";
private String sizePercentParam = "size.percent";
private String sizeUniformParam = "size.uniform";
private String regionWidthParam = "aoi.w";
private String regionHeightParam = "aoi.h";
private String regionLeftParam = "aoi.x";
private String regionTopParam = "aoi.y";
private String regionPercentParam = "aoi.percent";
private String regionUniformParam = "aoi.uniform";
public void setRegionHeightParam(String pRegionHeightParam) {
regionHeightParam = pRegionHeightParam;
}
public void setRegionWidthParam(String pRegionWidthParam) {
regionWidthParam = pRegionWidthParam;
}
public void setRegionLeftParam(String pRegionLeftParam) {
regionLeftParam = pRegionLeftParam;
}
public void setRegionTopParam(String pRegionTopParam) {
regionTopParam = pRegionTopParam;
}
public void setSizeHeightParam(String pSizeHeightParam) {
sizeHeightParam = pSizeHeightParam;
}
public void setSizeWidthParam(String pSizeWidthParam) {
sizeWidthParam = pSizeWidthParam;
}
public void setRegionPercentParam(String pRegionPercentParam) {
regionPercentParam = pRegionPercentParam;
}
public void setRegionUniformParam(String pRegionUniformParam) {
regionUniformParam = pRegionUniformParam;
}
public void setSizePercentParam(String pSizePercentParam) {
sizePercentParam = pSizePercentParam;
}
public void setSizeUniformParam(String pSizeUniformParam) {
sizeUniformParam = pSizeUniformParam;
}
public void init() throws ServletException {
if (triggerParams == null) {
// Add all params as triggers
triggerParams = new String[]{sizeWidthParam, sizeHeightParam,
sizeUniformParam, sizePercentParam,
regionLeftParam, regionTopParam,
regionWidthParam, regionHeightParam,
regionUniformParam, regionPercentParam};
}
}
/**
* Extracts request parameters, and sets the corresponding request
* attributes if specified.
*
* @param pRequest
* @param pResponse
* @param pChain
* @throws IOException
* @throws ServletException
*/
protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException {
// TODO: Max size configuration, to avoid DOS attacks? OutOfMemory
// Size parameters
int width = ServletUtil.getIntParameter(pRequest, sizeWidthParam, -1);
int height = ServletUtil.getIntParameter(pRequest, sizeHeightParam, -1);
if (width > 0 || height > 0) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE, new Dimension(width, height));
}
// Size uniform/percent
boolean uniform = ServletUtil.getBooleanParameter(pRequest, sizeUniformParam, true);
if (!uniform) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.FALSE);
}
boolean percent = ServletUtil.getBooleanParameter(pRequest, sizePercentParam, false);
if (percent) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE);
}
// Area of interest parameters
int x = ServletUtil.getIntParameter(pRequest, regionLeftParam, -1); // Default is center
int y = ServletUtil.getIntParameter(pRequest, regionTopParam, -1); // Default is center
width = ServletUtil.getIntParameter(pRequest, regionWidthParam, -1);
height = ServletUtil.getIntParameter(pRequest, regionHeightParam, -1);
if (width > 0 || height > 0) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_AOI, new Rectangle(x, y, width, height));
}
// AOI uniform/percent
uniform = ServletUtil.getBooleanParameter(pRequest, regionUniformParam, false);
if (uniform) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE);
}
percent = ServletUtil.getBooleanParameter(pRequest, regionPercentParam, false);
if (percent) {
pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE);
}
super.doFilterImpl(pRequest, pResponse, pChain);
}
/**
* This implementation does no filtering, and simply returns the image
* passed in.
*
* @param pImage
* @param pRequest
* @param pResponse
* @return {@code pImage}
*/
protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) {
return pImage;
}
}