diff --git a/twelvemonkeys-servlet/pom.xml b/twelvemonkeys-servlet/pom.xml new file mode 100755 index 00000000..193dc13f --- /dev/null +++ b/twelvemonkeys-servlet/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + com.twelvemonkeys + twelvemonkeys-servlet + 2.1 + TwelveMonkeys Servlet + + + com.twelvemonkeys + twelvemonkeys-parent + 2.0 + + + + 2.1 + + + + + com.twelvemonkeys + twelvemonkeys-core + ${core.version} + compile + + + + com.twelvemonkeys + twelvemonkeys-core + ${core.version} + tests + test + + + + javax.servlet + servlet-api + 2.4 + provided + + + + javax.servlet + jsp-api + 2.0 + provided + + + + log4j + log4j + 1.2.14 + provided + + + + commons-fileupload + commons-fileupload + 1.2 + provided + + + + junit + junit + 4.3.1 + test + + + + jmock + jmock-cglib + 1.0.1 + test + + + + + + + maven-source-plugin + + + + maven-resources-plugin + + UTF-8 + + + + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java new file mode 100755 index 00000000..1dcf01f1 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java @@ -0,0 +1,149 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.CollectionUtil; + +import java.util.*; + +/** + * AbstractServletMapAdapter + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java#1 $ + */ +abstract class AbstractServletMapAdapter extends AbstractMap> { + // TODO: This map is now a little too lazy.. Should cache entries too (instead?) ! + + private final static List NULL_LIST = new ArrayList(); + + private transient Map> mCache = new HashMap>(); + private transient int mSize = -1; + private transient AbstractSet>> mEntries; + + protected abstract Iterator keysImpl(); + + protected abstract Iterator valuesImpl(String pName); + + @Override + public List get(Object pKey) { + if (pKey instanceof String) { + return getValues((String) pKey); + } + return null; + } + + private List getValues(String pName) { + List values = mCache.get(pName); + + if (values == null) { + //noinspection unchecked + Iterator headers = valuesImpl(pName); + if (headers == null) { + mCache.put(pName, NULL_LIST); + } + else { + values = toList(headers); + mCache.put(pName, values); + } + } + + return values == NULL_LIST ? null : values; + } + + private static List toList(final Iterator pValues) { + List list = new ArrayList(); + CollectionUtil.addAll(list, pValues); + return Collections.unmodifiableList(list); + } + + @Override + public int size() { + if (mSize == -1) { + computeSize(); + } + return mSize; + } + + private void computeSize() { + Iterator names = keysImpl(); + mSize = 0; + for (;names.hasNext(); names.next()) { + mSize++; + } + } + + public Set>> entrySet() { + if (mEntries == null) { + mEntries = new AbstractSet>>() { + public Iterator>> iterator() { + return new Iterator>>() { + Iterator mHeaderNames = keysImpl(); + + public boolean hasNext() { + return mHeaderNames.hasNext(); + } + + public Entry> next() { + // TODO: Replace with cached lookup + return new HeaderEntry(mHeaderNames.next()); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public int size() { + return AbstractServletMapAdapter.this.size(); + } + }; + } + + return mEntries; + } + + private class HeaderEntry implements Entry> { + String mHeaderName; + + public HeaderEntry(String pHeaderName) { + mHeaderName = pHeaderName; + } + + public String getKey() { + return mHeaderName; + } + + public List getValue() { + return get(mHeaderName); + } + + public List setValue(List pValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + List value; + return (mHeaderName == null ? 0 : mHeaderName.hashCode()) ^ + ((value = getValue()) == null ? 0 : value.hashCode()); + } + + @Override + public boolean equals(Object pOther) { + if (pOther == this) { + return true; + } + + if (pOther instanceof Entry) { + Entry other = (Entry) pOther; + return ((other.getKey() == null && getKey() == null) || + (getKey() != null && getKey().equals(other.getKey()))) && + ((other.getValue() == null && getValue() == null) || + (getValue() != null && getValue().equals(other.getValue()))); + } + + return false; + } + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java new file mode 100755 index 00000000..939d8588 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java @@ -0,0 +1,151 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; +import java.util.Properties; +import java.util.List; +import java.util.ArrayList; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * BrowserHelperFilter + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java#1 $ + */ +public class BrowserHelperFilter extends GenericFilter { + private static final String HTTP_HEADER_ACCEPT = "Accept"; + protected static final String HTTP_HEADER_USER_AGENT = "User-Agent"; + + private Pattern[] mKnownAgentPatterns; + private String[] mKnownAgentAccpets; + + /** + * Sets the accept-mappings for this filter + * @param pPropertiesFile name of accept-mappings properties files + * @throws ServletConfigException if the accept-mappings properties + * file cannot be read. + */ + public void setAcceptMappingsFile(String pPropertiesFile) throws ServletConfigException { + // NOTE: Format is: + // = + // .accept= + + Properties mappings = new Properties(); + try { + log("Reading Accept-mappings properties file: " + pPropertiesFile); + mappings.load(getServletContext().getResourceAsStream(pPropertiesFile)); + + List patterns = new ArrayList(); + List accepts = new ArrayList(); + + //System.out.println("--> Loaded file: " + pPropertiesFile); + + for (Object key : mappings.keySet()) { + String agent = (String) key; + if (agent.endsWith(".accept")) { + continue; + } + + //System.out.println("--> Adding accept-mapping for User-Agent: " + agent); + + try { + String accept = (String) mappings.get(agent + ".accept"); + if (!StringUtil.isEmpty(accept)) { + patterns.add(Pattern.compile((String) mappings.get(agent))); + accepts.add(accept); + //System.out.println("--> " + agent + " accepts: " + accept); + } + else { + log("Missing Accept mapping for User-Agent: " + agent); + } + } + catch (PatternSyntaxException e) { + log("Could not parse User-Agent identification for " + agent, e); + } + + mKnownAgentPatterns = patterns.toArray(new Pattern[patterns.size()]); + mKnownAgentAccpets = accepts.toArray(new String[accepts.size()]); + } + } + catch (IOException e) { + throw new ServletConfigException("Could not read Accept-mappings properties file: " + pPropertiesFile, e); + } + } + + public void init() throws ServletException { + if (mKnownAgentAccpets == null || mKnownAgentAccpets.length == 0) { + throw new ServletConfigException("No User-Agent/Accept mappings for filter: " + getFilterName()); + } + } + + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + if (pRequest instanceof HttpServletRequest) { + //System.out.println("--> Trying to find User-Agent/Accept headers..."); + HttpServletRequest request = (HttpServletRequest) pRequest; + // Check if User-Agent is in list of known agents + if (mKnownAgentPatterns != null && mKnownAgentPatterns.length > 0) { + String agent = request.getHeader(HTTP_HEADER_USER_AGENT); + //System.out.println("--> User-Agent: " + agent); + + for (int i = 0; i < mKnownAgentPatterns.length; i++) { + Pattern pattern = mKnownAgentPatterns[i]; + //System.out.println("--> Pattern: " + pattern); + if (pattern.matcher(agent).matches()) { + // TODO: Consider merge known with real accpet, in case plugins add extra capabilities? + final String fakeAccept = mKnownAgentAccpets[i]; + + //System.out.println("--> User-Agent: " + agent + " accepts: " + fakeAccept); + + pRequest = new HttpServletRequestWrapper(request) { + public String getHeader(String pName) { + if (HTTP_HEADER_ACCEPT.equals(pName)) { + return fakeAccept; + } + return super.getHeader(pName); + } + }; + break; + } + } + } + } + pChain.doFilter(pRequest, pResponse); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java new file mode 100755 index 00000000..015e84e8 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java @@ -0,0 +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 "TwelveMonkeys" 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 OWNER 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.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Enumeration; + +/** + * DebugServlet class description. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java#1 $ + */ +public class DebugServlet extends GenericServlet { + private long mDateModified; + + public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException { + service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse); + } + + public void init() throws ServletException { + super.init(); + mDateModified = System.currentTimeMillis(); + } + + public void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { + pResponse.setContentType("text/plain"); + // Include these to allow browser caching + pResponse.setDateHeader("Last-Modified", mDateModified); + pResponse.setHeader("ETag", getServletName()); + + ServletOutputStream out = pResponse.getOutputStream(); + + out.println("Remote address: " + pRequest.getRemoteAddr()); + out.println("Remote host name: " + pRequest.getRemoteHost()); + out.println("Remote user: " + pRequest.getRemoteUser()); + out.println(); + + out.println("Request Method: " + pRequest.getMethod()); + out.println("Request Scheme: " + pRequest.getScheme()); + out.println("Request URI: " + pRequest.getRequestURI()); + out.println("Request URL: " + pRequest.getRequestURL().toString()); + out.println("Request PathInfo: " + pRequest.getPathInfo()); + out.println("Request ContentLength: " + pRequest.getContentLength()); + out.println(); + + out.println("Request Headers:"); + Enumeration headerNames = pRequest.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + Enumeration headerValues = pRequest.getHeaders(headerName); + + if (headerName != null) { + while (headerValues.hasMoreElements()) { + String value = (String) headerValues.nextElement(); + out.println(" " + headerName + ": " + value); + } + } + } + out.println(); + + out.println("Request parameters:"); + Enumeration paramNames = pRequest.getParameterNames(); + while (paramNames.hasMoreElements()) { + String name = (String) paramNames.nextElement(); + String[] values = pRequest.getParameterValues(name); + + for (String value : values) { + out.println(" " + name + ": " + value); + } + } + out.println(); + + out.println("Request attributes:"); + Enumeration attribNames = pRequest.getAttributeNames(); + while (attribNames.hasMoreElements()) { + String name = (String) attribNames.nextElement(); + Object value = pRequest.getAttribute(name); + out.println(" " + name + ": " + value); + } + + + out.flush(); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java new file mode 100755 index 00000000..0a98a8f3 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java @@ -0,0 +1,383 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * {@code GenericFilter} is inspired by {@link GenericServlet}, and + * implements the {@code Filter} and {@code FilterConfig} interfaces. + *

+ * {@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. + *

+ * To write a generic filter, you need only override the abstract + * {@link #doFilterImpl doFilterImpl} method. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java#1 $ + * + * @see Filter + * @see FilterConfig + */ +public abstract class GenericFilter implements Filter, FilterConfig, Serializable { + + /** + * The filter config. + */ + private transient FilterConfig mFilterConfig = null; + + /** + * Makes sure the filter runs once per request + *

+ * see #isRunOnce + * + * @see #mOncePerRequest + * see #ATTRIB_RUN_ONCE_VALUE + */ + 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 FitlerConfig object. + *

+ * see #isRunOnce + * + * @see #mOncePerRequest + * see #ATTRIB_RUN_ONCE_VALUE + */ + private String mAttribRunOnce = null; + + /** + * Makes sure the filter runs once per request + *

+ * see #isRunOnce + * + * @see #mOncePerRequest + * see #ATTRIB_RUN_ONCE_EXT + */ + 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}). + *

+ * Set this variable to true, to make sure the filter runs once per request. + * + * NOTE: As of Servlet 2.4, this field + * should always be left to it's default value ({@code false}). + *
+ * To run the filter once per request, the {@code filter-mapping} element + * of the web-descriptor should include a {@code dispatcher} element: + *

<dispatcher>REQUEST</dispatcher>
+ * + */ + protected boolean mOncePerRequest = false; + + /** + * Does nothing. + */ + public GenericFilter() {} + + /** + * Called by the web container to indicate to a filter that it is being + * placed into service. + *

+ * 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, if you are + * overriding this form of the method, + * always call {@code super.init(config)}. + *

+ * 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 + * @see #init() init + * @see BeanUtil#configure(Object, java.util.Map, boolean) + */ + public void init(FilterConfig pConfig) throws ServletException { + if (pConfig == null) { + throw new ServletConfigException("filterconfig == null"); + } + + // Store filterconfig + mFilterConfig = 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 + mAttribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT; + log("init (oncePerRequest=" + mOncePerRequest + ", attribRunOnce=" + mAttribRunOnce + ")"); + 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. + *

+ * Subclasses should not override this method, but rather the + * abstract {@link #doFilterImpl doFilterImpl} method. + * + * @param pRequest the servlet request + * @param pResponse the servlet response + * @param pFilterChain the filter chain + * + * @throws IOException + * @throws ServletException + * + * @see Filter#doFilter Filter.doFilter + * @see #doFilterImpl doFilterImpl + */ + public final void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pFilterChain) throws IOException, ServletException { + // If request filter and allready run, continue chain and return fast + if (mOncePerRequest && 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 contine. + *

+ * Note that the method will mark the request as filtered on first + * invocation. + *

+ * see #ATTRIB_RUN_ONCE_EXT + * see #ATTRIB_RUN_ONCE_VALUE + * + * @param pRequest the servlet request + * @return {@code true} if the request is allready filtered, otherwise + * {@code false}. + */ + private boolean isRunOnce(ServletRequest pRequest) { + // If request allready filtered, return true (skip) + if (pRequest.getAttribute(mAttribRunOnce) == ATTRIB_RUN_ONCE_VALUE) { + return true; + } + + // Set attribute and return false (continue) + pRequest.setAttribute(mAttribRunOnce, ATTRIB_RUN_ONCE_VALUE); + return false; + } + + /** + * Invoked once, or each time a request/response pair is passed through the + * chain, depending on the {@link #mOncePerRequest} 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 #mOncePerRequest + * @see #doFilter doFilter + * @see Filter#doFilter 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"); + mFilterConfig = 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 mFilterConfig.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() { + // TODO: Create a servlet context wrapper that lets you log to a log4j appender? + return mFilterConfig.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(String pKey) { + return mFilterConfig.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 mFilterConfig.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(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(String pMessage, 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. + */ + public void setFilterConfig(FilterConfig pFilterConfig) { + try { + init(pFilterConfig); + } + catch (ServletException e) { + log("Error in init(), see stacktrace for details.", e); + } + } + + /** + * Gets the {@code FilterConfig} for this filter. + * + * @return the {@code FilterConfig} for this filter + * @see FilterConfig + */ + public FilterConfig getFilterConfig() { + return mFilterConfig; + } + + /** + * 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 #mOncePerRequest + */ + @InitParam + public void setOncePerRequest(boolean pOncePerRequest) { + mOncePerRequest = pOncePerRequest; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java new file mode 100755 index 00000000..6a120cc6 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java @@ -0,0 +1,87 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * {@code GenericServlet} has an auto-init system, that automatically invokes + * the method matching the signature {@code void setX(<Type>)}, + * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * naming is supported, lisp-style names will be converted to camelCase. + * Parameter values are automatically converted from string represenation to + * most basic types, if neccessary. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java#1 $ + */ +public abstract class GenericServlet extends javax.servlet.GenericServlet { + + /** + * Called by the web container to indicate to a servlet that it is being + * placed into service. + *

+ * 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)}. + *

+ * 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(ServletConfig pConfig) throws ServletException { + if (pConfig == null) { + throw new ServletConfigException("servletconfig == null"); + } + + try { + BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); + } + catch (InvocationTargetException e) { + throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); + } + + super.init(pConfig); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java new file mode 100755 index 00000000..2b546317 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java @@ -0,0 +1,87 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * {@code HttpServlet} has an auto-init system, that automatically invokes + * the method matching the signature {@code void setX(<Type>)}, + * for every init-parameter {@code x}. Both camelCase and lisp-style paramter + * naming is supported, lisp-style names will be converted to camelCase. + * Parameter values are automatically converted from string represenation to + * most basic types, if neccessary. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java#1 $ + */ +public abstract class HttpServlet extends javax.servlet.http.HttpServlet { + + /** + * Called by the web container to indicate to a servlet that it is being + * placed into service. + *

+ * 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)}. + *

+ * 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 ouccured 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("servletconfig == null"); + } + + try { + BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); + } + catch (InvocationTargetException e) { + throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); + } + + super.init(pConfig); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java new file mode 100755 index 00000000..6fb02155 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java @@ -0,0 +1,50 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 java.lang.annotation.*; + +/** + * Annotation to be used by serlvets/filters, to have their init-method + * automatically convert and set values from their respective configuration. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java#1 $ + * @see com.twelvemonkeys.servlet.GenericFilter#init(javax.servlet.FilterConfig) + * @see com.twelvemonkeys.servlet.GenericServlet#init(javax.servlet.ServletConfig) + * @see com.twelvemonkeys.servlet.HttpServlet#init(javax.servlet.ServletConfig) + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InitParam { + String name() default ""; +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java new file mode 100755 index 00000000..046b5fc6 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java @@ -0,0 +1,122 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 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:

+ * ByteArrayOutputStream buffer = new ByteArraOutputStream();
+ * ServletOutputStream adapter = new OutputStreamAdapter(buffer);
+ * 
+ *

+ * 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:

+ * 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);
+ * 
+ * @author Harald Kuhr + * @author $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java#1 $ + * + */ +public class OutputStreamAdapter extends ServletOutputStream { + + /** The wrapped {@code OutputStream}. */ + protected final OutputStream mOut; + + /** + * Creates an {@code OutputStreamAdapter}. + * + * @param pOut the wrapped {@code OutputStream} + * + * @throws IllegalArgumentException if {@code pOut} is {@code null}. + */ + public OutputStreamAdapter(OutputStream pOut) { + if (pOut == null) { + throw new IllegalArgumentException("out == null"); + } + mOut = pOut; + } + + /** + * Returns the wrapped {@code OutputStream}. + * + * @return the wrapped {@code OutputStream}. + */ + public OutputStream getOutputStream() { + return mOut; + } + + public String toString() { + return "ServletOutputStream adapted from " + mOut.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(int pByte) + throws IOException { + mOut.write(pByte); + } + + // Overide for efficiency + public void write(byte pBytes[]) + throws IOException { + mOut.write(pBytes); + } + + // Overide for efficiency + public void write(byte pBytes[], int pOff, int pLen) + throws IOException { + mOut.write(pBytes, pOff, pLen); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java new file mode 100755 index 00000000..898e7222 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java @@ -0,0 +1,435 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * Note: The servlet is not a true HTTP proxy as described in + * RFC 2616, + * 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. + *

+ * At the moment, no caching of content is implemented. + *

+ * 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 Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java#1 $ + */ +public class ProxyServlet extends GenericServlet { + + /** Remote server host name or IP address */ + protected String mRemoteServer = null; + /** Remote server port */ + protected int mRemotePort = 80; + /** Remote server "mount" path */ + protected String mRemotePath = ""; + + 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) { + mRemoteServer = 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 { + mRemotePort = 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; + } + + mRemotePath = 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 (mRemoteServer == 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(), mRemoteServer, mRemotePort, 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 PUT. + * 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(mRemotePath); + 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()); + } + } + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java new file mode 100755 index 00000000..3db799fe --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java @@ -0,0 +1,40 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.CollectionUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * HeaderMap + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java#1 $ + */ +class SerlvetHeadersMapAdapter extends AbstractServletMapAdapter { + + protected final HttpServletRequest mRequest; + + public SerlvetHeadersMapAdapter(HttpServletRequest pRequest) { + if (pRequest == null) { + throw new IllegalArgumentException("request == null"); + } + mRequest = pRequest; + } + + + protected Iterator valuesImpl(String pName) { + //noinspection unchecked + Enumeration headers = mRequest.getHeaders(pName); + return headers == null ? null : CollectionUtil.iterator(headers); + } + + protected Iterator keysImpl() { + //noinspection unchecked + Enumeration headerNames = mRequest.getHeaderNames(); + return headerNames == null ? null : CollectionUtil.iterator(headerNames); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java new file mode 100755 index 00000000..0665e1fd --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java @@ -0,0 +1,38 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.CollectionUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Iterator; +import java.util.Enumeration; + +/** + * HeaderMap + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java#1 $ + */ +class SerlvetParametersMapAdapter extends AbstractServletMapAdapter { + + protected final HttpServletRequest mRequest; + + public SerlvetParametersMapAdapter(HttpServletRequest pRequest) { + if (pRequest == null) { + throw new IllegalArgumentException("request == null"); + } + mRequest = pRequest; + } + + protected Iterator valuesImpl(String pName) { + String[] values = mRequest.getParameterValues(pName); + return values == null ? null : CollectionUtil.iterator(values); + } + + protected Iterator keysImpl() { + //noinspection unchecked + Enumeration names = mRequest.getParameterNames(); + return names == null ? null : CollectionUtil.iterator(names); + } + +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java new file mode 100755 index 00000000..3d189490 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java @@ -0,0 +1,92 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.ServletException; + +/** + * ServletConfigException. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java#2 $ + */ +public class ServletConfigException extends ServletException { + + /** + * Creates a {@code ServletConfigException} with the given message. + * + * @param pMessage the exception message + */ + public ServletConfigException(String pMessage) { + super(pMessage); + } + + /** + * Creates a {@code ServletConfigException} with the given message and cause. + * + * @param pMessage the exception message + * @param pCause the exception cause + */ + public ServletConfigException(String pMessage, Throwable pCause) { + super(pMessage, pCause); + if (getCause() == null) { + initCause(pCause); + } + } + + /** + * Creates a {@code ServletConfigException} with the cause. + * + * @param pCause the exception cause + */ + public ServletConfigException(Throwable pCause) { + super("Erorr in Servlet configuration: " + pCause.getMessage(), pCause); + if (getCause() == null) { + initCause(pCause); + } + } + + /** + * Gets the cause of this {@code ServletConfigException}. + * + * @return the cause, or {@code null} if unknown. + * @see #getRootCause() + */ +// public final Throwable getCause() { +// Throwable cause = super.getCause(); +// return cause != null ? cause : super.getRootCause(); +// } + + /** + * @deprecated Use {@link #getCause()} instead. + */ +// public final Throwable getRootCause() { +// return getCause(); +// } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java new file mode 100755 index 00000000..d01d611a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java @@ -0,0 +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 "TwelveMonkeys" 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 OWNER 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 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. + *

+ * This {@code Map} is not synchronized. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java#2 $ + */ +class ServletConfigMapAdapter extends AbstractMap implements Map, Serializable, Cloneable { + + enum ConfigType { + ServletConfig, FilterConfig, ServletContext + } + +// private final boolean mIsServlet; + private final ConfigType mType; + + private final ServletConfig mServletConfig; + private final FilterConfig mFilterConfig; + private final ServletContext mServletContext; + + // Cache the entry set + private transient Set> mEntrySet; + + public ServletConfigMapAdapter(ServletConfig pConfig) { + this(pConfig, ConfigType.ServletConfig); + } + + public ServletConfigMapAdapter(FilterConfig pConfig) { + this(pConfig, ConfigType.FilterConfig); + } + + public ServletConfigMapAdapter(ServletContext pContext) { + this(pContext, ConfigType.ServletContext); + } + + private ServletConfigMapAdapter(Object pConfig, ConfigType pType) { + if (pConfig == null) { + // Could happen of client code invokes with null reference + throw new IllegalArgumentException("Config == null"); + } + + mType = pType; + + switch (mType) { + case ServletConfig: + mServletConfig = (ServletConfig) pConfig; + mFilterConfig = null; + mServletContext = null; + break; + case FilterConfig: + mServletConfig = null; + mFilterConfig = (FilterConfig) pConfig; + mServletContext = null; + break; + case ServletContext: + mServletConfig = null; + mFilterConfig = null; + mServletContext = (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 (mType) { + case ServletConfig: + return mServletConfig.getServletName(); + case FilterConfig: + return mFilterConfig.getFilterName(); + case ServletContext: + return mServletContext.getServletContextName(); + default: + throw new IllegalStateException(); + } + } + + /** + * Gets the servlet context from the config. + * + * @return the servlet context + */ + public final ServletContext getServletContext() { + switch (mType) { + case ServletConfig: + return mServletConfig.getServletContext(); + case FilterConfig: + return mFilterConfig.getServletContext(); + case ServletContext: + return mServletContext; + default: + throw new IllegalStateException(); + } + } + + public final Enumeration getInitParameterNames() { + switch (mType) { + case ServletConfig: + return mServletConfig.getInitParameterNames(); + case FilterConfig: + return mFilterConfig.getInitParameterNames(); + case ServletContext: + return mServletContext.getInitParameterNames(); + default: + throw new IllegalStateException(); + } + } + + public final String getInitParameter(final String pName) { + switch (mType) { + case ServletConfig: + return mServletConfig.getInitParameter(pName); + case FilterConfig: + return mFilterConfig.getInitParameter(pName); + case ServletContext: + return mServletContext.getInitParameter(pName); + default: + throw new IllegalStateException(); + } + } + + public Set> entrySet() { + if (mEntrySet == null) { + mEntrySet = createEntrySet(); + } + return mEntrySet; + } + + private Set> createEntrySet() { + return new AbstractSet>() { + // Cache size, if requested, -1 means not calculated + private int mSize = -1; + + public Iterator> iterator() { + return new Iterator>() { + // Iterator is backed by initParameterNames enumeration + final Enumeration mNames = getInitParameterNames(); + + public boolean hasNext() { + return mNames.hasMoreElements(); + } + + public Entry next() { + final String key = (String) mNames.nextElement(); + return new Entry() { + 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 (mSize < 0) { + mSize = calculateSize(); + } + + return mSize; + } + + 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(); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java new file mode 100755 index 00000000..1c29a951 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java @@ -0,0 +1,114 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.OutputStream; + +/** + * A delegate for handling stream support in wrapped servlet responses. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java#2 $ + */ +public class ServletResponseStreamDelegate { + private Object mOut = null; + protected final ServletResponse mResponse; + + public ServletResponseStreamDelegate(ServletResponse pResponse) { + if (pResponse == null) { + throw new IllegalArgumentException("response == null"); + } + mResponse = pResponse; + } + + // NOTE: Intentionally NOT threadsafe, as one request/response should be + // handled by one thread ONLY. + public final ServletOutputStream getOutputStream() throws IOException { + if (mOut == null) { + OutputStream out = createOutputStream(); + mOut = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); + } + else if (mOut instanceof PrintWriter) { + throw new IllegalStateException("getWriter() allready called."); + } + + return (ServletOutputStream) mOut; + } + + // NOTE: Intentionally NOT threadsafe, as one request/response should be + // handled by one thread ONLY. + public final PrintWriter getWriter() throws IOException { + if (mOut == null) { + // NOTE: getCharacterEncoding may should not return null + OutputStream out = createOutputStream(); + String charEncoding = mResponse.getCharacterEncoding(); + mOut = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); + } + else if (mOut instanceof ServletOutputStream) { + throw new IllegalStateException("getOutputStream() allready called."); + } + + return (PrintWriter) mOut; + } + + /** + * Returns the {@code OutputStream}. + * Override this method to provide a decoreated outputstream. + * This method is guaranteed to be invoked only once for a request/response. + *

+ * 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 mResponse.getOutputStream(); + } + + public void flushBuffer() throws IOException { + if (mOut instanceof ServletOutputStream) { + ((ServletOutputStream) mOut).flush(); + } + else if (mOut != null) { + ((PrintWriter) mOut).flush(); + } + } + + public void resetBuffer() { + // TODO: Is this okay? Probably not... :-) + mOut = null; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java new file mode 100755 index 00000000..89724afe --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java @@ -0,0 +1,1060 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.util.DebugUtil; +import com.twelvemonkeys.util.convert.ConversionException; +import com.twelvemonkeys.util.convert.Converter; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + + +/** + * Various servlet related helper methods. + * + * @author Harald Kuhr + * @author Eirik Torske + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java#3 $ + */ +public final class ServletUtil { + + /** + * "javax.servlet.include.request_uri" + */ + private final static String ATTRIB_INC_REQUEST_URI = "javax.servlet.include.request_uri"; + + /** + * "javax.servlet.include.context_path" + */ + private final static String ATTRIB_INC_CONTEXT_PATH = "javax.servlet.include.context_path"; + + /** + * "javax.servlet.include.servlet_path" + */ + private final static String ATTRIB_INC_SERVLET_PATH = "javax.servlet.include.servlet_path"; + + /** + * "javax.servlet.include.path_info" + */ + private final static String ATTRIB_INC_PATH_INFO = "javax.servlet.include.path_info"; + + /** + * "javax.servlet.include.query_string" + */ + private final static String ATTRIB_INC_QUERY_STRING = "javax.servlet.include.query_string"; + + /** + * "javax.servlet.forward.request_uri" + */ + private final static String ATTRIB_FWD_REQUEST_URI = "javax.servlet.forward.request_uri"; + + /** + * "javax.servlet.forward.context_path" + */ + private final static String ATTRIB_FWD_CONTEXT_PATH = "javax.servlet.forward.context_path"; + + /** + * "javax.servlet.forward.servlet_path" + */ + private final static String ATTRIB_FWD_SERVLET_PATH = "javax.servlet.forward.servlet_path"; + + /** + * "javax.servlet.forward.path_info" + */ + private final static String ATTRIB_FWD_PATH_INFO = "javax.servlet.forward.path_info"; + + /** + * "javax.servlet.forward.query_string" + */ + private final static String ATTRIB_FWD_QUERY_STRING = "javax.servlet.forward.query_string"; + + /** + * Don't create, static methods only + */ + private ServletUtil() { + } + + /** + * Gets the value of the given parameter from the request, or if the + * parameter is not set, the default value. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter, or the default value, if the + * parameter is not set. + */ + public static String getParameter(ServletRequest pReq, String pName, String pDefault) { + String str = pReq.getParameter(pName); + + return ((str != null) ? str : pDefault); + } + + /** + * Gets the value of the given parameter from the request converted to + * an Object. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pType the type of object (class) to return + * @param pFormat the format to use (might be {@code null} in many cases) + * @param pDefault the default value + * @return the value of the parameter converted to a boolean, or the + * default value, if the parameter is not set. + * @throws IllegalArgumentException if {@code pDefault} is + * non-{@code null} and not an instance of {@code pType} + * @throws NullPointerException if {@code pReq}, {@code pName} or + * {@code pType} is {@code null}. + * @todo Well, it's done. Need some thinking... + * @see Converter#toObject + */ + + // public static T getParameter(ServletRequest pReq, String pName, + // String pFormat, T pDefault) { + static Object getParameter(ServletRequest pReq, String pName, Class pType, String pFormat, Object pDefault) { + // Test if pDefault is either null or instance of pType + if (pDefault != null && !pType.isInstance(pDefault)) { + throw new IllegalArgumentException("default value not instance of " + pType + ": " + pDefault.getClass()); + } + + String str = pReq.getParameter(pName); + + if (str == null) { + return pDefault; + } + try { + return Converter.getInstance().toObject(str, pType, pFormat); + } + catch (ConversionException ce) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a boolean. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to a boolean, or the + * default value, if the parameter is not set. + */ + public static boolean getBooleanParameter(ServletRequest pReq, String pName, boolean pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? Boolean.valueOf(str) : pDefault); + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * an int. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to an int, or the default + * value, if the parameter is not set. + */ + public static int getIntParameter(ServletRequest pReq, String pName, int pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? Integer.parseInt(str) : pDefault); + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * an long. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to an long, or the default + * value, if the parameter is not set. + */ + public static long getLongParameter(ServletRequest pReq, String pName, long pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? Long.parseLong(str) : pDefault); + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a float. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to a float, or the default + * value, if the parameter is not set. + */ + public static float getFloatParameter(ServletRequest pReq, String pName, float pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? Float.parseFloat(str) : pDefault); + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a double. If the parameter is not set or not parseable, the default + * value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to n double, or the default + * value, if the parameter is not set. + */ + public static double getDoubleParameter(ServletRequest pReq, String pName, double pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? Double.parseDouble(str) : pDefault); + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a Date. If the parameter is not set or not parseable, the + * default value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter converted to a Date, or the + * default value, if the parameter is not set. + * @see com.twelvemonkeys.lang.StringUtil#toDate(String) + */ + public static long getDateParameter(ServletRequest pReq, String pName, long pDefault) { + String str = pReq.getParameter(pName); + try { + return ((str != null) ? StringUtil.toDate(str).getTime() : pDefault); + } + catch (IllegalArgumentException iae) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a Date. If the parameter is not set or not parseable, the + * default value is returned. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pFormat the date format to use + * @param pDefault the default value + * @return the value of the parameter converted to a Date, or the + * default value, if the parameter is not set. + * @see com.twelvemonkeys.lang.StringUtil#toDate(String,String) + */ + /* + public static long getDateParameter(ServletRequest pReq, String pName, String pFormat, long pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? StringUtil.toDate(str, pFormat).getTime() : pDefault); + } + catch (IllegalArgumentException iae) { + return pDefault; + } + } + */ + + /** + * Builds a full-blown HTTP/HTTPS URL from a + * {@code javax.servlet.http.HttpServletRequest} object. + *

+ * + * @param pRequest The HTTP servlet request object. + * @return the reproduced URL + * @deprecated Use {@link javax.servlet.http.HttpServletRequest#getRequestURL()} + * instead. + */ + static StringBuffer buildHTTPURL(HttpServletRequest pRequest) { + StringBuffer resultURL = new StringBuffer(); + + // Scheme, as in http, https, ftp etc + String scheme = pRequest.getScheme(); + resultURL.append(scheme); + resultURL.append("://"); + resultURL.append(pRequest.getServerName()); + + // Append port only if not default port + int port = pRequest.getServerPort(); + if (port > 0 && + !(("http".equals(scheme) && port == 80) || + ("https".equals(scheme) && port == 443))) { + resultURL.append(":"); + resultURL.append(port); + } + + // Append URI + resultURL.append(pRequest.getRequestURI()); + + // If present, append extra path info + String pathInfo = pRequest.getPathInfo(); + if (pathInfo != null) { + resultURL.append(pathInfo); + } + + return resultURL; + } + + /** + * Gets the URI of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.request_uri"} + * + * @param pRequest the servlet request + * @return the URI of the included resource, or {@code null} if no include + * @see HttpServletRequest#getRequestURI + * @since Servlet 2.2 + */ + public static String getIncludeRequestURI(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_REQUEST_URI); + } + + /** + * Gets the context path of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.context_path"} + * + * @param pRequest the servlet request + * @return the context path of the included resource, or {@code null} if no include + * @see HttpServletRequest#getContextPath + * @since Servlet 2.2 + */ + public static String getIncludeContextPath(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_CONTEXT_PATH); + } + + /** + * Gets the servlet path of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.servlet_path"} + * + * @param pRequest the servlet request + * @return the servlet path of the included resource, or {@code null} if no include + * @see HttpServletRequest#getServletPath + * @since Servlet 2.2 + */ + public static String getIncludeServletPath(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_SERVLET_PATH); + } + + /** + * Gets the path info of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.path_info"} + * + * @param pRequest the servlet request + * @return the path info of the included resource, or {@code null} if no include + * @see HttpServletRequest#getPathInfo + * @since Servlet 2.2 + */ + public static String getIncludePathInfo(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_PATH_INFO); + } + + /** + * Gets the query string of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.query_string"} + * + * @param pRequest the servlet request + * @return the query string of the included resource, or {@code null} if no include + * @see HttpServletRequest#getQueryString + * @since Servlet 2.2 + */ + public static String getIncludeQueryString(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_QUERY_STRING); + } + + /** + * Gets the URI of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.request_uri"} + * + * @param pRequest the servlet request + * @return the URI of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getRequestURI + * @since Servlet 2.4 + */ + public static String getForwardRequestURI(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_REQUEST_URI); + } + + /** + * Gets the context path of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.context_path"} + * + * @param pRequest the servlet request + * @return the context path of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getContextPath + * @since Servlet 2.4 + */ + public static String getForwardContextPath(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_CONTEXT_PATH); + } + + /** + * Gets the servlet path of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.servlet_path"} + * + * @param pRequest the servlet request + * @return the servlet path of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getServletPath + * @since Servlet 2.4 + */ + public static String getForwardServletPath(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_SERVLET_PATH); + } + + /** + * Gets the path info of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.path_info"} + * + * @param pRequest the servlet request + * @return the path info of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getPathInfo + * @since Servlet 2.4 + */ + public static String getForwardPathInfo(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_PATH_INFO); + } + + /** + * Gets the query string of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.query_string"} + * + * @param pRequest the servlet request + * @return the query string of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getQueryString + * @since Servlet 2.4 + */ + public static String getForwardQueryString(ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_QUERY_STRING); + } + + /** + * Gets the name of the servlet or the script that generated the servlet. + * + * @param pRequest The HTTP servlet request object. + * @return the script name. + * @todo Read the spec, seems to be a mismatch with the Servlet API... + * @see javax.servlet.http.HttpServletRequest#getServletPath() + */ + static String getScriptName(HttpServletRequest pRequest) { + String requestURI = pRequest.getRequestURI(); + return StringUtil.getLastElement(requestURI, "/"); + } + + /** + * Gets the request URI relative to the current context path. + *

+ * As an example:

+     * requestURI = "/webapp/index.jsp"
+     * contextPath = "/webapp"
+     * 
+ * The method will return {@code "/index.jsp"}. + * + * @param pRequest the current HTTP request + * @return the request URI relative to the current context path. + */ + public static String getContextRelativeURI(HttpServletRequest pRequest) { + String context = pRequest.getContextPath(); + if (!StringUtil.isEmpty(context)) { // "" for root context + return pRequest.getRequestURI().substring(context.length()); + } + return pRequest.getRequestURI(); + } + + /** + * Returns a {@code URL} containing the real path for a given virtual + * path, on URL form. + * Note that this mehtod will return {@code null} for all the same reasons + * as {@code ServletContext.getRealPath(java.lang.String)} does. + * + * @param pContext the servlet context + * @param pPath the virtual path + * @return a {@code URL} object containing the path, or {@code null}. + * @throws MalformedURLException if the path refers to a malformed URL + * @see ServletContext#getRealPath(java.lang.String) + * @see ServletContext#getResource(java.lang.String) + */ + public static URL getRealURL(ServletContext pContext, String pPath) throws MalformedURLException { + String realPath = pContext.getRealPath(pPath); + if (realPath != null) { + // NOTE: First convert to URI, as of Java 6 File.toURL is deprecated + return new File(realPath).toURI().toURL(); + } + return null; + } + + /** + * Gets the temp directory for the given {@code ServletContext} (webapp). + * + * @param pContext the servlet context + * @return the temp directory + */ + public static File getTempDir(ServletContext pContext) { + return (File) pContext.getAttribute("javax.servlet.context.tempdir"); + } + + /** + * Gets the identificator string containing the unique identifier assigned + * to this session. + * The identifier is assigned by the servlet container and is implementation + * dependent. + * + * @param pRequest The HTTP servlet request object. + * @return the session Id + */ + public static String getSessionId(HttpServletRequest pRequest) { + HttpSession session = pRequest.getSession(); + + return (session != null) ? session.getId() : null; + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code ServletConfig}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @param pConfig the serlvet configuration + * @return a {@code Map} view of the config + * @throws IllegalArgumentException if {@code pConfig} is {@code null} + */ + public static Map asMap(ServletConfig pConfig) { + return new ServletConfigMapAdapter(pConfig); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code FilterConfig}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @param pConfig the servlet filter configuration + * @return a {@code Map} view of the config + * @throws IllegalArgumentException if {@code pConfig} is {@code null} + */ + public static Map asMap(FilterConfig pConfig) { + return new ServletConfigMapAdapter(pConfig); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code ServletContext}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @param pContext the servlet context + * @return a {@code Map} view of the init parameters + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map initParamsAsMap(final ServletContext pContext) { + return new ServletConfigMapAdapter(pContext); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code HttpServletRequest}s request parameters. + * + * @param pRequest the request + * @return a {@code Map} view of the request parameters + * @throws IllegalArgumentException if {@code pRequest} is {@code null} + */ + public static Map> parametersAsMap(final HttpServletRequest pRequest) { + return new SerlvetParametersMapAdapter(pRequest); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code HttpServletRequest}s request headers. + * + * @param pRequest the request + * @return a {@code Map} view of the request headers + * @throws IllegalArgumentException if {@code pRequest} is {@code null} + */ + public static Map> headersAsMap(final HttpServletRequest pRequest) { + return new SerlvetHeadersMapAdapter(pRequest); + } + + /** + * Creates a wrapper that implements either {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()}. + * + * @param pImplementation the servlet response to create a wrapper for + * @return a {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()} + */ + public static ServletResponse createWrapper(final ServletResponseWrapper pImplementation) { + // TODO: Get all interfaces from implementation + if (pImplementation.getResponse() instanceof HttpServletResponse) { + return (HttpServletResponse) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), + new Class[]{HttpServletResponse.class, ServletResponse.class}, + new HttpServletResponseHandler(pImplementation)); + } + return pImplementation; + } + + /** + * Creates a wrapper that implements either {@code ServletRequest} or + * {@code HttpServletRequest}, depending on the type of + * {@code pImplementation.getRequest()}. + * + * @param pImplementation the servlet request to create a wrapper for + * @return a {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()} + */ + public static ServletRequest createWrapper(final ServletRequestWrapper pImplementation) { + // TODO: Get all interfaces from implementation + if (pImplementation.getRequest() instanceof HttpServletRequest) { + return (HttpServletRequest) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), + new Class[]{HttpServletRequest.class, ServletRequest.class}, + new HttpServletRequestHandler(pImplementation)); + } + return pImplementation; + } + + + /** + * Prints the init parameters in a {@code javax.servlet.ServletConfig} + * object to a {@code java.io.PrintStream}. + *

+ * + * @param pServletConfig The Servlet Config object. + * @param pPrintStream The {@code java.io.PrintStream} for flushing + * the results. + */ + public static void printDebug(final ServletConfig pServletConfig, final PrintStream pPrintStream) { + Enumeration parameterNames = pServletConfig.getInitParameterNames(); + + while (parameterNames.hasMoreElements()) { + String initParameterName = (String) parameterNames.nextElement(); + + pPrintStream.println(initParameterName + ": " + pServletConfig.getInitParameter(initParameterName)); + } + } + + /** + * Prints the init parameters in a {@code javax.servlet.ServletConfig} + * object to {@code System.out}. + * + * @param pServletConfig the Servlet Config object. + */ + public static void printDebug(final ServletConfig pServletConfig) { + printDebug(pServletConfig, System.out); + } + + /** + * Prints the init parameters in a {@code javax.servlet.ServletContext} + * object to a {@code java.io.PrintStream}. + * + * @param pServletContext the Servlet Context object. + * @param pPrintStream the {@code java.io.PrintStream} for flushing the + * results. + */ + public static void printDebug(final ServletContext pServletContext, final PrintStream pPrintStream) { + Enumeration parameterNames = pServletContext.getInitParameterNames(); + + while (parameterNames.hasMoreElements()) { + String initParameterName = (String) parameterNames.nextElement(); + + pPrintStream.println(initParameterName + ": " + pServletContext.getInitParameter(initParameterName)); + } + } + + /** + * Prints the init parameters in a {@code javax.servlet.ServletContext} + * object to {@code System.out}. + * + * @param pServletContext The Servlet Context object. + */ + public static void printDebug(final ServletContext pServletContext) { + printDebug(pServletContext, System.out); + } + + /** + * Prints an excerpt of the residing information in a + * {@code javax.servlet.http.HttpServletRequest} object to a + * {@code java.io.PrintStream}. + * + * @param pRequest The HTTP servlet request object. + * @param pPrintStream The {@code java.io.PrintStream} for flushing + * the results. + */ + public static void printDebug(final HttpServletRequest pRequest, final PrintStream pPrintStream) { + String indentation = " "; + StringBuilder buffer = new StringBuilder(); + + // Returns the name of the authentication scheme used to protect the + // servlet, for example, "BASIC" or "SSL," or null if the servlet was + // not protected. + buffer.append(indentation); + buffer.append("Authentication scheme: "); + buffer.append(pRequest.getAuthType()); + buffer.append("\n"); + + // Returns the portion of the request URI that indicates the context + // of the request. + buffer.append(indentation); + buffer.append("Context path: "); + buffer.append(pRequest.getContextPath()); + buffer.append("\n"); + + // Returns an enumeration of all the header mNames this request contains. + buffer.append(indentation); + buffer.append("Header:"); + buffer.append("\n"); + Enumeration headerNames = pRequest.getHeaderNames(); + + while (headerNames.hasMoreElements()) { + String headerElement = (String) headerNames.nextElement(); + + buffer.append(indentation); + buffer.append(indentation); + buffer.append(headerElement); + buffer.append(": "); + buffer.append(pRequest.getHeader(headerElement)); + buffer.append("\n"); + } + + // Returns the name of the HTTP method with which this request was made, + // for example, GET, POST, or PUT. + buffer.append(indentation); + buffer.append("HTTP method: "); + buffer.append(pRequest.getMethod()); + buffer.append("\n"); + + // Returns any extra path information associated with the URL the client + // sent when it made this request. + buffer.append(indentation); + buffer.append("Extra path information from client: "); + buffer.append(pRequest.getPathInfo()); + buffer.append("\n"); + + // Returns any extra path information after the servlet name but before + // the query string, and translates it to a real path. + buffer.append(indentation); + buffer.append("Extra translated path information from client: "); + buffer.append(pRequest.getPathTranslated()); + buffer.append("\n"); + + // Returns the login of the user making this request, if the user has + // been authenticated, or null if the user has not been authenticated. + buffer.append(indentation); + String userInfo = pRequest.getRemoteUser(); + + if (StringUtil.isEmpty(userInfo)) { + buffer.append("User is not authenticated"); + } + else { + buffer.append("User logint: "); + buffer.append(userInfo); + } + buffer.append("\n"); + + // Returns the session ID specified by the client. + buffer.append(indentation); + buffer.append("Session ID from client: "); + buffer.append(pRequest.getRequestedSessionId()); + buffer.append("\n"); + + // Returns the server name. + buffer.append(indentation); + buffer.append("Server name: "); + buffer.append(pRequest.getServerName()); + buffer.append("\n"); + + // Returns the part of this request's URL from the protocol name up + // to the query string in the first line of the HTTP request. + buffer.append(indentation); + buffer.append("Request URI: ").append(pRequest.getRequestURI()); + buffer.append("\n"); + + // Returns the path info. + buffer.append(indentation); + buffer.append("Path information: ").append(pRequest.getPathInfo()); + buffer.append("\n"); + + // Returns the part of this request's URL that calls the servlet. + buffer.append(indentation); + buffer.append("Servlet path: ").append(pRequest.getServletPath()); + buffer.append("\n"); + + // Returns the query string that is contained in the request URL after + // the path. + buffer.append(indentation); + buffer.append("Query string: ").append(pRequest.getQueryString()); + buffer.append("\n"); + + // Returns an enumeration of all the parameters bound to this request. + buffer.append(indentation); + buffer.append("Parameters:"); + buffer.append("\n"); + Enumeration parameterNames = pRequest.getParameterNames(); + while (parameterNames.hasMoreElements()) { + String parameterName = (String) parameterNames.nextElement(); + + buffer.append(indentation); + buffer.append(indentation); + buffer.append(parameterName); + buffer.append(": "); + buffer.append(pRequest.getParameter(parameterName)); + buffer.append("\n"); + } + + // Returns an enumeration of all the attribute objects bound to this + // request. + buffer.append(indentation); + buffer.append("Attributes:"); + buffer.append("\n"); + Enumeration attributeNames = pRequest.getAttributeNames(); + while (attributeNames.hasMoreElements()) { + String attributeName = (String) attributeNames.nextElement(); + + buffer.append(indentation); + buffer.append(indentation); + buffer.append(attributeName); + buffer.append(": "); + buffer.append(pRequest.getAttribute(attributeName).toString()); + buffer.append("\n"); + } + pPrintStream.println(buffer.toString()); + } + + /** + * Prints an excerpt of the residing information in a + * {@code javax.servlet.http.HttpServletRequest} object to + * {@code System.out}. + * + * @param pRequest The HTTP servlet request object. + */ + public static void printDebug(final HttpServletRequest pRequest) { + printDebug(pRequest, System.out); + } + + /** + * Prints an excerpt of a {@code javax.servlet.http.HttpSession} object + * to a {@code java.io.PrintStream}. + * + * @param pHttpSession The HTTP Session object. + * @param pPrintStream The {@code java.io.PrintStream} for flushing + * the results. + */ + public static void printDebug(final HttpSession pHttpSession, final PrintStream pPrintStream) { + String indentation = " "; + StringBuilder buffer = new StringBuilder(); + + if (pHttpSession == null) { + buffer.append(indentation); + buffer.append("No session object available"); + buffer.append("\n"); + } + else { + + // Returns a string containing the unique identifier assigned to + //this session + buffer.append(indentation); + buffer.append("Session ID: ").append(pHttpSession.getId()); + buffer.append("\n"); + + // Returns the last time the client sent a request associated with + // this session, as the number of milliseconds since midnight + // January 1, 1970 GMT, and marked by the time the container + // recieved the request + buffer.append(indentation); + buffer.append("Last accessed time: "); + buffer.append(DebugUtil.getTimestamp(pHttpSession.getLastAccessedTime())); + buffer.append("\n"); + + // Returns the time when this session was created, measured in + // milliseconds since midnight January 1, 1970 GMT + buffer.append(indentation); + buffer.append("Creation time: "); + buffer.append(DebugUtil.getTimestamp(pHttpSession.getCreationTime())); + buffer.append("\n"); + + // Returns true if the client does not yet know about the session + // or if the client chooses not to join the session + buffer.append(indentation); + buffer.append("New session?: "); + buffer.append(pHttpSession.isNew()); + buffer.append("\n"); + + // Returns the maximum time interval, in seconds, that the servlet + // container will keep this session open between client accesses + buffer.append(indentation); + buffer.append("Max inactive interval: "); + buffer.append(pHttpSession.getMaxInactiveInterval()); + buffer.append("\n"); + + // Returns an enumeration of all the attribute objects bound to + // this session + buffer.append(indentation); + buffer.append("Attributes:"); + buffer.append("\n"); + Enumeration attributeNames = pHttpSession.getAttributeNames(); + + while (attributeNames.hasMoreElements()) { + String attributeName = (String) attributeNames.nextElement(); + + buffer.append(indentation); + buffer.append(indentation); + buffer.append(attributeName); + buffer.append(": "); + buffer.append(pHttpSession.getAttribute(attributeName).toString()); + buffer.append("\n"); + } + } + pPrintStream.println(buffer.toString()); + } + + /** + * Prints an excerpt of a {@code javax.servlet.http.HttpSession} + * object to {@code System.out}. + *

+ * + * @param pHttpSession The HTTP Session object. + */ + public static void printDebug(final HttpSession pHttpSession) { + printDebug(pHttpSession, System.out); + } + + private static class HttpServletResponseHandler implements InvocationHandler { + private ServletResponse mResponse; + private HttpServletResponse mHttpResponse; + + HttpServletResponseHandler(ServletResponseWrapper pResponse) { + mResponse = pResponse; + mHttpResponse = (HttpServletResponse) pResponse.getResponse(); + } + + public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { + try { + if (pMethod.getDeclaringClass().isInstance(mResponse)) { + //System.out.println("Invoking " + pMethod + " on wrapper"); + return pMethod.invoke(mResponse, pArgs); + } + // Method is not implemented in wrapper + //System.out.println("Invoking " + pMethod + " on wrapped object"); + return pMethod.invoke(mHttpResponse, pArgs); + } + catch (InvocationTargetException e) { + // Unwrap, to avoid UndeclaredThrowableException... + throw e.getTargetException(); + } + } + } + + private static class HttpServletRequestHandler implements InvocationHandler { + private ServletRequest mRequest; + private HttpServletRequest mHttpRequest; + + HttpServletRequestHandler(ServletRequestWrapper pRequest) { + mRequest = pRequest; + mHttpRequest = (HttpServletRequest) pRequest.getRequest(); + } + + public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { + try { + if (pMethod.getDeclaringClass().isInstance(mRequest)) { + //System.out.println("Invoking " + pMethod + " on wrapper"); + return pMethod.invoke(mRequest, pArgs); + } + // Method is not implemented in wrapper + //System.out.println("Invoking " + pMethod + " on wrapped object"); + return pMethod.invoke(mHttpRequest, pArgs); + } + catch (InvocationTargetException e) { + // Unwrap, to avoid UndeclaredThrowableException... + throw e.getTargetException(); + } + } + } +} + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java new file mode 100755 index 00000000..295a0b3a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java @@ -0,0 +1,311 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + * + * Intercepts requests, and returns HTTP response 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 Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java#1 $ + * @see #setMaxConcurrentThreadCount + * @see #setResponseMessages + */ +public class ThrottleFilter extends GenericFilter { + + /** + * Minimum free thread count, defaults to {@code 10} + */ + protected int mMaxConcurrentThreadCount = 10; + + /** + * The number of running request threads + */ + private int mRunningThreads = 0; + private final Object mRunningThreadsLock = 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 mResponseMessageNames = new HashMap(10); + + /** + * The reposne message sent to user agents, if the request is rejected + */ + private String[] mResponseMessageTypes = null; + + /** + * Cache for response messages + */ + private Map mResponseCache = new HashMap(10); + + + /** + * Sets the minimum free thread count. + * + * @param pMaxConcurrentThreadCount + */ + public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) { + if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) { + try { + mMaxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount); + } + catch (NumberFormatException nfe) { + // Use default + } + } + } + + /** + * Sets the response message sent to the user agent, if the request is + * rejected. + *
+ * The format is {@code <mime-type>=<filename>, + * <mime-type>=<filename>}. + *
+ * Example: {@code <text/vnd.wap.wmlgt;=</errors/503.wml>, + * <text/html>=</errors/503.html>} + * + * @param pResponseMessages + */ + public void setResponseMessages(String pResponseMessages) { + // Split string in type=filename pairs + String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t"); + List types = new ArrayList(); + + for (int i = 0; i < mappings.length; i++) { + // Split pairs on '=' + String[] mapping = StringUtil.toStringArray(mappings[i], "= "); + + // Test for wrong mapping + if ((mapping == null) || (mapping.length < 2)) { + log("Error in init param \"responseMessages\": " + pResponseMessages); + continue; + } + types.add(mapping[0]); + mResponseMessageNames.put(mapping[0], mapping[1]); + } + + // Create arrays + mResponseMessageTypes = (String[]) 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 true if the request should be handled. + */ + private boolean beginRequest() { + synchronized (mRunningThreadsLock) { + mRunningThreads++; + } + return (mRunningThreads <= mMaxConcurrentThreadCount); + } + + /** + * Marks the end of the request + */ + private void doneRequest() { + synchronized (mRunningThreadsLock) { + mRunningThreads--; + } + } + + /** + * 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 (mResponseMessageTypes != null) { + String accept = pRequest.getHeader("Accept"); + + for (int i = 0; i < mResponseMessageTypes.length; i++) { + String type = mResponseMessageTypes[i]; + + // 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 = (String) mResponseMessageNames.get(pContentType); + + // Get cached value + CacheEntry entry = (CacheEntry) mResponseCache.get(fileName); + + if ((entry == null) || entry.isExpired()) { + + // Create and add or replace cached value + entry = new CacheEntry(readMessage(fileName)); + mResponseCache.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 mValue; + private long mTimestamp = -1; + + CacheEntry(Object pValue) { + mValue = pValue; + mTimestamp = System.currentTimeMillis(); + } + + Object getValue() { + return mValue; + } + + boolean isExpired() { + return (System.currentTimeMillis() - mTimestamp) > 60000; // Cache 1 minute + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java new file mode 100755 index 00000000..cad2bdf3 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java @@ -0,0 +1,113 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * TimingFilter class description. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java#1 $ + */ +public class TimingFilter extends GenericFilter { + + private String mAttribUsage = null; + + /** + * Method init + * + * @throws ServletException + */ + public void init() throws ServletException { + mAttribUsage = getFilterName() + ".timerDelta"; + } + + /** + * + * @param pRequest + * @param pResponse + * @param pChain + * @throws IOException + * @throws ServletException + */ + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) + throws IOException, ServletException { + // Get total usage of earlier filters on same level + Object usageAttrib = pRequest.getAttribute(mAttribUsage); + long total = 0; + + if (usageAttrib instanceof Long) { + // If set, get value, and remove attribute for nested resources + total = ((Long) usageAttrib).longValue(); + pRequest.removeAttribute(mAttribUsage); + } + + // Start timing + long start = System.currentTimeMillis(); + + try { + // Continue chain + pChain.doFilter(pRequest, pResponse); + } + finally { + // Stop timing + long end = System.currentTimeMillis(); + + // Get time usage of included resources, add to total usage + usageAttrib = pRequest.getAttribute(mAttribUsage); + long usage = 0; + if (usageAttrib instanceof Long) { + usage = ((Long) usageAttrib).longValue(); + } + + // Get the name of the included resource + String resourceURI = ServletUtil.getIncludeRequestURI(pRequest); + + // If none, this is probably the parent page itself + if (resourceURI == null) { + resourceURI = ((HttpServletRequest) pRequest).getRequestURI(); + } + long delta = end - start; + + log("Request processing time for resource \"" + resourceURI + "\": " + + (delta - usage) + " ms (accumulated: " + delta + " ms)."); + + // Store total usage + total += delta; + pRequest.setAttribute(mAttribUsage, new Long(total)); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java new file mode 100755 index 00000000..cc9782b0 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java @@ -0,0 +1,238 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.ServletResponseWrapper; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.OutputStream; +import java.io.FilterOutputStream; + +/** + * Removes extra unneccessary white space from a servlet response. + * White space is defined as per {@link Character#isWhitespace(char)}. + *

+ * 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). + *

+ * At the moment this filter has no concept of encoding. + * This means, that if some multi-byte escape sequence contains one or more + * bytes that individually is treated as a white space, these bytes + * may be skipped. + * As UTF-8 + * guarantees that no bytes are repeated in this way, this filter can safely + * filter UTF-8. + * Simple 8 bit character encodings, like the + * ISO/IEC 8859 standard, or + * + * are always safe. + *

+ * Configuration
+ * 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. + *

+ * Servlet 2.4 version, filter section:
+ *

+ * <!-- TrimWS Filter Configuration -->
+ * <filter>
+ *      <filter-name>trimws</filter-name>
+ *      <filter-class>com.twelvemonkeys.servlet.TrimWhiteSpaceFilter</filter-class>
+ *      <!-- auto-flush=true is the default, may be omitted -->
+ *      <init-param>
+ *          <param-name>auto-flush</param-name>
+ *          <param-value>true</param-value>
+ *      </init-param>
+ * </filter>
+ * 
+ * Filter-mapping section:
+ *
+ * <!-- TimWS Filter Mapping -->
+ * <filter-mapping>
+ *      <filter-name>trimws</filter-name>
+ *      <url-pattern>*.html</url-pattern>
+ *      <dispatcher>REQUEST</dispatcher>
+ *      <dispatcher>FORWARD</dispatcher>
+ * </filter-mapping>
+ * <filter-mapping>
+ *      <filter-name>trimws</filter-name>
+ *      <url-pattern>*.jsp</url-pattern>
+ *      <dispatcher>REQUEST</dispatcher>
+ *      <dispatcher>FORWARD</dispatcher>
+ * </filter-mapping>
+ * 
+ * + * @author
Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java#2 $ + */ +public class TrimWhiteSpaceFilter extends GenericFilter { + + private boolean mAutoFlush = true; + + @InitParam + public void setAutoFlush(final boolean pAutoFlush) { + mAutoFlush = pAutoFlush; + } + + public void init() throws ServletException { + super.init(); + log("Automatic flushing is " + (mAutoFlush ? "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 (mAutoFlush) { + wrapped.flushBuffer(); + } + } + + static final class TrimWSFilterOutputStream extends FilterOutputStream { + boolean mLastWasWS = 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); + mLastWasWS = false; + } + else { + // TODO: Consider writing only 0x0a (LF) and 0x20 (space) + // Else, if char is WS, store first, skip the rest + if (!mLastWasWS) { + if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a + super.write(0x0a); + } + else { + super.write(pByte); + } + } + mLastWasWS = true; + } + } + } + + private static class TrimWSStreamDelegate extends ServletResponseStreamDelegate { + public TrimWSStreamDelegate(ServletResponse pResponse) { + super(pResponse); + } + + protected OutputStream createOutputStream() throws IOException { + return new TrimWSFilterOutputStream(mResponse.getOutputStream()); + } + } + + static class TrimWSServletResponseWrapper extends ServletResponseWrapper { + private final ServletResponseStreamDelegate mStreamDelegate = new TrimWSStreamDelegate(getResponse()); + + public TrimWSServletResponseWrapper(ServletResponse pResponse) { + super(pResponse); + } + + public ServletOutputStream getOutputStream() throws IOException { + return mStreamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return mStreamDelegate.getWriter(); + } + + public void setContentLength(int pLength) { + // Will be changed by filter, so don't set. + } + + @Override + public void flushBuffer() throws IOException { + mStreamDelegate.flushBuffer(); + } + + @Override + public void resetBuffer() { + mStreamDelegate.resetBuffer(); + } + + // TODO: Consider picking up content-type/encoding, as we can only + // filter US-ASCII, UTF-8 and other compatible encodings? + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java new file mode 100755 index 00000000..a4ad2d15 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java @@ -0,0 +1,47 @@ +package com.twelvemonkeys.servlet.cache; + +import java.io.File; +import java.net.URI; + +/** + * AbstractCacheRequest + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java#1 $ + */ +public abstract class AbstractCacheRequest implements CacheRequest { + private final URI mRequestURI; + private final String mMethod; + + protected AbstractCacheRequest(final URI pRequestURI, final String pMethod) { + if (pRequestURI == null) { + throw new IllegalArgumentException("request URI == null"); + } + if (pMethod == null) { + throw new IllegalArgumentException("method == null"); + } + + mRequestURI = pRequestURI; + mMethod = pMethod; + } + + public URI getRequestURI() { + return mRequestURI; + } + + public String getMethod() { + return mMethod; + } + + // TODO: Consider overriding equals/hashcode + + @Override + public String toString() { + return new StringBuilder(getClass().getSimpleName()) + .append("[URI=").append(mRequestURI) + .append(", parameters=").append(getParameters()) + .append(", headers=").append(getHeaders()) + .append("]").toString(); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java new file mode 100755 index 00000000..3379b526 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java @@ -0,0 +1,45 @@ +package com.twelvemonkeys.servlet.cache; + +import java.util.*; + +/** + * AbstractCacheResponse + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java#1 $ + */ +public abstract class AbstractCacheResponse implements CacheResponse { + private int mStatus; + private final Map> mHeaders = new LinkedHashMap>(); // Insertion order + private final Map> mReadableHeaders = Collections.unmodifiableMap(mHeaders); + + public int getStatus() { + return mStatus; + } + + public void setStatus(int pStatusCode) { + mStatus = pStatusCode; + } + + public void addHeader(String pHeaderName, String pHeaderValue) { + setHeader(pHeaderName, pHeaderValue, true); + } + + public void setHeader(String pHeaderName, String pHeaderValue) { + setHeader(pHeaderName, pHeaderValue, false); + } + + private void setHeader(String pHeaderName, String pHeaderValue, boolean pAdd) { + List values = pAdd ? mHeaders.get(pHeaderName) : null; + if (values == null) { + values = new ArrayList(); + mHeaders.put(pHeaderName, values); + } + values.add(pHeaderValue); + } + + public Map> getHeaders() { + return mReadableHeaders; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java new file mode 100755 index 00000000..fb9be851 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java @@ -0,0 +1,14 @@ +package com.twelvemonkeys.servlet.cache; + +/** + * CacheException + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java#1 $ + */ +public class CacheException extends Exception { + public CacheException(Throwable pCause) { + super(pCause); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java new file mode 100755 index 00000000..94eca85c --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java @@ -0,0 +1,199 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * Originally based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java#4 $ + * + */ +public class CacheFilter extends GenericFilter { + + HTTPCache mCache; + + /** + * Initializes the filter + * + * @throws javax.servlet.ServletException + */ + public void init() throws ServletException { + FilterConfig config = getFilterConfig(); + + // Default don't delete cache files on exit (peristent 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 { + 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 { + mCache = 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: " + mCache); + } + 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: " + mCache); + mCache = 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 { + mCache.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 mContext; + + public ServletContextLoggerAdapter(String pName, ServletContext pContext) { + super(pName, null); + mContext = pContext; + } + + @Override + public void log(Level pLevel, String pMessage) { + mContext.log(pMessage); + } + + @Override + public void log(Level pLevel, String pMessage, Throwable pThrowable) { + mContext.log(pMessage, pThrowable); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java new file mode 100755 index 00000000..93ddde33 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java @@ -0,0 +1,26 @@ +package com.twelvemonkeys.servlet.cache; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** + * CacheRequest + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java#1 $ + */ +public interface CacheRequest { + URI getRequestURI(); + + String getMethod(); + + Map> getHeaders(); + + Map> getParameters(); + + String getServerName(); + + int getServerPort(); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java new file mode 100755 index 00000000..94328669 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java @@ -0,0 +1,27 @@ +package com.twelvemonkeys.servlet.cache; + +import java.io.OutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * CacheResponse + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java#1 $ + */ +public interface CacheResponse { + OutputStream getOutputStream() throws IOException; + + void setStatus(int pStatusCode); + + int getStatus(); + + void addHeader(String pHeaderName, String pHeaderValue); + + void setHeader(String pHeaderName, String pHeaderValue); + + Map> getHeaders(); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java new file mode 100755 index 00000000..bd2f051a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -0,0 +1,261 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.NetUtil; +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. + *

+ * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java#3 $ + */ +class CacheResponseWrapper extends HttpServletResponseWrapper { + private ServletResponseStreamDelegate mStreamDelegate; + + private CacheResponse mResponse; + private CachedEntity mCached; + private WritableCachedResponse mCachedResponse; + + private Boolean mCachable; + private int mStatus; + + public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) { + super(pResponse.getResponse()); + mResponse = pResponse; + mCached = pCached; + init(); + } + + /* + NOTE: This class defers determining if a response is cachable until the + output stream is needed. + This it the reason for the somewhat complicated logic in the add/setHeader + methods below. + */ + private void init() { + mCachable = null; + mStatus = SC_OK; + mCachedResponse = mCached.createCachedResponse(); + mStreamDelegate = new ServletResponseStreamDelegate(this) { + protected OutputStream createOutputStream() throws IOException { + // Test if this request is really cachable, otherwise, + // just write through to underlying response, and don't cache + if (isCachable()) { + return mCachedResponse.getOutputStream(); + } + else { + mCachedResponse.setStatus(mStatus); + mCachedResponse.writeHeadersTo(CacheResponseWrapper.this.mResponse); + return super.getOutputStream(); + } + } + }; + } + + CachedResponse getCachedResponse() { + return mCachedResponse.getCachedResponse(); + } + + public boolean isCachable() { + // NOTE: Intentionally not synchronized + if (mCachable == null) { + mCachable = isCachableImpl(); + } + + return mCachable; + } + + private boolean isCachableImpl() { + if (mStatus != SC_OK) { + return false; + } + + // Vary: * + String[] values = mCachedResponse.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 = mCachedResponse.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 = mCachedResponse.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 { + mStreamDelegate.flushBuffer(); + } + + public void resetBuffer() { + // Servlet 2.3 + mStreamDelegate.resetBuffer(); + } + + public void reset() { + if (Boolean.FALSE.equals(mCachable)) { + super.reset(); + } + // No else, might be cachable after all.. + init(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return mStreamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return mStreamDelegate.getWriter(); + } + + public boolean containsHeader(String name) { + return mCachedResponse.getHeaderValues(name) != null; + } + + public void sendError(int pStatusCode, String msg) throws IOException { + // NOT cachable + mStatus = pStatusCode; + super.sendError(pStatusCode, msg); + } + + public void sendError(int pStatusCode) throws IOException { + // NOT cachable + mStatus = pStatusCode; + super.sendError(pStatusCode); + } + + public void setStatus(int pStatusCode, String sm) { + // NOTE: This method is deprecated + setStatus(pStatusCode); + } + + public void setStatus(int pStatusCode) { + // NOT cachable unless pStatusCode == 200 (or a FEW others?) + if (pStatusCode != SC_OK) { + mStatus = pStatusCode; + super.setStatus(pStatusCode); + } + } + + public void sendRedirect(String pLocation) throws IOException { + // NOT cachable + mStatus = 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(mCachable)) { + super.setDateHeader(pName, pValue); + } + mCachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + } + + public void addDateHeader(String pName, long pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addDateHeader(pName, pValue); + } + mCachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + } + + public void setHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.setHeader(pName, pValue); + } + mCachedResponse.setHeader(pName, pValue); + } + + public void addHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addHeader(pName, pValue); + } + mCachedResponse.addHeader(pName, pValue); + } + + public void setIntHeader(String pName, int pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.setIntHeader(pName, pValue); + } + mCachedResponse.setHeader(pName, String.valueOf(pValue)); + } + + public void addIntHeader(String pName, int pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addIntHeader(pName, pValue); + } + mCachedResponse.addHeader(pName, String.valueOf(pValue)); + } + + public final void setContentType(String type) { + setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java new file mode 100755 index 00000000..7e29bdc2 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java @@ -0,0 +1,75 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 java.io.IOException; + +/** + * CachedEntity + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java#3 $ + */ +interface CachedEntity { + + /** + * Renders the cached entity to the response. + * + * @param pRequest the request + * @param pResponse the response + * @throws java.io.IOException if an I/O exception occurs + */ + void render(CacheRequest pRequest, CacheResponse pResponse) throws IOException; + + /** + * Captures (caches) the response for the given request. + * + * @param pRequest the request + * @param pResponse the response + * @throws java.io.IOException if an I/O exception occurs + * + * @see #createCachedResponse() + */ + void capture(CacheRequest pRequest, CachedResponse pResponse) throws IOException; + + /** + * Tests if the content of this entity is stale for the given request. + * + * @param pRequest the request + * @return {@code true} if content is stale + */ + boolean isStale(CacheRequest pRequest); + + /** + * Creates a {@code WritableCachedResponse} to use to capture the response. + * + * @return a {@code WritableCachedResponse} + */ + WritableCachedResponse createCachedResponse(); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java new file mode 100755 index 00000000..af0c37a7 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java @@ -0,0 +1,172 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * CachedEntity + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java#3 $ + */ +class CachedEntityImpl implements CachedEntity { + private String mCacheURI; + private HTTPCache mCache; + + CachedEntityImpl(String pCacheURI, HTTPCache pCache) { + if (pCacheURI == null) { + throw new IllegalArgumentException("cacheURI == null"); + } + + mCacheURI = pCacheURI; + mCache = pCache; + } + + public void render(CacheRequest pRequest, CacheResponse pResponse) throws IOException { + // Get cached content + CachedResponse cached = mCache.getContent(mCacheURI, pRequest); + + // Sanity check + if (cached == null) { + throw new IllegalStateException("Tried to render non-cached response (cache == null)."); + } + + // If the cached entity is not modified since the date of the browsers + // version, then simply send a "304 Not Modified" response + // Otherwise send the full response. + + // TODO: WHY DID I COMMENT OUT THIS LINE AND REPLACE IT WITH THE ONE BELOW?? + //long lastModified = HTTPCache.getDateHeader(cached.getHeaderValue(HTTPCache.HEADER_LAST_MODIFIED)); + long lastModified = HTTPCache.getDateHeader(cached.getHeaderValue(HTTPCache.HEADER_CACHED_TIME)); + + // TODO: Consider handling time skews between server "now" and client "now"? + // NOTE: The If-Modified-Since is probably right according to the server + // even in a time skew situation, as the client should use either the + // Date or Last-Modifed dates from the response headers (server generated) + long ifModifiedSince = -1L; + try { + List ifmh = pRequest.getHeaders().get(HTTPCache.HEADER_IF_MODIFIED_SINCE); + ifModifiedSince = ifmh != null ? HTTPCache.getDateHeader(ifmh.get(0)) : -1L; + if (ifModifiedSince != -1L) { + /* + long serverTime = DateUtil.currentTimeMinute(); + long clientTime = DateUtil.roundToMinute(pRequest.getDateHeader(HTTPCache.HEADER_DATE)); + + // Test if time skew is greater than time skew threshold (currently 1 minute) + if (Math.abs(serverTime - clientTime) > 1) { + // TODO: Correct error in ifModifiedSince? + } + */ + + // System.out.println(" << CachedEntity >> If-Modified-Since present: " + ifModifiedSince + " --> " + NetUtil.formatHTTPDate(ifModifiedSince) + "==" + pRequest.getHeader(HTTPCache.HEADER_IF_MODIFIED_SINCE)); + // System.out.println(" << CachedEntity >> Last-Modified for entity: " + lastModified + " --> " + NetUtil.formatHTTPDate(lastModified)); + } + } + catch (IllegalArgumentException e) { + // Seems to be a bug in FireFox 1.0.2..?! + mCache.log("Error in date header from user-agent. User-Agent: " + pRequest.getHeaders().get("User-Agent"), e); + } + + if (lastModified == -1L || (ifModifiedSince < (lastModified / 1000L) * 1000L)) { + pResponse.setStatus(cached.getStatus()); + cached.writeHeadersTo(pResponse); + if (isStale(pRequest)) { + // Add warning header + // Warning: 110 : Content is stale + pResponse.addHeader(HTTPCache.HEADER_WARNING, "110 " + getHost(pRequest) + " Content is stale."); + } + + // NOTE: At the moment we only ever try to cache HEAD and GET requests + if (!"HEAD".equals(pRequest.getMethod())) { + cached.writeContentsTo(pResponse.getOutputStream()); + } + } + else { + pResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + // System.out.println(" << CachedEntity >> Not modified: " + toString()); + if (isStale(pRequest)) { + // Add warning header + // Warning: 110 : Content is stale + pResponse.addHeader(HTTPCache.HEADER_WARNING, "110 " + getHost(pRequest) + " Content is stale."); + } + } + } + + /* Utility method to get Host header */ + private static String getHost(CacheRequest pRequest) { + return pRequest.getServerName() + ":" + pRequest.getServerPort(); + } + + public void capture(CacheRequest pRequest, CachedResponse pResponse) throws IOException { +// if (!(pResponse instanceof CacheResponseWrapper)) { +// throw new IllegalArgumentException("Response must be created by CachedEntity.createResponseWrapper()"); +// } +// +// CacheResponseWrapper response = (CacheResponseWrapper) pResponse; + +// if (response.isCachable()) { + mCache.registerContent( + mCacheURI, + pRequest, + pResponse instanceof WritableCachedResponse ? ((WritableCachedResponse) pResponse).getCachedResponse() : pResponse + ); +// } +// else { + // Else store that the response for this request is not cachable +// pRequest.setAttribute(ATTRIB_NOT_CACHEABLE, Boolean.TRUE); + + // TODO: Store this in HTTPCache, for subsequent requests to same resource? +// } + } + + public boolean isStale(CacheRequest pRequest) { + return mCache.isContentStale(mCacheURI, pRequest); + } + + public WritableCachedResponse createCachedResponse() { + return new WritableCachedResponseImpl(); + } + + public int hashCode() { + return (mCacheURI != null ? mCacheURI.hashCode() : 0) + 1397; + } + + public boolean equals(Object pOther) { + return pOther instanceof CachedEntityImpl && + ((mCacheURI == null && ((CachedEntityImpl) pOther).mCacheURI == null) || + mCacheURI != null && mCacheURI.equals(((CachedEntityImpl) pOther).mCacheURI)); + } + + public String toString() { + return "CachedEntity[URI=" + mCacheURI + "]"; + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java new file mode 100755 index 00000000..14ba1626 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java @@ -0,0 +1,95 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 java.io.IOException; +import java.io.OutputStream; + +/** + * CachedResponse + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java#3 $ + */ +interface CachedResponse { + /** + * Writes the cached headers to the response + * + * @param pResponse the servlet response + */ + void writeHeadersTo(CacheResponse pResponse); + + /** + * Writes the cahced content to the response + * + * @param pStream the response output stream + * @throws IOException if an I/O exception occurs during write + */ + void writeContentsTo(OutputStream pStream) throws IOException; + + int getStatus(); + + // TODO: Map> getHeaders() + + /** + * Gets the header names of all headers set in this response. + * + * @return an array of {@code String}s + */ + String[] getHeaderNames(); + + /** + * Gets all header values set for the given header in this response. If the + * header is not set, {@code null} is returned. + * + * @param pHeaderName the header name + * @return an array of {@code String}s, or {@code null} if there is no + * such header in this response. + */ + String[] getHeaderValues(String pHeaderName); + + /** + * Gets the first header value set for the given header in this response. + * If the header is not set, {@code null} is returned. + * Useful for headers that don't have multiple values, like + * {@code "Content-Type"} or {@code "Content-Length"}. + * + * @param pHeaderName the header name + * @return a {@code String}, or {@code null} if there is no + * such header in this response. + */ + String getHeaderValue(String pHeaderName); + + /** + * Returns the size of this cached response in bytes. + * + * @return the size + */ + int size(); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java new file mode 100755 index 00000000..1dcf8c46 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java @@ -0,0 +1,220 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.FastByteArrayOutputStream; +import com.twelvemonkeys.util.LinkedMap; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * CachedResponseImpl + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java#4 $ + */ +class CachedResponseImpl implements CachedResponse { + final protected Map> mHeaders; + protected int mHeadersSize; + protected ByteArrayOutputStream mContent = null; + int mStatus; + + protected CachedResponseImpl() { + mHeaders = new LinkedMap>(); // Keep headers in insertion order + } + + // For use by HTTPCache, when recreating CachedResponses from disk cache + CachedResponseImpl(final int pStatus, final LinkedMap> pHeaders, final int pHeaderSize, final byte[] pContent) { + if (pHeaders == null) { + throw new IllegalArgumentException("headers == null"); + } + mStatus = pStatus; + mHeaders = pHeaders; + mHeadersSize = pHeaderSize; + mContent = new FastByteArrayOutputStream(pContent); + } + + public int getStatus() { + return mStatus; + } + + /** + * Writes the cached headers to the response + * + * @param pResponse the response + */ + public void writeHeadersTo(final CacheResponse pResponse) { + String[] headers = getHeaderNames(); + for (String header : headers) { + // HACK... + // Strip away internal headers + if (HTTPCache.HEADER_CACHED_TIME.equals(header)) { + continue; + } + + // TODO: Replace Last-Modified with X-Cached-At? See CachedEntityImpl, line 50 + + String[] headerValues = getHeaderValues(header); + + for (int i = 0; i < headerValues.length; i++) { + String headerValue = headerValues[i]; + if (i == 0) { + pResponse.setHeader(header, headerValue); + } + else { + pResponse.addHeader(header, headerValue); + } + } + } + } + + /** + * Writes the cahced content to the response + * + * @param pStream the response stream + * @throws java.io.IOException + */ + public void writeContentsTo(final OutputStream pStream) throws IOException { + if (mContent == null) { + throw new IOException("Cache is null, no content to write."); + } + + mContent.writeTo(pStream); + } + + /** + * Gets the header names of all headers set in this response. + * + * @return an array of {@code String}s + */ + public String[] getHeaderNames() { + Set headers = mHeaders.keySet(); + return headers.toArray(new String[headers.size()]); + } + + /** + * Gets all header values set for the given header in this response. If the + * header is not set, {@code null} is returned. + * + * @param pHeaderName the header name + * @return an array of {@code String}s, or {@code null} if there is no + * such header in this response. + */ + public String[] getHeaderValues(final String pHeaderName) { + List values = mHeaders.get(pHeaderName); + if (values == null) { + return null; + } + else { + return values.toArray(new String[values.size()]); + } + } + + /** + * Gets the first header value set for the given header in this response. + * If the header is not set, {@code null} is returned. + * Useful for headers that don't have multiple values, like + * {@code "Content-Type"} or {@code "Content-Length"}. + * + * @param pHeaderName the header name + * @return a {@code String}, or {@code null} if there is no + * such header in this response. + */ + public String getHeaderValue(final String pHeaderName) { + List values = mHeaders.get(pHeaderName); + return (values != null && values.size() > 0) ? values.get(0) : null; + } + + public int size() { + // mContent.size() is exact size in bytes, mHeadersSize is an estimate + return (mContent != null ? mContent.size() : 0) + mHeadersSize; + } + + public boolean equals(final Object pOther) { + if (this == pOther) { + return true; + } + + if (pOther instanceof CachedResponseImpl) { + // "Fast" + return equalsImpl((CachedResponseImpl) pOther); + } + else if (pOther instanceof CachedResponse) { + // Slow + return equalsGeneric((CachedResponse) pOther); + } + + return false; + } + + private boolean equalsImpl(final CachedResponseImpl pOther) { + return mHeadersSize == pOther.mHeadersSize && + (mContent == null ? pOther.mContent == null : mContent.equals(pOther.mContent)) && + mHeaders.equals(pOther.mHeaders); + } + + private boolean equalsGeneric(final CachedResponse pOther) { + if (size() != pOther.size()) { + return false; + } + + String[] headers = getHeaderNames(); + String[] otherHeaders = pOther.getHeaderNames(); + if (!Arrays.equals(headers, otherHeaders)) { + return false; + } + + if (headers != null) { + for (String header : headers) { + String[] values = getHeaderValues(header); + String[] otherValues = pOther.getHeaderValues(header); + + if (!Arrays.equals(values, otherValues)) { + return false; + } + } + } + + return true; + } + + public int hashCode() { + int result; + result = mHeaders.hashCode(); + result = 29 * result + mHeadersSize; + result = 37 * result + (mContent != null ? mContent.hashCode() : 0); + return result; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java new file mode 100755 index 00000000..2c1287c0 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java @@ -0,0 +1,44 @@ +package com.twelvemonkeys.servlet.cache; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * ClientCacheRequest + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java#1 $ + */ +public final class ClientCacheRequest extends AbstractCacheRequest { + private Map> mParameters; + private Map> mHeaders; + + public ClientCacheRequest(final URI pRequestURI,final Map> pParameters, final Map> pHeaders) { + super(pRequestURI, "GET"); // TODO: Consider supporting more than get? At least HEAD and OPTIONS... + mParameters = normalizeMap(pParameters); + mHeaders = normalizeMap(pHeaders); + } + + private Map normalizeMap(Map pMap) { + return pMap == null ? Collections.emptyMap() : Collections.unmodifiableMap(pMap); + } + + public Map> getParameters() { + return mParameters; + } + + public Map> getHeaders() { + return mHeaders; + } + + public String getServerName() { + return getRequestURI().getAuthority(); + } + + public int getServerPort() { + return getRequestURI().getPort(); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java new file mode 100755 index 00000000..6b1ae816 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java @@ -0,0 +1,25 @@ +package com.twelvemonkeys.servlet.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * ClientCacheResponse + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java#2 $ + */ +public final class ClientCacheResponse extends AbstractCacheResponse { + // It's quite useless to cahce the data either on disk or in memory, as it already is cached in the client's cache... + // It would be nice if we could bypass that... + + public OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException("Method getOutputStream not implemented"); // TODO: Implement + } + + public InputStream getInputStream() { + throw new UnsupportedOperationException("Method getInputStream not implemented"); // TODO: Implement + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java new file mode 100755 index 00000000..7dcf8f26 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -0,0 +1,1167 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.net.MIMEUtil; +import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.util.LRUHashMap; +import com.twelvemonkeys.util.LinkedMap; +import com.twelvemonkeys.util.NullMap; + +import javax.servlet.ServletContext; +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A "simple" HTTP cache. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java#4 $ + * @todo OMPTIMIZE: Cache parsed vary-info objects, not the properties-files + * @todo BUG: Better filename handling, as some filenames become too long.. + * - Use a mix of parameters and hashcode + lenght with fixed (max) lenght? + * (Hashcodes of Strings are constant). + * - Store full filenames in .vary, instead of just extension, and use + * short filenames? (and only one .vary per dir). + *

+ * + * @todo TEST: Battle-testing using some URL-hammer tool and maybe a profiler + * @todo ETag/Conditional (If-None-Match) support! + * @todo Rewrite to use java.util.concurrent Locks (if possible) for performance + * Maybe use ConcurrentHashMap instead fo synchronized HashMap? + * @todo Rewrite to use NIO for performance + * @todo Allow no tempdir for in-memory only cache + * @todo Specify max size of disk-cache + */ +public class HTTPCache { + /** + * The HTTP header {@code "Cache-Control"} + */ + protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; + /** + * The HTTP header {@code "Content-Type"} + */ + protected static final String HEADER_CONTENT_TYPE = "Content-Type"; + /** + * The HTTP header {@code "Date"} + */ + protected static final String HEADER_DATE = "Date"; + /** + * The HTTP header {@code "ETag"} + */ + protected static final String HEADER_ETAG = "ETag"; + /** + * The HTTP header {@code "Expires"} + */ + protected static final String HEADER_EXPIRES = "Expires"; + /** + * The HTTP header {@code "If-Modified-Since"} + */ + protected static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + /** + * The HTTP header {@code "If-None-Match"} + */ + protected static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + /** + * The HTTP header {@code "Last-Modified"} + */ + protected static final String HEADER_LAST_MODIFIED = "Last-Modified"; + /** + * The HTTP header {@code "Pragma"} + */ + protected static final String HEADER_PRAGMA = "Pragma"; + /** + * The HTTP header {@code "Vary"} + */ + protected static final String HEADER_VARY = "Vary"; + /** + * The HTTP header {@code "Warning"} + */ + protected static final String HEADER_WARNING = "Warning"; + /** + * HTTP extension header {@code "X-Cached-At"} + */ + protected static final String HEADER_CACHED_TIME = "X-Cached-At"; + + /** + * The file extension for header files ({@code ".headers"}) + */ + protected static final String FILE_EXT_HEADERS = ".headers"; + /** + * The file extension for varation-info files ({@code ".vary"}) + */ + protected static final String FILE_EXT_VARY = ".vary"; + + protected static final int STATUS_OK = 200; + + /** + * The directory used for the disk-based cache + */ + private File mTempDir; + + /** + * Indicates wether the disk-based cache should be deleted when the + * container shuts down/VM exits + */ + private boolean mDeleteCacheOnExit; + + /** + * In-memory content cache + */ + private final Map mContentCache; + /** + * In-memory enity cache + */ + private final Map mEntityCache; + /** + * In-memory varyiation-info cache + */ + private final Map mVaryCache; + + private long mDefaultExpiryTime = -1; + + private final Logger mLogger; + + // Internal constructor for sublcasses only + protected HTTPCache( + final File pTempFolder, + final long pDefaultCacheExpiryTime, + final int pMaxMemCacheSize, + final int pMaxCachedEntites, + final boolean pDeleteCacheOnExit, + final Logger pLogger + ) { + if (pTempFolder == null) { + throw new IllegalArgumentException("temp folder == null"); + } + if (!pTempFolder.exists() && !pTempFolder.mkdirs()) { + throw new IllegalArgumentException("Could not create required temp directory: " + mTempDir.getAbsolutePath()); + } + if (!(pTempFolder.canRead() && pTempFolder.canWrite())) { + throw new IllegalArgumentException("Must have read/write access to temp folder: " + mTempDir.getAbsolutePath()); + } + if (pDefaultCacheExpiryTime < 0) { + throw new IllegalArgumentException("Negative expiry time"); + } + if (pMaxMemCacheSize < 0) { + throw new IllegalArgumentException("Negative maximum memory cache size"); + } + if (pMaxCachedEntites < 0) { + throw new IllegalArgumentException("Negative maximum number of cached entries"); + } + + mDefaultExpiryTime = pDefaultCacheExpiryTime; + + if (pMaxMemCacheSize > 0) { +// Map backing = new SizedLRUMap(pMaxMemCacheSize); // size in bytes +// mContentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime); + mContentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes + } + else { + mContentCache = new NullMap(); + } + + mEntityCache = new LRUHashMap(pMaxCachedEntites); + mVaryCache = new LRUHashMap(pMaxCachedEntites); + + mDeleteCacheOnExit = pDeleteCacheOnExit; + + mTempDir = pTempFolder; + + mLogger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName()); + } + + /** + * Creates an {@code HTTPCache}. + * + * @param pTempFolder the temp folder for this cache. + * @param pDefaultCacheExpiryTime Default expiry time for cached entities, + * {@code >= 0} + * @param pMaxMemCacheSize Maximum size of in-memory cache for content + * in bytes, {@code >= 0} ({@code 0} means no + * in-memory cache) + * @param pMaxCachedEntites Maximum number of entities in cache + * @param pDeleteCacheOnExit specifies wether the file cache should be + * deleted when the application or VM shuts down + * @throws IllegalArgumentException if {@code pName} or {@code pContext} is + * {@code null} or if any of {@code pDefaultCacheExpiryTime}, + * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are + * negative, + * or if the directory as given in the context attribute + * {@code "javax.servlet.context.tempdir"} does not exist, and + * cannot be created. + */ + public HTTPCache(final File pTempFolder, + final long pDefaultCacheExpiryTime, + final int pMaxMemCacheSize, final int pMaxCachedEntites, + final boolean pDeleteCacheOnExit) { + this(pTempFolder, pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, null); + } + + + /** + * Creates an {@code HTTPCache}. + * + * @param pName Name of this cache (should be unique per application). + * Used for temp folder + * @param pContext Servlet context for the application. + * @param pDefaultCacheExpiryTime Default expiry time for cached entities, + * {@code >= 0} + * @param pMaxMemCacheSize Maximum size of in-memory cache for content + * in bytes, {@code >= 0} ({@code 0} means no + * in-memory cache) + * @param pMaxCachedEntites Maximum number of entities in cache + * @param pDeleteCacheOnExit specifies wether the file cache should be + * deleted when the application or VM shuts down + * @throws IllegalArgumentException if {@code pName} or {@code pContext} is + * {@code null} or if any of {@code pDefaultCacheExpiryTime}, + * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are + * negative, + * or if the directory as given in the context attribute + * {@code "javax.servlet.context.tempdir"} does not exist, and + * cannot be created. + * @deprecated Use {@link #HTTPCache(File, long, int, int, boolean)} instead. + */ + public HTTPCache(final String pName, final ServletContext pContext, + final int pDefaultCacheExpiryTime, final int pMaxMemCacheSize, + final int pMaxCachedEntites, final boolean pDeleteCacheOnExit) { + this( + getTempFolder(pName, pContext), + pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, + new CacheFilter.ServletContextLoggerAdapter(pName, pContext) + ); + } + + private static File getTempFolder(String pName, ServletContext pContext) { + if (pName == null) { + throw new IllegalArgumentException("name == null"); + } + if (pName.trim().length() == 0) { + throw new IllegalArgumentException("Empty name"); + } + if (pContext == null) { + throw new IllegalArgumentException("servlet context == null"); + } + File tempRoot = (File) pContext.getAttribute("javax.servlet.context.tempdir"); + if (tempRoot == null) { + throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\""); + } + return new File(tempRoot, pName); + } + + public String toString() { + StringBuilder buf = new StringBuilder(getClass().getSimpleName()); + buf.append("["); + buf.append("Temp dir: "); + buf.append(mTempDir.getAbsolutePath()); + if (mDeleteCacheOnExit) { + buf.append(" (non-persistent)"); + } + else { + buf.append(" (persistent)"); + } + buf.append(", EntityCache: {"); + buf.append(mEntityCache.size()); + buf.append(" entries in a "); + buf.append(mEntityCache.getClass().getName()); + buf.append("}, VaryCache: {"); + buf.append(mVaryCache.size()); + buf.append(" entries in a "); + buf.append(mVaryCache.getClass().getName()); + buf.append("}, ContentCache: {"); + buf.append(mContentCache.size()); + buf.append(" entries in a "); + buf.append(mContentCache.getClass().getName()); + buf.append("}]"); + + return buf.toString(); + } + + void log(final String pMessage) { + mLogger.log(Level.INFO, pMessage); + } + + void log(final String pMessage, Throwable pException) { + mLogger.log(Level.WARNING, pMessage, pException); + } + + /** + * Looks up the {@code CachedEntity} for the given request. + * + * @param pRequest the request + * @param pResponse the response + * @param pResolver the resolver + * @throws java.io.IOException if an I/O error occurs + * @throws CacheException if the cached entity can't be resolved for some reason + */ + public void doCached(final CacheRequest pRequest, final CacheResponse pResponse, final ResponseResolver pResolver) throws IOException, CacheException { + // TODO: Expire cached items on PUT/POST/DELETE/PURGE + // If not cachable request, resolve directly + if (!isCacheable(pRequest)) { + pResolver.resolve(pRequest, pResponse); + } + else { + // Generate cacheURI + String cacheURI = generateCacheURI(pRequest); +// System.out.println(" ## HTTPCache ## Request Id (cacheURI): " + cacheURI); + + // Get/create cached entity + CachedEntity cached; + synchronized (mEntityCache) { + cached = mEntityCache.get(cacheURI); + if (cached == null) { + cached = new CachedEntityImpl(cacheURI, this); + mEntityCache.put(cacheURI, cached); + } + } + + + // else if (not cached || stale), resolve through wrapped (caching) response + // else render to response + + // TODO: This is a bottleneck for uncachable resources. Should not + // synchronize, if we know (HOW?) the resource is not cachable. + synchronized (cached) { + if (cached.isStale(pRequest) /* TODO: NOT CACHED?! */) { + // Go fetch... + WritableCachedResponse cachedResponse = cached.createCachedResponse(); + pResolver.resolve(pRequest, cachedResponse); + + if (isCachable(cachedResponse)) { +// System.out.println("Registering content: " + cachedResponse.getCachedResponse()); + registerContent(cacheURI, pRequest, cachedResponse.getCachedResponse()); + } + else { + // TODO: What about non-cachable responses? We need to either remove them from cache, or mark them as stale... + // Best is probably to mark as non-cacheable for later, and NOT store content (performance) +// System.out.println("Non-cacheable response: " + cachedResponse); + + // TODO: Write, but should really do this unbuffered.... And some resolver might be able to do just that? + // Might need a resolver.isWriteThroughForUncachableResources() method... + pResponse.setStatus(cachedResponse.getStatus()); + cachedResponse.writeHeadersTo(pResponse); + cachedResponse.writeContentsTo(pResponse.getOutputStream()); + return; + } + } + } + + cached.render(pRequest, pResponse); + } + } + + protected void invalidate(CacheRequest pRequest) { + // Generate cacheURI + String cacheURI = generateCacheURI(pRequest); + + // Get/create cached entity + CachedEntity cached; + synchronized (mEntityCache) { + cached = mEntityCache.get(cacheURI); + if (cached != null) { + // TODO; Remove all variants + mEntityCache.remove(cacheURI); + } + } + + } + + private boolean isCacheable(final CacheRequest pRequest) { + // TODO: Support public/private cache (a cache probably have to be one of the two, when created) + // TODO: Only private caches should cache requests with Authorization + + // TODO: OptimizeMe! + // It's probably best to cache the "cacheableness" of a request and a resource separately + List cacheControlValues = pRequest.getHeaders().get(HEADER_CACHE_CONTROL); + if (cacheControlValues != null) { + Map cacheControl = new HashMap(); + for (String cc : cacheControlValues) { + List directives = Arrays.asList(cc.split(",")); + for (String directive : directives) { + directive = directive.trim(); + if (directive.length() > 0) { + String[] directiveParts = directive.split("=", 2); + cacheControl.put(directiveParts[0], directiveParts.length > 1 ? directiveParts[1] : null); + } + } + } + + if (cacheControl.containsKey("no-cache") || cacheControl.containsKey("no-store")) { + return false; + } + + /* + "no-cache" ; Section 14.9.1 + | "no-store" ; Section 14.9.2 + | "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4 + | "max-stale" [ "=" delta-seconds ] ; Section 14.9.3 + | "min-fresh" "=" delta-seconds ; Section 14.9.3 + | "no-transform" ; Section 14.9.5 + | "only-if-cached" + */ + } + + return true; + } + + private boolean isCachable(final CacheResponse pResponse) { + if (pResponse.getStatus() != STATUS_OK) { + return false; + } + + // Vary: * + List values = pResponse.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 = pResponse.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 = pResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache")) { + return false; + } + } + } + + return true; + } + + + /** + * Allows a server-side cache mechanism to peek at the real file. + * Default implementation return {@code null}. + * + * @param pRequest the request + * @return {@code null}, always + */ + protected File getRealFile(final CacheRequest pRequest) { + // TODO: Create callback for this? Only possible for server-side cache... Maybe we can get away without this? + // For now: Default implementation that returns null + return null; +/* + String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); + // System.out.println(" ## HTTPCache ## Context relative URI: " + contextRelativeURI); + + String path = mContext.getRealPath(contextRelativeURI); + // System.out.println(" ## HTTPCache ## Real path: " + path); + + if (path != null) { + return new File(path); + } + + return null; +*/ + } + + private File getCachedFile(final String pCacheURI, final CacheRequest pRequest) { + File file = null; + + // Get base dir + File base = new File(mTempDir, "./" + pCacheURI); + final String basePath = base.getAbsolutePath(); + File directory = base.getParentFile(); + + // Get list of files that are candidates + File[] candidates = directory.listFiles(new FileFilter() { + public boolean accept(File pFile) { + return pFile.getAbsolutePath().startsWith(basePath) + && !pFile.getName().endsWith(FILE_EXT_HEADERS) + && !pFile.getName().endsWith(FILE_EXT_VARY); + } + }); + + // Negotiation + if (candidates != null) { + String extension = getVaryExtension(pCacheURI, pRequest); + //System.out.println("-- Vary ext: " + extension); + if (extension != null) { + for (File candidate : candidates) { + //System.out.println("-- Candidate: " + candidates[i]); + + if (extension.equals("ANY") || extension.equals(FileUtil.getExtension(candidate))) { + //System.out.println("-- Candidate selected"); + file = candidate; + break; + } + } + } + } + else if (base.exists()) { + //System.out.println("-- File not a directory: " + directory); + log("File not a directory: " + directory); + } + + return file; + } + + private String getVaryExtension(final String pCacheURI, final CacheRequest pRequest) { + Properties variations = getVaryProperties(pCacheURI); + + String[] varyHeaders = StringUtil.toStringArray(variations.getProperty(HEADER_VARY, "")); +// System.out.println("-- Vary: \"" + variations.getProperty(HEADER_VARY) + "\""); + + String varyKey = createVaryKey(varyHeaders, pRequest); +// System.out.println("-- Vary key: \"" + varyKey + "\""); + + // If no vary, just go with any version... + return StringUtil.isEmpty(varyKey) ? "ANY" : variations.getProperty(varyKey, null); + } + + private String createVaryKey(final String[] pVaryHeaders, final CacheRequest pRequest) { + if (pVaryHeaders == null) { + return null; + } + + StringBuilder headerValues = new StringBuilder(); + for (String varyHeader : pVaryHeaders) { + List varies = pRequest.getHeaders().get(varyHeader); + String headerValue = varies != null && varies.size() > 0 ? varies.get(0) : null; + + headerValues.append(varyHeader); + headerValues.append("__V_"); + headerValues.append(createSafeHeader(headerValue)); + } + + return headerValues.toString(); + } + + private void storeVaryProperties(final String pCacheURI, final Properties pVariations) { + synchronized (pVariations) { + try { + File file = getVaryPropertiesFile(pCacheURI); + if (!file.exists() && mDeleteCacheOnExit) { + file.deleteOnExit(); + } + + FileOutputStream out = new FileOutputStream(file); + try { + pVariations.store(out, pCacheURI + " Vary info"); + } + finally { + out.close(); + } + } + catch (IOException ioe) { + log("Error: Could not store Vary info: " + ioe); + } + } + } + + private Properties getVaryProperties(final String pCacheURI) { + Properties variations; + + synchronized (mVaryCache) { + variations = mVaryCache.get(pCacheURI); + if (variations == null) { + variations = loadVaryProperties(pCacheURI); + mVaryCache.put(pCacheURI, variations); + } + } + + return variations; + } + + private Properties loadVaryProperties(final String pCacheURI) { + // Read Vary info, for content negotiation + Properties variations = new Properties(); + File vary = getVaryPropertiesFile(pCacheURI); + if (vary.exists()) { + try { + FileInputStream in = new FileInputStream(vary); + try { + variations.load(in); + } + finally { + in.close(); + } + } + catch (IOException ioe) { + log("Error: Could not load Vary info: " + ioe); + } + } + return variations; + } + + private File getVaryPropertiesFile(final String pCacheURI) { + return new File(mTempDir, "./" + pCacheURI + FILE_EXT_VARY); + } + + private static String generateCacheURI(final CacheRequest pRequest) { + StringBuilder buffer = new StringBuilder(); + + // Note: As the '/'s are not replaced, the directory structure will be recreated + // TODO: Old mehtod relied on context relativization, that must now be handled byt the ServletCacheRequest +// String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); + String contextRelativeURI = pRequest.getRequestURI().getPath(); + buffer.append(contextRelativeURI); + + // Create directory for all resources + if (contextRelativeURI.charAt(contextRelativeURI.length() - 1) != '/') { + buffer.append('/'); + } + + // Get parameters from request, and recreate query to avoid unneccessary + // regeneration/caching when parameters are out of order + // Also makes caching work for POST + appendSortedRequestParams(pRequest, buffer); + + return buffer.toString(); + } + + private static void appendSortedRequestParams(final CacheRequest pRequest, final StringBuilder pBuffer) { + Set names = pRequest.getParameters().keySet(); + if (names.isEmpty()) { + pBuffer.append("defaultVersion"); + return; + } + + // We now have parameters + pBuffer.append('_'); // append '_' for '?', to avoid clash with default + + // Create a sorted map + SortedMap> sortedQueryMap = new TreeMap>(); + for (String name : names) { + List values = pRequest.getParameters().get(name); + + sortedQueryMap.put(name, values); + } + + // Iterate over sorted map, and append to stringbuffer + for (Iterator>> iterator = sortedQueryMap.entrySet().iterator(); iterator.hasNext();) { + Map.Entry> entry = iterator.next(); + pBuffer.append(createSafe(entry.getKey())); + + List values = entry.getValue(); + if (values != null && values.size() > 0) { + pBuffer.append("_V"); // = + for (int i = 0; i < values.size(); i++) { + String value = values.get(i); + if (i != 0) { + pBuffer.append(','); + } + pBuffer.append(createSafe(value)); + } + } + + if (iterator.hasNext()) { + pBuffer.append("_P"); // & + } + } + } + + private static String createSafe(final String pKey) { + return pKey.replace('/', '-') + .replace('&', '-') // In case they are encoded + .replace('#', '-') + .replace(';', '-'); + } + + private static String createSafeHeader(final String pHeaderValue) { + if (pHeaderValue == null) { + return "NULL"; + } + + return pHeaderValue.replace(' ', '_') + .replace(':', '_') + .replace('=', '_'); + } + + /** + * Registers content for the given URI in the cache. + * + * @param pCacheURI the cache URI + * @param pRequest the request + * @param pCachedResponse the cached response + * @throws IOException if the content could not be cached + */ + void registerContent( + final String pCacheURI, + final CacheRequest pRequest, + final CachedResponse pCachedResponse + ) throws IOException { + // System.out.println(" ## HTTPCache ## Registering content for " + pCacheURI); + +// pRequest.removeAttribute(ATTRIB_IS_STALE); +// pRequest.setAttribute(ATTRIB_CACHED_RESPONSE, pCachedResponse); + + if ("HEAD".equals(pRequest.getMethod())) { + // System.out.println(" ## HTTPCache ## Was HEAD request, will NOT store content."); + return; + } + + // TODO: Several resources may have same extension... + String extension = MIMEUtil.getExtension(pCachedResponse.getHeaderValue(HEADER_CONTENT_TYPE)); + if (extension == null) { + extension = "[NULL]"; + } + + synchronized (mContentCache) { + mContentCache.put(pCacheURI + '.' + extension, pCachedResponse); + + // This will be the default version + if (!mContentCache.containsKey(pCacheURI)) { + mContentCache.put(pCacheURI, pCachedResponse); + } + } + + // Write the cached content to disk + File content = new File(mTempDir, "./" + pCacheURI + '.' + extension); + if (mDeleteCacheOnExit && !content.exists()) { + content.deleteOnExit(); + } + + File parent = content.getParentFile(); + if (!(parent.exists() || parent.mkdirs())) { + log("Could not create directory " + parent.getAbsolutePath()); + + // TODO: Make sure vary-info is still created in memory + + return; + } + + OutputStream mContentStream = new BufferedOutputStream(new FileOutputStream(content)); + + try { + pCachedResponse.writeContentsTo(mContentStream); + } + finally { + try { + mContentStream.close(); + } + catch (IOException e) { + log("Error closing content stream: " + e.getMessage(), e); + } + } + + // Write the cached headers to disk (in pseudo-properties-format) + File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); + if (mDeleteCacheOnExit && !headers.exists()) { + headers.deleteOnExit(); + } + + FileWriter writer = new FileWriter(headers); + PrintWriter headerWriter = new PrintWriter(writer); + try { + String[] names = pCachedResponse.getHeaderNames(); + + for (String name : names) { + String[] values = pCachedResponse.getHeaderValues(name); + + headerWriter.print(name); + headerWriter.print(": "); + headerWriter.println(StringUtil.toCSVString(values, "\\")); + } + } + finally { + headerWriter.flush(); + try { + writer.close(); + } + catch (IOException e) { + log("Error closing header stream: " + e.getMessage(), e); + } + } + + // TODO: Make this more robust, if some weird entity is not + // consistent in it's vary-headers.. + // (sometimes Vary, sometimes not, or somtimes different Vary headers). + + // Write extra Vary info to disk + String[] varyHeaders = pCachedResponse.getHeaderValues(HEADER_VARY); + + // If no variations, then don't store vary info + if (varyHeaders != null && varyHeaders.length > 0) { + Properties variations = getVaryProperties(pCacheURI); + + String vary = StringUtil.toCSVString(varyHeaders); + variations.setProperty(HEADER_VARY, vary); + + // Create Vary-key and map to file extension... + String varyKey = createVaryKey(varyHeaders, pRequest); +// System.out.println("varyKey: " + varyKey); +// System.out.println("extension: " + extension); + variations.setProperty(varyKey, extension); + + storeVaryProperties(pCacheURI, variations); + } + } + + /** + * @param pCacheURI the cache URI + * @param pRequest the request + * @return a {@code CachedResponse} object + */ + CachedResponse getContent(final String pCacheURI, final CacheRequest pRequest) { +// System.err.println(" ## HTTPCache ## Looking up content for " + pCacheURI); +// Thread.dumpStack(); + + String extension = getVaryExtension(pCacheURI, pRequest); + + CachedResponse response; + synchronized (mContentCache) { +// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + mContentCache /*.size()*/ + " entries)..."); + if ("ANY".equals(extension)) { + response = mContentCache.get(pCacheURI); + } + else { + response = mContentCache.get(pCacheURI + '.' + extension); + } + + if (response == null) { +// System.out.println(" ## HTTPCache ## Content not found in memory cache."); +// +// System.out.println(" ## HTTPCache ## Looking up content from disk cache..."); + // Read from disk-cache + response = readFromDiskCache(pCacheURI, pRequest); + } + +// if (response == null) { +// System.out.println(" ## HTTPCache ## Content not found in disk cache."); +// } +// else { +// System.out.println(" ## HTTPCache ## Content for " + pCacheURI + " found: " + response); +// } + } + + return response; + } + + private CachedResponse readFromDiskCache(String pCacheURI, CacheRequest pRequest) { + CachedResponse response = null; + try { + File content = getCachedFile(pCacheURI, pRequest); + if (content != null && content.exists()) { + // Read contents + byte[] contents = FileUtil.read(content); + + // Read headers + File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); + int headerSize = (int) headers.length(); + + BufferedReader reader = new BufferedReader(new FileReader(headers)); + LinkedMap> headerMap = new LinkedMap>(); + String line; + while ((line = reader.readLine()) != null) { + int colIdx = line.indexOf(':'); + String name; + String value; + if (colIdx >= 0) { + name = line.substring(0, colIdx); + value = line.substring(colIdx + 2); // ": " + } + else { + name = line; + value = ""; + } + + headerMap.put(name, Arrays.asList(StringUtil.toStringArray(value, "\\"))); + } + + response = new CachedResponseImpl(STATUS_OK, headerMap, headerSize, contents); + mContentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response); + } + } + catch (IOException e) { + log("Error reading from cache: " + e.getMessage(), e); + } + return response; + } + + boolean isContentStale(final String pCacheURI, final CacheRequest pRequest) { + // NOTE: Content is either stale or not, for the duration of one request, unless re-fetched + // Means that we must retry after a registerContent(), if caching as request-attribute + Boolean stale; +// stale = (Boolean) pRequest.getAttribute(ATTRIB_IS_STALE); +// if (stale != null) { +// return stale; +// } + + stale = isContentStaleImpl(pCacheURI, pRequest); +// pRequest.setAttribute(ATTRIB_IS_STALE, stale); + + return stale; + } + + private boolean isContentStaleImpl(final String pCacheURI, final CacheRequest pRequest) { + CachedResponse response = getContent(pCacheURI, pRequest); + + if (response == null) { + // System.out.println(" ## HTTPCache ## Content is stale (no content)."); + return true; + } + + // TODO: Get max-age=... from REQUEST too! + + // TODO: What about time skew? Now should be (roughly) same as: + // long now = pRequest.getDateHeader("Date"); + // TODO: If the time differs (server "now" vs client "now"), should we + // take that into consideration when testing for stale content? + // Probably, yes. + // TODO: Define rules for how to handle time skews + + // Set timestamp check + // NOTE: HTTP Dates are always in GMT time zone + long now = (System.currentTimeMillis() / 1000L) * 1000L; + long expires = getDateHeader(response.getHeaderValue(HEADER_EXPIRES)); + //long lastModified = getDateHeader(response, HEADER_LAST_MODIFIED); + long lastModified = getDateHeader(response.getHeaderValue(HEADER_CACHED_TIME)); + + // If expires header is not set, compute it + if (expires == -1L) { + /* + // Note: Not all content has Last-Modified header. We should then + // use lastModified() of the cached file, to compute expires time. + if (lastModified == -1L) { + File cached = getCachedFile(pCacheURI, pRequest); + if (cached != null && cached.exists()) { + lastModified = cached.lastModified(); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + } + } + */ + + // If Cache-Control: max-age is present, use it, otherwise default + int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age"); + if (maxAge == -1) { + expires = lastModified + mDefaultExpiryTime; + //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); + } + else { + expires = lastModified + (maxAge * 1000L); // max-age is seconds + //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); + } + } + /* + else { + // System.out.println(" ## HTTPCache ## Expires header is " + response.getHeaderValue(HEADER_EXPIRES)); + } + */ + + // Expired? + if (expires < now) { + // System.out.println(" ## HTTPCache ## Content is stale (content expired: " + // + NetUtil.formatHTTPDate(expires) + " before " + NetUtil.formatHTTPDate(now) + ")."); + return true; + } + + /* + if (lastModified == -1L) { + // Note: Not all content has Last-Modified header. We should then + // use lastModified() of the cached file, to compute expires time. + File cached = getCachedFile(pCacheURI, pRequest); + if (cached != null && cached.exists()) { + lastModified = cached.lastModified(); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + } + } + */ + + // Get the real file for this request, if any + File real = getRealFile(pRequest); + //noinspection RedundantIfStatement + if (real != null && real.exists() && real.lastModified() > lastModified) { + // System.out.println(" ## HTTPCache ## Content is stale (new content" + // + NetUtil.formatHTTPDate(lastModified) + " before " + NetUtil.formatHTTPDate(real.lastModified()) + ")."); + return true; + } + + return false; + } + + /** + * Parses a cached header with directive to an int. + * E.g: Cache-Control: max-age=60, returns 60 + * + * @param pCached the cached response + * @param pHeaderName the header name (e.g: {@code CacheControl}) + * @param pDirective the directive (e.g: {@code max-age} + * @return the int value, or {@code -1} if not found + */ + private int getIntHeader(final CachedResponse pCached, final String pHeaderName, final String pDirective) { + String[] headerValues = pCached.getHeaderValues(pHeaderName); + int value = -1; + + if (headerValues != null) { + for (String headerValue : headerValues) { + if (pDirective == null) { + if (!StringUtil.isEmpty(headerValue)) { + value = Integer.parseInt(headerValue); + } + break; + } + else { + int start = headerValue.indexOf(pDirective); + + // Directive found + if (start >= 0) { + + int end = headerValue.lastIndexOf(','); + if (end < start) { + end = headerValue.length(); + } + + headerValue = headerValue.substring(start, end); + + if (!StringUtil.isEmpty(headerValue)) { + value = Integer.parseInt(headerValue); + } + + break; + } + } + } + } + + return value; + } + + /** + * Utility to read a date header from a cached response. + * + * @param pHeaderValue the header value + * @return the parsed date as a long, or {@code -1L} if not found + * @see javax.servlet.http.HttpServletRequest#getDateHeader(String) + */ + static long getDateHeader(final String pHeaderValue) { + long date = -1L; + if (pHeaderValue != null) { + date = NetUtil.parseHTTPDate(pHeaderValue); + } + return date; + } + + // TODO: Extract and make public? + final static class SizedLRUMap extends LRUHashMap { + int mSize; + int mMaxSize; + + public SizedLRUMap(int pMaxSize) { + //super(true); + super(); // Note: super.mMaxSize doesn't count... + mMaxSize = pMaxSize; + } + + + // In super (LRUMap?) this could just return 1... + protected int sizeOf(Object pValue) { + // HACK: As this is used as a backing for a TimeoutMap, the values + // will themselves be Entries... + while (pValue instanceof Map.Entry) { + pValue = ((Map.Entry) pValue).getValue(); + } + + CachedResponse cached = (CachedResponse) pValue; + return (cached != null ? cached.size() : 0); + } + + @Override + public V put(K pKey, V pValue) { + mSize += sizeOf(pValue); + + V old = super.put(pKey, pValue); + if (old != null) { + mSize -= sizeOf(old); + } + return old; + } + + @Override + public V remove(Object pKey) { + V old = super.remove(pKey); + if (old != null) { + mSize -= sizeOf(old); + } + return old; + } + + @Override + protected boolean removeEldestEntry(Map.Entry pEldest) { + if (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size + removeLRU(); + } + return false; + } + + @Override + public void removeLRU() { + while (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size + super.removeLRU(); + } + } + } + +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java new file mode 100755 index 00000000..a92a6801 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java @@ -0,0 +1,14 @@ +package com.twelvemonkeys.servlet.cache; + +import java.io.IOException; + +/** + * ResponseResolver + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java#2 $ + */ +public interface ResponseResolver { + void resolve(CacheRequest pRequest, CacheResponse pResponse) throws IOException, CacheException; +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java new file mode 100755 index 00000000..22b91de4 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java @@ -0,0 +1,273 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.NetUtil; +import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +/** + * CacheResponseWrapper class description. + *

+ * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java#2 $ + */ +class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { + private ServletResponseStreamDelegate mStreamDelegate; + + private CacheResponse mCacheResponse; + + private Boolean mCachable; + private int mStatus; + + public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) { + super(pServletResponse); + mCacheResponse = pResponse; + init(); + } + + + /* + NOTE: This class defers determining if a response is cachable until the + output stream is needed. + This it the reason for the somewhat complicated logic in the add/setHeader + methods below. + */ + private void init() { + mCachable = null; + mStatus = SC_OK; + mStreamDelegate = new ServletResponseStreamDelegate(this) { + protected OutputStream createOutputStream() throws IOException { + // Test if this request is really cachable, otherwise, + // just write through to underlying response, and don't cache + if (isCachable()) { + return mCacheResponse.getOutputStream(); + } + else { + // TODO: We need to tell the cache about this, somehow... + writeHeaders(mCacheResponse, (HttpServletResponse) getResponse()); + return super.getOutputStream(); + } + } + }; + } + + private void writeHeaders(final CacheResponse pResponse, final HttpServletResponse pServletResponse) { + Map> headers = pResponse.getHeaders(); + for (Map.Entry> 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 isCachable() { + // NOTE: Intentionally not synchronized + if (mCachable == null) { + mCachable = isCachableImpl(); + } + + return mCachable; + } + + private boolean isCachableImpl() { + // TODO: This code is duped in the cache... + if (mStatus != SC_OK) { + return false; + } + + // Vary: * + List values = mCacheResponse.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 = mCacheResponse.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 = mCacheResponse.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 { + mStreamDelegate.flushBuffer(); + } + + public void resetBuffer() { + // Servlet 2.3 + mStreamDelegate.resetBuffer(); + } + + public void reset() { + if (Boolean.FALSE.equals(mCachable)) { + super.reset(); + } + // No else, might be cachable after all.. + init(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return mStreamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return mStreamDelegate.getWriter(); + } + + public boolean containsHeader(String name) { + return mCacheResponse.getHeaders().get(name) != null; + } + + public void sendError(int pStatusCode, String msg) throws IOException { + // NOT cachable + mStatus = pStatusCode; + super.sendError(pStatusCode, msg); + } + + public void sendError(int pStatusCode) throws IOException { + // NOT cachable + mStatus = pStatusCode; + super.sendError(pStatusCode); + } + + public void setStatus(int pStatusCode, String sm) { + // NOTE: This method is deprecated + setStatus(pStatusCode); + } + + public void setStatus(int pStatusCode) { + // NOT cachable unless pStatusCode == 200 (or a FEW others?) + if (pStatusCode != SC_OK) { + mStatus = pStatusCode; + super.setStatus(pStatusCode); + } + } + + public void sendRedirect(String pLocation) throws IOException { + // NOT cachable + mStatus = 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(mCachable)) { + super.setDateHeader(pName, pValue); + } + mCacheResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + } + + public void addDateHeader(String pName, long pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addDateHeader(pName, pValue); + } + mCacheResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + } + + public void setHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.setHeader(pName, pValue); + } + mCacheResponse.setHeader(pName, pValue); + } + + public void addHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addHeader(pName, pValue); + } + mCacheResponse.addHeader(pName, pValue); + } + + public void setIntHeader(String pName, int pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.setIntHeader(pName, pValue); + } + mCacheResponse.setHeader(pName, String.valueOf(pValue)); + } + + public void addIntHeader(String pName, int pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(mCachable)) { + super.addIntHeader(pName, pValue); + } + mCacheResponse.addHeader(pName, String.valueOf(pValue)); + } + + public final void setContentType(String type) { + setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java new file mode 100755 index 00000000..40ef8d0d --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java @@ -0,0 +1,56 @@ +package com.twelvemonkeys.servlet.cache; + +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** + * ServletCacheRequest + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java#1 $ + */ +public final class ServletCacheRequest extends AbstractCacheRequest { + private final HttpServletRequest mRequest; + + private Map> mHeaders; + private Map> mParameters; + + protected ServletCacheRequest(final HttpServletRequest pRequest) { + super(URI.create(pRequest.getRequestURI()), pRequest.getMethod()); + mRequest = pRequest; + } + + public Map> getHeaders() { + if (mHeaders == null) { + mHeaders = ServletUtil.headersAsMap(mRequest); + } + + return mHeaders; + } + + public Map> getParameters() { + if (mParameters == null) { + mParameters = ServletUtil.parametersAsMap(mRequest); + } + + return mParameters; + } + + public String getServerName() { + return mRequest.getServerName(); + } + + public int getServerPort() { + return mRequest.getServerPort(); + } + + HttpServletRequest getRequest() { + return mRequest; + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java new file mode 100755 index 00000000..7ffebc95 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java @@ -0,0 +1,46 @@ +package com.twelvemonkeys.servlet.cache; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; + +/** + * ServletCacheResponse + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java#2 $ + */ +public final class ServletCacheResponse extends AbstractCacheResponse { + private HttpServletResponse mResponse; + + public ServletCacheResponse(HttpServletResponse pResponse) { + mResponse = pResponse; + } + + public OutputStream getOutputStream() throws IOException { + return mResponse.getOutputStream(); + } + + @Override + public void setStatus(int pStatusCode) { + mResponse.setStatus(pStatusCode); + super.setStatus(pStatusCode); + } + + @Override + public void addHeader(String pHeaderName, String pHeaderValue) { + mResponse.addHeader(pHeaderName, pHeaderValue); + super.addHeader(pHeaderName, pHeaderValue); + } + + @Override + public void setHeader(String pHeaderName, String pHeaderValue) { + mResponse.setHeader(pHeaderName, pHeaderValue); + super.setHeader(pHeaderName, pHeaderValue); + } + + HttpServletResponse getResponse() { + return mResponse; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java new file mode 100755 index 00000000..5f7decab --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java @@ -0,0 +1,40 @@ +package com.twelvemonkeys.servlet.cache; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * ServletResponseResolver + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java#2 $ + */ +final class ServletResponseResolver implements ResponseResolver { + final private ServletCacheRequest mRequest; + final private ServletCacheResponse mResponse; + final private FilterChain mChain; + + ServletResponseResolver(final ServletCacheRequest pRequest, final ServletCacheResponse pResponse, final FilterChain pChain) { + mRequest = pRequest; + mResponse = pResponse; + mChain = pChain; + } + + public void resolve(final CacheRequest pRequest, final CacheResponse pResponse) throws IOException, CacheException { + // Need only wrap if pResponse is not mResponse... + HttpServletResponse response = pResponse == mResponse ? mResponse.getResponse() : new SerlvetCacheResponseWrapper(mResponse.getResponse(), pResponse); + + try { + mChain.doFilter(mRequest.getRequest(), response); + } + catch (ServletException e) { + throw new CacheException(e); + } + finally { + response.flushBuffer(); + } + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java new file mode 100755 index 00000000..c6ce7e6a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java @@ -0,0 +1,77 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 java.io.OutputStream; + +/** + * WritableCachedResponse + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java#2 $ + */ +public interface WritableCachedResponse extends CachedResponse, CacheResponse { + /** + * Gets the {@code OutputStream} for this cached response. + * This allows a client to write to the cached response. + * + * @return the {@code OutputStream} for this response. + */ + OutputStream getOutputStream(); + + /** + * Sets a header key/value pair for this response. + * Any prior header value for the given header key will be overwritten. + * + * @see #addHeader(String, String) + * + * @param pName the header name + * @param pValue the header value + */ + void setHeader(String pName, String pValue); + + /** + * Adds a header key/value pair for this response. + * If a value allready exists for the given key, the value will be appended. + * + * @see #setHeader(String, String) + * + * @param pName the header name + * @param pValue the header value + */ + void addHeader(String pName, String pValue); + + /** + * Returns the final (immutable) {@code CachedResponse} created by this + * {@code WritableCachedResponse}. + * + * @return the {@code CachedResponse} + */ + CachedResponse getCachedResponse(); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java new file mode 100755 index 00000000..ffeb54b5 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java @@ -0,0 +1,188 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.FastByteArrayOutputStream; +import com.twelvemonkeys.net.NetUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * WritableCachedResponseImpl + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java#3 $ + */ +class WritableCachedResponseImpl implements WritableCachedResponse { + private final CachedResponseImpl mCachedResponse; + + /** + * Creates a {@code WritableCachedResponseImpl}. + */ + protected WritableCachedResponseImpl() { + mCachedResponse = new CachedResponseImpl(); + // Hmmm.. + setHeader(HTTPCache.HEADER_CACHED_TIME, NetUtil.formatHTTPDate(System.currentTimeMillis())); + } + + public CachedResponse getCachedResponse() { + return mCachedResponse; + } + + public void setHeader(String pName, String pValue) { + setHeader(pName, pValue, false); + } + + public void addHeader(String pName, String pValue) { + setHeader(pName, pValue, true); + } + + public Map> getHeaders() { + return mCachedResponse.mHeaders; + } + + /** + * + * @param pName the header name + * @param pValue the new header value + * @param pAdd {@code true} if the value should add to the list of values, not replace existing value + */ + private void setHeader(String pName, String pValue, boolean pAdd) { + // System.out.println(" ++ CachedResponse ++ " + (pAdd ? "addHeader(" : "setHeader(") + pName + ", " + pValue + ")"); + // If adding, get list and append, otherwise replace list + List values = null; + if (pAdd) { + values = mCachedResponse.mHeaders.get(pName); + } + + if (values == null) { + values = new ArrayList(); + + if (pAdd) { + // Add length of pName + mCachedResponse.mHeadersSize += (pName != null ? pName.length() : 0); + } + else { + // Remove length of potential replaced old values + pName + String[] oldValues = getHeaderValues(pName); + if (oldValues != null) { + for (String oldValue : oldValues) { + mCachedResponse.mHeadersSize -= oldValue.length(); + } + } + else { + mCachedResponse.mHeadersSize += (pName != null ? pName.length() : 0); + } + } + } + + // Add value, if not null + if (pValue != null) { + values.add(pValue); + + // Add length of pValue + mCachedResponse.mHeadersSize += pValue.length(); + } + + // Always add to headers + mCachedResponse.mHeaders.put(pName, values); + } + + public OutputStream getOutputStream() { + // TODO: Hmm.. Smells like DCL..? + if (mCachedResponse.mContent == null) { + createOutputStream(); + } + return mCachedResponse.mContent; + } + + public void setStatus(int pStatusCode) { + mCachedResponse.mStatus = pStatusCode; + } + + public int getStatus() { + return mCachedResponse.getStatus(); + } + + private synchronized void createOutputStream() { + ByteArrayOutputStream cache = mCachedResponse.mContent; + if (cache == null) { + String contentLengthStr = getHeaderValue("Content-Length"); + if (contentLengthStr != null) { + int contentLength = Integer.parseInt(contentLengthStr); + cache = new FastByteArrayOutputStream(contentLength); + } + else { + cache = new FastByteArrayOutputStream(1024); + } + mCachedResponse.mContent = cache; + } + } + + public void writeHeadersTo(CacheResponse pResponse) { + mCachedResponse.writeHeadersTo(pResponse); + } + + public void writeContentsTo(OutputStream pStream) throws IOException { + mCachedResponse.writeContentsTo(pStream); + } + + public String[] getHeaderNames() { + return mCachedResponse.getHeaderNames(); + } + + public String[] getHeaderValues(String pHeaderName) { + return mCachedResponse.getHeaderValues(pHeaderName); + } + + public String getHeaderValue(String pHeaderName) { + return mCachedResponse.getHeaderValue(pHeaderName); + } + + public int size() { + return mCachedResponse.size(); + } + + public boolean equals(Object pOther) { + if (pOther instanceof WritableCachedResponse) { + // Take advantage of faster implementation + return mCachedResponse.equals(((WritableCachedResponse) pOther).getCachedResponse()); + } + return mCachedResponse.equals(pOther); + } + + public int hashCode() { + return mCachedResponse.hashCode(); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/todo.txt b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/todo.txt new file mode 100755 index 00000000..7e3fb530 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/todo.txt @@ -0,0 +1,3 @@ +- Keep filter and servlet specific implementations in servlet module +- Move most of the implementation out of servlet module (HTTPCache + interfaces + abstract impl) +- Move client cache implementation classes out of servlet module, and to separate package \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java new file mode 100755 index 00000000..c55090cc --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java @@ -0,0 +1,42 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java#1 $ + */ +public class FileSizeExceededException extends FileUploadException { + public FileSizeExceededException(Throwable pCause) { + super(pCause.getMessage(), pCause); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java new file mode 100755 index 00000000..fd8175e8 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java @@ -0,0 +1,52 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/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); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java new file mode 100755 index 00000000..c4176d0d --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java @@ -0,0 +1,137 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 com.twelvemonkeys.servlet.GenericFilter; +import com.twelvemonkeys.servlet.ServletUtil; +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 java.io.IOException; +import java.io.File; +import java.net.URL; +import java.net.MalformedURLException; + +/** + * A servlet {@code Filter} for processing HTTP file upload requests, as + * specified by + * Form-based File Upload in HTML (RFC1867). + * + * @see HttpFileUploadRequest + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java#1 $ + */ +public class FileUploadFilter extends GenericFilter { + private File mUploadDir; + private long mMaxFileSize = 1024 * 1024; // 1 MByte + + /** + * This method is called by the server before the filter goes into service, + * and here it determines the file upload directory. + * + * @throws ServletException + */ + public void init() throws ServletException { + // Get the name of the upload directory. + String uploadDirParam = getInitParameter("uploadDir"); + if (!StringUtil.isEmpty(uploadDirParam)) { + try { + URL uploadDirURL = getServletContext().getResource(uploadDirParam); + mUploadDir = FileUtil.toFile(uploadDirURL); + } + catch (MalformedURLException e) { + throw new ServletException(e.getMessage(), e); + } + } + if (mUploadDir == null) { + mUploadDir = ServletUtil.getTempDir(getServletContext()); + } + } + + /** + * Sets max filesize allowed for upload. + * + * + * @param pMaxSize + */ +// public void setMaxFileSize(String pMaxSize) { +// try { +// setMaxFileSize(Long.parseLong(pMaxSize)); +// } +// catch (NumberFormatException e) { +// log("Error setting maxFileSize, using default: " + mMaxFileSize, e); +// } +// } + + /** + * Sets max filesize allowed for upload. + * + * @param pMaxSize + */ + public void setMaxFileSize(long pMaxSize) { + log("maxFileSize=" + pMaxSize); + mMaxFileSize = pMaxSize; + } + + /** + * Examines the request content type, and if it is a + * {@code multipart/*} request, wraps the request with a + * {@code HttpFileUploadRequest}. + * + * @param pRequest The servlet request + * @param pResponse The servlet response + * @param pChain The filter chain + * + * @throws ServletException + * @throws IOException + */ + public void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) pRequest; + + // Get the content type from the request + String contentType = request.getContentType(); + + // If the content type is multipart, wrap + if (isMultipartFileUpload(contentType)) { + pRequest = new HttpFileUploadRequestWrapper(request, mUploadDir, mMaxFileSize); + } + + pChain.doFilter(pRequest, pResponse); + } + + private boolean isMultipartFileUpload(String pContentType) { + return pContentType != null && pContentType.startsWith("multipart/"); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java new file mode 100755 index 00000000..92a3e990 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java @@ -0,0 +1,63 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.http.HttpServletRequest; + +/** + * This interface represents an HTTP file upload request, as specified by + * Form-based File Upload in HTML (RFC1867). + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java#1 $ + */ +public interface HttpFileUploadRequest extends HttpServletRequest { + /** + * Returns the value of a request parameter as an {@code UploadedFile}, + * or {@code null} if the parameter does not exist. + * You should only use this method when you are sure the parameter has only + * one value. + * + * @param pName the name of the requested parameter + * @return a {@code UoploadedFile} or {@code null} + * + * @see #getUploadedFiles(String) + */ + UploadedFile getUploadedFile(String pName); + + /** + * Returns an array of {@code UploadedFile} objects containing all the + * values for the given request parameter, + * or {@code null} if the parameter does not exist. + * + * @param pName the name of the requested parameter + * @return an array of {@code UoploadedFile}s or {@code null} + */ + UploadedFile[] getUploadedFiles(String pName); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java new file mode 100755 index 00000000..1a371ceb --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java @@ -0,0 +1,154 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 org.apache.commons.fileupload.*; +import org.apache.commons.fileupload.servlet.ServletRequestContext; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; + +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletException; +import java.io.File; +import java.util.*; + +/** + * An {@code HttpFileUploadRequest} implementation, based on + * Jakarta Commons FileUpload. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java#1 $ + */ +class HttpFileUploadRequestWrapper extends HttpServletRequestWrapper implements HttpFileUploadRequest { + + private final Map mParameters = new HashMap(); + private final Map mFiles = new HashMap(); + + public HttpFileUploadRequestWrapper(HttpServletRequest pRequest, File pUploadDir, long pMaxSize) throws ServletException { + super(pRequest); + + DiskFileItemFactory factory = new DiskFileItemFactory( + 128 * 1024, // 128 KByte + new File(pUploadDir.getAbsolutePath()) + ); + FileUpload upload = new FileUpload(factory); + upload.setSizeMax(pMaxSize); + + // TODO: Defer request parsing?? + try { + //noinspection unchecked + List items = upload.parseRequest(new ServletRequestContext(pRequest)); + for (FileItem item : items) { + if (item.isFormField()) { + processFormField(item.getFieldName(), item.getString()); + } + else { + processeFile(item); + } + } + } + catch (FileUploadBase.SizeLimitExceededException e) { + throw new FileSizeExceededException(e); + } + catch (org.apache.commons.fileupload.FileUploadException e) { + throw new FileUploadException(e); + } + } + + private void processeFile(final FileItem pItem) { + UploadedFile value = new UploadedFileImpl(pItem); + String name = pItem.getFieldName(); + + UploadedFile[] values; + UploadedFile[] oldValues = mFiles.get(name); + + if (oldValues != null) { + values = new UploadedFile[oldValues.length + 1]; + System.arraycopy(oldValues, 0, values, 0, oldValues.length); + values[oldValues.length] = value; + } + else { + values = new UploadedFile[] {value}; + } + + mFiles.put(name, values); + + // Also add to normal fields + processFormField(name, value.getName()); + } + + private void processFormField(String pName, String pValue) { + // Multiple parameter values are not that common, so it's + // probably faster to just use arrays... + // TODO: Research and document... + String[] values; + String[] oldValues = mParameters.get(pName); + + if (oldValues != null) { + values = new String[oldValues.length + 1]; + System.arraycopy(oldValues, 0, values, 0, oldValues.length); + values[oldValues.length] = pValue; + } + else { + values = new String[] {pValue}; + } + + mParameters.put(pName, values); + } + + public Map getParameterMap() { + // TODO: The spec dicates immutable map, but what about the value arrays?! + // Probably just leave as-is, for performance + return Collections.unmodifiableMap(mParameters); + } + + public Enumeration getParameterNames() { + return Collections.enumeration(mParameters.keySet()); + } + + public String getParameter(String pString) { + String[] values = getParameterValues(pString); + return values != null ? values[0] : null; + } + + public String[] getParameterValues(String pString) { + // TODO: Optimize? + return mParameters.get(pString).clone(); + } + + public UploadedFile getUploadedFile(String pName) { + UploadedFile[] files = getUploadedFiles(pName); + return files != null ? files[0] : null; + } + + public UploadedFile[] getUploadedFiles(String pName) { + // TODO: Optimize? + return mFiles.get(pName).clone(); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java new file mode 100755 index 00000000..f4c1e6df --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java @@ -0,0 +1,86 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 java.io.File; +import java.io.InputStream; +import java.io.IOException; + +/** + * This class represents an uploaded file. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java#1 $ + */ +public interface UploadedFile { + /** + * Returns the length of file, in bytes. + * + * @return length of file + */ + long length(); + + /** + * Returns the original file name (from client). + * + * @return original name + */ + String getName(); + + /** + * Returns the content type of the file. + * + * @return the content type + */ + String getContentType(); + + /** + * Returns the file data, as an {@code InputStream}. + * The file data may be read from disk, or from an in-memory source, + * depending on implementation. + * + * @return an {@code InputStream} containing the file data + * @throws IOException + * @throws RuntimeException + */ + InputStream getInputStream() throws IOException; + + /** + * Writes the file data to the given {@code File}. + * Note that implementations are free to optimize this to a rename + * operation, if the file is allready cached to disk. + * + * @param pFile the {@code File} (file name) to write to. + * @throws IOException + * @throws RuntimeException + */ + void writeTo(File pFile) throws IOException; + + // TODO: void delete()? +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java new file mode 100755 index 00000000..5c19cd98 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java @@ -0,0 +1,91 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; + +import java.io.InputStream; +import java.io.IOException; +import java.io.File; + +/** + * An {@code UploadedFile} implementation, based on + * Jakarta Commons FileUpload. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java#1 $ + */ +class UploadedFileImpl implements UploadedFile { + private final FileItem mItem; + + public UploadedFileImpl(FileItem pItem) { + if (pItem == null) { + throw new IllegalArgumentException("fileitem == null"); + } + + mItem = pItem; + } + + public String getContentType() { + return mItem.getContentType(); + } + + public InputStream getInputStream() throws IOException { + return mItem.getInputStream(); + } + + public String getName() { + return mItem.getName(); + } + + public long length() { + return mItem.getSize(); + } + + public void writeTo(File pFile) throws IOException { + try { + mItem.write(pFile); + } + catch(RuntimeException e) { + throw e; + } + catch (IOException e) { + throw e; + } + catch (FileUploadException e) { + // We deliberately change this exception to an IOException, as it really is + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + catch (Exception e) { + // Should not really happen, ever + throw new RuntimeException(e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java new file mode 100755 index 00000000..1fbe64ff --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java @@ -0,0 +1,141 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * 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. + *

+ * 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. + *

+ * Configuration
+ * 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. + * Servlet 2.4 version, filter section:
+ *

+ * <!-- GZIP Filter Configuration -->
+ * <filter>
+ *      <filter-name>gzip</filter-name>
+ *      <filter-class>com.twelvemonkeys.servlet.GZIPFilter</filter-class>
+ * </filter>
+ * 
+ * Filter-mapping section:
+ *
+ * <!-- GZIP Filter Mapping -->
+ * <filter-mapping>
+ *      <filter-name>gzip</filter-name>
+ *      <url-pattern>*.html</url-pattern>
+ *      <dispatcher>REQUEST</dispatcher>
+ *      <dispatcher>FORWARD</dispatcher>
+ * </filter-mapping>
+ * <filter-mapping>
+ *      <filter-name>gzip</filter-name>
+ *      <url-pattern>*.jsp< /url-pattern>
+ *      <dispatcher>REQUEST</dispatcher>
+ *      <dispatcher>FORWARD</dispatcher>
+ * </filter-mapping>
+ * 
+ *

+ * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

+ * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java#1 $ + */ +public class GZIPFilter extends GenericFilter { + + { + mOncePerRequest = 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.indexOf("gzip") != -1) { + //System.out.println("GZIP supported, compressing."); + // TODO: Set Vary: Accept-Encoding ?! + + GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response); + try { + pChain.doFilter(pRequest, wrapped); + } + finally { + wrapped.flushResponse(); + } + return; + } + } + + // Else, contiue chain + pChain.doFilter(pRequest, pResponse); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java new file mode 100755 index 00000000..60579c9f --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java @@ -0,0 +1,146 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ * Based on ideas and code found in the ONJava article + * Two Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java#1 $ + */ +public class GZIPResponseWrapper extends HttpServletResponseWrapper { + protected ServletOutputStream mOut = null; + protected PrintWriter mWriter = null; + protected GZIPOutputStream mGZIPOut = null; + protected int mContentLength = -1; + + public GZIPResponseWrapper(HttpServletResponse response) { + super(response); + response.addHeader("Content-Encoding", "gzip"); + } + + 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 (mContentLength >= 0) { + mGZIPOut = new GZIPOutputStream(getResponse().getOutputStream(), mContentLength); + } + else { + mGZIPOut = new GZIPOutputStream(getResponse().getOutputStream()); + } + + // Wrap in ServletOutputStream and return + return new OutputStreamAdapter(mGZIPOut); + } + + // TODO: Move this to flushbuffer or something? Hmmm.. + public void flushResponse() { + try { + try { + // Finish GZIP encodig + if (mGZIPOut != null) { + mGZIPOut.finish(); + } + + flushBuffer(); + } + finally { + // Close stream + if (mWriter != null) { + mWriter.close(); + } + else { + if (mOut != null) { + mOut.close(); + } + } + } + } + catch (IOException e) { + // TODO: Fix this one... + e.printStackTrace(); + } + } + + public void flushBuffer() throws IOException { + if (mWriter != null) { + mWriter.flush(); + } + else if (mOut != null) { + mOut.flush(); + } + } + + public ServletOutputStream getOutputStream() throws IOException { + if (mWriter != null) { + throw new IllegalStateException("getWriter() has already been called!"); + } + + if (mOut == null) { + mOut = createOutputStream(); + } + return (mOut); + } + + public PrintWriter getWriter() throws IOException { + if (mWriter != null) { + return (mWriter); + } + + if (mOut != null) { + throw new IllegalStateException("getOutputStream() has already been called!"); + } + + mOut = createOutputStream(); + // TODO: This is wrong. Should use getCharacterEncoding() or "ISO-8859-1" if gCE returns null. + mWriter = new PrintWriter(new OutputStreamWriter(mOut, "UTF-8")); + return (mWriter); + } + + public void setContentLength(int pLength) { + // NOTE: Do not call super, as we will shrink the size. + mContentLength = pLength; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java new file mode 100755 index 00000000..10164832 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java @@ -0,0 +1,72 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 javax.servlet.ServletRequest; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; + +/** + * AWTImageFilterAdapter + * + * @author $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java#1 $ + * + */ +public class AWTImageFilterAdapter extends ImageFilter { + + private java.awt.image.ImageFilter mFilter = null; + + public void setImageFilter(String pFilterClass) { + try { + Class filterClass = Class.forName(pFilterClass); + mFilter = (java.awt.image.ImageFilter) filterClass.newInstance(); + } + catch (ClassNotFoundException e) { + log("Could not load filter class.", e); + } + catch (InstantiationException e) { + log("Could not instantiate filter.", e); + } + catch (IllegalAccessException e) { + log("Could not access filter class.", e); + } + } + + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + // Filter + Image img = ImageUtil.filter(pImage, mFilter); + + // Create BufferedImage & return + return ImageUtil.toBuffered(img, BufferedImage.TYPE_INT_RGB); // TODO: This is for JPEG only... + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java new file mode 100755 index 00000000..68d4ea50 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java @@ -0,0 +1,67 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.BufferedImageOp; +import java.awt.image.RenderedImage; + +/** + * BufferedImageOpAdapter + * + * @author $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java#1 $ + * + */ +public class BufferedImageOpAdapter extends ImageFilter { + + private BufferedImageOp mFilter = null; + + public void setImageFilter(String pFilterClass) { + try { + Class filterClass = Class.forName(pFilterClass); + mFilter = (BufferedImageOp) filterClass.newInstance(); + } + catch (ClassNotFoundException e) { + log("Could not instantiate filter class.", e); + } + catch (InstantiationException e) { + log("Could not instantiate filter.", e); + } + catch (IllegalAccessException e) { + log("Could not access filter class.", e); + } + } + + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + // Filter & return + return mFilter.filter(pImage, null); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java new file mode 100755 index 00000000..e97833b4 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java @@ -0,0 +1,212 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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). + *

+ * The class does only byte manipulation, there is no server-side image + * processing involving AWT ({@code Toolkit} class) of any kind. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/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 mCRC = 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 (mCRC) { + mCRC.reset(); + mCRC.update(pBytes, pOff, pLen); + value = (int) mCRC.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; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java new file mode 100755 index 00000000..0952c366 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java @@ -0,0 +1,59 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.RenderedImage; +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * ComposeFilter + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/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; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java new file mode 100755 index 00000000..4d2996ee --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java @@ -0,0 +1,436 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.imageio.ImageIO; +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.*; + +/** + * 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). + */ +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[] mKnownFormatQuality = new float[] { + 1f, 1f, 0.99f, 0.5f + }; + + private HashMap mFormatQuality; // HashMap, as I need to clone this for each request + private final Object mLock = new Object(); + + /* + private Pattern[] mKnownAgentPatterns; + private String[] mKnownAgentAccpets; + */ + { + // Hack: Make sure the filter don't trigger all the time + // See: super.trigger(ServletRequest) + mTriggerParams = new String[] {}; + } + + /* + public void setAcceptMappings(String pPropertiesFile) { + // NOTE: Supposed to be: + // = + // .accept= + + 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 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 getFormatQualityMapping() { + synchronized(mLock) { + if (mFormatQuality == null) { + mFormatQuality = new HashMap(); + + // 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) { + mFormatQuality.put(format, getKnownFormatQuality(format)); + } + } + } + //noinspection unchecked + return (Map) mFormatQuality.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 pFormatQuality) { + String acceptable = null; + float acceptQuality = 0.0f; + for (Map.Entry 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 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 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 + * @param pFormat the format + * @param pFactor the quality factor + */ + private static void adjustQuality(Map 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 mKnownFormatQuality[i]; + } + } + return 0.1f; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java new file mode 100755 index 00000000..e3a7bdbf --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java @@ -0,0 +1,232 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + * + *


+ * + * Parameters:
+ *

+ *
{@code cropX}
+ *
integer, the new left edge of the image. + *
{@code cropY}
+ *
integer, the new top of the image. + *
{@code cropWidth}
+ *
integer, the new width of the image. + *
{@code cropHeight}
+ *
integer, the new height of the image. + *
{@code cropUniform}
+ *
boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + *
{@code cropUnits}
+ *
string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + * + * + * + *
{@code image}
+ *
string, the URL of the image to scale. + * + *
{@code scaleX}
+ *
integer, the new width of the image. + * + *
{@code scaleY}
+ *
integer, the new height of the image. + * + *
{@code scaleUniform}
+ *
boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + * + *
{@code scaleUnits}
+ *
string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + * + *
{@code scaleQuality}
+ *
string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST}, + * {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}. + * {@code SCALE_DEFAULT} is default. + * + *
+ * + * @example + * <IMG src="/crop/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=500&cropUniform=true"> + * + * @example + * <IMG src="/crop/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=50&cropUnits=PERCENT"> + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/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); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java new file mode 100755 index 00000000..04978301 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java @@ -0,0 +1,199 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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 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 Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java#2 $ + * + */ +public abstract class ImageFilter extends GenericFilter { + + protected String[] mTriggerParams = null; + + /** + * The {@code doFilterImpl} method is called once, or each time a + * request/response pair is passed through the chain, depending on the + * {@link #mOncePerRequest} member variable. + * + * @see #mOncePerRequest + * @see com.twelvemonkeys.servlet.GenericFilter#doFilterImpl doFilter + * @see Filter#doFilter Filter.doFilter + * + * @param pRequest the servlet request + * @param pResponse the servlet response + * @param pChain the filter chain + * + * @throws IOException + * @throws ServletException + */ + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, 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 { + // For images, we do post filtering only and need to wrap the response + ImageServletResponse imageResponse; + boolean encode; + if (pResponse instanceof ImageServletResponse) { + //System.out.println("Allready ImageServletResponse"); + imageResponse = (ImageServletResponse) pResponse; + encode = false; // Allready wrapped, will be encoded later in the chain + } + else { + //System.out.println("Wrapping in ImageServletResponse"); + imageResponse = new ImageServletResponseImpl(pRequest, pResponse, getServletContext()); + encode = true; // This is first filter in chain, must encode when done + } + + //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 repsonse + 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()) + "\""; + ((ImageServletResponseImpl) imageResponse).setHeader("ETag", etag); + ((ImageServletResponseImpl) imageResponse).setDateHeader("Last-Modified", (System.currentTimeMillis() / 1000) * 1000); + imageResponse.flush(); + } + //System.out.println("Done encoding."); + } + } + } + //System.out.println("Filtering done."); + } + + /** + * Tests if the filter should do image filtering/processing. + *

+ * This default implementation uses {@link #mTriggerParams} to test if: + *

+ *
{@code mTriggerParams == null}
+ *
{@code return true}
+ *
{@code mTriggerParams != null}, loop through parameters, and test + * if {@code pRequest} contains the parameter. If match
+ *
{@code return true}
+ *
Otherwise
+ *
{@code return false}
+ *
+ * + * + * @param pRequest the servlet request + * @return {@code true} if the filter should do image filtering + */ + protected boolean trigger(ServletRequest pRequest) { + // If triggerParams not set, assume always trigger + if (mTriggerParams == null) { + return true; + } + + // Trigger only for certain request parameters + for (String triggerParam : mTriggerParams) { + 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. + */ + public void setTriggerParams(String pTriggerParams) { + mTriggerParams = 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; +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java new file mode 100755 index 00000000..6e4de253 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java @@ -0,0 +1,55 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.*; + +/** + * This excpetion is a subclass of ServletException, and acts just as a marker + * for excpetions thrown by the ImageServlet API. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java#2 $ + */ +public class ImageServletException extends ServletException { + + public ImageServletException(String pMessage) { + super(pMessage); + } + + public ImageServletException(Throwable pThrowable) { + super(pThrowable); + } + + public ImageServletException(String pMessage, Throwable pThrowable) { + super(pMessage, pThrowable); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java new file mode 100755 index 00000000..772bd548 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java @@ -0,0 +1,193 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.IOException; +import java.awt.image.RenderedImage; +import java.awt.image.BufferedImage; + +/** + * ImageServletResponse. + *

+ * 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 Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/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. + *

+ * 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. + *

+ * 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. + *

+ * 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). + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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>}. + *

+ * 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}. + *

+ * 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. + *

+ * 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". + *

+ * 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. + *

+ * 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); +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java new file mode 100755 index 00000000..dc24d041 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java @@ -0,0 +1,747 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.io.FastByteArrayOutputStream; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.imageio.*; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.servlet.ServletContext; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Iterator; + +/** + * This {@link ImageServletResponse} implementation can be used with image + * requests, to have the image immediately decoded to a {@code BufferedImage}. + * The image may be optionally subsampled, scaled and/or cropped. + * The response also automtically handles writing the image back to the underlying response stream + * in the preferred format, when the response is flushed. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java#10 $ + * + */ +// TODO: Refactor out HTTP specifcs (if possible). +// TODO: Is it a good ide to throw IIOException? +class ImageServletResponseImpl extends HttpServletResponseWrapper implements ImageServletResponse { + + private final ServletRequest mOriginalRequest; + private final ServletContext mContext; + private final ServletResponseStreamDelegate mStreamDelegate; + + private FastByteArrayOutputStream mBufferedOut; + + private RenderedImage mImage; + private String mOutputContentType; + + private String mOriginalContentType; + private int mOriginalContentLength = -1; + + /** + * Creates an {@code ImageServletResponseImpl}. + * + * @param pRequest the request + * @param pResponse the response + * @param pContext the servlet context + */ + public ImageServletResponseImpl(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final ServletContext pContext) { + super(pResponse); + mOriginalRequest = pRequest; + mStreamDelegate = new ServletResponseStreamDelegate(pResponse) { + @Override + protected OutputStream createOutputStream() throws IOException { + if (mOriginalContentLength >= 0) { + mBufferedOut = new FastByteArrayOutputStream(mOriginalContentLength); + } + else { + mBufferedOut = new FastByteArrayOutputStream(0); + } + + return mBufferedOut; + } + }; + mContext = pContext; + } + + /** + * Creates an {@code ImageServletResponseImpl}. + * + * @param pRequest the request + * @param pResponse the response + * @param pContext the servlet context + * + * @throws ClassCastException if {@code pRequest} is not an {@link javax.servlet.http.HttpServletRequest} or + * {@code pResponse} is not an {@link javax.servlet.http.HttpServletResponse}. + */ + public ImageServletResponseImpl(final ServletRequest pRequest, final ServletResponse pResponse, final ServletContext pContext) { + // Cheat for now... + this((HttpServletRequest) pRequest, (HttpServletResponse) pResponse, pContext); + } + + /** + * Called by the container, do not invoke. + * + * @param pMimeType the content (MIME) type + */ + public void setContentType(final String pMimeType) { + // Throw exception is allready set + if (mOriginalContentType != null) { + throw new IllegalStateException("ContentType allready set."); + } + + mOriginalContentType = pMimeType; + } + + /** + * Called by the container. Do not invoke. + * + * @return the response's {@code OutputStream} + * @throws IOException + */ + public ServletOutputStream getOutputStream() throws IOException { + return mStreamDelegate.getOutputStream(); + } + + /** + * Called by the container. Do not invoke. + * + * @return the response's {@code PrintWriter} + * @throws IOException + */ + public PrintWriter getWriter() throws IOException { + return mStreamDelegate.getWriter(); + } + + /** + * Called by the container. Do not invoke. + * + * @param pLength the content length + */ + public void setContentLength(final int pLength) { + if (mOriginalContentLength != -1) { + throw new IllegalStateException("ContentLength already set."); + } + + mOriginalContentLength = pLength; + } + + /** + * Writes the image to the original {@code ServletOutputStream}. + * If no format is set in this response, the image is encoded in the same + * format as the original image. + * + * @throws IOException if an I/O exception occurs during writing + */ + public void flush() throws IOException { + String outputType = getOutputContentType(); + + // Force transcoding, if no other filtering is done + if (!outputType.equals(mOriginalContentType)) { + getImage(); + } + + // This is stupid, but don't know how to work around... + // TODO: Test what types of images that work with JPEG, consider reporting it as a bug + if (("image/jpeg".equals(outputType) || "image/jpg".equals(outputType) + || "image/bmp".equals(outputType) || "image/x-bmp".equals(outputType)) && + mImage instanceof BufferedImage && ((BufferedImage) mImage).getType() == BufferedImage.TYPE_INT_ARGB) { + mImage = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_INT_RGB); + } + + //System.out.println("Writing image, content-type: " + getContentType(outputType)); + //System.out.println("Writing image, outputType: " + outputType); + //System.out.println("Writing image: " + mImage); + if (mImage != null) { + Iterator writers = ImageIO.getImageWritersByMIMEType(outputType); + if (writers.hasNext()) { + super.setContentType(outputType); + OutputStream out = super.getOutputStream(); + + ImageWriter writer = (ImageWriter) writers.next(); + try { + ImageWriteParam param = writer.getDefaultWriteParam(); + + Float requestQuality = (Float) mOriginalRequest.getAttribute(ImageServletResponse.ATTRIB_OUTPUT_QUALITY); + + // The default JPEG quality is not good enough, so always apply compression + if ((requestQuality != null || "jpeg".equalsIgnoreCase(getFormatNameSafe(writer))) && param.canWriteCompressed()) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(requestQuality != null ? requestQuality : 0.8f); + } + + ImageOutputStream stream = ImageIO.createImageOutputStream(out); + + //System.out.println("-ISR- Image: " + mImage); + //System.out.println("-ISR- ImageWriter: " + writer); + //System.out.println("-ISR- ImageOutputStream: " + stream); + + writer.setOutput(stream); + try { + writer.write(null, new IIOImage(mImage, null, null), param); + } + finally { + stream.close(); + } + } + finally { + writer.dispose(); + out.flush(); +// out.close(); + } + } + else { + mContext.log("ERROR: No writer for content-type: " + outputType); +// sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to encode image: No writer for content-type " + outputType); + throw new IIOException("Unable to transcode image: No suitable image writer found (content-type: " + outputType + ")."); + } + } + else { + super.setContentType(mOriginalContentType); + ServletOutputStream out = super.getOutputStream(); + try { + mBufferedOut.writeTo(out); + } + finally { + out.flush(); + } + } + } + + private String getFormatNameSafe(final ImageWriter pWriter) { + try { + return pWriter.getOriginatingProvider().getFormatNames()[0]; + } + catch (RuntimeException e) { + // NPE, AIOOBE, etc.. + return null; + } + } + + public String getOutputContentType() { + return mOutputContentType != null ? mOutputContentType : mOriginalContentType; + } + + public void setOutputContentType(final String pImageFormat) { + mOutputContentType = pImageFormat; + } + + /** + * Sets the image for this response. + * + * @param pImage the {@code RenderedImage} that will be written to the + * response stream + */ + public void setImage(final RenderedImage pImage) { + mImage = pImage; + } + + /** + * 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 + */ + public BufferedImage getImage() throws IOException { + if (mImage == null) { + // No content, no image + if (mBufferedOut == null) { + return null; + } + + // Read from the byte buffer + InputStream byteStream = mBufferedOut.createInputStream(); + ImageInputStream input = null; + try { + input = ImageIO.createImageInputStream(byteStream); + Iterator readers = ImageIO.getImageReaders(input); + if (readers.hasNext()) { + // Get the correct reader + ImageReader reader = (ImageReader) readers.next(); + try { + reader.setInput(input); + + ImageReadParam param = reader.getDefaultReadParam(); + + // Get default size + int originalWidth = reader.getWidth(0); + int originalHeight = reader.getHeight(0); + + // Extract AOI from request + Rectangle aoi = extractAOIFromRequest(originalWidth, originalHeight); + if (aoi != null) { + param.setSourceRegion(aoi); + originalWidth = aoi.width; + originalHeight = aoi.height; + } + + // If possible, extract size from request + Dimension size = extractSizeFromRequest(originalWidth, originalHeight); + double readSubSamplingFactor = getReadSubsampleFactorFromRequest(); + if (size != null) { + //System.out.println("Size: " + size); + if (param.canSetSourceRenderSize()) { + param.setSourceRenderSize(size); + } + else { + int subX = (int) Math.max(originalWidth / (double) (size.width * readSubSamplingFactor), 1.0); + int subY = (int) Math.max(originalHeight / (double) (size.height * readSubSamplingFactor), 1.0); + + if (subX > 1 || subY > 1) { + param.setSourceSubsampling(subX, subY, subX > 1 ? subX / 2 : 0, subY > 1 ? subY / 2 : 0); + } + } + } + + // Need base URI for SVG with links/stylesheets etc + maybeSetBaseURIFromRequest(param); + + // Finally, read the image using the supplied parameter + BufferedImage image = reader.read(0, param); + + // If reader doesn't support dynamic sizing, scale now + if (image != null && size != null + && (image.getWidth() != size.width || image.getHeight() != size.height)) { + + int resampleAlgorithm = getResampleAlgorithmFromRequest(); + // NOTE: Only use createScaled if IndexColorModel, + // as it's more expensive due to color conversion + if (image.getColorModel() instanceof IndexColorModel) { + image = ImageUtil.createScaled(image, size.width, size.height, resampleAlgorithm); + } + else { + image = ImageUtil.createResampled(image, size.width, size.height, resampleAlgorithm); + } + } + + // Fill bgcolor behind image, if transparent + extractAndSetBackgroundColor(image); + + //System.out.println("-ISR- Image: " + image); + + // Set image + mImage = image; + } + finally { + reader.dispose(); + } + } + else { + mContext.log("ERROR: No suitable image reader found (content-type: " + mOriginalContentType + ")."); + mContext.log("ERROR: Available formats: " + getFormatsString()); + + throw new IIOException("Unable to transcode image: No suitable image reader found (content-type: " + mOriginalContentType + ")."); + } + + // Free resources, as the image is now either read, or unreadable + mBufferedOut = null; + } + finally { + if (input != null) { + input.close(); + } + } + } + + // Image is usually a BufferedImage, but may also be a RenderedImage + return mImage != null ? ImageUtil.toBuffered(mImage) : null; + } + + private int getResampleAlgorithmFromRequest() { + int resampleAlgoithm; + + Object algorithm = mOriginalRequest.getAttribute(ATTRIB_IMAGE_RESAMPLE_ALGORITHM); + if (algorithm instanceof Integer && ((Integer) algorithm == Image.SCALE_SMOOTH || (Integer) algorithm == Image.SCALE_FAST || (Integer) algorithm == Image.SCALE_DEFAULT)) { + resampleAlgoithm = (Integer) algorithm; + } + else { + if (algorithm != null) { + mContext.log("WARN: Illegal image resampling algorithm: " + algorithm); + } + resampleAlgoithm = BufferedImage.SCALE_DEFAULT; + } + + return resampleAlgoithm; + } + + private double getReadSubsampleFactorFromRequest() { + double subsampleFactor; + + Object factor = mOriginalRequest.getAttribute(ATTRIB_READ_SUBSAMPLING_FACTOR); + if (factor instanceof Number && ((Number) factor).doubleValue() >= 1.0) { + subsampleFactor = ((Number) factor).doubleValue(); + } + else { + if (factor != null) { + mContext.log("WARN: Illegal read subsampling factor: " + factor); + } + subsampleFactor = 2.0; + } + + return subsampleFactor; + } + + private void extractAndSetBackgroundColor(final BufferedImage pImage) { + // TODO: bgColor request attribute instead of parameter? + if (pImage.getColorModel().hasAlpha()) { + String bgColor = mOriginalRequest.getParameter("bg.color"); + if (bgColor != null) { + Color color = StringUtil.toColor(bgColor); + + Graphics2D g = pImage.createGraphics(); + try { + g.setColor(color); + g.setComposite(AlphaComposite.DstOver); + g.fillRect(0, 0, pImage.getWidth(), pImage.getHeight()); + } + finally { + g.dispose(); + } + } + } + } + + private static String getFormatsString() { + String[] formats = ImageIO.getReaderFormatNames(); + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < formats.length; i++) { + String format = formats[i]; + if (i > 0) { + buf.append(", "); + } + buf.append(format); + } + return buf.toString(); + } + + private void maybeSetBaseURIFromRequest(final ImageReadParam pParam) { + if (mOriginalRequest instanceof HttpServletRequest) { + try { + // If there's a setBaseURI method, we'll try to use that (uses reflection, to avoid dependency on plugins) + Method setBaseURI; + try { + setBaseURI = pParam.getClass().getMethod("setBaseURI", String.class); + } + catch (NoSuchMethodException ignore) { + return; + } + + // Get URL for resource and set as base + String baseURI = ServletUtil.getContextRelativeURI((HttpServletRequest) mOriginalRequest); + + URL resourceURL = mContext.getResource(baseURI); + if (resourceURL == null) { + resourceURL = ServletUtil.getRealURL(mContext, baseURI); + } + + if (resourceURL != null) { + setBaseURI.invoke(pParam, resourceURL.toExternalForm()); + } + else { + mContext.log("WARN: Resource URL not found for URI: " + baseURI); + } + } + catch (Exception e) { + mContext.log("WARN: Could not set base URI: ", e); + } + } + } + + private Dimension extractSizeFromRequest(final int pDefaultWidth, final int pDefaultHeight) { + // TODO: Allow extraction from request parameters + /* + int sizeW = ServletUtil.getIntParameter(mOriginalRequest, "size.w", -1); + int sizeH = ServletUtil.getIntParameter(mOriginalRequest, "size.h", -1); + boolean sizePercent = ServletUtil.getBooleanParameter(mOriginalRequest, "size.percent", false); + boolean sizeUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "size.uniform", true); + */ + Dimension size = (Dimension) mOriginalRequest.getAttribute(ATTRIB_SIZE); + int sizeW = size != null ? size.width : -1; + int sizeH = size != null ? size.height : -1; + + Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_PERCENT); + boolean sizePercent = b != null && b; // default: false + + b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_SIZE_UNIFORM); + boolean sizeUniform = b == null || b; // default: true + + if (sizeW >= 0 || sizeH >= 0) { + size = getSize(pDefaultWidth, pDefaultHeight, sizeW, sizeH, sizePercent, sizeUniform); + } + + return size; + } + + private Rectangle extractAOIFromRequest(final int pDefaultWidth, final int pDefaultHeight) { + // TODO: Allow extraction from request parameters + /* + int aoiX = ServletUtil.getIntParameter(mOriginalRequest, "aoi.x", -1); + int aoiY = ServletUtil.getIntParameter(mOriginalRequest, "aoi.y", -1); + int aoiW = ServletUtil.getIntParameter(mOriginalRequest, "aoi.w", -1); + int aoiH = ServletUtil.getIntParameter(mOriginalRequest, "aoi.h", -1); + boolean aoiPercent = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.percent", false); + boolean aoiUniform = ServletUtil.getBooleanParameter(mOriginalRequest, "aoi.uniform", false); + */ + Rectangle aoi = (Rectangle) mOriginalRequest.getAttribute(ATTRIB_AOI); + int aoiX = aoi != null ? aoi.x : -1; + int aoiY = aoi != null ? aoi.y : -1; + int aoiW = aoi != null ? aoi.width : -1; + int aoiH = aoi != null ? aoi.height : -1; + + Boolean b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_PERCENT); + boolean aoiPercent = b != null && b; // default: false + + b = (Boolean) mOriginalRequest.getAttribute(ATTRIB_AOI_UNIFORM); + boolean aoiUniform = b != null && b; // default: false + + if (aoiX >= 0 || aoiY >= 0 || aoiW >= 0 || aoiH >= 0) { + aoi = getAOI(pDefaultWidth, pDefaultHeight, aoiX, aoiY, aoiW, aoiH, aoiPercent, aoiUniform); + return aoi; + } + + return null; + } + + // TODO: Move these to ImageUtil or similar, as they are often used... + // TODO: Consider separate methods for percent and pixels + /** + * 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 pOriginalWidth the original width of the image + * @param pOriginalHeight the original height of the image + * @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 pPercent 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 static Dimension getSize(int pOriginalWidth, int pOriginalHeight, + int pWidth, int pHeight, + boolean pPercent, 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 + + float ratio; + + if (pPercent) { + if (pWidth >= 0 && pHeight >= 0) { + // Non-uniform + pWidth = Math.round((float) pOriginalWidth * (float) pWidth / 100f); + pHeight = Math.round((float) pOriginalHeight * (float) pHeight / 100f); + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / 100f; + pWidth = Math.round((float) pOriginalWidth * ratio); + pHeight = Math.round((float) pOriginalHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / 100f; + pWidth = Math.round((float) pOriginalWidth * ratio); + pHeight = Math.round((float) pOriginalHeight * ratio); + } + // Else: No scale + } + else { + if (pUniformScale) { + if (pWidth >= 0 && pHeight >= 0) { + // Compute both ratios + ratio = (float) pWidth / (float) pOriginalWidth; + float heightRatio = (float) pHeight / (float) pOriginalHeight; + + // Find the largest ratio, and use that for both + if (heightRatio < ratio) { + ratio = heightRatio; + pWidth = Math.round((float) pOriginalWidth * ratio); + } + else { + pHeight = Math.round((float) pOriginalHeight * ratio); + } + + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / (float) pOriginalWidth; + pHeight = Math.round((float) pOriginalHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / (float) pOriginalHeight; + pWidth = Math.round((float) pOriginalWidth * ratio); + } + // Else: No scale + } + } + + // Default is no scale, just work as a proxy + if (pWidth < 0) { + pWidth = pOriginalWidth; + } + if (pHeight < 0) { + pHeight = pOriginalHeight; + } + + // Create new Dimension object and return + return new Dimension(pWidth, pHeight); + } + + protected static Rectangle getAOI(int pOriginalWidth, int pOriginalHeight, + int pX, int pY, int pWidth, int pHeight, + boolean pPercent, boolean pUniform) { + // 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 + // + + float ratio; + + if (pPercent) { + if (pWidth >= 0 && pHeight >= 0) { + // Non-uniform + pWidth = Math.round((float) pOriginalWidth * (float) pWidth / 100f); + pHeight = Math.round((float) pOriginalHeight * (float) pHeight / 100f); + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / 100f; + pWidth = Math.round((float) pOriginalWidth * ratio); + pHeight = Math.round((float) pOriginalHeight * ratio); + + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / 100f; + pWidth = Math.round((float) pOriginalWidth * ratio); + pHeight = Math.round((float) pOriginalHeight * ratio); + } + // Else: No crop + } + else { + // Uniform + if (pUniform) { + if (pWidth >= 0 && pHeight >= 0) { + // Compute both ratios + ratio = (float) pWidth / (float) pHeight; + float originalRatio = (float) pOriginalWidth / (float) pOriginalHeight; + if (ratio > originalRatio) { + pWidth = pOriginalWidth; + pHeight = Math.round((float) pOriginalWidth / ratio); + } + else { + pHeight = pOriginalHeight; + pWidth = Math.round((float) pOriginalHeight * ratio); + } + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / (float) pOriginalWidth; + pHeight = Math.round((float) pOriginalHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / (float) pOriginalHeight; + pWidth = Math.round((float) pOriginalWidth * ratio); + } + // Else: No crop + } + } + + // Not specified, or outside bounds: Use original dimensions + if (pWidth < 0 || (pX < 0 && pWidth > pOriginalWidth) + || (pX >= 0 && (pX + pWidth) > pOriginalWidth)) { + pWidth = (pX >= 0 ? pOriginalWidth - pX : pOriginalWidth); + } + if (pHeight < 0 || (pY < 0 && pHeight > pOriginalHeight) + || (pY >= 0 && (pY + pHeight) > pOriginalHeight)) { + pHeight = (pY >= 0 ? pOriginalHeight - pY : pOriginalHeight); + } + + // Center + if (pX < 0) { + pX = (pOriginalWidth - pWidth) / 2; + } + if (pY < 0) { + pY = (pOriginalHeight - pHeight) / 2; + } + +// System.out.println("x: " + pX + " y: " + pY +// + " w: " + pWidth + " h " + pHeight); + + return new Rectangle(pX, pY, pWidth, pHeight); + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java new file mode 100755 index 00000000..aa6e570d --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java @@ -0,0 +1,46 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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; + +/** + * An {@code ImageFilter} that does nothing. Useful for debugging purposes. + * + * @author $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java#2 $ + * + */ +public final class NullImageFilter extends ImageFilter { + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + return pImage; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java new file mode 100755 index 00000000..49cfc89b --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java @@ -0,0 +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 "TwelveMonkeys" 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 OWNER 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.MathUtil; +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. + * + *


+ * + * Parameters:
+ *

+ *
{@code cropX}
+ *
integer, the new left edge of the image. + *
{@code cropY}
+ *
integer, the new top of the image. + *
{@code cropWidth}
+ *
integer, the new width of the image. + *
{@code cropHeight}
+ *
integer, the new height of the image. + * + * + * + *
+ * + * @example + * JPEG: + * <IMG src="/scale/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=500&uniform=true"> + * + * PNG: + * <IMG src="/scale/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=50&units=PERCENT"> + * + * @todo Correct rounding errors, resulting in black borders when rotating 90 + * degrees, and one of width or height is odd length... + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/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 = MathUtil.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(); + } + } +} + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java new file mode 100755 index 00000000..1d7a11fa --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java @@ -0,0 +1,322 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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. + *

+ *


+ *

+ * Parameters:
+ *

+ *
{@code scaleX}
+ *
integer, the new width of the image. + *
{@code scaleY}
+ *
integer, the new height of the image. + *
{@code scaleUniform}
+ *
boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + *
{@code scaleUnits}
+ *
string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + *
{@code scaleQuality}
+ *
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). + *
+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java#1 $ + * + * @example <IMG src="/scale/test.jpg?scaleX=500&scaleUniform=false"> + * @example <IMG src="/scale/test.png?scaleY=50&scaleUnits=PERCENT"> + */ +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. + */ + 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 mDefaultScaleQuality = 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 mDefaultScaleQuality; + } + + public void setDefaultScaleQuality(String pDefaultScaleQuality) { + mDefaultScaleQuality = 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); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java new file mode 100755 index 00000000..37873803 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java @@ -0,0 +1,154 @@ +package com.twelvemonkeys.servlet.image; + +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import java.awt.image.RenderedImage; +import java.awt.image.BufferedImage; +import java.awt.*; +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 Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java#1 $ + */ +public class SourceRenderFilter extends ImageFilter { + private String mSizeWidthParam = "size.w"; + private String mSizeHeightParam = "size.h"; + private String mSizePercentParam = "size.percent"; + private String mSizeUniformParam = "size.uniform"; + + private String mRegionWidthParam = "aoi.w"; + private String mRegionHeightParam = "aoi.h"; + private String mRegionLeftParam = "aoi.x"; + private String mRegionTopParam = "aoi.y"; + private String mRegionPercentParam = "aoi.percent"; + private String mRegionUniformParam = "aoi.uniform"; + + public void setRegionHeightParam(String pRegionHeightParam) { + mRegionHeightParam = pRegionHeightParam; + } + + public void setRegionWidthParam(String pRegionWidthParam) { + mRegionWidthParam = pRegionWidthParam; + } + + public void setRegionLeftParam(String pRegionLeftParam) { + mRegionLeftParam = pRegionLeftParam; + } + + public void setRegionTopParam(String pRegionTopParam) { + mRegionTopParam = pRegionTopParam; + } + + public void setSizeHeightParam(String pSizeHeightParam) { + mSizeHeightParam = pSizeHeightParam; + } + + public void setSizeWidthParam(String pSizeWidthParam) { + mSizeWidthParam = pSizeWidthParam; + } + + public void setRegionPercentParam(String pRegionPercentParam) { + mRegionPercentParam = pRegionPercentParam; + } + + public void setRegionUniformParam(String pRegionUniformParam) { + mRegionUniformParam = pRegionUniformParam; + } + + public void setSizePercentParam(String pSizePercentParam) { + mSizePercentParam = pSizePercentParam; + } + + public void setSizeUniformParam(String pSizeUniformParam) { + mSizeUniformParam = pSizeUniformParam; + } + + public void init() throws ServletException { + if (mTriggerParams == null) { + // Add all params as triggers + mTriggerParams = new String[]{mSizeWidthParam, mSizeHeightParam, + mSizeUniformParam, mSizePercentParam, + mRegionLeftParam, mRegionTopParam, + mRegionWidthParam, mRegionHeightParam, + mRegionUniformParam, mRegionPercentParam}; + } + } + + /** + * 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, mSizeWidthParam, -1); + int height = ServletUtil.getIntParameter(pRequest, mSizeHeightParam, -1); + if (width > 0 || height > 0) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE, new Dimension(width, height)); + } + + // Size uniform/percent + boolean uniform = ServletUtil.getBooleanParameter(pRequest, mSizeUniformParam, true); + if (!uniform) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.FALSE); + } + boolean percent = ServletUtil.getBooleanParameter(pRequest, mSizePercentParam, false); + if (percent) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE); + } + + // Area of interest parameters + int x = ServletUtil.getIntParameter(pRequest, mRegionLeftParam, -1); // Default is center + int y = ServletUtil.getIntParameter(pRequest, mRegionTopParam, -1); // Default is center + width = ServletUtil.getIntParameter(pRequest, mRegionWidthParam, -1); + height = ServletUtil.getIntParameter(pRequest, mRegionHeightParam, -1); + if (width > 0 || height > 0) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_AOI, new Rectangle(x, y, width, height)); + } + + // AOI uniform/percent + uniform = ServletUtil.getBooleanParameter(pRequest, mRegionUniformParam, false); + if (uniform) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE); + } + percent = ServletUtil.getBooleanParameter(pRequest, mRegionPercentParam, 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; + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java new file mode 100755 index 00000000..b4380bf3 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java @@ -0,0 +1,348 @@ +/* + * 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 "TwelveMonkeys" 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 OWNER 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.lang.MathUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import java.awt.*; +import java.awt.geom.Rectangle2D; + +/** + * This servlet is capable of rendereing a text string and output it as an + * image. The text can be rendered in any given font, size, + * style or color, into an image, and output it as a GIF, JPEG or PNG image, + * with optional caching of the rendered image files. + * + *


+ * + * Parameters:
+ *

+ *
{@code text}
+ *
string, the text string to render. + *
{@code width}
+ *
integer, the width of the image + *
{@code height}
+ *
integer, the height of the image + *
{@code fontFamily}
+ *
string, the name of the font family. + * Default is {@code "Helvetica"}. + *
{@code fontSize}
+ *
integer, the size of the font. Default is {@code 12}. + *
{@code fontStyle}
+ *
string, the tyle of the font. Can be one of the constants + * {@code plain} (default), {@code bold}, {@code italic} or + * {@code bolditalic}. Any other will result in {@code plain}. + *
{@code fgcolor}
+ *
color (HTML form, {@code #RRGGBB}), or color constant from + * {@link java.awt.Color}, default is {@code "black"}. + *
{@code bgcolor}
+ *
color (HTML form, {@code #RRGGBB}), or color constant from + * {@link java.awt.Color}, default is {@code "transparent"}. + * Note that the hash character ({@code "#"}) used in colors must be + * escaped as {@code %23} in the query string. See + * {@link StringUtil#toColor(String)}, examples. + * + * + * + *
{@code cache}
+ *
boolean, {@code true} if you want to cache the result + * to disk (default). + * + *
{@code compression}
+ *
float, the optional compression ratio for the output image. For JPEG + * images, the quality is the inverse of the compression ratio. See + * {@link #JPEG_DEFAULT_COMPRESSION_LEVEL}, + * {@link #PNG_DEFAULT_COMPRESSION_LEVEL}. + *
Applies to JPEG and PNG images only. + * + *
{@code dither}
+ *
enumerated, one of {@code NONE}, {@code DEFAULT} or + * {@code FS}, if you want to dither the result ({@code DEFAULT} is + * default). + * {@code FS} will produce the best results, but it's slower. + *
Use in conjuction with {@code indexed}, {@code palette} + * and {@code websafe}. + *
Applies to GIF and PNG images only. + * + *
{@code fileName}
+ *
string, an optional filename. If not set, the path after the servlet + * ({@link HttpServletRequest#getPathInfo}) will be used for the cache + * filename. See {@link #getCacheFile(ServletRequest)}, + * {@link #getCacheRoot}. + * + *
{@code height}
+ *
integer, the height of the image. + * + *
{@code width}
+ *
integer, the width of the image. + * + *
{@code indexed}
+ *
integer, the number of colors in the resulting image, or -1 (default). + * If the value is set and positive, the image will use an + * {@code IndexColorModel} with + * the number of colors specified. Otherwise the image will be true color. + *
Applies to GIF and PNG images only. + * + *
{@code palette}
+ *
string, an optional filename. If set, the image will use IndexColorModel + * with a palette read from the given file. + *
Applies to GIF and PNG images only. + * + *
{@code websafe}
+ *
boolean, {@code true} if you want the result to use the 216 color + * websafe palette (default is false). + *
Applies to GIF and PNG images only. + *
+ * + * @example + * <IMG src="/text/test.gif?height=40&width=600 + * &fontFamily=TimesRoman&fontSize=30&fontStyle=italic&fgcolor=%23990033 + * &bgcolor=%23cccccc&text=the%20quick%20brown%20fox%20jumps%20over%20the + * %20lazy%20dog&cache=false" /> + * + * @example + * <IMG src="/text/test.jpg?height=40&width=600 + * &fontFamily=TimesRoman&fontSize=30&fontStyle=italic&fgcolor=black + * &bgcolor=%23cccccc&text=the%20quick%20brown%20fox%20jumps%20over%20the + * %20lazy%20dog&compression=3&cache=false" /> + * + * @example + * <IMG src="/text/test.png?height=40&width=600 + * &fontFamily=TimesRoman&fontSize=30&fontStyle=italic&fgcolor=%23336699 + * &bgcolor=%23cccccc&text=the%20quick%20brown%20fox%20jumps%20over%20the + * %20lazy%20dog&cache=true" /> + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java#2 $ + */ + +class TextRenderer /*extends ImageServlet implements ImagePainterServlet*/ { + // TODO: Create something useable out of this piece of old junk.. ;-) + // It just needs a graphics object to write onto + // Alternatively, defer, and compute the size needed + // Or, make it a filter... + + /** {@code "italic"} */ + public final static String FONT_STYLE_ITALIC = "italic"; + /** {@code "plain"} */ + public final static String FONT_STYLE_PLAIN = "plain"; + /** {@code "bold"} */ + public final static String FONT_STYLE_BOLD = "bold"; + + /** {@code text} */ + public final static String PARAM_TEXT = "text"; + /** {@code marginLeft} */ + public final static String PARAM_MARGIN_LEFT = "marginLeft"; + /** {@code marginTop} */ + public final static String PARAM_MARGIN_TOP = "marginTop"; + /** {@code fontFamily} */ + public final static String PARAM_FONT_FAMILY = "fontFamily"; + /** {@code fontSize} */ + public final static String PARAM_FONT_SIZE = "fontSize"; + /** {@code fontStyle} */ + public final static String PARAM_FONT_STYLE = "fontStyle"; + /** {@code textRotation} */ + public final static String PARAM_TEXT_ROTATION = "textRotation"; + /** {@code textRotation} */ + public final static String PARAM_TEXT_ROTATION_UNITS = "textRotationUnits"; + + /** {@code bgcolor} */ + public final static String PARAM_BGCOLOR = "bgcolor"; + /** {@code fgcolor} */ + public final static String PARAM_FGCOLOR = "fgcolor"; + + protected final static String ROTATION_DEGREES = "DEGREES"; + protected final static String ROTATION_RADIANS = "RADIANS"; + + /** + * Creates the TextRender servlet. + */ + + public TextRenderer() { + } + + /** + * Renders the text string for this servlet request. + */ + private void paint(ServletRequest pReq, Graphics2D pRes, + int pWidth, int pHeight) + throws ImageServletException { + + // Get parameters + String text = pReq.getParameter(PARAM_TEXT); + String[] lines = StringUtil.toStringArray(text, "\n\r"); + + String fontFamily = pReq.getParameter(PARAM_FONT_FAMILY); + String fontSize = pReq.getParameter(PARAM_FONT_SIZE); + String fontStyle = pReq.getParameter(PARAM_FONT_STYLE); + + String bgcolor = pReq.getParameter(PARAM_BGCOLOR); + String fgcolor = pReq.getParameter(PARAM_FGCOLOR); + + // TODO: Make them static.. + pRes.addRenderingHints(new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)); + pRes.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); + pRes.addRenderingHints(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY)); + // pRes.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); + + //System.out.println(pRes.getBackground()); + + // Clear area with bgcolor + if (!StringUtil.isEmpty(bgcolor)) { + pRes.setBackground(StringUtil.toColor(bgcolor)); + pRes.clearRect(0, 0, pWidth, pHeight); + + //System.out.println(pRes.getBackground()); + } + + // Create and set font + Font font = new Font((fontFamily != null ? fontFamily : "Helvetica"), + getFontStyle(fontStyle), + (fontSize != null ? Integer.parseInt(fontSize) + : 12)); + pRes.setFont(font); + + // Set rotation + double angle = getAngle(pReq); + pRes.rotate(angle, pWidth / 2.0, pHeight / 2.0); + + // Draw string in fgcolor + pRes.setColor(fgcolor != null ? StringUtil.toColor(fgcolor) + : Color.black); + + float x = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_LEFT, + Float.MIN_VALUE); + Rectangle2D[] bounds = new Rectangle2D[lines.length]; + if (x <= Float.MIN_VALUE) { + // Center + float longest = 0f; + for (int i = 0; i < lines.length; i++) { + bounds[i] = font.getStringBounds(lines[i], + pRes.getFontRenderContext()); + if (bounds[i].getWidth() > longest) { + longest = (float) bounds[i].getWidth(); + } + } + + //x = (float) ((pWidth - bounds.getWidth()) / 2f); + x = (float) ((pWidth - longest) / 2f); + + //System.out.println("marginLeft: " + x); + } + //else { + //System.out.println("marginLeft (from param): " + x); + //} + + float y = ServletUtil.getFloatParameter(pReq, PARAM_MARGIN_TOP, + Float.MIN_VALUE); + float lineHeight = (float) (bounds[0] != null ? bounds[0].getHeight() : + font.getStringBounds(lines[0], + pRes.getFontRenderContext()).getHeight()); + + if (y <= Float.MIN_VALUE) { + // Center + y = (float) ((pHeight - lineHeight) / 2f) + - (lineHeight * (lines.length - 2.5f) / 2f); + + //System.out.println("marginTop: " + y); + } + else { + // Todo: Correct for font height? + y += font.getSize2D(); + //System.out.println("marginTop (from param):" + y); + + } + + //System.out.println("Font size: " + font.getSize2D()); + //System.out.println("Line height: " + lineHeight); + + // Draw + for (int i = 0; i < lines.length; i++) { + pRes.drawString(lines[i], x, y + lineHeight * i); + } + } + + /** + * Returns the font style constant. + * + * @param pStyle a string containing either the word {@code "plain"} or one + * or more of {@code "bold"} and {@code italic}. + * @return the font style constant as defined in {@link Font}. + * + * @see Font#PLAIN + * @see Font#BOLD + * @see Font#ITALIC + */ + private int getFontStyle(String pStyle) { + if (pStyle == null + || StringUtil.containsIgnoreCase(pStyle, FONT_STYLE_PLAIN)) { + return Font.PLAIN; + } + + // Try to find bold/italic + int style = Font.PLAIN; + if (StringUtil.containsIgnoreCase(pStyle, FONT_STYLE_BOLD)) { + style |= Font.BOLD; + } + if (StringUtil.containsIgnoreCase(pStyle, FONT_STYLE_ITALIC)) { + style |= Font.ITALIC; + } + + return style; + } + + /** + * Gets the angle of rotation from the request. + * + * @param pRequest the servlet request to get parameters from + * @return the angle in radians. + */ + private double getAngle(ServletRequest pRequest) { + // Get angle + double angle = + ServletUtil.getDoubleParameter(pRequest, PARAM_TEXT_ROTATION, 0.0); + + // Convert to radians, if needed + String units = pRequest.getParameter(PARAM_TEXT_ROTATION_UNITS); + if (!StringUtil.isEmpty(units) + && ROTATION_DEGREES.equalsIgnoreCase(units)) { + angle = MathUtil.toRadians(angle); + } + + return angle; + } + +} + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/package.html new file mode 100755 index 00000000..2e0cc142 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/package.html @@ -0,0 +1,38 @@ + + + +Contains various image-outputting servlets, that should run under any servlet engine. To create your own image servlet, simply subclass the servlet +ImageServlet. Optionally implement the interface +ImagePainterServlet, if you want to do painting. +

+Some of these methods may require use of the native graphics libraries +supported by the JVM, like the X libraries on Unix systems, and should be +run with JRE 1.4 or later, and with the option: +

+
-Djawa.awt.headless=true
+
+See the document +AWT Enhancements and bugtraq report +4281163 for more information on this issue. +

+If you cannot use JRE 1.4 for any reason, or do not want to use the X +libraries, a possibilty is to use the +PJA package (com.eteks.pja), +and start the JVM with the following options: +

+
-Xbootclasspath/a:<path to pja.jar>
+
-Dawt.toolkit=com.eteks.awt.PJAToolkit
+
-Djava.awt.graphicsenv=com.eteks.java2d.PJAGraphicsEnvironment
+
-Djava.awt.fonts=<path where True Type fonts files will be loaded from>
+
+

+Please note that creation of PNG images (from bytes or URL's) are only +supported in JRE 1.3 and later, trying to load them from an earlier version, +will result in errors. + +@see com.twelvemonkeys.servlet.image.ImageServlet +@see com.twelvemonkeys.servlet.image.ImagePainterServlet + + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java new file mode 100755 index 00000000..9a3d2c1a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: Droplet.java,v $ + * Revision 1.3 2003/10/06 14:25:19 WMHAKUR + * Code clean-up only. + * + * Revision 1.2 2002/10/18 14:12:16 WMHAKUR + * Now, it even compiles. :-/ + * + * Revision 1.1 2002/10/18 14:02:16 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet; + +import java.io.*; + +import javax.servlet.*; +import javax.servlet.http.*; +import javax.servlet.jsp.*; + +import com.twelvemonkeys.servlet.jsp.droplet.taglib.*; + +/** + * Dynamo Droplet like Servlet. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + * + */ +public abstract class Droplet extends HttpServlet implements JspFragment { + + // Copy doc + public abstract void service(PageContext pPageContext) + throws ServletException, IOException; + + /** + * Services a parameter. Programatically equivalent to the + * JSP tag. + */ + public void serviceParameter(String pParameter, PageContext pPageContext) + throws ServletException, IOException { + Object param = pPageContext.getRequest().getAttribute(pParameter); + + if (param != null) { + if (param instanceof Param) { + ((Param) param).service(pPageContext); + } + else { + pPageContext.getOut().print(param); + } + } + else { + // Try to get value from parameters + Object obj = pPageContext.getRequest().getParameter(pParameter); + + // Print parameter or default value + pPageContext.getOut().print((obj != null) ? obj : ""); + } + } + + /** + * "There's no need to override this method." :-) + */ + final public void service(HttpServletRequest pRequest, + HttpServletResponse pResponse) + throws ServletException, IOException { + PageContext pageContext = + (PageContext) pRequest.getAttribute(IncludeTag.PAGE_CONTEXT); + + // TODO: What if pageContext == null + service(pageContext); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java new file mode 100755 index 00000000..8d6e2c84 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: JspFragment.java,v $ + * Revision 1.2 2003/10/06 14:25:36 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/10/18 14:02:16 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet; + +import java.io.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; + +/** + * Interface for JSP sub pages or page fragments to implement. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + */ +public interface JspFragment { + + /** + * Services a sub page or a page fragment inside another page + * (or PageContext). + * + * @param pContext the PageContext that is used to render the subpage. + * + * @throws ServletException if an exception occurs that interferes with the + * subpage's normal operation + * @throws IOException if an input or output exception occurs + */ + public void service(PageContext pContext) + throws ServletException, IOException; +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java new file mode 100755 index 00000000..46e4a8f0 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java @@ -0,0 +1,29 @@ + +package com.twelvemonkeys.servlet.jsp.droplet; + +import java.io.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; + +/** + * Oparam (Open parameter) + */ +public class Oparam extends Param implements JspFragment { + /** + * Creates an Oparam. + * + * @param pValue the value of the parameter + */ + public Oparam(String pValue) { + super(pValue); + } + + public void service(PageContext pContext) + throws ServletException, IOException { + pContext.getServletContext().log("Service subpage " + pContext.getServletContext().getRealPath(mValue)); + + pContext.include(mValue); + } +} + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java new file mode 100755 index 00000000..3e86ce75 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java @@ -0,0 +1,42 @@ + +package com.twelvemonkeys.servlet.jsp.droplet; + +import java.io.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; + +/** + * Param + */ +public class Param implements JspFragment { + + /** The value member field. */ + protected String mValue = null; + + /** + * Creates a Param. + * + * @param pValue the value of the parameter + */ + public Param(String pValue) { + mValue = pValue; + } + + /** + * Gets the value of the parameter. + */ + public String getValue() { + return mValue; + } + + /** + * Services the page fragment. This version simply prints the value of + * this parameter to teh PageContext's out. + */ + public void service(PageContext pContext) + throws ServletException, IOException { + JspWriter writer = pContext.getOut(); + writer.print(mValue); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package.html new file mode 100755 index 00000000..ae726db4 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package.html @@ -0,0 +1,14 @@ + + + +Dynamo Droplet-like functionality for JSP. + +This package is early beta, not for commercial use! :-) +Read: The interfaces and classes in this package (and subpackages) will be +developed and modified for a while. + +TODO: Insert taglib-descriptor here? + + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java new file mode 100755 index 00000000..b109afb0 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: IncludeTag.java,v $ + * Revision 1.2 2003/10/06 14:25:36 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/10/18 14:03:09 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import com.twelvemonkeys.servlet.jsp.taglib.ExTagSupport; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Include tag tag that emulates ATG Dynamo Droplet tag JHTML behaviour for + * JSP. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + * + */ +public class IncludeTag extends ExTagSupport { + /** + * This will contain the names of all the parameters that have been + * added to the PageContext.REQUEST_SCOPE scope by this tag. + */ + private ArrayList mParameterNames = null; + + /** + * If any of the parameters we insert for this tag already exist, then + * we back up the older parameter in this {@code HashMap} and + * restore them when the tag is finished. + */ + private HashMap mOldParameters = null; + + /** + * This is the URL for the JSP page that the parameters contained in this + * tag are to be inserted into. + */ + private String mPage; + + /** + * The name of the PageContext attribute + */ + public final static String PAGE_CONTEXT = "com.twelvemonkeys.servlet.jsp.PageContext"; + + /** + * Sets the value for the JSP page to insert the parameters into. This + * will be set by the tag attribute within the original JSP page. + * + * @param pPage The URL for the JSP page to insert parameters into. + */ + public void setPage(String pPage) { + mPage = pPage; + } + + /** + * Adds a parameter to the {@code PageContext.REQUEST_SCOPE} scope. + * If a parameter with the same name as {@code pName} already exists, + * then the old parameter is first placed in the {@code OldParameters} + * member variable. When this tag is finished, the old value will be + * restored. + * + * @param pName The name of the new parameter to be stored in the + * {@code PageContext.REQUEST_SCOPE} scope. + * @param pValue The value for the parmeter to be stored in the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + public void addParameter(String pName, Object pValue) { + // Check that we haven't already saved this parameter + if (!mParameterNames.contains(pName)) { + mParameterNames.add(pName); + + // Now check if this parameter already exists in the page. + Object obj = getRequest().getAttribute(pName); + if (obj != null) { + mOldParameters.put(pName, obj); + } + } + + // Finally, insert the parameter in the request scope. + getRequest().setAttribute(pName, pValue); + } + + /** + * This is the method called when the JSP interpreter first hits the tag + * associated with this class. This method will firstly determine whether + * the page referenced by the {@code page} attribute exists. If the + * page doesn't exist, this method will throw a {@code JspException}. + * If the page does exist, this method will hand control over to that JSP + * page. + * + * @exception JspException + */ + public int doStartTag() throws JspException { + mOldParameters = new HashMap(); + mParameterNames = new ArrayList(); + + return EVAL_BODY_INCLUDE; + } + + /** + * This method is called when the JSP page compiler hits the end tag. By + * now all the data should have been passed and parameters entered into + * the {@code PageContext.REQUEST_SCOPE} scope. This method includes + * the JSP page whose URL is stored in the {@code mPage} member + * variable. + * + * @exception JspException + */ + public int doEndTag() throws JspException { + String msg; + + try { + Iterator iterator; + String parameterName; + + // -- Harald K 20020726 + // Include the page, in place + //getDispatcher().include(getRequest(), getResponse()); + addParameter(PAGE_CONTEXT, pageContext); // Will be cleared later + pageContext.include(mPage); + + // Remove all the parameters that were added to the request scope + // for this insert tag. + iterator = mParameterNames.iterator(); + + while (iterator.hasNext()) { + parameterName = (String) iterator.next(); + + getRequest().removeAttribute(parameterName); + } + + iterator = mOldParameters.keySet().iterator(); + + // Restore the parameters we temporarily replaced (if any). + while (iterator.hasNext()) { + parameterName = (String) iterator.next(); + + getRequest().setAttribute(parameterName, mOldParameters.get(parameterName)); + } + + return super.doEndTag(); + } + catch (IOException ioe) { + msg = "Caught an IOException while including " + mPage + + "\n" + ioe.toString(); + log(msg, ioe); + throw new JspException(msg); + } + catch (ServletException se) { + msg = "Caught a ServletException while including " + mPage + + "\n" + se.toString(); + log(msg, se); + throw new JspException(msg); + } + } + + /** + * Free up the member variables that we've used throughout this tag. + */ + protected void clearServiceState() { + mOldParameters = null; + mParameterNames = null; + } + + /** + * Returns the request dispatcher for the JSP page whose URL is stored in + * the {@code mPage} member variable. + * + * @return The RequestDispatcher for the JSP page whose URL is stored in + * the {@code mPage} member variable. + */ + /* + private RequestDispatcher getDispatcher() { + return getRequest().getRequestDispatcher(mPage); + } + */ + + /** + * Returns the HttpServletRequest object for the current user request. + * + * @return The HttpServletRequest object for the current user request. + */ + private HttpServletRequest getRequest() { + return (HttpServletRequest) pageContext.getRequest(); + } + + /** + * Returns the HttpServletResponse object for the current user request. + * + * @return The HttpServletResponse object for the current user request. + */ + private HttpServletResponse getResponse() { + return (HttpServletResponse) pageContext.getResponse(); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java new file mode 100755 index 00000000..ce1dff21 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: NestingHandler.java,v $ + * Revision 1.4 2003/10/06 14:25:44 WMHAKUR + * Code clean-up only. + * + * Revision 1.3 2003/08/04 15:26:30 WMHAKUR + * Code clean-up. + * + * Revision 1.2 2002/10/18 14:28:07 WMHAKUR + * Fixed package error. + * + * Revision 1.1 2002/10/18 14:03:09 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import com.twelvemonkeys.lang.StringUtil; + +import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; + +/** + * A SAX handler that returns an exception if the nesting of + * {@code param}, {@code oparam}, {@code droplet} and + * {@code valueof} is not correct. + * + * Based on the NestingHandler.java, + * taken from More Servlets and JavaServer Pages + * from Prentice Hall and Sun Microsystems Press, + * http://www.moreservlets.com/. + * © 2002 Marty Hall; may be freely used or adapted. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + */ + +public class NestingHandler extends DefaultHandler { + private String mIncludeTagName = "include"; + private String mParamTagName = "param"; + private String mOpenParamTagName = "oparam"; + + //private Stack mParents = new Stack(); + + private boolean mInIncludeTag = false; + + private String mNamespacePrefix = null; + private String mNamespaceURI = null; + + private NestingValidator mValidator = null; + + public NestingHandler(String pNamespacePrefix, String pNameSpaceURI, + NestingValidator pValidator) { + mNamespacePrefix = pNamespacePrefix; + mNamespaceURI = pNameSpaceURI; + + mValidator = pValidator; + } + + public void startElement(String pNamespaceURI, String pLocalName, + String pQualifiedName, Attributes pAttributes) + throws SAXException { + String namespacePrefix = !StringUtil.isEmpty(pNamespaceURI) + ? getNSPrefixFromURI(pNamespaceURI) + : getNamespacePrefix(pQualifiedName); + + String localName = !StringUtil.isEmpty(pLocalName) + ? pLocalName : getLocalName(pQualifiedName); + /* + if (namespacePrefix.equals(mNamespacePrefix)) { + System.out.println("startElement:\nnamespaceURI=" + pNamespaceURI + + " namespacePrefix=" + namespacePrefix + + " localName=" + localName + + " qName=" + pQualifiedName + + " attributes=" + pAttributes); + } + */ + if (localName.equals(mIncludeTagName)) { + // include + //System.out.println("<" + mNamespacePrefix + ":" + // + mIncludeTagName + ">"); + if (mInIncludeTag) { + mValidator.reportError("Cannot nest " + namespacePrefix + ":" + + mIncludeTagName); + } + mInIncludeTag = true; + } + else if (localName.equals(mParamTagName)) { + // param + //System.out.println("<" + mNamespacePrefix + ":" + // + mParamTagName + "/>"); + if (!mInIncludeTag) { + mValidator.reportError(mNamespacePrefix + ":" + + mParamTagName + + " can only appear within " + + mNamespacePrefix + ":" + + mIncludeTagName); + } + } + else if (localName.equals(mOpenParamTagName)) { + // oparam + //System.out.println("<" + mNamespacePrefix + ":" + // + mOpenParamTagName + ">"); + if (!mInIncludeTag) { + mValidator.reportError(mNamespacePrefix + ":" + + mOpenParamTagName + + " can only appear within " + + mNamespacePrefix + ":" + + mIncludeTagName); + } + mInIncludeTag = false; + } + else { + // Only jsp:text allowed inside include! + if (mInIncludeTag && !localName.equals("text")) { + mValidator.reportError(namespacePrefix + ":" + localName + + " can not appear within " + + mNamespacePrefix + ":" + + mIncludeTagName); + } + } + } + + public void endElement(String pNamespaceURI, + String pLocalName, + String pQualifiedName) + throws SAXException { + String namespacePrefix = !StringUtil.isEmpty(pNamespaceURI) + ? getNSPrefixFromURI(pNamespaceURI) + : getNamespacePrefix(pQualifiedName); + + String localName = !StringUtil.isEmpty(pLocalName) + ? pLocalName : getLocalName(pQualifiedName); + /* + if (namespacePrefix.equals(mNamespacePrefix)) { + System.out.println("endElement:\nnamespaceURI=" + pNamespaceURI + + " namespacePrefix=" + namespacePrefix + + " localName=" + localName + + " qName=" + pQualifiedName); + } + */ + if (namespacePrefix.equals(mNamespacePrefix) + && localName.equals(mIncludeTagName)) { + + //System.out.println(""); + + mInIncludeTag = false; + } + else if (namespacePrefix.equals(mNamespacePrefix) + && localName.equals(mOpenParamTagName)) { + + //System.out.println(""); + + mInIncludeTag = true; // assuming no errors before this... + } + } + + /** + * Stupid broken namespace-support "fix".. + */ + + private String getNSPrefixFromURI(String pNamespaceURI) { + return (pNamespaceURI.equals(mNamespaceURI) + ? mNamespacePrefix : ""); + } + + private String getNamespacePrefix(String pQualifiedName) { + return pQualifiedName.substring(0, pQualifiedName.indexOf(':')); + } + + private String getLocalName(String pQualifiedName) { + return pQualifiedName.substring(pQualifiedName.indexOf(':') + 1); + } +} + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java new file mode 100755 index 00000000..1d05a09c --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: NestingValidator.java,v $ + * Revision 1.4 2003/08/04 15:26:40 WMHAKUR + * Code clean-up. + * + * Revision 1.3 2002/11/18 14:12:43 WMHAKUR + * *** empty log message *** + * + * Revision 1.2 2002/10/18 14:28:07 WMHAKUR + * Fixed package error. + * + * Revision 1.1 2002/10/18 14:03:09 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + + +import java.util.*; + +import javax.servlet.jsp.tagext.*; +import javax.xml.parsers.*; + +import org.xml.sax.*; +import org.xml.sax.helpers.*; + +import com.twelvemonkeys.util.*; + +/** + * A validator that verifies that tags follow + * proper nesting order. + *

+ * Based on NestingValidator.java, + * taken from More Servlets and JavaServer Pages + * from Prentice Hall and Sun Microsystems Press, + * http://www.moreservlets.com/. + * © 2002 Marty Hall; may be freely used or adapted. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + * + */ + +public class NestingValidator extends TagLibraryValidator { + + private Vector errors = new Vector(); + + /** + * + */ + + public ValidationMessage[] validate(String pPrefix, + String pURI, + PageData pPage) { + + //System.out.println("Validating " + pPrefix + " (" + pURI + ") for " + // + pPage + "."); + + // Pass the parser factory in on the command line with + // -D to override the use of the Apache parser. + + DefaultHandler handler = new NestingHandler(pPrefix, pURI, this); + SAXParserFactory factory = SAXParserFactory.newInstance(); + + try { + // FileUtil.copy(pPage.getInputStream(), System.out); + + SAXParser parser = factory.newSAXParser(); + InputSource source = + new InputSource(pPage.getInputStream()); + + // Parse, handler will use callback to report errors + parser.parse(source, handler); + + + } + catch (Exception e) { + String errorMessage = e.getMessage(); + + reportError(errorMessage); + } + + // Return any errors and exceptions, empty array means okay + return (ValidationMessage[]) + errors.toArray(new ValidationMessage[errors.size()]); + } + + /** + * Callback method for the handler to report errors + */ + + public void reportError(String pMessage) { + // The first argument to the ValidationMessage + // constructor can be a tag ID. Since tag IDs + // are not universally supported, use null for + // portability. The important part is the second + // argument: the error message. + errors.add(new ValidationMessage(null, pMessage)); + } +} + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java new file mode 100755 index 00000000..9ef4c596 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: OparamTag.java,v $ + * Revision 1.4 2003/10/06 14:25:53 WMHAKUR + * Code clean-up only. + * + * Revision 1.3 2002/11/18 14:12:43 WMHAKUR + * *** empty log message *** + * + * Revision 1.2 2002/11/07 12:20:14 WMHAKUR + * Updated to reflect changes in com.twelvemonkeys.util.*Util + * + * Revision 1.1 2002/10/18 14:03:09 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.jsp.droplet.Oparam; +import com.twelvemonkeys.servlet.jsp.taglib.BodyReaderTag; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyTag; +import javax.servlet.jsp.tagext.Tag; +import java.io.File; +import java.io.IOException; + + +/** + * Open parameter tag that emulates ATG Dynamo JHTML behaviour for JSP. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java#1 $ + */ + +public class OparamTag extends BodyReaderTag { + + protected final static String COUNTER = "com.twelvemonkeys.servlet.jsp.taglib.OparamTag.counter"; + + + private File mSubpage = null; + + /** + * This is the name of the parameter to be inserted into the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + + private String mParameterName = null; + + private String mLanguage = null; + + private String mPrefix = null; + + /** + * This method allows the JSP page to set the name for the parameter by + * using the {@code name} tag attribute. + * + * @param pName The name for the parameter to insert into the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + + public void setName(String pName) { + mParameterName = pName; + } + + public void setLanguage(String pLanguage) { + //System.out.println("setLanguage:"+pLanguage); + mLanguage = pLanguage; + } + + public void setPrefix(String pPrefix) { + //System.out.println("setPrefix:"+pPrefix); + mPrefix = pPrefix; + } + + /** + * Ensure that the tag implemented by this class is enclosed by an {@code + * IncludeTag}. If the tag is not enclosed by an + * {@code IncludeTag} then a {@code JspException} is thrown. + * + * @return If this tag is enclosed within an {@code IncludeTag}, then + * the default return value from this method is the {@code + * TagSupport.EVAL_BODY_TAG} value. + * @exception JspException + */ + + public int doStartTag() throws JspException { + //checkEnclosedInIncludeTag(); // Moved to TagLibValidator + + // Get request + HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); + + // Get filename + mSubpage = createFileNameFromRequest(request); + + // Get include tag, and add to parameters + IncludeTag includeTag = (IncludeTag) getParent(); + includeTag.addParameter(mParameterName, new Oparam(mSubpage.getName())); + + // if ! subpage.exist || jsp newer than subpage, write new + File jsp = new File(pageContext.getServletContext() + .getRealPath(request.getServletPath())); + + if (!mSubpage.exists() || jsp.lastModified() > mSubpage.lastModified()) { + return BodyTag.EVAL_BODY_BUFFERED; + } + + // No need to evaluate body again! + return Tag.SKIP_BODY; + } + + /** + * This is the method responsible for actually testing that the tag + * implemented by this class is enclosed within an {@code IncludeTag}. + * + * @exception JspException + */ + /* + protected void checkEnclosedInIncludeTag() throws JspException { + Tag parentTag = getParent(); + + if ((parentTag != null) && (parentTag instanceof IncludeTag)) { + return; + } + + String msg = "A class that extends EnclosedIncludeBodyReaderTag " + + "is not enclosed within an IncludeTag."; + log(msg); + throw new JspException(msg); + } + */ + + /** + * This method cleans up the member variables for this tag in preparation + * for being used again. This method is called when the tag finishes it's + * current call with in the page but could be called upon again within this + * same page. This method is also called in the release stage of the tag + * life cycle just in case a JspException was thrown during the tag + * execution. + */ + + protected void clearServiceState() { + mParameterName = null; + } + + /** + * This is the method responsible for taking the result of the JSP code + * that forms the body of this tag and inserts it as a parameter into the + * request scope session. If any problems occur while loading the body + * into the session scope then a {@code JspException} will be thrown. + * + * @param pContent The body of the tag as a String. + * + * @exception JspException + */ + + protected void processBody(String pContent) throws JspException { + // Okay, we have the content, we need to write it to disk somewhere + String content = pContent; + + if (!StringUtil.isEmpty(mLanguage)) { + content = "<%@page language=\"" + mLanguage + "\" %>" + content; + } + + if (!StringUtil.isEmpty(mPrefix)) { + content = "<%@taglib uri=\"/twelvemonkeys-common\" prefix=\"" + mPrefix + "\" %>" + content; + } + + // Write the content of the oparam to disk + try { + log("Processing subpage " + mSubpage.getPath()); + FileUtil.write(mSubpage, content.getBytes()); + + } + catch (IOException ioe) { + throw new JspException(ioe); + } + } + + /** + * Creates a unique filename for each (nested) oparam + */ + private File createFileNameFromRequest(HttpServletRequest pRequest) { + //System.out.println("ServletPath" + pRequest.getServletPath()); + String path = pRequest.getServletPath(); + + // Find last '/' + int splitIndex = path.lastIndexOf("/"); + + // Split -> path + name + String name = path.substring(splitIndex + 1); + path = path.substring(0, splitIndex); + + // Replace special chars in name with '_' + name = name.replace('.', '_'); + String param = mParameterName.replace('.', '_'); + param = param.replace('/', '_'); + param = param.replace('\\', '_'); + param = param.replace(':', '_'); + + // tempfile = realPath(path) + name + "_oparam_" + number + ".jsp" + int count = getOparamCountFromRequest(pRequest); + + // Hmm.. Would be great, but seems like I can't serve pages from within the temp dir + //File temp = (File) getServletContext().getAttribute("javax.servlet.context.tempdir"); + //return new File(new File(temp, path), name + "_oparam_" + count + "_" + param + ".jsp"); + + return new File(new File(pageContext.getServletContext().getRealPath(path)), name + "_oparam_" + count + "_" + param + ".jsp"); + } + + /** + * Gets the current oparam count for this request + */ + private int getOparamCountFromRequest(HttpServletRequest pRequest) { + // Use request.attribute for incrementing oparam counter + Integer count = (Integer) pRequest.getAttribute(COUNTER); + if (count == null) + count = new Integer(0); + else + count = new Integer(count.intValue() + 1); + + // ... and set it back + pRequest.setAttribute(COUNTER, count); + + return count.intValue(); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java new file mode 100755 index 00000000..3f24a48a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ParamTag.java,v $ + * Revision 1.2 2003/10/06 14:26:00 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/10/18 14:03:09 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import java.io.IOException; + +import javax.servlet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +import com.twelvemonkeys.servlet.jsp.droplet.*; +import com.twelvemonkeys.servlet.jsp.taglib.*; + +/** + * Parameter tag that emulates ATG Dynamo JHTML behaviour for JSP. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + * + */ + +public class ParamTag extends ExTagSupport { + + /** + * This is the name of the parameter to be inserted into the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + + private String mParameterName; + + /** + * This is the value for the parameter to be inserted into the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + + private Object mParameterValue; + + /** + * This method allows the JSP page to set the name for the parameter by + * using the {@code name} tag attribute. + * + * @param pName The name for the parameter to insert into the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + + public void setName(String pName) { + mParameterName = pName; + } + + /** + * This method allows the JSP page to set the value for hte parameter by + * using the {@code value} tag attribute. + * + * @param pValue The value for the parameter to insert into the + * PageContext.REQUEST_SCOPE scope. + */ + + public void setValue(String pValue) { + mParameterValue = new Param(pValue); + } + + /** + * Ensure that the tag implemented by this class is enclosed by an {@code + * IncludeTag}. If the tag is not enclosed by an + * {@code IncludeTag} then a {@code JspException} is thrown. + * + * @return If this tag is enclosed within an {@code IncludeTag}, then + * the default return value from this method is the {@code + * TagSupport.SKIP_BODY} value. + * @exception JspException + */ + + public int doStartTag() throws JspException { + //checkEnclosedInIncludeTag(); + + addParameter(); + + return SKIP_BODY; + } + + /** + * This is the method responsible for actually testing that the tag + * implemented by this class is enclosed within an {@code IncludeTag}. + * + * @exception JspException + */ + /* + protected void checkEnclosedInIncludeTag() throws JspException { + Tag parentTag = getParent(); + + if ((parentTag != null) && (parentTag instanceof IncludeTag)) { + return; + } + + String msg = "A class that extends EnclosedIncludeBodyReaderTag " + + "is not enclosed within an IncludeTag."; + log(msg); + throw new JspException(msg); + } + */ + + /** + * This method adds the parameter whose name and value were passed to this + * object via the tag attributes to the parent {@code Include} tag. + */ + + private void addParameter() { + IncludeTag includeTag = (IncludeTag) getParent(); + + includeTag.addParameter(mParameterName, mParameterValue); + } + + /** + * This method cleans up the member variables for this tag in preparation + * for being used again. This method is called when the tag finishes it's + * current call with in the page but could be called upon again within this + * same page. This method is also called in the release stage of the tag + * life cycle just in case a JspException was thrown during the tag + * execution. + */ + + protected void clearServiceState() { + mParameterName = null; + mParameterValue = null; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java new file mode 100755 index 00000000..25aae40d --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ValueOfTEI.java,v $ + * Revision 1.3 2003/10/06 14:26:07 WMHAKUR + * Code clean-up only. + * + * Revision 1.2 2002/10/18 14:28:07 WMHAKUR + * Fixed package error. + * + * Revision 1.1 2002/10/18 14:03:52 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import java.io.IOException; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * TagExtraInfo for ValueOf. + * @todo More meaningful response to the user. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + * + */ + +public class ValueOfTEI extends TagExtraInfo { + + public boolean isValid(TagData pTagData) { + Object nameAttr = pTagData.getAttribute("name"); + Object paramAttr = pTagData.getAttribute("param"); + + if ((nameAttr != null && paramAttr == null) || + (nameAttr == null && paramAttr != null)) { + return true; // Exactly one of name or param set + } + + // Either both or none, + return false; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java new file mode 100755 index 00000000..b3171222 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ValueOfTag.java,v $ + * Revision 1.2 2003/10/06 14:26:14 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/10/18 14:03:52 WMHAKUR + * Moved to com.twelvemonkeys.servlet.jsp.droplet.taglib + * + * + */ + +package com.twelvemonkeys.servlet.jsp.droplet.taglib; + +import java.io.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; + +import com.twelvemonkeys.servlet.jsp.droplet.*; +import com.twelvemonkeys.servlet.jsp.taglib.*; + +/** + * ValueOf tag that emulates ATG Dynamo JHTML behaviour for JSP. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Revision: #1 $, ($Date: 2008/05/05 $) + */ +public class ValueOfTag extends ExTagSupport { + + /** + * This is the name of the parameter whose value is to be inserted into + * the current JSP page. This value will be set via the {@code name} + * attribute. + */ + private String mParameterName; + + /** + * This is the value of the parameter read from the {@code + * PageContext.REQUEST_SCOPE} scope. If the parameter doesn't exist, + * then this will be null. + */ + private Object mParameterValue; + + /** + * This method is called as part of the initialisation phase of the tag + * life cycle. It sets the parameter name to be read from the {@code + * PageContext.REQUEST_SCOPE} scope. + * + * @param pName The name of the parameter to be read from the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + public void setName(String pName) { + mParameterName = pName; + } + + /** + * This method is called as part of the initialisation phase of the tag + * life cycle. It sets the parameter name to be read from the {@code + * PageContext.REQUEST_SCOPE} scope. This is just a synonym for + * setName, to be more like ATG Dynamo. + * + * @param pName The name of the parameter to be read from the {@code + * PageContext.REQUEST_SCOPE} scope. + */ + public void setParam(String pName) { + mParameterName = pName; + } + + /** + * This method looks in the session scope for the session-scoped attribute + * whose name matches the {@code name} tag attribute for this tag. + * If it finds it, then it replaces this tag with the value for the + * session-scoped attribute. If it fails to find the session-scoped + * attribute, it displays the body for this tag. + * + * @return If the session-scoped attribute is found, then this method will + * return {@code TagSupport.SKIP_BODY}, otherwise it will return + * {@code TagSupport.EVAL_BODY_INCLUDE}. + * @exception JspException + * + */ + public int doStartTag() throws JspException { + try { + if (parameterExists()) { + if (mParameterValue instanceof JspFragment) { + // OPARAM or PARAM + ((JspFragment) mParameterValue).service(pageContext); + /* + log("Service subpage " + pageContext.getServletContext().getRealPath(((Oparam) mParameterValue).getName())); + + pageContext.include(((Oparam) mParameterValue).getName()); + */ + } + else { + // Normal JSP parameter value + JspWriter writer = pageContext.getOut(); + writer.print(mParameterValue); + } + + return SKIP_BODY; + } + else { + return EVAL_BODY_INCLUDE; + } + } + catch (ServletException se) { + log(se.getMessage(), se); + throw new JspException(se); + } + catch (IOException ioe) { + String msg = "Caught an IOException in ValueOfTag.doStartTag()\n" + + ioe.toString(); + log(msg, ioe); + throw new JspException(msg); + } + } + + /** + * This method is used to determine whether the parameter whose name is + * stored in {@code mParameterName} exists within the {@code + * PageContext.REQUEST_SCOPE} scope. If the parameter does exist, + * then this method will return {@code true}, otherwise it returns + * {@code false}. This method has the side affect of loading the + * parameter value into {@code mParameterValue} if the parameter + * does exist. + * + * @return {@code true} if the parameter whose name is in {@code + * mParameterName} exists in the {@code PageContext.REQUEST_SCOPE + * } scope, {@code false} otherwise. + */ + private boolean parameterExists() { + mParameterValue = pageContext.getAttribute(mParameterName, PageContext.REQUEST_SCOPE); + + // -- Harald K 20020726 + if (mParameterValue == null) { + mParameterValue = pageContext.getRequest().getParameter(mParameterName); + } + + return (mParameterValue != null); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html new file mode 100755 index 00000000..8aa9a145 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html @@ -0,0 +1,10 @@ + + + +The TwelveMonkeys droplet TagLib. + +TODO: Insert taglib-descriptor here? + + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package.html new file mode 100755 index 00000000..9c843494 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package.html @@ -0,0 +1,7 @@ + + + +JSP + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java new file mode 100755 index 00000000..a7b2ae27 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java @@ -0,0 +1,43 @@ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import javax.servlet.jsp.JspException; + +/** + * + * + * @author Thomas Purcell (CSC Australia) + * + * @version 1.0 + */ + +public abstract class BodyReaderTag extends ExBodyTagSupport { + /** + * This is the method called by the JSP engine when the body for a tag + * has been parsed and is ready for inclusion in this current tag. This + * method takes the content as a string and passes it to the {@code + * processBody} method. + * + * @return This method returns the {@code BodyTagSupport.SKIP_BODY} + * constant. This means that the body of the tag will only be + * processed the one time. + * @exception JspException + */ + + public int doAfterBody() throws JspException { + processBody(bodyContent.getString()); + return SKIP_BODY; + } + + /** + * This is the method that child classes must implement. It takes the + * body of the tag converted to a String as it's parameter. The body of + * the tag will have been interpreted to a String by the JSP engine before + * this method is called. + * + * @param pContent The body for the custom tag converted to a String. + * @exception JscException + */ + + protected abstract void processBody(String pContent) throws JspException; +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java new file mode 100755 index 00000000..cd27b166 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: CSVToTableTag.java,v $ + * Revision 1.3 2003/10/06 14:24:50 WMHAKUR + * Code clean-up only. + * + * Revision 1.2 2002/11/26 17:33:49 WMHAKUR + * Added documentation & removed System.out.println()s. + * + * Revision 1.1 2002/11/19 10:50:10 WMHAKUR + * Renamed from CSVToTable, to follow naming conventions. + * + * Revision 1.1 2002/11/18 22:11:16 WMHAKUR + * Tag to convert CSV to HTML table. + * Can be further transformed, using XSLT. + * + */ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import java.util.*; +import java.io.*; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * Creates a table from a string of "comma-separated values" (CSV). + * The delimiter character can be any character (or combination of characters). + * The default delimiter is TAB ({@code \t}). + * + *

+ *


+ *

+ * + * The input may look like this: + *

+ * <c:totable firstRowIsHeader="true" delimiter=";">
+ *   header A;header B
+ *   data 1A; data 1B
+ *   data 2A; data 2B
+ * </c:totable>
+ * 
+ * + * The output (source) will look like this: + *
+ * <TABLE>
+ *   <TR>
+ *      <TH>header A</TH><TH>header B</TH>
+ *   </TR>
+ *   <TR>
+ *      <TD>data 1A</TD><TD>data 1B</TD>
+ *   </TR>
+ *   <TR>
+ *      <TD>data 2A</TD><TD>data 2B</TD>
+ *   </TR>
+ * </TABLE>
+ * 
+ * You wil probably want to use XSLT to make the final output look nicer. :-) + * + * @see StringTokenizer + * @see XSLT spec + * + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java#1 $ + */ + +public class CSVToTableTag extends ExBodyTagSupport { + + public final static String TAB = "\t"; + + protected String mDelimiter = null; + protected boolean mFirstRowIsHeader = false; + protected boolean mFirstColIsHeader = false; + + public void setDelimiter(String pDelimiter) { + mDelimiter = pDelimiter; + } + + public String getDelimiter() { + return mDelimiter != null ? mDelimiter : TAB; + } + + public void setFirstRowIsHeader(String pBoolean) { + mFirstRowIsHeader = Boolean.valueOf(pBoolean).booleanValue(); + } + + public void setFirstColIsHeader(String pBoolean) { + mFirstColIsHeader = Boolean.valueOf(pBoolean).booleanValue(); + } + + + public int doEndTag() throws JspException { + BodyContent content = getBodyContent(); + + try { + Table table = + Table.parseContent(content.getReader(), getDelimiter()); + + JspWriter out = pageContext.getOut(); + + //System.out.println("CSVToTable: " + table.getRows() + " rows, " + // + table.getCols() + " cols."); + + if (table.getRows() > 0) { + out.println(""); + // Loop over rows + for (int row = 0; row < table.getRows(); row++) { + out.println(""); + + // Loop over cells in each row + for (int col = 0; col < table.getCols(); col++) { + // Test if we are using headers, else normal cell + if (mFirstRowIsHeader && row == 0 + || mFirstColIsHeader && col == 0) { + out.println(""); + } + else { + out.println(""); + } + } + + out.println(""); + + } + out.println("
" + table.get(row, col) + + " " + table.get(row, col) + + "
"); + } + } + catch (IOException ioe) { + throw new JspException(ioe); + } + + return super.doEndTag(); + } + + static class Table { + List mRows = null; + int mCols = 0; + + private Table(List pRows, int pCols) { + mRows = pRows; + mCols = pCols; + } + + int getRows() { + return mRows != null ? mRows.size() : 0; + } + + int getCols() { + return mCols; + } + + List getTableRows() { + return mRows; + } + + List getTableRow(int pRow) { + return mRows != null + ? (List) mRows.get(pRow) + : Collections.EMPTY_LIST; + } + + String get(int pRow, int pCol) { + List row = getTableRow(pRow); + // Rows may contain unequal number of cols + return (row.size() > pCol) ? (String) row.get(pCol) : ""; + } + + /** + * Parses a BodyContent to a table. + * + */ + + static Table parseContent(Reader pContent, String pDelim) + throws IOException { + ArrayList tableRows = new ArrayList(); + int tdsPerTR = 0; + + // Loop through TRs + BufferedReader reader = new BufferedReader(pContent); + String tr = null; + while ((tr = reader.readLine()) != null) { + // Discard blank lines + if (tr != null + && tr.trim().length() <= 0 && tr.indexOf(pDelim) < 0) { + continue; + } + + //System.out.println("CSVToTable: read LINE=\"" + tr + "\""); + + ArrayList tableDatas = new ArrayList(); + StringTokenizer tableRow = new StringTokenizer(tr, pDelim, + true); + + boolean lastWasDelim = false; + while (tableRow.hasMoreTokens()) { + String td = tableRow.nextToken(); + + //System.out.println("CSVToTable: read data=\"" + td + "\""); + + // Test if we have empty TD + if (td.equals(pDelim)) { + if (lastWasDelim) { + // Add empty TD + tableDatas.add(""); + } + + // We just read a delimitter + lastWasDelim = true; + } + else { + // No tab, normal data + lastWasDelim = false; + + // Add normal TD + tableDatas.add(td); + } + } // end while (tableRow.hasNext()) + + // Store max TD count + if (tableDatas.size() > tdsPerTR) { + tdsPerTR = tableDatas.size(); + } + + // Add a table row + tableRows.add(tableDatas); + } + + // Return TABLE + return new Table(tableRows, tdsPerTR); + } + } + + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java new file mode 100755 index 00000000..c24ab33a --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ExBodyTagSupport.java,v $ + * Revision 1.3 2003/10/06 14:24:57 WMHAKUR + * Code clean-up only. + * + * Revision 1.2 2002/11/18 22:10:27 WMHAKUR + * *** empty log message *** + * + * + */ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import java.io.*; +import java.net.*; +import java.util.*; + +import javax.servlet.*; +import javax.servlet.http.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * This is the class that should be extended by all jsp pages that do use their + * body. It contains a lot of helper methods for simplifying common tasks. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java#1 $ + */ + +public class ExBodyTagSupport extends BodyTagSupport implements ExTag { + /** + * writeHtml ensures that the text being outputted appears as it was + * entered. This prevents users from hacking the system by entering + * html or jsp code into an entry form where that value will be displayed + * later in the site. + * + * @param pOut The JspWriter to write the output to. + * @param pHtml The original html to filter and output to the user. + * @throws IOException If the user clicks Stop in the browser, or their + * browser crashes, then the JspWriter will throw an IOException when + * the jsp tries to write to it. + */ + + public void writeHtml(JspWriter pOut, String pHtml) throws IOException { + StringTokenizer parser = new StringTokenizer(pHtml, "<>&", true); + + while (parser.hasMoreTokens()) { + String token = parser.nextToken(); + + if (token.equals("<")) { + pOut.print("<"); + } + else if (token.equals(">")) { + pOut.print(">"); + } + else if (token.equals("&")) { + pOut.print("&"); + } + else { + pOut.print(token); + } + } + } + + /** + * Log a message to the servlet context. + * + * @param pMsg The error message to log. + */ + + public void log(String pMsg) { + getServletContext().log(pMsg); + } + + /** + * Log a message to the servlet context and include the exception that is + * passed in as the second parameter. + * + * @param pMsg The error message to log. + * @param pException The exception that caused this error message to be + * logged. + */ + + public void log(String pMsg, Throwable pException) { + getServletContext().log(pMsg, pException); + } + + /** + * Retrieves the ServletContext object associated with the current + * PageContext object. + * + * @return The ServletContext object associated with the current + * PageContext object. + */ + + public ServletContext getServletContext() { + return pageContext.getServletContext(); + } + + /** + * Called when the tag has finished running. Any clean up that needs + * to be done between calls to this tag but within the same JSP page is + * called in the {@code clearServiceState()} method call. + * + * @exception JspException + */ + + public int doEndTag() throws JspException { + clearServiceState(); + return super.doEndTag(); + } + + /** + * Called when a tag's role in the current JSP page is finished. After + * the {@code clearProperties()} method is called, the custom tag + * should be in an identical state as when it was first created. The + * {@code clearServiceState()} method is called here just in case an + * exception was thrown in the custom tag. If an exception was thrown, + * then the {@code doEndTag()} method will not have been called and + * the tag might not have been cleaned up properly. + */ + + public void release() { + clearServiceState(); + + clearProperties(); + super.release(); + } + + /** + * The default implementation for the {@code clearProperties()}. Not + * all tags will need to overload this method call. By implementing it + * here, all classes that extend this object are able to call {@code + * super.clearProperties()}. So, if the class extends a different + * tag, or this one, the parent method should always be called. This + * method will be called when the tag is to be released. That is, the + * tag has finished for the current page and should be returned to it's + * initial state. + */ + + protected void clearProperties() { + } + + /** + * The default implementation for the {@code clearServiceState()}. + * Not all tags will need to overload this method call. By implementing it + * here, all classes that extend this object are able to call {@code + * super.clearServiceState()}. So, if the class extends a different + * tag, or this one, the parent method should always be called. This + * method will be called when the tag has finished it's current tag + * within the page, but may be called upon again in this same JSP page. + */ + + protected void clearServiceState() { + } + + /** + * Returns the initialisation parameter from the {@code + * PageContext.APPLICATION_SCOPE} scope. These initialisation + * parameters are defined in the {@code web.xml} configuration file. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @return The value for the parameter whose name was passed in as a + * parameter. If the parameter does not exist, then {@code null} + * will be returned. + */ + + public String getInitParameter(String pName) { + return getInitParameter(pName, PageContext.APPLICATION_SCOPE); + } + + /** + * Returns an Enumeration containing all the names for all the + * initialisation parametes defined in the {@code + * PageContext.APPLICATION_SCOPE} scope. + * + * @return An {@code Enumeration} containing all the names for all the + * initialisation parameters. + */ + + public Enumeration getInitParameterNames() { + return getInitParameterNames(PageContext.APPLICATION_SCOPE); + } + + /** + * Returns the initialisation parameter from the scope specified with the + * name specified. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @param pScope The scope to search for the initialisation parameter + * within. + * @return The value of the parameter found. If no parameter with the + * name specified is found in the scope specified, then {@code null + * } is returned. + */ + + public String getInitParameter(String pName, int pScope) { + switch (pScope) { + case PageContext.PAGE_SCOPE: + return getServletConfig().getInitParameter(pName); + case PageContext.APPLICATION_SCOPE: + return getServletContext().getInitParameter(pName); + default: + throw new IllegalArgumentException("Illegal scope."); + } + } + + /** + * Returns an enumeration containing all the parameters defined in the + * scope specified by the parameter. + * + * @param pScope The scope to return the names of all the parameters + * defined within. + * @return An {@code Enumeration} containing all the names for all the + * parameters defined in the scope passed in as a parameter. + */ + + public Enumeration getInitParameterNames(int pScope) { + switch (pScope) { + case PageContext.PAGE_SCOPE: + return getServletConfig().getInitParameterNames(); + case PageContext.APPLICATION_SCOPE: + return getServletContext().getInitParameterNames(); + default: + throw new IllegalArgumentException("Illegal scope"); + } + } + + /** + * Returns the servlet config associated with the current JSP page request. + * + * @return The {@code ServletConfig} associated with the current + * request. + */ + + public ServletConfig getServletConfig() { + return pageContext.getServletConfig(); + } + + /** + * Gets the context path associated with the current JSP page request. + * If the request is not a HttpServletRequest, this method will + * return "/". + * + * @return a path relative to the current context's root, or + * {@code "/"} if this is not a HTTP request. + */ + + public String getContextPath() { + ServletRequest request = pageContext.getRequest(); + if (request instanceof HttpServletRequest) { + return ((HttpServletRequest) request).getContextPath(); + } + return "/"; + } + + /** + * Gets the resource associated with the given relative path for the + * current JSP page request. + * The path may be absolute, or relative to the current context root. + * + * @param pPath the path + * + * @return a path relative to the current context root + */ + + public InputStream getResourceAsStream(String pPath) { + // throws MalformedURLException { + String path = pPath; + + if (pPath != null && !pPath.startsWith("/")) { + path = getContextPath() + pPath; + } + + return pageContext.getServletContext().getResourceAsStream(path); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java new file mode 100755 index 00000000..58e21648 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ExTag.java,v $ + * Revision 1.2 2003/10/06 14:25:05 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/11/18 22:10:27 WMHAKUR + * *** empty log message *** + * + * + */ + +package com.twelvemonkeys.servlet.jsp.taglib; + + +import java.io.*; +import java.util.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * This interface contains a lot of helper methods for simplifying common + * taglib related tasks. + * + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java#1 $ + */ + +public interface ExTag extends Tag { + + /** + * writeHtml ensures that the text being outputted appears as it was + * entered. This prevents users from hacking the system by entering + * html or jsp code into an entry form where that value will be displayed + * later in the site. + * + * @param pOut The JspWriter to write the output to. + * @param pHtml The original html to filter and output to the user. + * @throws IOException If the user clicks Stop in the browser, or their + * browser crashes, then the JspWriter will throw an IOException when + * the jsp tries to write to it. + */ + + public void writeHtml(JspWriter pOut, String pHtml) throws IOException; + + /** + * Log a message to the servlet context. + * + * @param pMsg The error message to log. + */ + + public void log(String pMsg); + + /** + * Logs a message to the servlet context and include the exception that is + * passed in as the second parameter. + * + * @param pMsg The error message to log. + * @param pException The exception that caused this error message to be + * logged. + */ + + public void log(String pMsg, Throwable pException); + + /** + * Retrieves the ServletContext object associated with the current + * PageContext object. + * + * @return The ServletContext object associated with the current + * PageContext object. + */ + + public ServletContext getServletContext(); + + /** + * Returns the initialisation parameter from the {@code + * PageContext.APPLICATION_SCOPE} scope. These initialisation + * parameters are defined in the {@code web.xml} configuration file. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @return The value for the parameter whose name was passed in as a + * parameter. If the parameter does not exist, then {@code null} + * will be returned. + */ + + public String getInitParameter(String pName); + + /** + * Returns an Enumeration containing all the names for all the + * initialisation parametes defined in the {@code + * PageContext.APPLICATION_SCOPE} scope. + * + * @return An {@code Enumeration} containing all the names for all the + * initialisation parameters. + */ + + public Enumeration getInitParameterNames(); + + /** + * Returns the initialisation parameter from the scope specified with the + * name specified. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @param pScope The scope to search for the initialisation parameter + * within. + * @return The value of the parameter found. If no parameter with the + * name specified is found in the scope specified, then {@code null + * } is returned. + */ + + public String getInitParameter(String pName, int pScope); + + /** + * Returns an enumeration containing all the parameters defined in the + * scope specified by the parameter. + * + * @param pScope The scope to return the names of all the parameters + * defined within. + * @return An {@code Enumeration} containing all the names for all the + * parameters defined in the scope passed in as a parameter. + */ + + public Enumeration getInitParameterNames(int pScope); + + /** + * Returns the servlet config associated with the current JSP page request. + * + * @return The {@code ServletConfig} associated with the current + * request. + */ + + public ServletConfig getServletConfig(); + + /** + * Gets the context path associated with the current JSP page request. + * + * @return a path relative to the current context's root. + */ + + public String getContextPath(); + + + /** + * Gets the resource associated with the given relative path for the + * current JSP page request. + * The path may be absolute, or relative to the current context root. + * + * @param pPath the path + * + * @return a path relative to the current context root + */ + + public InputStream getResourceAsStream(String pPath); + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java new file mode 100755 index 00000000..4cd688ce --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: ExTagSupport.java,v $ + * Revision 1.3 2003/10/06 14:25:11 WMHAKUR + * Code clean-up only. + * + * Revision 1.2 2002/11/18 22:10:27 WMHAKUR + * *** empty log message *** + * + * + */ + +package com.twelvemonkeys.servlet.jsp.taglib; + + +import java.io.*; +import java.net.*; +import java.util.*; + +import javax.servlet.*; +import javax.servlet.http.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * This is the class that should be extended by all jsp pages that don't use + * their body. It contains a lot of helper methods for simplifying common + * tasks. + * + * @author Thomas Purcell (CSC Australia) + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java#1 $ + */ + +public class ExTagSupport extends TagSupport implements ExTag { + /** + * writeHtml ensures that the text being outputted appears as it was + * entered. This prevents users from hacking the system by entering + * html or jsp code into an entry form where that value will be displayed + * later in the site. + * + * @param pOut The JspWriter to write the output to. + * @param pHtml The original html to filter and output to the user. + * @throws IOException If the user clicks Stop in the browser, or their + * browser crashes, then the JspWriter will throw an IOException when + * the jsp tries to write to it. + */ + + public void writeHtml(JspWriter pOut, String pHtml) throws IOException { + StringTokenizer parser = new StringTokenizer(pHtml, "<>&", true); + + while (parser.hasMoreTokens()) { + String token = parser.nextToken(); + + if (token.equals("<")) { + pOut.print("<"); + } + else if (token.equals(">")) { + pOut.print(">"); + } + else if (token.equals("&")) { + pOut.print("&"); + } + else { + pOut.print(token); + } + } + } + + /** + * Log a message to the servlet context. + * + * @param pMsg The error message to log. + */ + + public void log(String pMsg) { + getServletContext().log(pMsg); + } + + /** + * Log a message to the servlet context and include the exception that is + * passed in as the second parameter. + * + * @param pMsg The error message to log. + * @param pException The exception that caused this error message to be + * logged. + */ + + public void log(String pMsg, Throwable pException) { + getServletContext().log(pMsg, pException); + } + + /** + * Retrieves the ServletContext object associated with the current + * PageContext object. + * + * @return The ServletContext object associated with the current + * PageContext object. + */ + + public ServletContext getServletContext() { + return pageContext.getServletContext(); + } + + /** + * Called when the tag has finished running. Any clean up that needs + * to be done between calls to this tag but within the same JSP page is + * called in the {@code clearServiceState()} method call. + * + * @exception JspException + */ + + public int doEndTag() throws JspException { + clearServiceState(); + return super.doEndTag(); + } + + /** + * Called when a tag's role in the current JSP page is finished. After + * the {@code clearProperties()} method is called, the custom tag + * should be in an identical state as when it was first created. The + * {@code clearServiceState()} method is called here just in case an + * exception was thrown in the custom tag. If an exception was thrown, + * then the {@code doEndTag()} method will not have been called and + * the tag might not have been cleaned up properly. + */ + + public void release() { + clearServiceState(); + + clearProperties(); + super.release(); + } + + /** + * The default implementation for the {@code clearProperties()}. Not + * all tags will need to overload this method call. By implementing it + * here, all classes that extend this object are able to call {@code + * super.clearProperties()}. So, if the class extends a different + * tag, or this one, the parent method should always be called. This + * method will be called when the tag is to be released. That is, the + * tag has finished for the current page and should be returned to it's + * initial state. + */ + + protected void clearProperties() { + } + + /** + * The default implementation for the {@code clearServiceState()}. + * Not all tags will need to overload this method call. By implementing it + * here, all classes that extend this object are able to call {@code + * super.clearServiceState()}. So, if the class extends a different + * tag, or this one, the parent method should always be called. This + * method will be called when the tag has finished it's current tag + * within the page, but may be called upon again in this same JSP page. + */ + + protected void clearServiceState() { + } + + /** + * Returns the initialisation parameter from the {@code + * PageContext.APPLICATION_SCOPE} scope. These initialisation + * parameters are defined in the {@code web.xml} configuration file. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @return The value for the parameter whose name was passed in as a + * parameter. If the parameter does not exist, then {@code null} + * will be returned. + */ + + public String getInitParameter(String pName) { + return getInitParameter(pName, PageContext.APPLICATION_SCOPE); + } + + /** + * Returns an Enumeration containing all the names for all the + * initialisation parametes defined in the {@code + * PageContext.APPLICATION_SCOPE} scope. + * + * @return An {@code Enumeration} containing all the names for all the + * initialisation parameters. + */ + + public Enumeration getInitParameterNames() { + return getInitParameterNames(PageContext.APPLICATION_SCOPE); + } + + /** + * Returns the initialisation parameter from the scope specified with the + * name specified. + * + * @param pName The name of the initialisation parameter to return the + * value for. + * @param pScope The scope to search for the initialisation parameter + * within. + * @return The value of the parameter found. If no parameter with the + * name specified is found in the scope specified, then {@code null + * } is returned. + */ + + public String getInitParameter(String pName, int pScope) { + switch (pScope) { + case PageContext.PAGE_SCOPE: + return getServletConfig().getInitParameter(pName); + case PageContext.APPLICATION_SCOPE: + return getServletContext().getInitParameter(pName); + default: + throw new IllegalArgumentException("Illegal scope."); + } + } + + /** + * Returns an enumeration containing all the parameters defined in the + * scope specified by the parameter. + * + * @param pScope The scope to return the names of all the parameters + * defined within. + * @return An {@code Enumeration} containing all the names for all the + * parameters defined in the scope passed in as a parameter. + */ + + public Enumeration getInitParameterNames(int pScope) { + switch (pScope) { + case PageContext.PAGE_SCOPE: + return getServletConfig().getInitParameterNames(); + case PageContext.APPLICATION_SCOPE: + return getServletContext().getInitParameterNames(); + default: + throw new IllegalArgumentException("Illegal scope"); + } + } + + /** + * Returns the servlet config associated with the current JSP page request. + * + * @return The {@code ServletConfig} associated with the current + * request. + */ + + public ServletConfig getServletConfig() { + return pageContext.getServletConfig(); + } + + /** + * Gets the context path associated with the current JSP page request. + * If the request is not a HttpServletRequest, this method will + * return "/". + * + * @return a path relative to the current context's root, or + * {@code "/"} if this is not a HTTP request. + */ + + public String getContextPath() { + ServletRequest request = pageContext.getRequest(); + if (request instanceof HttpServletRequest) { + return ((HttpServletRequest) request).getContextPath(); + } + return "/"; + } + + /** + * Gets the resource associated with the given relative path for the + * current JSP page request. + * The path may be absolute, or relative to the current context root. + * + * @param pPath the path + * + * @return a path relative to the current context root + */ + + public InputStream getResourceAsStream(String pPath) { + //throws MalformedURLException { + String path = pPath; + + if (pPath != null && !pPath.startsWith("/")) { + path = getContextPath() + pPath; + } + + return pageContext.getServletContext().getResourceAsStream(path); + } + + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java new file mode 100755 index 00000000..e4d3deb0 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java @@ -0,0 +1,21 @@ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +/** + * TagExtraInfo for LastModifiedTag + * + * @author Harald Kuhr + * + * @version 1.1 + */ + +public class LastModifiedTEI extends TagExtraInfo { + public VariableInfo[] getVariableInfo(TagData pData) { + return new VariableInfo[]{ + new VariableInfo("lastModified", "java.lang.String", true, VariableInfo.NESTED), + }; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java new file mode 100755 index 00000000..8f823389 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java @@ -0,0 +1,54 @@ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import java.io.File; +import java.util.Date; + +import javax.servlet.http.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; + +import com.twelvemonkeys.util.convert.*; + +/** + * Prints the last modified + */ + +public class LastModifiedTag extends TagSupport { + private String mFileName = null; + private String mFormat = null; + + public void setFile(String pFileName) { + mFileName = pFileName; + } + + public void setFormat(String pFormat) { + mFormat = pFormat; + } + + public int doStartTag() throws JspException { + File file = null; + + if (mFileName != null) { + file = new File(pageContext.getServletContext() + .getRealPath(mFileName)); + } + else { + HttpServletRequest request = + (HttpServletRequest) pageContext.getRequest(); + + // Get the file containing the servlet + file = new File(pageContext.getServletContext() + .getRealPath(request.getServletPath())); + } + + Date lastModified = new Date(file.lastModified()); + Converter conv = Converter.getInstance(); + + // Set the last modified value back + pageContext.setAttribute("lastModified", + conv.toString(lastModified, mFormat)); + + return Tag.EVAL_BODY_INCLUDE; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java new file mode 100755 index 00000000..6ce1fcbb --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java @@ -0,0 +1,89 @@ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import java.io.IOException; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyTag; + +/** + * This tag truncates all consecutive whitespace in sequence inside its body, + * to one whitespace character. The first whitespace character in the sequence + * will be left untouched (except for CR/LF, which will always leave a LF). + * + * @author Harald Kuhr + * + * @version 1.0 + */ + +public class TrimWhiteSpaceTag extends ExBodyTagSupport { + + /** + * doStartTag implementation, simply returns + * {@code BodyTag.EVAL_BODY_BUFFERED}. + * + * @return {@code BodyTag.EVAL_BODY_BUFFERED} + */ + + public int doStartTag() throws JspException { + return BodyTag.EVAL_BODY_BUFFERED; + } + + /** + * doEndTag implementation, truncates all whitespace. + * + * @return {@code super.doEndTag()} + */ + + public int doEndTag() throws JspException { + // Trim + String trimmed = truncateWS(bodyContent.getString()); + try { + // Print trimmed content + //pageContext.getOut().print("\n"); + pageContext.getOut().print(trimmed); + //pageContext.getOut().print("\n"); + } + catch (IOException ioe) { + throw new JspException(ioe); + } + + return super.doEndTag(); + } + + /** + * Truncates whitespace from the given string. + * + * @todo Candidate for StringUtil? + */ + + private static String truncateWS(String pStr) { + char[] chars = pStr.toCharArray(); + + int count = 0; + boolean lastWasWS = true; // Avoids leading WS + for (int i = 0; i < chars.length; i++) { + if (!Character.isWhitespace(chars[i])) { + // if char is not WS, just store + chars[count++] = chars[i]; + lastWasWS = false; + } + else { + // else, if char is WS, store first, skip the rest + if (!lastWasWS) { + if (chars[i] == 0x0d) { + chars[count++] = 0x0a; //Always new line + } + else { + chars[count++] = chars[i]; + } + } + lastWasWS = true; + } + } + + // Return the trucated string + return new String(chars, 0, count); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java new file mode 100755 index 00000000..4e92a463 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2002 TwelveMonkeys. + * All rights reserved. + * + * $Log: XMLTransformTag.java,v $ + * Revision 1.2 2003/10/06 14:25:43 WMHAKUR + * Code clean-up only. + * + * Revision 1.1 2002/11/19 10:50:41 WMHAKUR + * *** empty log message *** + * + */ + +package com.twelvemonkeys.servlet.jsp.taglib; + +import java.io.*; +import java.net.*; + +import javax.servlet.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; +import javax.xml.transform.*; +import javax.xml.transform.stream.*; + +import com.twelvemonkeys.servlet.jsp.*; + +/** + * This tag performs XSL Transformations (XSLT) on a given XML document or its + * body content. + * + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java#1 $ + */ + +public class XMLTransformTag extends ExBodyTagSupport { + private String mDocumentURI = null; + private String mStylesheetURI = null; + + /** + * Sets the document attribute for this tag. + */ + + public void setDocumentURI(String pDocumentURI) { + mDocumentURI = pDocumentURI; + } + + /** + * Sets the stylesheet attribute for this tag. + */ + + public void setStylesheetURI(String pStylesheetURI) { + mStylesheetURI = pStylesheetURI; + } + + + /** + * doStartTag implementation, that performs XML Transformation on the + * given document, if any. + * If the documentURI attribute is set, then the transformation is + * performed on the document at that location, and + * {@code Tag.SKIP_BODY} is returned. + * Otherwise, this method simply returns + * {@code BodyTag.EVAL_BODY_BUFFERED} and leaves the transformation to + * the doEndTag. + * + * @return {@code Tag.SKIP_BODY} if {@code documentURI} is not + * {@code null}, otherwise + * {@code BodyTag.EVAL_BODY_BUFFERED}. + * + * @todo Is it really a good idea to allow "inline" XML in a JSP? + */ + + public int doStartTag() throws JspException { + //log("XML: " + mDocumentURI + " XSL: " + mStylesheetURI); + + if (mDocumentURI != null) { + // If document given, transform and skip body... + try { + transform(getSource(mDocumentURI)); + } + catch (MalformedURLException murle) { + throw new JspException(murle.getMessage(), murle); + } + catch (IOException ioe) { + throw new JspException(ioe.getMessage(), ioe); + } + + return Tag.SKIP_BODY; + } + + // ...else process the body + return BodyTag.EVAL_BODY_BUFFERED; + } + + /** + * doEndTag implementation, that will perform XML Transformation on the + * body content. + * + * @return super.doEndTag() + */ + + public int doEndTag() throws JspException { + // Get body content (trim is CRUCIAL, as some XML parsers are picky...) + String body = bodyContent.getString().trim(); + + // Do transformation + transform(new StreamSource(new ByteArrayInputStream(body.getBytes()))); + + return super.doEndTag(); + } + + /** + * Performs the transformation and writes the result to the JSP writer. + * + * @param in the source document to transform. + */ + + public void transform(Source pIn) throws JspException { + try { + // Create transformer + Transformer transformer = TransformerFactory.newInstance() + .newTransformer(getSource(mStylesheetURI)); + + // Store temporary output in a bytearray, as the transformer will + // usually try to flush the stream (illegal operation from a custom + // tag). + ByteArrayOutputStream os = new ByteArrayOutputStream(); + StreamResult out = new StreamResult(os); + + // Perform the transformation + transformer.transform(pIn, out); + + // Write the result back to the JSP writer + pageContext.getOut().print(os.toString()); + } + catch (MalformedURLException murle) { + throw new JspException(murle.getMessage(), murle); + } + catch (IOException ioe) { + throw new JspException(ioe.getMessage(), ioe); + } + catch (TransformerException te) { + throw new JspException("XSLT Trandformation failed: " + te.getMessage(), te); + } + } + + /** + * Returns a StreamSource object, for the given URI + */ + + private StreamSource getSource(String pURI) + throws IOException, MalformedURLException { + if (pURI != null && pURI.indexOf("://") < 0) { + // If local, get as stream + return new StreamSource(getResourceAsStream(pURI)); + } + + // ...else, create from URI string + return new StreamSource(pURI); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java new file mode 100755 index 00000000..1ecc0ed7 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java @@ -0,0 +1,140 @@ +/**************************************************** + * * + * (c) 2000-2003 TwelveMonkeys * + * All rights reserved * + * http://www.twelvemonkeys.no * + * * + * $RCSfile: ConditionalTagBase.java,v $ + * @version $Revision: #1 $ + * $Date: 2008/05/05 $ + * * + * @author Last modified by: $Author: haku $ + * * + ****************************************************/ + + + +/* + * Produced (p) 2002 TwelveMonkeys + * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. + * Phone : +47 22 57 70 00 + * Fax : +47 22 57 70 70 + */ +package com.twelvemonkeys.servlet.jsp.taglib.logic; + + +import java.lang.*; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.TagSupport; + + +/** + *

An abstract base class for tags with some kind of conditional presentation of the tag body.

+ * + * @version 1.0 + * @author Eirik Torske + */ +public abstract class ConditionalTagBase extends TagSupport { + + // Members + protected String mObjectName; + protected String mObjectValue; + + // Properties + + /** + * Method getName + * + * + * @return + * + */ + public String getName() { + return mObjectName; + } + + /** + * Method setName + * + * + * @param pObjectName + * + */ + public void setName(String pObjectName) { + this.mObjectName = pObjectName; + } + + /** + * Method getValue + * + * + * @return + * + */ + public String getValue() { + return mObjectValue; + } + + /** + * Method setValue + * + * + * @param pObjectValue + * + */ + public void setValue(String pObjectValue) { + this.mObjectValue = pObjectValue; + } + + /** + *

Perform the test required for this particular tag, and either evaluate or skip the body of this tag.

+ * + * + * @return + * @exception JspException if a JSP exception occurs. + */ + public int doStartTag() throws JspException { + + if (condition()) { + return (EVAL_BODY_INCLUDE); + } else { + return (SKIP_BODY); + } + } + + /** + *

Evaluate the remainder of the current page as normal.

+ * + * + * @return + * @exception JspException if a JSP exception occurs. + */ + public int doEndTag() throws JspException { + return (EVAL_PAGE); + } + + /** + *

Release all allocated resources.

+ */ + public void release() { + + super.release(); + mObjectName = null; + mObjectValue = null; + } + + /** + *

The condition that must be met in order to display the body of this tag.

+ * + * @exception JspException if a JSP exception occurs. + * @return {@code true} if and only if all conditions are met. + */ + protected abstract boolean condition() throws JspException; +} + + +/*--- Formatted in Sun Java Convention Style on ma, des 1, '03 ---*/ + + +/*------ Formatted by Jindent 3.23 Basic 1.0 --- http://www.jindent.de ------*/ diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java new file mode 100755 index 00000000..e0dafbb3 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java @@ -0,0 +1,170 @@ +/* + * Produced (p) 2002 TwelveMonkeys + * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. + * Phone : +47 22 57 70 00 + * Fax : +47 22 57 70 70 + */ + +package com.twelvemonkeys.servlet.jsp.taglib.logic; + + +import java.lang.*; + +import javax.servlet.http.Cookie; +import javax.servlet.jsp.JspException; + +import com.twelvemonkeys.lang.StringUtil; + + +/** + *

+ * Custom tag for testing equality of an attribute against a given value. + * The attribute types supported so far is: + *

    + *
  • {@code java.lang.String} (ver. 1.0) + *
  • {@code javax.servlet.http.Cookie} (ver. 1.0) + *
+ *

+ * See the implemented {@code condition} method for details regarding the equality conditions. + * + *


+ * + *

Tag Reference

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
equalAvailability: 1.0

Tag for testing if an attribute is equal to a given value.

Tag BodyJSP    
Restrictions

None

AttributesNameRequiredRuntime Expression EvaluationAvailability
 name Yes Yes 1.0
 

The attribute name

 value No Yes 1.0
 

The value for equality testing

VariablesNone
Examples + *
+ *<%@ taglib prefix="twelvemonkeys" uri="twelvemonkeys-logic" %>
+ *<bean:cookie id="logonUsernameCookie"
+ *    name="<%= com.strutscommand.Constants.LOGON_USERNAME_COOKIE_NAME %>"
+ *    value="no_username_set" />
+ *<twelvemonkeys:equal name="logonUsernameCookie" value="no_username_set">
+ *    <html:text property="username" />
+ *</twelvemonkeys:equal>
+ *      
+ *
+ * + *
+ * + * @version 1.0 + * @author Eirik Torske + * @see notEqual + */ +public class EqualTag extends ConditionalTagBase { + + /** + * + * + * The conditions that must be met in order to display the body of this tag: + *
    + *
  1. The attribute name property ({@code name} -> {@code mObjectName}) must not be empty. + *
  2. The attribute must exist. + *
  3. The attribute must be an instance of one of the supported classes: + *
      + *
    • {@code java.lang.String} + *
    • {@code javax.servlet.http.Cookie} + *
    + *
  4. The value of the attribute must be equal to the object value property ({@code value} -> {@code mObjectValue}). + *
+ *

+ * NB! If the object value property ({@code value} -> {@code mObjectValue}) is empty than {@code true} will be returned. + *

+ * + * @return {@code true} if and only if all conditions are met. + */ + protected boolean condition() throws JspException { + + if (StringUtil.isEmpty(mObjectName)) { + return false; + } + + if (StringUtil.isEmpty(mObjectValue)) { + return true; + } + + Object pageScopedAttribute = pageContext.getAttribute(mObjectName); + if (pageScopedAttribute == null) { + return false; + } + + String pageScopedStringAttribute; + + // String + if (pageScopedAttribute instanceof String) { + pageScopedStringAttribute = (String) pageScopedAttribute; + + // Cookie + } + else if (pageScopedAttribute instanceof Cookie) { + pageScopedStringAttribute = ((Cookie) pageScopedAttribute).getValue(); + + // Type not yet supported... + } + else { + return false; + } + + return (pageScopedStringAttribute.equals(mObjectValue)); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java new file mode 100755 index 00000000..e0610cec --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java @@ -0,0 +1,41 @@ + +package com.twelvemonkeys.servlet.jsp.taglib.logic; + +import javax.servlet.jsp.tagext.*; + +/** + * TagExtraInfo class for IteratorProvider tags. + * + * @author Harald Kuhr + * @version $id: $ + */ +public class IteratorProviderTEI extends TagExtraInfo { + /** + * Gets the variable info for IteratorProvider tags. The attribute with the + * name defined by the "id" attribute and type defined by the "type" + * attribute is declared with scope {@code VariableInfo.AT_END}. + * + * @param pData TagData instance provided by container + * @return an VariableInfo array of lenght 1, containing the attribute + * defined by the id parameter, declared, and with scope + * {@code VariableInfo.AT_END}. + */ + public VariableInfo[] getVariableInfo(TagData pData) { + // Get attribute name + String attributeName = pData.getId(); + if (attributeName == null) { + attributeName = IteratorProviderTag.getDefaultIteratorName(); + } + + // Get type + String type = pData.getAttributeString(IteratorProviderTag.ATTRIBUTE_TYPE); + if (type == null) { + type = IteratorProviderTag.getDefaultIteratorType(); + } + + // Return the variable info + return new VariableInfo[]{ + new VariableInfo(attributeName, type, true, VariableInfo.AT_END), + }; + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java new file mode 100755 index 00000000..f4ae2b1c --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java @@ -0,0 +1,87 @@ + +package com.twelvemonkeys.servlet.jsp.taglib.logic; + +import java.util.Iterator; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.*; + +/** + * Abstract base class for adding iterators to a page. + * + * @todo Possible to use same strategy for all types of objects? Rename class + * to ObjectProviderTag? Hmmm... Might work. + * + * @author Harald Kuhr + * @version $id: $ + */ +public abstract class IteratorProviderTag extends TagSupport { + /** {@code iterator} */ + protected final static String DEFAULT_ITERATOR_NAME = "iterator"; + /** {@code java.util.iterator} */ + protected final static String DEFAULT_ITERATOR_TYPE = "java.util.Iterator"; + /** {@code type} */ + public final static String ATTRIBUTE_TYPE = "type"; + + /** */ + private String mType = null; + + /** + * Gets the type. + * + * @return the type (class name) + */ + public String getType() { + return mType; + } + + /** + * Sets the type. + * + * @param pType + */ + + public void setType(String pType) { + mType = pType; + } + + /** + * doEndTag implementation. + * + * @return {@code Tag.EVAL_PAGE} + * @throws JspException + */ + + public int doEndTag() throws JspException { + // Set the iterator + pageContext.setAttribute(getId(), getIterator()); + + return Tag.EVAL_PAGE; + } + + /** + * Gets the iterator for this tag. + * + * @return an {@link java.util.Iterator} + */ + protected abstract Iterator getIterator(); + + /** + * Gets the default iterator name. + * + * @return {@link #DEFAULT_ITERATOR_NAME} + */ + protected static String getDefaultIteratorName() { + return DEFAULT_ITERATOR_NAME; + } + + /** + * Gets the default iterator type. + * + * @return {@link #DEFAULT_ITERATOR_TYPE} + */ + protected static String getDefaultIteratorType() { + return DEFAULT_ITERATOR_TYPE; + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java new file mode 100755 index 00000000..47f59cee --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java @@ -0,0 +1,168 @@ +/* + * Produced (p) 2002 TwelveMonkeys + * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. + * Phone : +47 22 57 70 00 + * Fax : +47 22 57 70 70 + */ + +package com.twelvemonkeys.servlet.jsp.taglib.logic; + + +import com.twelvemonkeys.lang.StringUtil; + +import javax.servlet.http.Cookie; +import javax.servlet.jsp.JspException; + + +/** + *

+ * Custom tag for testing non-equality of an attribute against a given value. + * The attribute types supported so far is: + *

    + *
  • {@code java.lang.String} (ver. 1.0) + *
  • {@code javax.servlet.http.Cookie} (ver. 1.0) + *
+ *

+ * See the implemented {@code condition} method for details regarding the non-equality conditions. + * + *


+ * + *

Tag Reference

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
notEqualAvailability: 1.0

Tag for testing if an attribute is NOT equal to a given value.

Tag BodyJSP    
Restrictions

None

AttributesNameRequiredRuntime Expression EvaluationAvailability
 name Yes Yes 1.0
 

The attribute name

 value No Yes 1.0
 

The value for equality testing

VariablesNone
Examples + *
+ *<%@ taglib prefix="twelvemonkeys" uri="twelvemonkeys-logic" %>
+ *<bean:cookie id="logonUsernameCookie"
+ *    name="<%= com.strutscommand.Constants.LOGON_USERNAME_COOKIE_NAME %>"
+ *    value="no_username_set" />
+ *<twelvemonkeys:notEqual name="logonUsernameCookie" value="no_username_set">
+ *    <html:text property="username" value="<%= logonUsernameCookie.getValue() %>" />
+ *</twelvemonkeys:notEqual>
+ *      
+ *
+ * + *
+ * + * @version 1.0 + * @author Eirik Torske + * @see equal + */ +public class NotEqualTag extends ConditionalTagBase { + + /** + * + * + * The condition that must be met in order to display the body of this tag: + *
    + *
  1. The attribute name property ({@code name} -> {@code mObjectName}) must not be empty. + *
  2. The attribute must exist. + *
  3. The attribute must be an instance of one of the supported classes: + *
      + *
    • {@code java.lang.String} + *
    • {@code javax.servlet.http.Cookie} + *
    + *
  4. The value of the attribute must NOT be equal to the object value property ({@code value} -> {@code mObjectValue}). + *
+ *

+ * NB! If the object value property ({@code value} -> {@code mObjectValue}) is empty than {@code true} will be returned. + *

+ * + * @return {@code true} if and only if all conditions are met. + */ + protected boolean condition() throws JspException { + + if (StringUtil.isEmpty(mObjectName)) { + return false; + } + + if (StringUtil.isEmpty(mObjectValue)) { + return true; + } + + Object pageScopedAttribute = pageContext.getAttribute(mObjectName); + if (pageScopedAttribute == null) { + return false; + } + + String pageScopedStringAttribute; + + // String + if (pageScopedAttribute instanceof String) { + pageScopedStringAttribute = (String) pageScopedAttribute; + + // Cookie + } + else if (pageScopedAttribute instanceof Cookie) { + pageScopedStringAttribute = ((Cookie) pageScopedAttribute).getValue(); + + // Type not yet supported... + } + else { + return false; + } + + return (!(pageScopedStringAttribute.equals(mObjectValue))); + } + +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package.html new file mode 100755 index 00000000..3bee289e --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package.html @@ -0,0 +1,7 @@ + + + +The TwelveMonkeys common TagLib. + + + diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java new file mode 100755 index 00000000..a629c438 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java @@ -0,0 +1,183 @@ +package com.twelvemonkeys.servlet.log4j; + +import org.apache.log4j.Logger; + +import java.util.Enumeration; +import java.util.Set; +import java.net.URL; +import java.net.MalformedURLException; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import javax.servlet.ServletContext; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +/** + * Log4JContextWrapper + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java#1 $ + */ +final class Log4JContextWrapper implements ServletContext { + + // TODO: This solution sucks... + // How about starting to create some kind of pluggable decorator system, + // something along the lines of AOP mixins/interceptor pattern.. + // Probably using a dynamic Proxy, delegating to the mixins and or the + // wrapped object based on configuration. + // This way we could simply call ServletUtil.decorate(ServletContext):ServletContext + // And the context would be decorated with all configured mixins at once, + // requiring less bolierplate delegation code, and less layers of wrapping + // (alternatively we could decorate the Servlet/FilterConfig objects). + // See the ServletUtil.createWrapper methods for some hints.. + + + // Something like this: + public static ServletContext wrap(final ServletContext pContext, final Object[] pDelegates, final ClassLoader pLoader) { + ClassLoader cl = pLoader != null ? pLoader : Thread.currentThread().getContextClassLoader(); + + // TODO: Create a "static" mapping between methods in the ServletContext + // and the corresponding delegate + + // TODO: Resolve super-invokations, to delegate to next delegate in + // chain, and finally invoke pContext + + return (ServletContext) Proxy.newProxyInstance(cl, new Class[] {ServletContext.class}, new InvocationHandler() { + public Object invoke(Object pProxy, Method pMethod, Object[] pArgs) throws Throwable { + // TODO: Test if any of the delegates should receive, if so invoke + + // Else, invoke on original object + return pMethod.invoke(pContext, pArgs); + } + }); + } + + private final ServletContext mContext; + + private final Logger mLogger; + + Log4JContextWrapper(ServletContext pContext) { + mContext = pContext; + + // TODO: We want a logger per servlet, not per servlet context, right? + mLogger = Logger.getLogger(pContext.getServletContextName()); + + // TODO: Automatic init/config of Log4J using context parameter for log4j.xml? + // See Log4JInit.java + + // TODO: Automatic config of properties in the context wrapper? + } + + public final void log(final Exception pException, final String pMessage) { + log(pMessage, pException); + } + + // TODO: Add more logging methods to interface info/warn/error? + // TODO: Implement these mehtods in GenericFilter/GenericServlet? + + public void log(String pMessage) { + // TODO: Get logger for caller.. + // Should be possible using some stack peek hack, but that's slow... + // Find a good way... + // Maybe just pass it into the constuctor, and have one wrapper per servlet + mLogger.info(pMessage); + } + + public void log(String pMessage, Throwable pCause) { + // TODO: Get logger for caller.. + + mLogger.error(pMessage, pCause); + } + + public Object getAttribute(String pMessage) { + return mContext.getAttribute(pMessage); + } + + public Enumeration getAttributeNames() { + return mContext.getAttributeNames(); + } + + public ServletContext getContext(String pMessage) { + return mContext.getContext(pMessage); + } + + public String getInitParameter(String pMessage) { + return mContext.getInitParameter(pMessage); + } + + public Enumeration getInitParameterNames() { + return mContext.getInitParameterNames(); + } + + public int getMajorVersion() { + return mContext.getMajorVersion(); + } + + public String getMimeType(String pMessage) { + return mContext.getMimeType(pMessage); + } + + public int getMinorVersion() { + return mContext.getMinorVersion(); + } + + public RequestDispatcher getNamedDispatcher(String pMessage) { + return mContext.getNamedDispatcher(pMessage); + } + + public String getRealPath(String pMessage) { + return mContext.getRealPath(pMessage); + } + + public RequestDispatcher getRequestDispatcher(String pMessage) { + return mContext.getRequestDispatcher(pMessage); + } + + public URL getResource(String pMessage) throws MalformedURLException { + return mContext.getResource(pMessage); + } + + public InputStream getResourceAsStream(String pMessage) { + return mContext.getResourceAsStream(pMessage); + } + + public Set getResourcePaths(String pMessage) { + return mContext.getResourcePaths(pMessage); + } + + public String getServerInfo() { + return mContext.getServerInfo(); + } + + public Servlet getServlet(String pMessage) throws ServletException { + //noinspection deprecation + return mContext.getServlet(pMessage); + } + + public String getServletContextName() { + return mContext.getServletContextName(); + } + + public Enumeration getServletNames() { + //noinspection deprecation + return mContext.getServletNames(); + } + + public Enumeration getServlets() { + //noinspection deprecation + return mContext.getServlets(); + } + + public void removeAttribute(String pMessage) { + mContext.removeAttribute(pMessage); + } + + public void setAttribute(String pMessage, Object pExtension) { + mContext.setAttribute(pMessage, pExtension); + } +} diff --git a/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/package.html b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/package.html new file mode 100755 index 00000000..64817968 --- /dev/null +++ b/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/package.html @@ -0,0 +1,7 @@ + + + +Contains servlet support classes. + + + diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java new file mode 100755 index 00000000..45a4ea6c --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java @@ -0,0 +1,438 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +import java.util.*; +import java.net.URL; +import java.net.MalformedURLException; +import java.io.*; + +import javax.servlet.*; + +/** + * FilterAbstractTestCase + *

+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java#1 $ + */ +public abstract class FilterAbstractTestCase extends ObjectAbstractTestCase { + protected Object makeObject() { + return makeFilter(); + } + + protected abstract Filter makeFilter(); + + // TODO: Is it a good thing to have an API like this? + protected FilterConfig makeFilterConfig() { + return makeFilterConfig(new HashMap()); + } + + protected FilterConfig makeFilterConfig(Map pParams) { + return new MockFilterConfig(pParams); + } + + protected ServletRequest makeRequest() { + return new MockServletRequest(); + } + + protected ServletResponse makeResponse() { + return new MockServletResponse(); + } + + protected FilterChain makeFilterChain() { + return new MockFilterChain(); + } + + public void testInitNull() { + Filter filter = makeFilter(); + + // The spec seems to be a little unclear on this issue, but anyway, + // the container should never invoke init(null)... + try { + filter.init(null); + fail("Should throw Exception on init(null)"); + } + catch (IllegalArgumentException e) { + // Good + } + catch (NullPointerException e) { + // Bad (but not unreasonable) + } + catch (ServletException e) { + // Hmmm.. The jury is still out. + } + } + + public void testInit() { + Filter filter = makeFilter(); + + try { + filter.init(makeFilterConfig()); + } + catch (ServletException e) { + assertNotNull(e.getMessage()); + } + finally { + filter.destroy(); + } + } + + public void testLifeCycle() throws ServletException { + Filter filter = makeFilter(); + + try { + filter.init(makeFilterConfig()); + } + finally { + filter.destroy(); + } + } + + public void testFilterBasic() throws ServletException, IOException { + Filter filter = makeFilter(); + + try { + filter.init(makeFilterConfig()); + + filter.doFilter(makeRequest(), makeResponse(), makeFilterChain()); + } + finally { + filter.destroy(); + } + } + + public void testDestroy() { + // TODO: Implement + } + + static class MockFilterConfig implements FilterConfig { + private final Map mParams; + + MockFilterConfig() { + this(new HashMap()); + } + + MockFilterConfig(Map pParams) { + if (pParams == null) { + throw new IllegalArgumentException("params == null"); + } + mParams = pParams; + } + + public String getFilterName() { + return "mock-filter"; + } + + public String getInitParameter(String pName) { + return (String) mParams.get(pName); + } + + public Enumeration getInitParameterNames() { + return Collections.enumeration(mParams.keySet()); + } + + public ServletContext getServletContext() { + return new MockServletContext(); + } + + private static class MockServletContext implements ServletContext { + private final Map mAttributes; + private final Map mParams; + + MockServletContext() { + mAttributes = new HashMap(); + mParams = new HashMap(); + } + + public Object getAttribute(String s) { + return mAttributes.get(s); + } + + public Enumeration getAttributeNames() { + return Collections.enumeration(mAttributes.keySet()); + } + + public ServletContext getContext(String s) { + return null; // TODO: Implement + } + + public String getInitParameter(String s) { + return (String) mParams.get(s); + } + + public Enumeration getInitParameterNames() { + return Collections.enumeration(mParams.keySet()); + } + + public int getMajorVersion() { + return 0; // TODO: Implement + } + + public String getMimeType(String s) { + return null; // TODO: Implement + } + + public int getMinorVersion() { + return 0; // TODO: Implement + } + + public RequestDispatcher getNamedDispatcher(String s) { + return null; // TODO: Implement + } + + public String getRealPath(String s) { + return null; // TODO: Implement + } + + public RequestDispatcher getRequestDispatcher(String s) { + return null; // TODO: Implement + } + + public URL getResource(String s) throws MalformedURLException { + return null; // TODO: Implement + } + + public InputStream getResourceAsStream(String s) { + return null; // TODO: Implement + } + + public Set getResourcePaths(String s) { + return null; // TODO: Implement + } + + public String getServerInfo() { + return null; // TODO: Implement + } + + public Servlet getServlet(String s) throws ServletException { + return null; // TODO: Implement + } + + public String getServletContextName() { + return "mock"; + } + + public Enumeration getServletNames() { + return null; // TODO: Implement + } + + public Enumeration getServlets() { + return null; // TODO: Implement + } + + public void log(Exception exception, String s) { + // TODO: Implement + } + + public void log(String s) { + // TODO: Implement + } + + public void log(String s, Throwable throwable) { + // TODO: Implement + } + + public void removeAttribute(String s) { + mAttributes.remove(s); + } + + public void setAttribute(String s, Object obj) { + mAttributes.put(s, obj); + } + } + } + + static class MockServletRequest implements ServletRequest { + final private Map mAttributes; + + public MockServletRequest() { + mAttributes = new HashMap(); + } + + public Object getAttribute(String pKey) { + return mAttributes.get(pKey); + } + + public Enumeration getAttributeNames() { + return Collections.enumeration(mAttributes.keySet()); + } + + public String getCharacterEncoding() { + return null; // TODO: Implement + } + + public void setCharacterEncoding(String pMessage) throws UnsupportedEncodingException { + // TODO: Implement + } + + public int getContentLength() { + return 0; // TODO: Implement + } + + public String getContentType() { + return null; // TODO: Implement + } + + public ServletInputStream getInputStream() throws IOException { + return null; // TODO: Implement + } + + public String getParameter(String pMessage) { + return null; // TODO: Implement + } + + public Enumeration getParameterNames() { + return null; // TODO: Implement + } + + public String[] getParameterValues(String pMessage) { + return new String[0]; // TODO: Implement + } + + public Map getParameterMap() { + return null; // TODO: Implement + } + + public String getProtocol() { + return null; // TODO: Implement + } + + public String getScheme() { + return null; // TODO: Implement + } + + public String getServerName() { + return null; // TODO: Implement + } + + public int getServerPort() { + return 0; // TODO: Implement + } + + public BufferedReader getReader() throws IOException { + return null; // TODO: Implement + } + + public String getRemoteAddr() { + return null; // TODO: Implement + } + + public String getRemoteHost() { + return null; // TODO: Implement + } + + public void setAttribute(String pKey, Object pValue) { + mAttributes.put(pKey, pValue); + } + + public void removeAttribute(String pKey) { + mAttributes.remove(pKey); + } + + public Locale getLocale() { + return null; // TODO: Implement + } + + public Enumeration getLocales() { + return null; // TODO: Implement + } + + public boolean isSecure() { + return false; // TODO: Implement + } + + public RequestDispatcher getRequestDispatcher(String pMessage) { + return null; // TODO: Implement + } + + public String getRealPath(String pMessage) { + return null; // TODO: Implement + } + + public int getRemotePort() { + throw new UnsupportedOperationException("Method getRemotePort not implemented");// TODO: Implement + } + + public String getLocalName() { + throw new UnsupportedOperationException("Method getLocalName not implemented");// TODO: Implement + } + + public String getLocalAddr() { + throw new UnsupportedOperationException("Method getLocalAddr not implemented");// TODO: Implement + } + + public int getLocalPort() { + throw new UnsupportedOperationException("Method getLocalPort not implemented");// TODO: Implement + } + } + + static class MockServletResponse implements ServletResponse { + public void flushBuffer() throws IOException { + // TODO: Implement + } + + public int getBufferSize() { + return 0; // TODO: Implement + } + + public String getCharacterEncoding() { + return null; // TODO: Implement + } + + public String getContentType() { + throw new UnsupportedOperationException("Method getContentType not implemented");// TODO: Implement + } + + public Locale getLocale() { + return null; // TODO: Implement + } + + public ServletOutputStream getOutputStream() throws IOException { + return null; // TODO: Implement + } + + public PrintWriter getWriter() throws IOException { + return null; // TODO: Implement + } + + public void setCharacterEncoding(String charset) { + throw new UnsupportedOperationException("Method setCharacterEncoding not implemented");// TODO: Implement + } + + public boolean isCommitted() { + return false; // TODO: Implement + } + + public void reset() { + // TODO: Implement + } + + public void resetBuffer() { + // TODO: Implement + } + + public void setBufferSize(int pLength) { + // TODO: Implement + } + + public void setContentLength(int pLength) { + // TODO: Implement + } + + public void setContentType(String pMessage) { + // TODO: Implement + } + + public void setLocale(Locale pLocale) { + // TODO: Implement + } + } + + static class MockFilterChain implements FilterChain { + public void doFilter(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException { + // TODO: Implement + } + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java new file mode 100755 index 00000000..75170b8d --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java @@ -0,0 +1,151 @@ +package com.twelvemonkeys.servlet; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +import javax.servlet.*; + +/** + * GenericFilterTestCase + *

+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java#1 $ + */ +public final class GenericFilterTestCase extends FilterAbstractTestCase { + protected Filter makeFilter() { + return new GenericFilterImpl(); + } + + public void testInitOncePerRequest() { + // Default FALSE + GenericFilter filter = new GenericFilterImpl(); + + try { + filter.init(makeFilterConfig()); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + assertFalse("OncePerRequest should default to false", filter.mOncePerRequest); + filter.destroy(); + + // TRUE + filter = new GenericFilterImpl(); + Map params = new HashMap(); + params.put("once-per-request", "true"); + + try { + filter.init(makeFilterConfig(params)); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + assertTrue("oncePerRequest should be true", filter.mOncePerRequest); + filter.destroy(); + + // TRUE + filter = new GenericFilterImpl(); + params = new HashMap(); + params.put("oncePerRequest", "true"); + + try { + filter.init(makeFilterConfig(params)); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + assertTrue("oncePerRequest should be true", filter.mOncePerRequest); + filter.destroy(); + } + + public void testFilterOnlyOnce() { + final GenericFilterImpl filter = new GenericFilterImpl(); + filter.setOncePerRequest(true); + + try { + filter.init(makeFilterConfig()); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + FilterChain chain = new MyFilterChain(new Filter[] {filter, filter, filter}); + + try { + chain.doFilter(makeRequest(), makeResponse()); + } + catch (IOException e) { + fail(e.getMessage()); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + assertEquals("Filter was invoked more than once!", 1, filter.invocationCount); + + filter.destroy(); + } + + public void testFilterMultiple() { + final GenericFilterImpl filter = new GenericFilterImpl(); + + try { + filter.init(makeFilterConfig()); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + FilterChain chain = new MyFilterChain(new Filter[] { + filter, filter, filter, filter, filter + }); + + try { + chain.doFilter(makeRequest(), makeResponse()); + } + catch (IOException e) { + fail(e.getMessage()); + } + catch (ServletException e) { + fail(e.getMessage()); + } + + assertEquals("Filter was invoked not invoked five times!", 5, filter.invocationCount); + + filter.destroy(); + } + + private static class GenericFilterImpl extends GenericFilter { + int invocationCount; + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + invocationCount++; + pChain.doFilter(pRequest, pResponse); + } + } + + private static class MyFilterChain implements FilterChain { + + Filter[] mFilters; + int mCurrentFilter; + + public MyFilterChain(Filter[] pFilters) { + if (pFilters == null) { + throw new IllegalArgumentException("filters == null"); + } + mFilters = pFilters; + mCurrentFilter = 0; + } + + public void doFilter(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException { + if (mCurrentFilter < mFilters.length) { + mFilters[mCurrentFilter++].doFilter(pRequest, pResponse, this); + } + } + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java new file mode 100755 index 00000000..db23185d --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java @@ -0,0 +1,93 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.io.NullOutputStream; + +import junit.framework.TestCase; + +import java.io.PrintWriter; + +/** + * ServletConfigExceptionTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java#2 $ + */ +public class ServletConfigExceptionTestCase extends TestCase { + public void testThrowCatchPrintStacktrace() { + try { + throw new ServletConfigException("FooBar!"); + } + catch (ServletConfigException e) { + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } + + public void testThrowCatchGetNoCause() { + try { + throw new ServletConfigException("FooBar!"); + } + catch (ServletConfigException e) { + assertEquals(null, e.getRootCause()); // Old API + assertEquals(null, e.getCause()); + + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } + + public void testThrowCatchInitCauseNull() { + try { + ServletConfigException e = new ServletConfigException("FooBar!"); + e.initCause(null); + throw e; + } + catch (ServletConfigException e) { + assertEquals(null, e.getRootCause()); // Old API + assertEquals(null, e.getCause()); + + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } + + public void testThrowCatchInitCause() { + //noinspection ThrowableInstanceNeverThrown + Exception cause = new Exception(); + try { + ServletConfigException exception = new ServletConfigException("FooBar!"); + exception.initCause(cause); + throw exception; + } + catch (ServletConfigException e) { + // NOTE: We don't know how the superclass is implemented, so we assume nothing here + //assertEquals(null, e.getRootCause()); // Old API + assertSame(cause, e.getCause()); + + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } + + public void testThrowCatchGetNullCause() { + try { + throw new ServletConfigException("FooBar!", null); + } + catch (ServletConfigException e) { + assertEquals(null, e.getRootCause()); // Old API + assertEquals(null, e.getCause()); + + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } + + public void testThrowCatchGetCause() { + IllegalStateException cause = new IllegalStateException(); + try { + throw new ServletConfigException("FooBar caused by stupid API!", cause); + } + catch (ServletConfigException e) { + assertSame(cause, e.getRootCause()); // Old API + assertSame(cause, e.getCause()); + + e.printStackTrace(new PrintWriter(new NullOutputStream())); + } + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java new file mode 100755 index 00000000..7ba56d50 --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java @@ -0,0 +1,192 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.MapAbstractTestCase; + +import javax.servlet.*; +import java.util.*; +import java.io.Serializable; +import java.io.InputStream; +import java.net.URL; +import java.net.MalformedURLException; + +/** + * ServletConfigMapAdapterTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java#3 $ + */ +public abstract class ServletConfigMapAdapterTestCase extends MapAbstractTestCase { + + public boolean isPutAddSupported() { + return false; + } + + public boolean isPutChangeSupported() { + return false; + } + + public boolean isRemoveSupported() { + return false; + } + + public boolean isSetValueSupported() { + return false; + } + + private static class TestConfig implements ServletConfig, FilterConfig, ServletContext, Serializable, Cloneable { + Map mMap = new HashMap(); + + public String getServletName() { + return "dummy"; // Not needed for this test + } + + public String getFilterName() { + return getServletName(); + } + + public String getServletContextName() { + return getServletName(); + } + + + public ServletContext getServletContext() { + throw new UnsupportedOperationException("Method getSerlvetContext not implemented"); + } + + public String getInitParameter(String s) { + return (String) mMap.get(s); + } + + public Enumeration getInitParameterNames() { + //noinspection unchecked + return Collections.enumeration(mMap.keySet()); + } + + public ServletContext getContext(String uripath) { + throw new UnsupportedOperationException("Method getContext not implemented"); + } + + public int getMajorVersion() { + throw new UnsupportedOperationException("Method getMajorVersion not implemented"); + } + + public int getMinorVersion() { + throw new UnsupportedOperationException("Method getMinorVersion not implemented"); + } + + public String getMimeType(String file) { + throw new UnsupportedOperationException("Method getMimeType not implemented"); + } + + public Set getResourcePaths(String path) { + throw new UnsupportedOperationException("Method getResourcePaths not implemented"); + } + + public URL getResource(String path) throws MalformedURLException { + throw new UnsupportedOperationException("Method getResource not implemented"); + } + + public InputStream getResourceAsStream(String path) { + throw new UnsupportedOperationException("Method getResourceAsStream not implemented"); + } + + public RequestDispatcher getRequestDispatcher(String path) { + throw new UnsupportedOperationException("Method getRequestDispatcher not implemented"); + } + + public RequestDispatcher getNamedDispatcher(String name) { + throw new UnsupportedOperationException("Method getNamedDispatcher not implemented"); + } + + public Servlet getServlet(String name) throws ServletException { + throw new UnsupportedOperationException("Method getServlet not implemented"); + } + + public Enumeration getServlets() { + throw new UnsupportedOperationException("Method getServlets not implemented"); + } + + public Enumeration getServletNames() { + throw new UnsupportedOperationException("Method getServletNames not implemented"); + } + + public void log(String msg) { + throw new UnsupportedOperationException("Method log not implemented"); + } + + public void log(Exception exception, String msg) { + throw new UnsupportedOperationException("Method log not implemented"); + } + + public void log(String message, Throwable throwable) { + throw new UnsupportedOperationException("Method log not implemented"); + } + + public String getRealPath(String path) { + throw new UnsupportedOperationException("Method getRealPath not implemented"); + } + + public String getServerInfo() { + throw new UnsupportedOperationException("Method getServerInfo not implemented"); + } + + public Object getAttribute(String name) { + throw new UnsupportedOperationException("Method getAttribute not implemented"); + } + + public Enumeration getAttributeNames() { + throw new UnsupportedOperationException("Method getAttributeNames not implemented"); + } + + public void setAttribute(String name, Object object) { + throw new UnsupportedOperationException("Method setAttribute not implemented"); + } + + public void removeAttribute(String name) { + throw new UnsupportedOperationException("Method removeAttribute not implemented"); + } + } + + public static final class ServletConfigMapTestCase extends ServletConfigMapAdapterTestCase { + + public Map makeEmptyMap() { + ServletConfig config = new TestConfig(); + return new ServletConfigMapAdapter(config); + } + + public Map makeFullMap() { + ServletConfig config = new TestConfig(); + addSampleMappings(((TestConfig) config).mMap); + return new ServletConfigMapAdapter(config); + } + } + + public static final class FilterConfigMapTestCase extends ServletConfigMapAdapterTestCase { + + public Map makeEmptyMap() { + FilterConfig config = new TestConfig(); + return new ServletConfigMapAdapter(config); + } + + public Map makeFullMap() { + FilterConfig config = new TestConfig(); + addSampleMappings(((TestConfig) config).mMap); + return new ServletConfigMapAdapter(config); + } + } + + public static final class ServletContextMapTestCase extends ServletConfigMapAdapterTestCase { + + public Map makeEmptyMap() { + ServletContext config = new TestConfig(); + return new ServletConfigMapAdapter(config); + } + + public Map makeFullMap() { + FilterConfig config = new TestConfig(); + addSampleMappings(((TestConfig) config).mMap); + return new ServletConfigMapAdapter(config); + } + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java new file mode 100755 index 00000000..cd078d9c --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java @@ -0,0 +1,103 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.MapAbstractTestCase; +import org.jmock.Mock; +import org.jmock.core.Invocation; +import org.jmock.core.Stub; +import org.jmock.core.stub.CustomStub; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * ServletConfigMapAdapterTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java#1 $ + */ +public class ServletHeadersMapAdapterTestCase extends MapAbstractTestCase { + private static final List HEADER_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\""); + private static final List HEADER_VALUE_DATE = Arrays.asList(new Date().toString()); + private static final List HEADER_VALUE_FOO = Arrays.asList("one", "two"); + + public boolean isPutAddSupported() { + return false; + } + + public boolean isPutChangeSupported() { + return false; + } + + public boolean isRemoveSupported() { + return false; + } + + public boolean isSetValueSupported() { + return false; + } + + @Override + public boolean isTestSerialization() { + return false; + } + + public Map makeEmptyMap() { + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getHeaderNames").will(returnValue(Collections.enumeration(Collections.emptyList()))); + mockRequest.stubs().method("getHeaders").will(returnValue(null)); + + return new SerlvetHeadersMapAdapter((HttpServletRequest) mockRequest.proxy()); + } + + @Override + public Map makeFullMap() { + Mock mockRequest = mock(HttpServletRequest.class); + + mockRequest.stubs().method("getHeaderNames").will(returnEnumeration("ETag", "Date", "X-Foo")); + mockRequest.stubs().method("getHeaders").with(eq("Date")).will(returnEnumeration(HEADER_VALUE_DATE)); + mockRequest.stubs().method("getHeaders").with(eq("ETag")).will(returnEnumeration(HEADER_VALUE_ETAG)); + mockRequest.stubs().method("getHeaders").with(eq("X-Foo")).will(returnEnumeration(HEADER_VALUE_FOO)); + mockRequest.stubs().method("getHeaders").with(not(or(eq("Date"), or(eq("ETag"), eq("X-Foo"))))).will(returnValue(null)); + + return new SerlvetHeadersMapAdapter((HttpServletRequest) mockRequest.proxy()); + } + + @Override + public Object[] getSampleKeys() { + return new String[] {"Date", "ETag", "X-Foo"}; + } + + @Override + public Object[] getSampleValues() { + return new Object[] {HEADER_VALUE_DATE, HEADER_VALUE_ETAG, HEADER_VALUE_FOO}; + } + + + @Override + public Object[] getNewSampleValues() { + // Needs to be same length but different values + return new Object[3]; + } + + protected Stub returnEnumeration(final Object... pValues) { + return new EnumerationStub(Arrays.asList(pValues)); + } + + protected Stub returnEnumeration(final List pValues) { + return new EnumerationStub(pValues); + } + + private static class EnumerationStub extends CustomStub { + private List mValues; + + public EnumerationStub(final List pValues) { + super("Returns a new enumeration"); + mValues = pValues; + } + + public Object invoke(Invocation invocation) throws Throwable { + return Collections.enumeration(mValues); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java new file mode 100755 index 00000000..af0f606e --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java @@ -0,0 +1,102 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.util.MapAbstractTestCase; +import org.jmock.Mock; +import org.jmock.core.Invocation; +import org.jmock.core.Stub; +import org.jmock.core.stub.CustomStub; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * ServletConfigMapAdapterTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java#1 $ + */ +public class ServletParametersMapAdapterTestCase extends MapAbstractTestCase { + private static final List PARAM_VALUE_ETAG = Arrays.asList("\"1234567890abcdef\""); + private static final List PARAM_VALUE_DATE = Arrays.asList(new Date().toString()); + private static final List PARAM_VALUE_FOO = Arrays.asList("one", "two"); + + public boolean isPutAddSupported() { + return false; + } + + public boolean isPutChangeSupported() { + return false; + } + + public boolean isRemoveSupported() { + return false; + } + + public boolean isSetValueSupported() { + return false; + } + + @Override + public boolean isTestSerialization() { + return false; + } + + public Map makeEmptyMap() { + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getParameterNames").will(returnValue(Collections.enumeration(Collections.emptyList()))); + mockRequest.stubs().method("getParameterValues").will(returnValue(null)); + + return new SerlvetParametersMapAdapter((HttpServletRequest) mockRequest.proxy()); + } + + @Override + public Map makeFullMap() { + Mock mockRequest = mock(HttpServletRequest.class); + + mockRequest.stubs().method("getParameterNames").will(returnEnumeration("tag", "date", "foo")); + mockRequest.stubs().method("getParameterValues").with(eq("date")).will(returnValue(PARAM_VALUE_DATE.toArray(new String[PARAM_VALUE_DATE.size()]))); + mockRequest.stubs().method("getParameterValues").with(eq("tag")).will(returnValue(PARAM_VALUE_ETAG.toArray(new String[PARAM_VALUE_ETAG.size()]))); + mockRequest.stubs().method("getParameterValues").with(eq("foo")).will(returnValue(PARAM_VALUE_FOO.toArray(new String[PARAM_VALUE_FOO.size()]))); + mockRequest.stubs().method("getParameterValues").with(not(or(eq("date"), or(eq("tag"), eq("foo"))))).will(returnValue(null)); + + return new SerlvetParametersMapAdapter((HttpServletRequest) mockRequest.proxy()); + } + + @Override + public Object[] getSampleKeys() { + return new String[] {"date", "tag", "foo"}; + } + + @Override + public Object[] getSampleValues() { + return new Object[] {PARAM_VALUE_DATE, PARAM_VALUE_ETAG, PARAM_VALUE_FOO}; + } + + @Override + public Object[] getNewSampleValues() { + // Needs to be same length but different values + return new Object[3]; + } + + protected Stub returnEnumeration(final Object... pValues) { + return new EnumerationStub(Arrays.asList(pValues)); + } + + protected Stub returnEnumeration(final List pValues) { + return new EnumerationStub(pValues); + } + + private static class EnumerationStub extends CustomStub { + private List mValues; + + public EnumerationStub(final List pValues) { + super("Returns a new enumeration"); + mValues = pValues; + } + + public Object invoke(Invocation invocation) throws Throwable { + return Collections.enumeration(mValues); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletResponseAbsrtactTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletResponseAbsrtactTestCase.java new file mode 100755 index 00000000..222b5ea4 --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletResponseAbsrtactTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +import javax.servlet.ServletResponse; + +/** + * ServletResponseAbsrtactTestCase + *

+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletResponseAbsrtactTestCase.java#1 $ + */ +public abstract class ServletResponseAbsrtactTestCase extends ObjectAbstractTestCase { + protected Object makeObject() { + return makeServletResponse(); + } + + protected abstract ServletResponse makeServletResponse(); + + // TODO: Implement +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java new file mode 100755 index 00000000..dc3f283f --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java @@ -0,0 +1,111 @@ +package com.twelvemonkeys.servlet; + +import com.twelvemonkeys.io.OutputStreamAbstractTestCase; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.servlet.Filter; +import javax.servlet.ServletResponse; + +/** + * TrimWhiteSpaceFilterTestCase + *

+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java#1 $ + */ +public class TrimWhiteSpaceFilterTestCase extends FilterAbstractTestCase { + protected Filter makeFilter() { + return new TrimWhiteSpaceFilter(); + } + + public static final class TrimWSFilterOutputStreamTestCase extends OutputStreamAbstractTestCase { + + protected OutputStream makeObject() { + // NOTE: ByteArrayOutputStream does not implement flush or close... + return makeOutputStream(new ByteArrayOutputStream(16)); + } + + protected OutputStream makeOutputStream(OutputStream pWrapped) { + return new TrimWhiteSpaceFilter.TrimWSFilterOutputStream(pWrapped); + } + + public void testTrimWSOnlyWS() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + OutputStream trim = makeOutputStream(out); + + String input = " \n\n\t \t" + (char) 0x0a + ' ' + (char) 0x0d + "\r "; + + trim.write(input.getBytes()); + trim.flush(); + trim.close(); + + assertEquals("Should be trimmed", "\"\"", '"' + new String(out.toByteArray()) + '"'); + } + + public void testTrimWSLeading() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + OutputStream trim = makeOutputStream(out); + + byte[] input = " \n\n\t \t".getBytes(); + String trimmed = "\n "; // TODO: This is pr spec (the trailing space). But probably quite stupid... + + trim.write(input); + trim.flush(); + trim.close(); + + assertEquals("Should be trimmed", '"' + trimmed + '"', '"' + new String(out.toByteArray()) + '"'); + } + + public void testTrimWSOffsetLength() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(64); + OutputStream trim = makeOutputStream(out); + + // Kindly generated by http://lipsum.org/ :-) + byte[] input = (" \n\tLorem ipsum dolor sit amet, consectetuer adipiscing elit.\n\r\n\r" + + "Etiam arcu neque, \n\rmalesuada blandit,\t\n\r\n\r\n\n\n\r\n\r\r\n\n\t rutrum quis, molestie at, diam.\n" + + " Nulla elementum elementum eros.\n \t\t\n\r" + + "Ut rhoncus, turpis in pellentesque volutpat, sapien sem accumsan augue, a scelerisque nibh erat vel magna.\n" + + " Phasellus diam orci, dignissim et, gravida vitae, venenatis eu, elit.\n" + + "\t\t\tSuspendisse dictum enim at nisl. Integer magna erat, viverra sit amet, consectetuer nec, accumsan ut, mi.\n" + + "\n\r\r\r\n\rNunc ultricies \n\n\n consectetuer mauris. " + + "Nulla lectus mauris, viverra ac, pulvinar a, commodo quis, nulla.\n " + + "Ut eget nulla. In est dolor, convallis \t non, tincidunt \tvestibulum, porttitor et, eros.\n " + + "\t\t \t \n\rDonec vehicula ultrices nisl.").getBytes(); + + String trimmed = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.\n" + + "Etiam arcu neque, malesuada blandit,\trutrum quis, molestie at, diam.\n" + + "Nulla elementum elementum eros.\n" + + "Ut rhoncus, turpis in pellentesque volutpat, sapien sem accumsan augue, a scelerisque nibh erat vel magna.\n" + + "Phasellus diam orci, dignissim et, gravida vitae, venenatis eu, elit.\n" + + "Suspendisse dictum enim at nisl. Integer magna erat, viverra sit amet, consectetuer nec, accumsan ut, mi.\n" + + "Nunc ultricies consectetuer mauris. Nulla lectus mauris, viverra ac, pulvinar a, commodo quis, nulla.\n" + + "Ut eget nulla. In est dolor, convallis non, tincidunt vestibulum, porttitor et, eros.\n" + + "Donec vehicula ultrices nisl."; + + int chunkLenght = 5; + int bytesLeft = input.length; + while (bytesLeft > chunkLenght) { + trim.write(input, input.length - bytesLeft, chunkLenght); + bytesLeft -= chunkLenght; + } + trim.write(input, input.length - bytesLeft, bytesLeft); + + trim.flush(); + trim.close(); + + assertEquals("Should be trimmed", '"' + trimmed + '"', '"' + new String(out.toByteArray()) + '"'); + } + + // TODO: Test that we DON'T remove too much... + } + + public static final class TrimWSServletResponseWrapperTestCase extends ServletResponseAbsrtactTestCase { + protected ServletResponse makeServletResponse() { + return new TrimWhiteSpaceFilter.TrimWSServletResponseWrapper(new MockServletResponse()); + } + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java new file mode 100755 index 00000000..a89e4a61 --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java @@ -0,0 +1,1306 @@ +package com.twelvemonkeys.servlet.cache; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.net.NetUtil; +import org.jmock.Mock; +import org.jmock.cglib.MockObjectTestCase; +import org.jmock.core.Invocation; +import org.jmock.core.stub.CustomStub; + +import javax.servlet.ServletContext; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.*; + +/** + * CacheManagerTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java#2 $ + */ +public class HTTPCacheTestCase extends MockObjectTestCase { + // TODO: Clean up! + + private static final File TEMP_ROOT = new File(FileUtil.getTempDirFile(), "cache-test"); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + assertTrue("Could not create temp dir, tests can not run", (TEMP_ROOT.exists() && TEMP_ROOT.isDirectory()) || TEMP_ROOT.mkdirs()); + // Clear temp dir + File[] files = TEMP_ROOT.listFiles(); + for (File file : files) { + file.delete(); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testCreateNegativeNoName() { + try { + new HTTPCache(null, (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true); + fail("Expected creation failure, no name"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("name")); + assertTrue(message.contains("null")); + } + + try { + new HTTPCache("", (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true); + fail("Expected creation failure, empty name"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("name")); + assertTrue(message.contains("empty")); + } + } + + public void testCreateNegativeNoContext() { + try { + new HTTPCache("Dummy", null, 500, 0, 10, true); + fail("Expected creation failure, no context"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("context")); + assertTrue(message.contains("null")); + } + + } + + public void testCreateNegativeNoTempFolder() { + try { + new HTTPCache(null, 500, 0, 10, true); + fail("Expected creation failure, no temp folder"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("temp")); + assertTrue(message.contains("folder")); + assertTrue(message.contains("null")); + } + } + + public void testCreateNegativeValues() { + try { + new HTTPCache(TEMP_ROOT, -1, 0, 10, true); + fail("Expected creation failure"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("negative")); + assertTrue(message.contains("expiry time")); + } + + try { + new HTTPCache(TEMP_ROOT, 1000, -1, 10, false); + fail("Expected creation failure"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("negative")); + assertTrue(message.contains("cache size")); + } + + try { + new HTTPCache(TEMP_ROOT, 1000, 128, -1, true); + fail("Expected creation failure"); + } + catch (IllegalArgumentException expected) { + String message = expected.getMessage().toLowerCase(); + assertTrue(message.contains("negative")); + assertTrue(message.contains("number")); + } + } + + public void testCreate() { + new HTTPCache(TEMP_ROOT, 500, 0, 10, true); + } + + public void testCreateServletContext() { + Mock mockContext = mock(ServletContext.class); + // Currently context is used for tempdir and logging + mockContext.stubs().method("getAttribute").with(eq("javax.servlet.context.tempdir")).will(returnValue(TEMP_ROOT)); + new HTTPCache("cache", (ServletContext) mockContext.proxy(), 500, 0, 10, true); + } + + public void testCacheableRequest() throws IOException, CacheException { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + private String createRequestURI() { + return "http://www.foo.com/" + getName() + ".bar"; + } + + public void testCacheableRequestWithParameters() throws IOException, CacheException { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + Map> parameters = new HashMap>(); + parameters.put("foo", Collections.singletonList("bar")); + parameters.put("params", Arrays.asList("une", "due", "tres")); + CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), parameters, null); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + private CacheRequest configureRequest(Mock pMockRequest, String pRequestURI) { + return configureRequest(pMockRequest, "GET", pRequestURI, null, null); + } + + private CacheRequest configureRequest(Mock pMockRequest, String pMethod, String pRequestURI, Map> pParameters, final Map> pHeaders) { + pMockRequest.reset(); + pMockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(pRequestURI))); + pMockRequest.stubs().method("getParameters").will(returnValue(pParameters == null ? Collections.emptyMap() : pParameters)); + pMockRequest.stubs().method("getHeaders").will(returnValue(pHeaders == null ? Collections.emptyMap() : pHeaders)); + pMockRequest.stubs().method("getMethod").will(returnValue(pMethod)); + return (CacheRequest) pMockRequest.proxy(); + } + + public void testCacheablePersistentRepeatedRequest() throws IOException, CacheException { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + mockRequest.verify(); + mockResponse.verify(); + mockResolver.verify(); + + // Reset + result.reset(); + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + // Test request again, make sure resolve is executed exactly once + HTTPCache cache2 = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + cache2.doCached(request, response, resolver); + + // Test that second response is equal to first + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testCacheableRepeatedRequest() throws IOException, CacheException { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + mockRequest.verify(); + mockResponse.verify(); + mockResolver.verify(); + + // Reset + result.reset(); + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + // Test request again, make sure resolve is executed exactly once + cache.doCached(request, response, resolver); + + // Test that second response is equal to first + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testNonCacheableRequestHeader() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store"))); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // TODO: How do we know that the response was NOT cached? + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testNonCacheableRequestHeaderRepeated() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + String requestURI = createRequestURI(); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, requestURI); + + Mock mockResponse = mock(CacheResponse.class); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockResolver.verify(); + + // Reset + result.reset(); + + mockRequest.reset(); + mockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(requestURI))); + mockRequest.stubs().method("getParameters").will(returnValue(Collections.emptyMap())); + mockRequest.stubs().method("getHeaders").will(returnValue(Collections.singletonMap("Cache-Control", Collections.singletonList("no-cache")))); // Force non-cached version of cached content + mockRequest.stubs().method("getMethod").will(returnValue("GET")); + + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub2") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + value[3] = 'B'; + + // This cache should not be cached + cache.doCached(request, response, resolver); + + // Verify that second reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testNonCacheableResponseHeader() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-cache")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Cache-Control", "no-cache"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testNonCacheableResponseHeaderRepeated() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Cache-Control", "no-store"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Cache-Control", "no-store"); + res.getOutputStream().write(value); + + return null; + } + }); + result.reset(); + value[3] = 'B'; + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + // Test non-cacheable response + public void testNonCacheableResponse() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + Mock mockResponse = mock(CacheResponse.class); + mockResponse.expects(once()).method("setStatus").with(eq(500)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(500); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + // Test non-cacheable response + public void testNonCacheableResponseRepeated() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + Mock mockResponse = mock(CacheResponse.class); + mockResponse.expects(once()).method("setStatus").with(eq(500)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(500); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockResolver.verify(); + + // Test request again, should do new resolve... + result.reset(); + value[3] = 'B'; + + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(500)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(500); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.getOutputStream().write(value); + + return null; + } + }); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + + // Test that request headers are forwarded to resolver... + public void testRequestHeadersForwarded() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + final Map> headers = new LinkedHashMap>(); + headers.put("Cache-Control", Arrays.asList("no-cache")); + headers.put("X-Custom", Arrays.asList("FOO", "BAR")); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, "HEAD", createRequestURI(), null, headers); + + Mock mockResponse = mock(CacheResponse.class); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + + Map> reqHeaders = req.getHeaders(); + assertEquals(headers, reqHeaders); + + // Make sure that we preserve insertion order + Set>> expected = headers.entrySet(); + Iterator>> actual = reqHeaders.entrySet().iterator(); + for (Map.Entry> entry : expected) { + assertEquals(entry, actual.next()); + } + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + } + + // Test that response headers are preserved + public void testCacheablePreserveResponseHeaders() throws IOException, CacheException { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + Mock mockResponse = mock(CacheResponse.class); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Cache-Control"), eq("public")); + mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("X-Custom"), eq("FOO")).id("firstCustom"); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("addHeader").with(eq("X-Custom"), eq("BAR")).after("firstCustom"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.stubs().method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Cache-Control", "public"); + res.addHeader("X-Custom", "FOO"); + res.addHeader("X-Custom", "BAR"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + // Test Vary + public void testVaryMissingRequestHeader() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar"); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("X-Foo", "foobar header"); + res.setHeader("X-Other", "don't care"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + result.reset(); + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testVaryMissingRequestHeaderRepeated() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + String requestURI = createRequestURI(); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, requestURI); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar"); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("X-Other", "don't care"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar"))); + + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("X-Other", "don't care"); + res.getOutputStream().write(value); + + return null; + } + }); + result.reset(); + value[3] = 'B'; + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testVarySameResourceIsCached() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar value"))); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("X-Other", "don't care"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + result.reset(); + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testVaryDifferentResources() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + String requestURI = createRequestURI(); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foo"))); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foo".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("X-Other", "don't care"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("bar"))); + + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "X-Foo"); + res.setHeader("Cache-Control", "no-store"); + res.getOutputStream().write(value); + + return null; + } + }); + result.reset(); + value[0] = 'b'; + value[1] = 'a'; + value[2] = 'r'; + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + public void testVaryVariations() { + fail("TODO"); + } + + public void testVarationsWithSameContentType() { + // I believe there is a bug if two variations has same content type... + fail("TODO"); + } + + public void testVaryStarNonCached() throws Exception { + HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true); + + // Custom setup + Mock mockRequest = mock(CacheRequest.class); + CacheRequest request = configureRequest(mockRequest, createRequestURI()); + + Mock mockResponse = mock(CacheResponse.class); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + CacheResponse response = (CacheResponse) mockResponse.proxy(); + + final byte[] value = "foobar".getBytes("UTF-8"); + + Mock mockResolver = mock(ResponseResolver.class); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "*"); + res.getOutputStream().write(value); + + return null; + } + }); + ResponseResolver resolver = (ResponseResolver) mockResolver.proxy(); + + // Do the invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + + mockRequest.verify(); + mockResponse.verify(); + mockRequest.verify(); + + // Reset + mockResponse.reset(); + mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK)); + mockResponse.stubs().method("setHeader"); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING); + mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*")); + mockResponse.stubs().method("addHeader"); + mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result)); + + mockResolver.reset(); + mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") { + public Void invoke(Invocation invocation) throws Throwable { + CacheRequest req = (CacheRequest) invocation.parameterValues.get(0); + CacheResponse res = (CacheResponse) invocation.parameterValues.get(1); + + res.setStatus(HTTPCache.STATUS_OK); + res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Vary", "*"); + res.getOutputStream().write(value); + + return null; + } + }); + result.reset(); + value[3] = 'B'; + + // Repeat invocation + cache.doCached(request, response, resolver); + + // Verify that reponse is ok + assertEquals(value.length, result.size()); + assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8")); + assertTrue(Arrays.equals(value, result.toByteArray())); + } + + // Test failing request (IOException) + public void testIOException() { + fail("TODO"); + } + + public void testCacheException() { + fail("TODO"); + } + + public void testRuntimeException() { + fail("TODO"); + } + + // Test failing (negative) HTTP response (401, 404, 410, 500, etc) + public void testNegativeCache() { + fail("TODO"); + } + + public void testNegativeCacheExpires() { + fail("TODO"); + } + + // Test If-None-Match/ETag support + public void testIfNoneMatch() { + fail("TODO"); + } + + // Test If-Modified-Since support + public void testIfModifiedSince() { + fail("TODO"); + } + + // Test that data really expires when TTL is over + public void testTimeToLive() { + fail("TODO"); + } + + public void testMaxAgeRequest() { + fail("TODO"); + } + + // Test that for requests with authorization, responses are not shared between different authorized users, unless response is marked as Cache-Control: public + public void testAuthorizedRequestPublic() { + fail("TODO"); + } + + public void testAuthorizedRequestPrivate() { + fail("TODO"); + } + + public void testPutPostDeleteInvalidatesCache() { + fail("TODO"); + } + + // TODO: Move out to separate package/test, just keep it here for PoC + public void testClientRequest() { + fail("Not implemented"); + } + + // TODO: Move out to separate package/test, just keep it here for PoC + public void testServletRequest() { + fail("Not implemented"); + } +} diff --git a/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java new file mode 100755 index 00000000..fe4fc100 --- /dev/null +++ b/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java @@ -0,0 +1,1184 @@ +package com.twelvemonkeys.servlet.image; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.servlet.OutputStreamAdapter; +import org.jmock.Mock; +import org.jmock.cglib.MockObjectTestCase; +import org.junit.Test; + +import javax.imageio.ImageIO; +import javax.servlet.ServletContext; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Arrays; + +/** + * ImageServletResponseImplTestCase + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java#6 $ + */ +public class ImageServletResponseImplTestCase extends MockObjectTestCase { + private static final String CONTENT_TYPE_BMP = "image/bmp"; + private static final String CONTENT_TYPE_FOO = "foo/bar"; + private static final String CONTENT_TYPE_JPEG = "image/jpeg"; + private static final String CONTENT_TYPE_PNG = "image/png"; + private static final String CONTENT_TYPE_TEXT = "text/plain"; + + private static final String IMAGE_NAME = "12monkeys-splash.png"; + + private static final Dimension IMAGE_DIMENSION = new Dimension(300, 410); + private HttpServletRequest mRequest; + private ServletContext mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").will(returnValue(null)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + mRequest = (HttpServletRequest) mockRequest.proxy(); + + Mock mockContext = mock(ServletContext.class); + mockContext.stubs().method("getResource").with(eq("/" + IMAGE_NAME)).will(returnValue(getClass().getResource(IMAGE_NAME))); + mockContext.stubs().method("log").withAnyArguments(); // Just supress the logging + mockContext.stubs().method("getMimeType").with(eq("file.bmp")).will(returnValue(CONTENT_TYPE_BMP)); + mockContext.stubs().method("getMimeType").with(eq("file.foo")).will(returnValue(CONTENT_TYPE_FOO)); + mockContext.stubs().method("getMimeType").with(eq("file.jpeg")).will(returnValue(CONTENT_TYPE_JPEG)); + mockContext.stubs().method("getMimeType").with(eq("file.png")).will(returnValue(CONTENT_TYPE_PNG)); + mockContext.stubs().method("getMimeType").with(eq("file.txt")).will(returnValue(CONTENT_TYPE_TEXT)); + mContext = (ServletContext) mockContext.proxy(); + } + + private void fakeResponse(HttpServletRequest pRequest, ImageServletResponseImpl pImageResponse) throws IOException { + String uri = pRequest.getRequestURI(); + int index = uri.lastIndexOf('/'); + assertTrue(uri, index >= 0); + + String name = uri.substring(index + 1); + InputStream in = getClass().getResourceAsStream(name); + + if (in == null) { + pImageResponse.sendError(HttpServletResponse.SC_NOT_FOUND, uri + " not found"); + } + else { + String ext = name.substring(name.lastIndexOf(".")); + pImageResponse.setContentType(mContext.getMimeType("file" + ext)); + pImageResponse.setContentLength(234); + try { + ServletOutputStream out = pImageResponse.getOutputStream(); + try { + FileUtil.copy(in, out); + } + finally { + out.close(); + } + } + finally { + in.close(); + } + } + } + + public void testBasicResponse() throws IOException { + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + fakeResponse(mRequest, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + assertEquals(IMAGE_DIMENSION.width, image.getWidth()); + assertEquals(IMAGE_DIMENSION.height, image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + // Test that wrapper works as a no-op, in case the image does not need to be decoded + // This is not a very common use case, as filters should avoid wrapping the response + // for performance reasons, but we still want that to work + public void testNoOpResponse() throws IOException { + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + fakeResponse(mRequest, imageResponse); + + // TODO: Is there a way we can avoid calling flush? + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is untouched + assertTrue("Data differs", Arrays.equals(FileUtil.read(getClass().getResourceAsStream(IMAGE_NAME)), out.toByteArray())); + } + + // Transcode original PNG to JPEG with no other changes + public void testTranscodeResponse() throws IOException { + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_JPEG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + fakeResponse(mRequest, imageResponse); + + // Force transcode to JPEG + imageResponse.setOutputContentType("image/jpeg"); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(IMAGE_DIMENSION.width, outImage.getWidth()); + assertEquals(IMAGE_DIMENSION.height, outImage.getHeight()); + } + + public void testReplaceResponse() throws IOException { + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_BMP)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + fakeResponse(mRequest, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + // Do something with image + // NOTE: BMP writer can't write ARGB so this image is converted (same goes for JPEG) + // TODO: Make conversion testing more explicit + image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + imageResponse.setImage(image); + imageResponse.setOutputContentType("image/bmp"); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + // TODO: Test with AOI attributes (rename thes to source-region?) + // TODO: Test with scale attributes + // More? + + // Make sure we don't change semantics here... + public void testNotFoundInput() throws IOException { + // Need speical setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").will(returnValue(null)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/monkey-business.gif")); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + mRequest = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("sendError").with(eq(404), ANYTHING); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + fakeResponse(mRequest, imageResponse); + } + + // NOTE: This means it's up to some Filter to decide wether we should filter the given request + public void testUnsupportedInput() throws IOException { + assertFalse("Test is invalid, rewrite test", ImageIO.getImageReadersByFormatName("txt").hasNext()); + + // Need speical setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").will(returnValue(null)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/foo.txt")); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + mRequest = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + + fakeResponse(mRequest, imageResponse); + try { + // Force transcode + imageResponse.setOutputContentType("image/png"); + + // Flush image to wrapped response + imageResponse.flush(); + + fail("Should throw IOException in case of unspupported input"); + } + catch (IOException e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("transcode") >= 0); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("reader") >= 0); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("text") >= 0); + // Failure here suggests a different failurfe condition than the one we expected + } + } + + public void testUnsupportedOutput() throws IOException { + assertFalse("Test is invalid, rewrite test", ImageIO.getImageWritersByFormatName("foo").hasNext()); + + Mock mockResponse = mock(HttpServletResponse.class); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(mRequest, response, mContext); + + fakeResponse(mRequest, imageResponse); + try { + // Force transcode to unsupported format + imageResponse.setOutputContentType("application/xml+foo"); + + // Flush image to wrapped response + imageResponse.flush(); + + fail("Should throw IOException in case of unspupported output"); + } + catch (IOException e) { + String message = e.getMessage().toLowerCase(); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("transcode") >= 0); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("writer") >= 0); + assertTrue("Wrong message: " + e.getMessage(), message.indexOf("foo") >= 0); + // Failure here suggests a different failurfe condition than the one we expected + } + } + + // TODO: Test that we handle image conversion to a suitable format, before writing response + // For example: Read a PNG with transparency and store as B/W WBMP + + + + // TODO: Create ImageFilter test case, that tests normal use, as well as chaining + + @Test + public void testReadWithSourceRegion() throws IOException { + Rectangle sourceRegion = new Rectangle(100, 100, 100, 100); + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + assertEquals(sourceRegion.width, image.getWidth()); + assertEquals(sourceRegion.height, image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithNonSquareSourceRegion() throws IOException { + Rectangle sourceRegion = new Rectangle(100, 100, 100, 80); + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + assertEquals(sourceRegion.width, image.getWidth()); + assertEquals(sourceRegion.height, image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithCenteredUniformSourceRegion() throws IOException { + // Negative x/y values means centered + Rectangle sourceRegion = new Rectangle(-1, -1, 300, 300); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + assertEquals(sourceRegion.width, image.getWidth()); + assertEquals(sourceRegion.height, image.getHeight()); + + BufferedImage original = ImageIO.read(getClass().getResource(IMAGE_NAME)); + + // Sanity check + assertNotNull(original); + assertEquals(IMAGE_DIMENSION.width, original.getWidth()); + assertEquals(IMAGE_DIMENSION.height, original.getHeight()); + + // Center + sourceRegion.setLocation( + (int) Math.round((IMAGE_DIMENSION.width - sourceRegion.getWidth()) / 2.0), + (int) Math.round((IMAGE_DIMENSION.height - sourceRegion.getHeight()) / 2.0) + ); + + // Test that we have exactly the pixels we should + for (int y = 0; y < sourceRegion.height; y++) { + for (int x = 0; x < sourceRegion.width; x++) { + assertEquals(original.getRGB(x + sourceRegion.x, y + sourceRegion.y), image.getRGB(x, y)); + } + } + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithCenteredUniformNonSquareSourceRegion() throws IOException { + // Negative x/y values means centered + Rectangle sourceRegion = new Rectangle(-1, -1, 410, 300); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Image wider than bounding box", IMAGE_DIMENSION.width >= image.getWidth()); + assertTrue("Image taller than bounding box", IMAGE_DIMENSION.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", IMAGE_DIMENSION.width == image.getWidth() || IMAGE_DIMENSION.height == image.getHeight()); + + // Above tests that one of the sides equal, we now need to test that the other follows aspect + double destAspect = sourceRegion.getWidth() / sourceRegion.getHeight(); + double srcAspect = IMAGE_DIMENSION.getWidth() / IMAGE_DIMENSION.getHeight(); + + if (srcAspect >= destAspect) { + // Dst is narrower than src + assertEquals(IMAGE_DIMENSION.height, image.getHeight()); + assertEquals( + "Image width does not follow aspect", + Math.round(IMAGE_DIMENSION.getHeight() * destAspect), image.getWidth() + ); + } + else { + // Dst is wider than src + assertEquals(IMAGE_DIMENSION.width, image.getWidth()); + assertEquals( + "Image height does not follow aspect", + Math.round(IMAGE_DIMENSION.getWidth() / destAspect), image.getHeight() + ); + } + + BufferedImage original = ImageIO.read(getClass().getResource(IMAGE_NAME)); + + // Sanity check + assertNotNull(original); + assertEquals(IMAGE_DIMENSION.width, original.getWidth()); + assertEquals(IMAGE_DIMENSION.height, original.getHeight()); + + // Center + sourceRegion.setLocation( + (int) Math.round((IMAGE_DIMENSION.width - image.getWidth()) / 2.0), + (int) Math.round((IMAGE_DIMENSION.height - image.getHeight()) / 2.0) + ); + sourceRegion.setSize(image.getWidth(), image.getHeight()); + + // Test that we have exactly the pixels we should + for (int y = 0; y < sourceRegion.height; y++) { + for (int x = 0; x < sourceRegion.width; x++) { + assertEquals(original.getRGB(x + sourceRegion.x, y + sourceRegion.y), image.getRGB(x, y)); + } + } + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithResize() throws IOException { + Dimension size = new Dimension(100, 120); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + assertTrue("Image wider than bounding box", size.width >= image.getWidth()); + assertTrue("Image taller than bounding box", size.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", size.width == image.getWidth() || size.height == image.getHeight()); + + // Above tests that one of the sides equal, we now need to test that the other follows aspect + if (size.width == image.getWidth()) { + assertEquals(Math.round(size.getWidth() * IMAGE_DIMENSION.getWidth() / IMAGE_DIMENSION.getHeight()), image.getHeight()); + } + else { + assertEquals(Math.round(size.getHeight() * IMAGE_DIMENSION.getWidth() / IMAGE_DIMENSION.getHeight()), image.getWidth()); + } + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithNonUniformResize() throws IOException { + Dimension size = new Dimension(150, 150); + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).will(returnValue(false)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + assertEquals(size.width, image.getWidth()); + assertEquals(size.height, image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithSourceRegionAndResize() throws IOException { + Rectangle sourceRegion = new Rectangle(100, 100, 200, 200); + Dimension size = new Dimension(100, 120); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + assertTrue("Image wider than bounding box", size.width >= image.getWidth()); + assertTrue("Image taller than bounding box", size.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", size.width == image.getWidth() || size.height == image.getHeight()); + + // Above tests that one of the sides equal, we now need to test that the other follows aspect + if (size.width == image.getWidth()) { + assertEquals(Math.round(size.getWidth() * sourceRegion.getWidth() / sourceRegion.getHeight()), image.getHeight()); + } + else { + assertEquals(Math.round(size.getHeight() * sourceRegion.getWidth() / sourceRegion.getHeight()), image.getWidth()); + } + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithSourceRegionAndNonUniformResize() throws IOException { + Rectangle sourceRegion = new Rectangle(100, 100, 200, 200); + Dimension size = new Dimension(150, 150); + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE_UNIFORM)).will(returnValue(false)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + assertEquals(size.width, image.getWidth()); + assertEquals(size.height, image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithUniformSourceRegionAndResizeSquare() throws IOException { + Rectangle sourceRegion = new Rectangle(-1, -1, 300, 300); + Dimension size = new Dimension(100, 120); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Image wider than bounding box", size.width >= image.getWidth()); + assertTrue("Image taller than bounding box", size.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", size.width == image.getWidth() || size.height == image.getHeight()); + + // Above tests that one of the sides equal, we now need to test that the other follows aspect + if (size.width == image.getWidth()) { + assertEquals( + "Image height does not follow aspect", + Math.round(size.getWidth() / (sourceRegion.getWidth() / sourceRegion.getHeight())), image.getHeight() + ); + } + else { + System.out.println("size: " + size); + System.out.println("image: " + new Dimension(image.getWidth(), image.getHeight())); + assertEquals( + "Image width does not follow aspect", + Math.round(size.getHeight() * (sourceRegion.getWidth() / sourceRegion.getHeight())), image.getWidth() + ); + } + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithNonSquareUniformSourceRegionAndResize() throws IOException { + Rectangle sourceRegion = new Rectangle(-1, -1, 170, 300); + Dimension size = new Dimension(150, 120); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + // Flush image to wrapped response + imageResponse.flush(); + +// File tempFile = File.createTempFile("test", ".png"); +// FileUtil.write(tempFile, out.toByteArray()); +// System.out.println("tempFile: " + tempFile); + + assertTrue("Image wider than bounding box", size.width >= image.getWidth()); + assertTrue("Image taller than bounding box", size.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", size.width == image.getWidth() || size.height == image.getHeight()); + + // Above tests that one of the sides equal, we now need to test that the other follows aspect + if (size.width == image.getWidth()) { + assertEquals( + "Image height does not follow aspect", + Math.round(size.getWidth() / (sourceRegion.getWidth() / sourceRegion.getHeight())), image.getHeight() + ); + } + else { +// System.out.println("size: " + size); +// System.out.println("image: " + new Dimension(image.getWidth(), image.getHeight())); + assertEquals( + "Image width does not follow aspect", + Math.round(size.getHeight() * (sourceRegion.getWidth() / sourceRegion.getHeight())), image.getWidth() + ); + } + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + @Test + public void testReadWithAllNegativeSourceRegion() throws IOException { + Rectangle sourceRegion = new Rectangle(-1, -1, -1, -1); + Dimension size = new Dimension(100, 120); + + // Custom setup + Mock mockRequest = mock(HttpServletRequest.class); + mockRequest.stubs().method("getAttribute").withAnyArguments().will(returnValue(null)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI_UNIFORM)).will(returnValue(true)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_AOI)).will(returnValue(sourceRegion)); + mockRequest.stubs().method("getAttribute").with(eq(ImageServletResponse.ATTRIB_SIZE)).will(returnValue(size)); + mockRequest.stubs().method("getContextPath").will(returnValue("/ape")); + mockRequest.stubs().method("getRequestURI").will(returnValue("/ape/" + IMAGE_NAME)); + mockRequest.stubs().method("getParameter").will(returnValue(null)); + HttpServletRequest request = (HttpServletRequest) mockRequest.proxy(); + + Mock mockResponse = mock(HttpServletResponse.class); + mockResponse.expects(once()).method("setContentType").with(eq(CONTENT_TYPE_PNG)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mockResponse.expects(once()).method("getOutputStream").will(returnValue(new OutputStreamAdapter(out))); + HttpServletResponse response = (HttpServletResponse) mockResponse.proxy(); + + ImageServletResponseImpl imageResponse = new ImageServletResponseImpl(request, response, mContext); + fakeResponse(request, imageResponse); + + // Make sure image is correctly loaedd + BufferedImage image = imageResponse.getImage(); + assertNotNull(image); + + assertTrue("Image wider than bounding box", size.width >= image.getWidth()); + assertTrue("Image taller than bounding box", size.height >= image.getHeight()); + assertTrue("Image not maximized to bounding box", size.width == image.getWidth() || size.height == image.getHeight()); + + // Flush image to wrapped response + imageResponse.flush(); + + assertTrue("Content has no data", out.size() > 0); + + // Test that image data is still readable + BufferedImage outImage = ImageIO.read(new ByteArrayInputStream(out.toByteArray())); + assertNotNull(outImage); + assertEquals(image.getWidth(), outImage.getWidth()); + assertEquals(image.getHeight(), outImage.getHeight()); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Absolute AOI + // ----------------------------------------------------------------------------------------------------------------- + public void testGetAOIAbsolute() { + assertEquals(new Rectangle(10, 10, 100, 100), ImageServletResponseImpl.getAOI(200, 200, 10, 10, 100, 100, false, false)); + } + + public void testGetAOIAbsoluteOverflowX() { + assertEquals(new Rectangle(10, 10, 90, 100), ImageServletResponseImpl.getAOI(100, 200, 10, 10, 100, 100, false, false)); + } + + public void testGetAOIAbsoluteOverflowW() { + assertEquals(new Rectangle(0, 10, 100, 100), ImageServletResponseImpl.getAOI(100, 200, 0, 10, 110, 100, false, false)); + } + + public void testGetAOIAbsoluteOverflowY() { + assertEquals(new Rectangle(10, 10, 100, 90), ImageServletResponseImpl.getAOI(200, 100, 10, 10, 100, 100, false, false)); + } + + public void testGetAOIAbsoluteOverflowH() { + assertEquals(new Rectangle(10, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, 10, 0, 100, 110, false, false)); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Uniform AOI centered + // ----------------------------------------------------------------------------------------------------------------- + @Test + public void testGetAOIUniformCenteredS2SUp() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 333, 333, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2SDown() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 33, 33, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2SNormalized() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2W() { + assertEquals(new Rectangle(0, 25, 100, 50), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 200, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2WNormalized() { + assertEquals(new Rectangle(0, 25, 100, 50), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 50, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2N() { + assertEquals(new Rectangle(25, 0, 50, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 200, false, true)); + } + + @Test + public void testGetAOIUniformCenteredS2NNormalized() { + assertEquals(new Rectangle(25, 0, 50, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 50, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2S() { + assertEquals(new Rectangle(50, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 333, 333, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2SNormalized() { + assertEquals(new Rectangle(50, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2W() { + assertEquals(new Rectangle(0, 0, 200, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 50, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2WW() { + assertEquals(new Rectangle(0, 25, 200, 50), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 200, 50, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2WN() { + assertEquals(new Rectangle(25, 0, 150, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 75, 50, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2WNNormalized() { + assertEquals(new Rectangle(25, 0, 150, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 150, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2WNormalized() { + assertEquals(new Rectangle(0, 0, 200, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 200, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2N() { + assertEquals(new Rectangle(75, 0, 50, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 200, false, true)); + } + + @Test + public void testGetAOIUniformCenteredW2NNormalized() { + assertEquals(new Rectangle(75, 0, 50, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 50, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2S() { + assertEquals(new Rectangle(0, 50, 100, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 333, 333, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2SNormalized() { + assertEquals(new Rectangle(0, 50, 100, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2W() { + assertEquals(new Rectangle(0, 75, 100, 50), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 200, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2WNormalized() { + assertEquals(new Rectangle(0, 75, 100, 50), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 50, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2N() { + assertEquals(new Rectangle(0, 0, 100, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 50, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2NN() { + assertEquals(new Rectangle(25, 0, 50, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 25, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2NW() { + assertEquals(new Rectangle(0, 33, 100, 133), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 75, 100, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2NWNormalized() { + assertEquals(new Rectangle(0, 37, 100, 125), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 125, false, true)); + } + + @Test + public void testGetAOIUniformCenteredN2NNormalized() { + assertEquals(new Rectangle(0, 0, 100, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 200, false, true)); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Absolute AOI centered + // ----------------------------------------------------------------------------------------------------------------- + @Test + public void testGetAOICenteredS2SUp() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 333, 333, false, false)); + } + + @Test + public void testGetAOICenteredS2SDown() { + assertEquals(new Rectangle(33, 33, 33, 33), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 33, 33, false, false)); + } + + @Test + public void testGetAOICenteredS2SSame() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 100, false, false)); + } + + @Test + public void testGetAOICenteredS2WOverflow() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 200, 100, false, false)); + } + + @Test + public void testGetAOICenteredS2W() { + assertEquals(new Rectangle(40, 45, 20, 10), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 20, 10, false, false)); + } + + @Test + public void testGetAOICenteredS2WMax() { + assertEquals(new Rectangle(0, 25, 100, 50), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 50, false, false)); + } + + @Test + public void testGetAOICenteredS2NOverflow() { + assertEquals(new Rectangle(0, 0, 100, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 100, 200, false, false)); + } + + @Test + public void testGetAOICenteredS2N() { + assertEquals(new Rectangle(45, 40, 10, 20), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 10, 20, false, false)); + } + + @Test + public void testGetAOICenteredS2NMax() { + assertEquals(new Rectangle(25, 0, 50, 100), ImageServletResponseImpl.getAOI(100, 100, -1, -1, 50, 100, false, false)); + } + + @Test + public void testGetAOICenteredW2SOverflow() { + assertEquals(new Rectangle(0, 0, 200, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 333, 333, false, false)); + } + + @Test + public void testGetAOICenteredW2S() { + assertEquals(new Rectangle(75, 25, 50, 50), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 50, 50, false, false)); + } + + @Test + public void testGetAOICenteredW2SMax() { + assertEquals(new Rectangle(50, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 100, false, false)); + } + + @Test + public void testGetAOICenteredW2WOverflow() { + assertEquals(new Rectangle(0, 0, 200, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 300, 200, false, false)); + } + + @Test + public void testGetAOICenteredW2W() { + assertEquals(new Rectangle(50, 25, 100, 50), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 50, false, false)); + } + + @Test + public void testGetAOICenteredW2WW() { + assertEquals(new Rectangle(10, 40, 180, 20), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 180, 20, false, false)); + } + + @Test + public void testGetAOICenteredW2WN() { + assertEquals(new Rectangle(62, 25, 75, 50), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 75, 50, false, false)); + } + + @Test + public void testGetAOICenteredW2WSame() { + assertEquals(new Rectangle(0, 0, 200, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 200, 100, false, false)); + } + + @Test + public void testGetAOICenteredW2NOverflow() { + assertEquals(new Rectangle(50, 0, 100, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 100, 200, false, false)); + } + + @Test + public void testGetAOICenteredW2N() { + assertEquals(new Rectangle(83, 25, 33, 50), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 33, 50, false, false)); + } + + @Test + public void testGetAOICenteredW2NMax() { + assertEquals(new Rectangle(75, 0, 50, 100), ImageServletResponseImpl.getAOI(200, 100, -1, -1, 50, 100, false, false)); + } + + @Test + public void testGetAOICenteredN2S() { + assertEquals(new Rectangle(33, 83, 33, 33), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 33, 33, false, false)); + } + + @Test + public void testGetAOICenteredN2SMax() { + assertEquals(new Rectangle(0, 50, 100, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 100, false, false)); + } + + @Test + public void testGetAOICenteredN2WOverflow() { + assertEquals(new Rectangle(0, 50, 100, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 200, 100, false, false)); + } + + @Test + public void testGetAOICenteredN2W() { + assertEquals(new Rectangle(40, 95, 20, 10), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 20, 10, false, false)); + } + + @Test + public void testGetAOICenteredN2WMax() { + assertEquals(new Rectangle(0, 75, 100, 50), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 50, false, false)); + } + + @Test + public void testGetAOICenteredN2N() { + assertEquals(new Rectangle(45, 90, 10, 20), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 10, 20, false, false)); + } + + @Test + public void testGetAOICenteredN2NSame() { + assertEquals(new Rectangle(0, 0, 100, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 200, false, false)); + } + + @Test + public void testGetAOICenteredN2NN() { + assertEquals(new Rectangle(37, 50, 25, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 25, 100, false, false)); + } + + @Test + public void testGetAOICenteredN2NW() { + assertEquals(new Rectangle(12, 50, 75, 100), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 75, 100, false, false)); + } + + @Test + public void testGetAOICenteredN2NWMax() { + assertEquals(new Rectangle(0, 37, 100, 125), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 125, false, false)); + } + + @Test + public void testGetAOICenteredN2NMax() { + assertEquals(new Rectangle(0, 0, 100, 200), ImageServletResponseImpl.getAOI(100, 200, -1, -1, 100, 200, false, false)); + } + +} diff --git a/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/12monkeys-splash.png b/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/12monkeys-splash.png new file mode 100755 index 00000000..7ec39a00 Binary files /dev/null and b/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/12monkeys-splash.png differ diff --git a/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/foo.txt b/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/foo.txt new file mode 100755 index 00000000..a95c54b3 --- /dev/null +++ b/twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/foo.txt @@ -0,0 +1 @@ +This is just a text file. \ No newline at end of file