From 87baad3f99d59347ddfa126ff345e072d1e70af2 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 3 Sep 2009 21:13:12 +0200 Subject: [PATCH] Servlet project --- twelvemonkeys-servlet/pom.xml | 95 ++ .../servlet/AbstractServletMapAdapter.java | 149 ++ .../servlet/BrowserHelperFilter.java | 151 ++ .../twelvemonkeys/servlet/DebugServlet.java | 118 ++ .../twelvemonkeys/servlet/GenericFilter.java | 383 +++++ .../twelvemonkeys/servlet/GenericServlet.java | 87 ++ .../twelvemonkeys/servlet/HttpServlet.java | 87 ++ .../com/twelvemonkeys/servlet/InitParam.java | 50 + .../servlet/OutputStreamAdapter.java | 122 ++ .../twelvemonkeys/servlet/ProxyServlet.java | 435 ++++++ .../servlet/SerlvetHeadersMapAdapter.java | 40 + .../servlet/SerlvetParametersMapAdapter.java | 38 + .../servlet/ServletConfigException.java | 92 ++ .../servlet/ServletConfigMapAdapter.java | 284 ++++ .../ServletResponseStreamDelegate.java | 114 ++ .../twelvemonkeys/servlet/ServletUtil.java | 1060 +++++++++++++ .../twelvemonkeys/servlet/ThrottleFilter.java | 311 ++++ .../twelvemonkeys/servlet/TimingFilter.java | 113 ++ .../servlet/TrimWhiteSpaceFilter.java | 238 +++ .../servlet/cache/AbstractCacheRequest.java | 47 + .../servlet/cache/AbstractCacheResponse.java | 45 + .../servlet/cache/CacheException.java | 14 + .../servlet/cache/CacheFilter.java | 199 +++ .../servlet/cache/CacheRequest.java | 26 + .../servlet/cache/CacheResponse.java | 27 + .../servlet/cache/CacheResponseWrapper.java | 261 ++++ .../servlet/cache/CachedEntity.java | 75 + .../servlet/cache/CachedEntityImpl.java | 172 +++ .../servlet/cache/CachedResponse.java | 95 ++ .../servlet/cache/CachedResponseImpl.java | 220 +++ .../servlet/cache/ClientCacheRequest.java | 44 + .../servlet/cache/ClientCacheResponse.java | 25 + .../servlet/cache/HTTPCache.java | 1167 +++++++++++++++ .../servlet/cache/ResponseResolver.java | 14 + .../cache/SerlvetCacheResponseWrapper.java | 273 ++++ .../servlet/cache/ServletCacheRequest.java | 56 + .../servlet/cache/ServletCacheResponse.java | 46 + .../cache/ServletResponseResolver.java | 40 + .../servlet/cache/WritableCachedResponse.java | 77 + .../cache/WritableCachedResponseImpl.java | 188 +++ .../com/twelvemonkeys/servlet/cache/todo.txt | 3 + .../fileupload/FileSizeExceededException.java | 42 + .../fileupload/FileUploadException.java | 52 + .../servlet/fileupload/FileUploadFilter.java | 137 ++ .../fileupload/HttpFileUploadRequest.java | 63 + .../HttpFileUploadRequestWrapper.java | 154 ++ .../servlet/fileupload/UploadedFile.java | 86 ++ .../servlet/fileupload/UploadedFileImpl.java | 91 ++ .../servlet/gzip/GZIPFilter.java | 141 ++ .../servlet/gzip/GZIPResponseWrapper.java | 146 ++ .../servlet/image/AWTImageFilterAdapter.java | 72 + .../servlet/image/BufferedImageOpAdapter.java | 67 + .../servlet/image/ColorServlet.java | 212 +++ .../servlet/image/ComposeFilter.java | 59 + .../image/ContentNegotiationFilter.java | 436 ++++++ .../servlet/image/CropFilter.java | 232 +++ .../servlet/image/ImageFilter.java | 199 +++ .../servlet/image/ImageServletException.java | 55 + .../servlet/image/ImageServletResponse.java | 193 +++ .../image/ImageServletResponseImpl.java | 747 ++++++++++ .../servlet/image/NullImageFilter.java | 46 + .../servlet/image/RotateFilter.java | 203 +++ .../servlet/image/ScaleFilter.java | 322 ++++ .../servlet/image/SourceRenderFilter.java | 154 ++ .../servlet/image/TextRenderer.java | 348 +++++ .../twelvemonkeys/servlet/image/package.html | 38 + .../servlet/jsp/droplet/Droplet.java | 80 + .../servlet/jsp/droplet/JspFragment.java | 44 + .../servlet/jsp/droplet/Oparam.java | 29 + .../servlet/jsp/droplet/Param.java | 42 + .../servlet/jsp/droplet/package.html | 14 + .../jsp/droplet/taglib/IncludeTag.java | 214 +++ .../jsp/droplet/taglib/NestingHandler.java | 184 +++ .../jsp/droplet/taglib/NestingValidator.java | 108 ++ .../servlet/jsp/droplet/taglib/OparamTag.java | 238 +++ .../servlet/jsp/droplet/taglib/ParamTag.java | 141 ++ .../jsp/droplet/taglib/ValueOfTEI.java | 50 + .../jsp/droplet/taglib/ValueOfTag.java | 147 ++ .../servlet/jsp/droplet/taglib/package.html | 10 + .../twelvemonkeys/servlet/jsp/package.html | 7 + .../servlet/jsp/taglib/BodyReaderTag.java | 43 + .../servlet/jsp/taglib/CSVToTableTag.java | 240 +++ .../servlet/jsp/taglib/ExBodyTagSupport.java | 286 ++++ .../servlet/jsp/taglib/ExTag.java | 162 ++ .../servlet/jsp/taglib/ExTagSupport.java | 289 ++++ .../servlet/jsp/taglib/LastModifiedTEI.java | 21 + .../servlet/jsp/taglib/LastModifiedTag.java | 54 + .../servlet/jsp/taglib/TrimWhiteSpaceTag.java | 89 ++ .../servlet/jsp/taglib/XMLTransformTag.java | 162 ++ .../jsp/taglib/logic/ConditionalTagBase.java | 140 ++ .../servlet/jsp/taglib/logic/EqualTag.java | 170 +++ .../jsp/taglib/logic/IteratorProviderTEI.java | 41 + .../jsp/taglib/logic/IteratorProviderTag.java | 87 ++ .../servlet/jsp/taglib/logic/NotEqualTag.java | 168 +++ .../servlet/jsp/taglib/package.html | 7 + .../servlet/log4j/Log4JContextWrapper.java | 183 +++ .../com/twelvemonkeys/servlet/package.html | 7 + .../servlet/FilterAbstractTestCase.java | 438 ++++++ .../servlet/GenericFilterTestCase.java | 151 ++ .../ServletConfigExceptionTestCase.java | 93 ++ .../ServletConfigMapAdapterTestCase.java | 192 +++ .../ServletHeadersMapAdapterTestCase.java | 103 ++ .../ServletParametersMapAdapterTestCase.java | 102 ++ .../ServletResponseAbsrtactTestCase.java | 23 + .../servlet/TrimWhiteSpaceFilterTestCase.java | 111 ++ .../servlet/cache/HTTPCacheTestCase.java | 1306 +++++++++++++++++ .../ImageServletResponseImplTestCase.java | 1184 +++++++++++++++ .../servlet/image/12monkeys-splash.png | Bin 0 -> 104417 bytes .../com/twelvemonkeys/servlet/image/foo.txt | 1 + 109 files changed, 18537 insertions(+) create mode 100755 twelvemonkeys-servlet/pom.xml create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/AbstractServletMapAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/BrowserHelperFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/DebugServlet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/InitParam.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetHeadersMapAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/SerlvetParametersMapAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigException.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TimingFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheRequest.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/AbstractCacheResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheException.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheRequest.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntity.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedEntityImpl.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/CachedResponseImpl.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheRequest.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ClientCacheResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ResponseResolver.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheRequest.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletCacheResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/ServletResponseResolver.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/todo.txt create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequest.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/HttpFileUploadRequestWrapper.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFile.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/UploadedFileImpl.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/AWTImageFilterAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/BufferedImageOpAdapter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletException.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponseImpl.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/NullImageFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/TextRenderer.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/image/package.html create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Droplet.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/JspFragment.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Oparam.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/Param.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/package.html create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/IncludeTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingHandler.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/NestingValidator.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/OparamTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ParamTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTEI.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/ValueOfTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/droplet/taglib/package.html create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/package.html create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/BodyReaderTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/CSVToTableTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExBodyTagSupport.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/ExTagSupport.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTEI.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/LastModifiedTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/TrimWhiteSpaceTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/XMLTransformTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/ConditionalTagBase.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/EqualTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTEI.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/IteratorProviderTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/logic/NotEqualTag.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/jsp/taglib/package.html create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/log4j/Log4JContextWrapper.java create mode 100755 twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/package.html create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/FilterAbstractTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/GenericFilterTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigExceptionTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletConfigMapAdapterTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletHeadersMapAdapterTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletParametersMapAdapterTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/ServletResponseAbsrtactTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilterTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/image/ImageServletResponseImplTestCase.java create mode 100755 twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/12monkeys-splash.png create mode 100755 twelvemonkeys-servlet/src/test/resources/com/twelvemonkeys/servlet/image/foo.txt 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 0000000000000000000000000000000000000000..7ec39a00a7c6aa8efeff870d5629238101190c66 GIT binary patch literal 104417 zcmYIvc{o(zJ|cEw(`+Dq{&nHEo(m4vDE0Nm>+hEJG!Y7E!6uqf)3- z30ZP1Nm5OchLR|eB+9kl065DSMJfN?m-EF4ZF;#}Y}4;=o|*46GVn~!ufpI_?@ zY)})tQG4f*nbBsb@%_e@{nM&0qmMeD$g6b7DJ$oR#P)ve(Xon8K{t1Ug-5;wg${j+ zeG~inV1$}0y6X7`wf4IWc7ruwJxj2-(naUiFtmP{sO8sp(0f=>@AB^lq%c4GgoAU} zKOl!ztO$Ex^kDo?*Wo@g{6-cRc~BacY9mHwKk+({@7yPo!hp% z-&VEb-mGE1Gxd6Q8p`>l^~iE(-F9jGld4S9;xCl_VMpV~-an@W1^#|8J-An;-_1?; z=vi^l?SUV>-q^8jY()eu{`0L{$66C}ml$`%-f--Em*e{M_+WXYWmMqC_Y)tA zUKMZM*4MkP@6+fC%jT->wR3@?$Eo^s!3Gv3m4MZzfLzJP0$WxRj*2tVzmh_{?D83R z6UiRAsRkzY$9u)$xA^;D&~wPVzY|Gr^M>&cx-G!E{i1I>J(l;@NZ*Qdi4Hh6?hKv# z4?n++j`r66Q-^UvpJPf;;gf9)1B=SvSjWuweI!8+=DNGqU*s9U9H-CspcZc6Ys@CAX@{%l_A=Ex z|GyKK=#|5#kRS^BY!oLknO!XCe|ie9dXa_BG*JhiM&c`67VtZNo+mRH4m`(An$r2>G4G-V>y37!v6k;#gfB!zL?fK zt;Ze;(>?Qg10Tc@#5`na8pWJbgS)IV<4OF>50wL`*D0JJVz|zn-*ZfS_&O3qL<=Tz zlsW+bBPM*F#dj}r=>R2Kz_gojg#}g-*R=^|Jy1-BsLLIE7ms%%LY+COb>(m@pPL%)doek z^duB#MK29nWs!WlrNq1|Fn_rf$C*c^n1P8;7lDcQjlsl$K(Cs5xz@iL`PW96VBN0; z5H!P|UC_l~=t>2}U-J0Lp?aJuia7lPmiZC!XCHpRPV>P3L-p}g#L0=?0(-OYoc*L< zGn+!ObB|8qM80R`z(mFZ5Y);S0Wrk(j0yW%iWQvOFwDn4bY6EA|Df-WQ86=GXAj-p z{qviPNG^jbTmzfU7Bd;<$bQ)lQM`_?NKbJw>qUm_Hvshg;*lldxK-^RAjp#+=X7f2 zcjD!MZw3#)OuL1ar6tth|C>%hXYE#TrXrPDM1%1o8d+0%W;QHd!e|qonobpd?sCdH z+OHMWWuxDa7w7g%kbcJ+Pt9?)CkaB}+}hdG{7x)k?HU1Mpgo`%sVBs@G=b_~g;;{c z9xD_J_Iz|*!4K|UoC*i{iDJw6-@|b2>yg13_N->U)F7Wfo7gs{LrhftZ{7OTnDI(~ z>77ibd;MaP1TTUih=SF6tW7~`1{G1IC$$(Whvs)gKWvXjUZ&3A5~43J$fyZ}zt&?y z+~UGiCM@+9iSaQCz5HZNEwg%NaECL?{dtD+}Nxcpa6a;~B_EwW`H!~OrRkxxqrk2e}O?%maStO1LpNSZc+KZr4X z8xiJ=7R$&gxPGU0w96tR6rHwug(-b`jd2JAej9h%DTR>GL&h|74zvU4Bsnz^lT31p z%B87*X6u<<6|{4td6t(2ExV{epkEW(x*4UE}>)B>*^B+~wPxclG*#AVPBtecgl_HJE> z+svEA`Tf&?34hdK!Up1Cg7W3IovwQp1)X=J_a&*h8nUYSlwCXWKP@m`fhFWKBq|5D z;_Uu7qhIC0y0Hid(nI!a;}_YjhLPLN4Yugv{z?mIg?42BpE>A{>*WgP$eD>S02Emde zWG2ZKNhK}UMqbLGB3bSNr^3M?rFzW+$(37x?S4N0{yqIWILb!1;bupt}FhgBW&tHHL=ovi_w)*6fd+?U!6@U zPWnOu>x^NI!MmpDvtappKg=@QPN^CU4S^d1Bgv8z0DZd#f6p_1)B#-xdLUBuj8|@y zpbzd@LD28}cr9DZcdb4xI!qb8IIhDZE$A<_5RdE2UgWQH@b*(gE_WWUgPWwa3{?*RS7$TVx#Rj@=? zXAnuYL3iE4x6no(F;TG{dHRsFUoiOp_BbAQp`GpdcV%)6ZFTF+S6SC-n}eV*KJkUD z!1R)u`v?IZ=LkKeG5?%*Ku#aAXAKsUTuo;IoU9&F)hKq4(ufP=

oP;RmGKWBA5)j`(bbyH26A32Cw>&Z~WxIVHZP6;oG-Ir*Seo zV1asYa>wK^2i^1WVjbuyCMYsS$T)jpQcxh#K17uQ)T$Mdz# z-$gF=*5k69Kx$8lWPJI&=ia61KnL*Ov%|S#dL1i_V|-5I-zLY>TB~Ep=nP?S@os?1 zjJ1dDl^nk(eK6;^f-x3>gF$dUK&LFo(l7WF^ws$yXT9MJOXCUZ9P$w24zGo8s)!w$*61M-_x)XkxdJ!orT2q}H2PYM~PvEHE zIUszPqul~5Ihy(pmQb`zE1-In-oH&(tJf1wicg541z8xHs*Hiuq*8X3=RFq7CxBqi z=Lg(UXWLePacY}%cGshFI#0j_KYm9Px5;zDoyC=4v+#k5uQ*MgXHJdg>7yTy4%&cq zmi@|h&DZsdSQoNg`ZM+Ek~WbO1l>ly#~a@2t&`h#an3X8`B@pjEC@OtKqAG9VzYy& z-a(UA8er3GVn8Tq8ImSV_R++F&$*FhVEcV38r{e5{woDh^+|%{E|85M+(?n8d;$ntUlI=J z9iRKKP(=H;gIjhhfg)&}+L4?a;$k}JVhnENikLK@h-1s$*Zq6W0tw&PVmox z1tE)E+Yc54BwsGy?=ZIi<0%CQy2`ebt@~*CPQ5`+d~N1xK4riPNh?QraCcP@YpKL<}IVzBYjl z>8)ij*6@kv2M+TLhE=%SPoXfEwE4L?uq1w-@gMfiO%b-`KXmX~!@cJ_9K7DJas zSupV>mDK2G;$hxt!X-&hE%eHuZsFMfUGyh6PGpqnEUTg#VK7Uuk53BPLLwy^LC|6h zIJrqxa%#GU&uxF2h~$#{8x+_pY)T*~`+=-Ldh_GRO;S9)gT~(JFJc9IF20|o^fd&A zep=eN^~M=JkuKP1&-hTjgj1@-W$#kV0fJ!+tbS4}O!FT;XT|AE0+uE%0E z?=(^BULyv6tjx?*8~oOC)~m=eFF`H+ZOzuyvUAH&lZ5aO3UDcnn^1y(C69|Cze0)@ zm3ApD8$R=s?JN&=*2-I;^S^^(E;P7!|IXyJ)ho#KYeW(R?Qnsh^T_;#(O|fYx_L%E zTBL_!SLDs1_4v-I6pcHqgfqy9E&uE-{BNUa!M7m|v8C|>#FfB%b0tEks>ANw^;V7~ zpu<~B)br!b?zvJ+6cze)&r}Q7c5OxGo4WDkOo>kCG#FJzl$_8njY>0(J@+4gNG$kB zUTl1%5MgR(JJ*@yulzYlLVD)JT0#N#T&W-c$6du(z(@E`tkX;C1U z9&v#`SaPo;HOi6z+o{zzn4-%%lJXHSre-DRzwI=_{9|!!tKw0|Ynq+sjtL;Cs#@K?jqsz_Wm^P??m5?Aq9SN{ZiVg;c`fgK-L)`&%p=%V^S4uo%g zUPv7U(cWls|INo3Urp18H)~vc( z@IwoMp6b8#R}d^5Cd91Hwap1Whh>_JEp*wX{udd!J0$v39)5J-s7?>JC#m^9xZb6#Ztk2XAipI^~BvuUewlsQfM*8=jJ@|IJLP zQGWM765_3B*udz($bf7dC-K%Gc=QhC!U(@paixfveUQI&d_50IUY-f;LALhClh-3~dxNLe0jv3z3=^8Hj;fBI2Uny04NNzC|Yt&L@RVAFMX` z`9K$-e!Pw{y|rLcd_5LPl$_cI45~=y$T|__^%$zv?`$MK6huRcbVbe~OOa$P6yp$c z%5j?~!2Eb{(bi9Oq+e14^@m))ZP)olqC1%DMxB1Lj!3Ia)Va<>hPNq~?EYNBpec=<&t`hiSERrB& zxmY0_J2FKnB+&8x@2!xaqlEr0dmp!GFNJ@}AoIm%U69b1pRI)@*W@M7PLAT~zDd5P zRX6cmp6@RcmcC&7FZ$v{*v4FVKUC`4+&U2k^9Z31XClP40-}xYC{^Xy9-`F_E_bNAz8PR;{kkHM(|eo!dIybNMn zT$0V%3--vfvq%6N4t+`+eRFuXz91+6a2jGD)dUl@(I+bVfbfg>nQf-nP@cRvos7{X zsveEYe0|xaBl9Wd(z3OK+hA^OzZL6UJb8Mo-i0K1*T?Vl7tJh2h!4Iv13Te=hkkme z85*fT-j_e0d{abF--FdUN9dvV@YB1S;`@Jfl+{XU^S&iSl>Y(9rly^CDnN?-MN>Yq zvi}3i+%LyUi$=1aMk#=9f8xD59rOOH%bSN3AF8xpP1zA~6Yyz#CffF}Yx5}|nEH){ z^2-&YQnZVoO`J2I`t_-}M5G&svfGJ+x^Y0s!`JcnV3-s}>e&n<969r5Ek3bTPX>l8S?_WPEkE_x z<+jc)0Xtx`pl0xb;K2uf~nPEd>eND?7|e&j6%441C}-Bh*mP9M^cSeOCjKS@uD zPF1HKxL>^<>9rEi`m6+q^wG&srue`+8G1E7-nrKo?y^z7V9p;f#ntswOROD~q-N=p zN#(s%Ug9#dyvHvT&3Ro3>(&ut>OLYxUF+FSH;{{2bS2MD0zKW$;w=7OGI3QeKwYwv z=w~y05|B>U$AUZ6gzzky(ZCFwkUP8g}~ zk`bdF2?tsQw^(-q_bXg>2K!h?pVY5lyy> z{4cT3<3+tj>{*@sc`CZgL7mIlhLn$BZNBC#AAXgaxci|S`G4o_Rl9I2F3HF7bgw;7 zFyZtVTICYT@Ok4x~X!Ig?&A{Bl7hnV%?Y(hE69$67zQ@6a#pCcmP ztyVfyS1qRNGo);YBmF=)d-@;x4e^27k`H34E`QQK{|i2}^29-esEvwR-UY*vK+LZt zNYU-T4~36B;lnfJ zk#(*_da9F4(K{ooxk`5YUqUS5KH<+k!V;(wi#VcG9f)lavgJ?)F(H7nG95+ zMhK6I0xS7NJut~|F~ETV|5RtB=*=oDbM38#XwBGZc1KWBvD_Fkzj2PZZJSh~Q%4l{ z>7aR?qE!FD7{0%>OXM$LQ_tpl+rU5i%}}8urDKaeh554yN710&oF)ECpVGnEBY~5C zx2vIVzp<16C*qzxmDq;!ZnvLrL5fJ(T}S+U^bd(DruobXI;tL}krj0vPgPnW7Tw2X z4#^F=6Snow+l%7%^#^bEgiWtO-}Jzq_2NV8MFewj^BY<@*Oi!0>en@bzb_`As&=tN zpR#QoSjXc6gJ211^A+&Q`^54wXW^TL3M)=B%Qbg=zJk=m8G(s?D`0zdNfVbg==eMAw^6E?@Uu4~37C>#dNXPsG7LxrAkCKE7pdEfIlt>bGMwmU7 zQ28f3@4D5~4sD6wQ84;>sA$fK&m0D5dBcE40+yyX1P+o6H1;?1h=x%DAah3(mgdy0 z4%TfE_xQjyktz~kS$_>Clp8$PAx$oH2*V4WIl~%j<@>wswV~9tHGr6I)Sn`fZS(z1t=%XBbQ4Szm6P;6#9Z34wrvDB- zg6xw9B^!%NZzFNUP=^I@t`m)670b5(wP5dV77uUA3NL4SE(0wjl;$trts!VB=4u(` z47x2Af4YqpIi?B!_Ps4_l9R47ixt^#tkdG?OL5jIvKme7kD`3jx|c2dbn4SsXoAo2 z%KrGnI2MjfsU&fJAoyvCrO$nNDW`LzZJYA+2Z8bpW6$TC=oXtdAK9qPNm*YiSQtm zOE7x@%K09}Ra7E3*TXxz-3aChdFllhPAv@CPh!2$$XP`lOT&k#U)=P3`_D}AjYyj_2{DXK$-ie{jdc;rOPw zp3Bp{pwWN@yLGduK>gqoo3sH#vIcI4{fTsL?x{|vuWwTv#tm3+p0<)%#`)O5&Bmoj zHN~aPq0hyIlHFnUR6m~o|^hX`X<{I0nPV!tln{MJ@d*nDxqt43TgGP&X%^6 zvrq3_?3E9ma5>PbDRg*qe1rP!yU#NY>wACNT6mDXX1H8&w^hi@gAVXlgyDhZU8%*( zCJwFtCNx$LD>6yjnjC_ruUUSS*0iAI)Q|ODt))}13>M#66?NpYg~NWopNZ7f)~jzz z;=QEf%I_7o?F&~mQ~Gpg#z?)+3cutA#T>5f!JBgS!md&HHr6`K=N0(V5N9;9fc13_ z_gGsOtXP3bWDHr))kv@;iO;N3;O;a5Z1=oeZ&2@;HdG;4=85Poe1>3$#i$(w~zvxvF@ zAr>1bQC%Z0;U61w=l^-a>+QJ(rl8rG;xPg11GG`IL;5Oknkdo>t7(oCX9+ag1A zFT6x1@wp5z?*-YN5ECxNou%ZMqPu_WJqgO4?&eI~VjG3@v`aG!h! zWpE{a&X_Q`CI(lxde}Md@i-(2&XNCfngs?EXN>Zv^v9XJQa$dMKQb{rWLINom>O=+ zBArlN;OL%z6bkq9Egoq|PY#3PS1mp4U*?3?&cu>R=4$g#?FI)PsP|t}5=kfdC#~hU z-8w-!z)_|?UvL5;{`?B6ZoX<2t*nD8O;W>Gb>eb%Oer1@G_5n*&r_HsFUn24{N9Og zL52?ampgHELEs9-z;Vp zy%V=7w%EvDs}@)77Y(Tk<6b9~IRn}nXeQ%vOU?RL;ExgtJ2oq}!>?uul087fgD(+Y zxy?{>qa{WO*J6F1f9U3)i4FG>N^DliNGE#>66pY`F->=2K!1&8%qp$t7l_k!t!n3X zI3VNN(zDM3q~cmCubRZVmL(bX38`H-KItmLrq3?%TVqijueKPn0r>m%kU+MQgOVc41z;!AwhgR?Fa;o44R&M+7o zL-tN^q$rLAjf<5*` z&V`AFl@M56;JPgo%GO*|i_-I?_ z1sf;+EXedVj8?BU7N0%ua@!vFYR>^+VAc9CY|S9mDsNbD1)p~Q5jHxWa#5}twf<5a zlBm3BE_chR?ajiInbjC&a777zwnk0{V7R5I2>fJv52LG~>NS+`oJ#*1j3b|R75wAw zp;TOOP)p*PPa2pZ`9+d1uOyrHKybmWHA)t4JDqGPCE-9n-Ao1z&Ys4aVt9MMd22 zlXF7~TFlOG+@~mT6vyOnaljfoQYg@d)p)ZS$`)RLgPF<)b z?Sf_W^BO&E;j;(#WK2UFNye#gUadLdm30!B%b;lAy-3g)Q_{1AD5ze_ zvXFDRM4z1opKQ}?t)f?w!qpQ=<;byT!Y~zNcmF#_xP#!E??H#4Tx?0x5vF8rg}HMe6_2L-Dx=aWjN&5DAFY%h^ zJ7_)^$WysrX)<(n@QOsvI9xO@0mm$cYM@)4K>U<;R4gvwDMM*eqWbm;r}}w=_oJe*wOB&NjB|*>>p(@Gx74}C;oTyV;6yBwarxx3oC_O= zAx|^;j+Pnu`ayITp^(?9!gW~PVqk0SM!#`yhRUofC)eBOE=Wu8JuUs36n+SAe~fSO zcR_{zm6H+RJM6GnG>vaO6!K+kH)i={%(-#iZGXSD?K$>?epLwi>Eeld;PU3z4E4vc zmADf22hgm#fL)@Es#nn$&ZkN;O`fhuiO=nps=X9~D3Gx$F^?%;t?@ffA!8O&VlP>Q zlDyxkIAYV^HKM2IO+vm?uUbolm)}8R%a_i;e)7JbIMYHR=Kh43<+WGA;H!T%-@iZV}cjD4a8zQvF8&-o0y&5_!MbGnkM&q1_sdBBXl5@1=D z7oIii%~#)4xCnJkJX9rhZAdld7cITL7PGu>4JIzg#}fAc)9o7uHxe#iBBh;I7a0{i zVS4#XPVKAi_B0RJ{4D|EJ-sR~&KVTe4iZ^sP6X>166tK*+dQmC+xS{y`ul|k+39zQ z@1y@jFc?R2OJB?3ZO4B)bI12|n^m&6d_Ccny9pOwuJzY7riY)R&z9ey_i$COu~Hy0 zw9!z!^e5JF_~PPkJ_pyq78TPJux<$)WTM!*Bu75jl-56J$o;EZyn0MEWUm4*#9mus zV>?4;QUL|Ez~pj;)?HQ42C6WG_{gtbO*3~_KeK7<>pR| zy^Xp77S9hV6mP^1?6jF&$jmw>jyhL?iNb2$o#TXw-dr5^@hXboI6O@dcOlU<^zm(x z{F9pg%kL+EmfuCy{l93_U{`+>ksfI)FGr7&u@!v{|P#lnIN^E=M(zC2TdUQS4f?&?J z;WO=VON>Z94#8MbDLpI8G4e-ey3wE9(ucETx_MX}o3yVcSbv-(GyI9Ifd(z%!EA=h zPZ8fj!4&?P4oVhJ0|Vy(e|M{`0M&@(wRIy)nJdKGCN3%V9@UH9W; zn(JQ&sTDgy8y(IYh@nMrLd7tF+YeM6t~Smvr3pWLksbZ;^lrWgbA+epV~2ig!;QvEtoiuh|a# z`bMHZyoS+5U-cS|#j$Ytb~(pW$MV&&h z@!aujT@TbHjaN$3;Fk-wGL-DUvRals!o7QE3F8mFk;(qL5_Nl^B<&`@vx)(iKA5sL z9=gOYKZa4d-HM^A__-{OX&Ugec;gV_OjbQYE%1i3B?rma~aHFj^h z@FitaA~H$wU&XKP7({jS_#3?frMJScTqZx9LW;N#w5`*#vSInmonEj36))xs%P$=l z*4NYa$OEdg`(TC7gy@eSehUwjeq>J^x=OsYTV@nGx11!n=)`1v2P*_xVXj+0#V5TU zol6M~wNz?(jv31wXdR*xU*%ZW z_!eCzqD~deeb+G{s$OxSVu@}j>=5w`Lw)czCGsUwUCy9c$Rb_ahq3W)BurfaDU$Al zAoW7`wh3pi)Scq?AwQWf^)m$R7X1^p($)9R`lB*%L+qcAaQk7ymH8pYA0?#;o4$=N zgB$viN{-FDUTO6?Lir$t@*-3XN{9)|0~DH~+ps9IgpxKZzbxnPj2>^VykrbL48seF zYvV!HSu>7?u^iV8nEONHmaTny_M5LFsovFfw1`zq5~WS6@0p0OF*Apa=cW`^tV*~5 zNS!j=)LP4LGCK@O>WmLT3r@=R`=xAQtxo?f4fc~;R4h}KHGdAds6s>8538SHkxG)g z%M58Nw~(Lt$okJM(U!h&_n{xL9wBDbZ>H8Hl0O`DkqB8eY-RtY$!rq9tw*wTIc48E zy`H>YPdUw9;M5fO$i@wjFZZpZ-GC_+z{!*qoKJ&S?EMDL9 zm9X$E*^f6D#0~(-{&#ETiqyV|hGyiq$j(EtHLTnq(uRdvl7NT}GBF;X_uKZL9Rrk( zx+gSA!m=`TUcf^1qhk3lKRJndCB=Lpzv#YuOn<|nJc>$dt?{uXz0M88sxQ5QbkjLfe|kSY7QK?bZA8r!{b!Y2XN_m50l0tP?5aJxGXS=yNuA=t z5aeEf(PLatB3&lmjA@bT)yG96&ntMHu?KjUn8#MqC*A7;q5N1)ro0*jeH3s&Z;?L?Yue(U zsiQBS^vaybiX28|7`t=tvd#Y~97p!tj`w=JRQ2RB=G%tD^ZL73c~C)k9i~M57)bUT znPu3#COI~JqpOBomdeZcti@Sbs08nn{lz|0jSfkV_~C*`6C zYL>#f=Yn7kaOfk#l(rr2zgcMf`nWaEn(cegn_Z`If$37zI>YTzjd3O914jvmDwHlr zsdxN6MOSV@5KUe`E=H3lBadP!-e%}YbzwWe{%5^4%V(3!cL$m;FXJCDpQ75xgAqYU zYr;JX@b1WoMhwl&8dgiXEWI9ynBW|*LlX5W1#{5?T;~+^n2!>u0saa*bLT%dZ>{p% zaIyOv@XV(OFZp@r4u=o%#X!2M<3ftl<4t^gU;9ucMlhlHI}?4-2CQWd*e}Rh`ahWn z8{5jhvsK)NKlbYE)h|fr107him#}l4r&GWe?AzXX8WA{{> zYCWPb5C

vWw)b($CRn!=salP4omaB>q4Mz!7Z3l~{5)Mv27Y!%ETjMO4sj zW)n%^A-b|Iv3a(UpY{4X`_J=0uONC4t=S!`Jxgp|lLa=jfAi(9XE!Fz>kjN!HvW4w zH_BOLp&{~jv0s+xZ*ZSE98qSXGP^4=sdp=PGI5X?6No(*D}B7NYp3Y-+86P;>aVsg zPsSGqqlC^ODC)j38blZz*e$biqg>JMua0|8Pgh!s@CBd@!v(WIa8hJTu2K-PI}K2h?dM<|OW&p;G8+`rTZo_8YEcj&s1 z>UZbL@X)2LQS2)%t{hD}U4A7`_2}HIK+@oqlSptg=89c8gMYAC^Lvcg_lB$k{6&&K z6SJ;3h+_DDegdx;SqLH1fPEM}o>H!rb@E0Gi?w}cf_aPS;!pFg6=y_g{c`FR;Fum6 z9C-T-eN?|F=V)S1=rpCHD#Y_)pR;_j!cx51wfzR-eTJqoC&7Z^;7KZzHZ!gFYCT#M z*!Z)aeQEV_BwFuGLed~F@U87QeJ)!)+=g0QZBA3mb2a@W)>$&=QWudf!1l!BwKC+@ ze8m(u|E<5@mX}NR1W1B6N2x3gH|$vg_9zv`3rU5JoFQW)`R5xpIsK|#tUS8n2o{+s z4u;WHPiMPVj0RIjqZ?C}pbDLOl<#2sSw3;pVimkCVMhl3-6B=anFe)|U@qJ}?~rSe z9$faY!|iMjsU!Z%m86lHE+{b!?_eV{OeudbNm6$-6z}gMJGL5%>lOFA%z8qCQ^T%r z0>Y58uTJQeR$_DgdVY4V9;!8Uium|FktBlRv>&wx>eXUm`S25~YsOW8iVHumdmrWD zd)4cj2X+4V-E%e;ELvu}HVw)*Q4!yFb(FPwnP#kvvE*UF=-uH^$yTq6N{`K+O0%-B zcP1bPmlN^U@2KWwPpkOG28HuGSABJ(X4_*9h2669MJ_xt8JD}|9M<+g1AW+zCG;69 zNCsIN-N0#LzJRvJPW4_6zW=;^;?#C?J}x+zk^4nYXRi|X`>f)=wR{#z9HVl7&f#?4 zQNz0}J~v^UF(DGfZW}hP*#R=O!8DGsjtG(19~t%Mw81~g{#;X zu?snq908>+bx$)^$3S6jpa5_ynOeDRm%_;K&so+78 zlgsmN+(;lS|MImDeDw!DM+Q({TJe)lDWcCV*O99BUt`*Qo|p!}Izzj>k1^<$Q)!Y; zsf+?0@+X@cePS~HY-TW^dHrW5=*J&Xs*LMC4x!X+eK*P%>$X6LLB3+ol|OD1772E< zJDtuC%J%LHmBhKM5ZUBtTCB;Z%^N+AgCEle`Bh!1u0M40KY~7$_p?r>$jk-w z6{)fc;jz)cbpYT*VG&-UJwL5k4#<87QgA3ts-UQK$(!k~%=i%};j`}fDxvRVhL^~+ zNh(Qjq@O|RKc;&r41!Ae7EiKenW5dk7(m%wyx(47(gLGU0veGylVE92fOqy@n=)c>_uMhb(Up0 zX!#Smxfj1nnUR6F(~j@{sY4PJP3Ye~*@&x1Simm|wS&vD@L%8JC3{xBrmQUphf;}^ z{oS>V{G&69qX}H`G5b)sX78bsNRT4AIGK1OP_d>`MibRNl(EK_4P3$wnEA<)2U@l) z24M7K#)d@FgH@+q#4m0qhF|u_sq)V)1rt|@70I07Wl$j#k&mq#dQ{%W>Lh!Hh$&+nWJ%hXXY_ znr=VaZCp(HxBF{T&3yCrE3(e{xblhQew7p}p|$EdGm>Br+!Hvb7sbY-dLE4qOQ|UQ9_Mkak~m5V5BZysLgi<7^sfZ#I!{2*Tq%PQ15!sF;BsqXOo8H~ zjZzUPX8OP3kBmYW@^`RU;ovR3L$PqFQa>+-do1*sYJ-^RLE4v>X)`)thXe}g#Q^I1 z>hmj%^XEzsZ{HCQ*?255_vfPTo3O^MBf)S!LEX1H!%rQn?V8RG7n4-4gY z-Kv9p%;{L|>oZzWZ)N!b1+yYz;jU!{$?9#E3mcbmoH2hxF9e)1-@{!su%9;;`<9IHv7f;Qk znG4q_Z5AE?JL?rM#HQ@EZ}85q$lVzxviTRvfth*@dFz0r!;3;(~G)L*Ym@ z{tmY>nzitxZA~Je8eKVZRF>Pq2x!lObi(Q79LG=PU%Qx**ViGR_W*R?!~E>Nz7-^a z5qsR5W2)-bF#vo_A3cpfXhw+^ZTHDSYP7PZYT3_QQQI#oS0`@AopKgU-IdY(${0|f zkyRtLKBuIsR(#IoVxPY$&;G1@%TBK?q3|#{X3K;u7f}Xm{tHPito*C-Vlk&6Tcbmh zIM7F+e-bRmqBMSdlS8+Ob}3@O4vJc6lW>!HI0 zFH}$1AFF?Xm!B^Hi7B9A&50^vHF`>k1Jf7rj%XAOxRlhqBuW%3AIM^TBL1nC?!rDf%oATav|FX_SS0J4itxne ztJz)?88V~(Cq80;j|65XMMkir=q{G zp|NeEk3$K5_gsb6X5um|0=_TWjL+{QFX06ht%Z>Zh%52N*PU18rHshp)b8-j&LUX= zPQj;?BnvZSCelk4OA=)L{$CBTUNJ(Hqm8AWJRl>>0lVu@POjLmaY6Yr&PY>Yl13i* zK=#OVuswNFZwVSFOF7TVlDB~9sYYR9p}_DKbyRQBJGbWv?7o*T@q2mx%J2thG7oklwK>iw}}jqsTTSq!TU?HGI#=`&#&Wn z%V*82neZ(*q3)$qWLC2@(rtnc_~$iwy>w*gFc>!`{w;^&=Jkk$qm!uW&qw`{16TJc z<=pyDP;n>UGSUAjXHhsW%*uYxWcb9n4b|_QUmuLTr0mYETG@oL^s;z6pj%pr@Xm#k z>)s|^+d*+OkY%X8Iwm$ON^2?R^T_TXWc>Pr zIivMf`QJn5Jw3kQsRdyLOvavS8aOoSGHJo#*3WA|Ikg= zl>Pd3Xfy}qr~g2QUBU(%PtwHyhom!)hw6Rb_{^MHFvDONGnR~fXKdLSlvI*bnn;c{ zg^DCenX!c=Wl5`1w4zfJUAYO%M2?xwp2gg4kDi8CZdbq(zF1^+dxe zt3o6wOr@^OiMKA*1WLhMmoFL(i7_=ndH+4=%H-`P%HULv&MSEO)(C()&lk^Ion+Kk^9Hn|kxO zD(H%l6O1TQw36V94?LCELjr8lXDn`7QvBhBJ(@sJ?~f?3&`esOot=b)0VJ{mGPmg_RxY60Wlm#XTo^3WADSY?UZzmH5_YtR(7C>_DQo%}XEaH!`pi3W zXfqo88Fl?6r{(GY3roJ%r!X>+50Js;b=PmUe2QY|T#ymc$AP!5iG@#9r=pgZUuIMB zn{fo~59s!)mxvxr5s2FwBvaEm_dS?Uz8^eWNP5Ng-hUnT?%sHlt8%^*m(5FV2i#zo zxpS!vKAN81wgLBQhD)q?En0y7j_N%oy?9k$v)E9gl!@y;C|a61bLNBgDr#^NAm<Y@S?1y7W?tDQRrC4HP$|$VN8p;s)S0tOoW)1;#=SedD{2uJOjj|J^DimpSqvw zs?q?Mos|`w@5JVN6_y~4jVg_H)$vvK5tb3qq>O5~K^Zw>FV%Vp&qT#IKLS3T)#NL^ zbiMrGC}w)=4@4%j2Cqkitb;X@|0~z>%~2F9)6g7UJz#FVp0+7UEvkJZ7$oL+bxwh^ zI@L0Yq{!0WR6+8iIG{lfOuk_cBWcjk4l8$jcUei|--pg$m0d(py+(~qi_{j9QEv%Cyav?2krS&(Bt{Enj9 zZsjgza-~Yw$eRL!Qaj@vE|@Y)YuaEYLC}&gb))%A^0QL*y-rTrO!@nggNhjLZ;p9Lh7hNd7h6Ak z!v%(iG77oNr<9Ct5cR!W_oV!DxM{2f7F#EvDl=#*f!b5BVX-B2=XZ@*?pzR9Kx`v* zJ3Ll5K$e&cgb%p5VliT zy2g4gQZRBpf<_xybJk5%ZcQ3#efYJ=SIQK8iBYI0<-wU2fFMzuQ1t^+E8AVyp3fH2I`xdX!yoS$Bxb@f|o}CY}kL$Bjx?skzd^TcT=qD z2A;WY4R`dL4<@Ri>WL+g)Z?cwD+S$>1Aa-Jo=g5AV|GM~_5u-UM!4Y3)rE?rNp+r_0@DXf%wrF{KhsFPKmk=|5_tZH%tz?K6FN zv^QPE+8$5qQbiaj?q`zs2jYq3nKBC=U5|pthk;{^aT3swi_9(JzQczljgb*M#bj+q z-w~cq;hEc>ss6O_cHKQ@4NY?%!(ybrVQthy5QFc5ynUeeiXLgBSb;H*tOdl;nZT% zwA`lW5;4US0KQS0sHr|pyPpdc`JCI(Nu#N(4ji4ObVc^}xYuTSWEGaUTkrwy- zSV=F%7$;ugV|K2wIz$6mQx2Vm9?yYjBvtl<~q^xX27nAJo&AVc+9c zpXIdy;hM_9$yBe}Y%vvD9};_iyhhid7)#Mz3VG>$R=K}Q0#2YSHLz(KnFwF!p797O z>M{-D5U3TIRrvjpy_X5TgyugmoIN9JGEtMK6m_XyC=Bfv^Zwgc4@-)dPCk3>Jf52g zG!sXpMMmgD!XLmpTM(_@mf2Mhm>PY|-Y5`k#XWc-KK?y^6`xLdCj*Ajdws)63m4Kt zk_+b75ZVgCU3XZ@ML$p3IFekmZH0WOWc9B+(BKZ@u8Wk}I-rS*8eizNF0P@+Zlq7c z_S%ZO4f}BNns6hNrhVr&u`D9=jF|<%;MqTN=u?g-4r278TF#P43WQsENRGnnan4n} z03qc{I+rV?34#Uvq!(YZMiOO0%1J=d5;9agi?lr%td@_Q^7Dj{2j&6%#8FBGBMz-Z zaej?W%xiN>6}`XlZARByo>SFhOnVL?CgGEScJJ6bc<`i9QrU+%xhSGp6%TkIhi-l-rvR*dh%UHMPa<7~ zCw^E;URy^0&)?#wYn7>_V{A$>RIdey1*34+B~xbPxdETgL_Nhd#1RjFeN^k=HZWkt$CzkhBxgkE#Z+;()ygc*Gh@HyAnqe%L_ zA9CKlN2BBk=J>Whbebn?Pb`|4t^{XJ6^I|aPCmKO9{O>IQ2M5QtlXklpSSO9ilQ;k@$6;Nz2*L+!Bqa{JmmCJ)NZbRVUP<0 zK}%A&yz-Oe>$i)=ly=6(5aPO|_~j0~91Q2;dz_od3UBv&m^6n_mLm5g=ZqbQBBWfb zq467tC-rHxx13(oc?VLbuNhEnBUxdVDAm$8W24^>*qzv?NYfGHA+18rOU(I4E1RNa z`*GBYscb3q`Uh*5*g9)xgSz!#%O&w|UqpSIImLn3Rl_ zrT|T*t;2-#_RA$m?D4GRn)F3Y*3+gRML3)s@cV7FKq(!C>&T& zegbFX{33APx1$odMalwft=OU6IsmcsNVa3&PUw6+%({qoIr>Cmh%IXc?)imc-a5wO z&PsI~4AG1IagFsi;e1)Bq zVeLKsEzUE|^nHws9VgY4|B$XK#pAY}-N*M5YNdVML`K&%qDiFlaP&io@cWwAkRCx* zZi~KauZ?HNiL>ioE=(GTr=Agcw%!rZ8dE#BoX~7#Q$p{Ey>=Hhs2fn4#-E%+NDxy- z{Bv|pN{0d;Wt6=(hwee{Wqg;*d$@cTdgLX8y>nS-mysTD`cakE?G(*$MAkY?(l0&p z=^ljnTn@Xf+>qMpUnfoHb+J(87tXq-Y1&AaT#xJD5a+iW_&KKRE3GtdFo5N| zCGln1=0vWesE^9B!lls{O^jiOcCn;z{g9L^fIik9MSP}Jx@$|VU(x90>k47nH7UZ2 z^*5BRDuXSbP{f>7WQ1`GDNMr&ZxeuOzw=T>%F(lmp{3_=1;6}|sH2+Zh^I)v>IC75 z|II_^?iNK#*x#Si1p+Wc1?0n{vKv%+Uiq3QtH^6?C+K%@$~4ammEc?rn zOWV)J!kSrs@HZ2(#Xs$Xz2L%~B+@6&@9#qAkJF-?$un`f9$(fJMo3x#tLO|0sQGG5 z%w!bzqaWp4dPrnkQ&iaWApq4>CJy05GKc@bZEMo(*f+4)cJSQ9TV>U(PL;~tOS#;I zD?w3HxS8DE%gc0HH+TF5Y_=*x)4bxDU464(A7TR4MMO5I)(-O`De~$%AW7DknBia6 zWdR>Ov&pf4vJicIuUKeJw!PVQ%G;gDQ3#rsUTaWn=m(VkiP^~0uYy99X~QYU))|Y~ zzzS=ozdagnsZcS8cKPKC23@r_W)`E>u86YxCY?G|t0R#YS&E51LioL!+VdHWugpMD zVTLwC@x@xKW%r~IKNf%T_TbxDDTH;3>+X8e46D2o;Kk6hJh|8 z#Yi2m#zW6*X|_52L(JVror$B(!2yTPfG%+orHlWLF&XdM&t@fUK@V;nAyDm<8Fv+- zME8GCAULs@VzzEU!BeDs?Oe&m&F=VBkqA4F8ogO!)OCy2j3ZDLAx8WHw~LMbltQzZ zaXS(EXh2gtbB*O=Wj$nxT0rhh1*xCf(MMT7NbzGU7i)&R<%bl-?4_4t~o&yUd z{su#=OuLwajzsQmsw^S@h7lPUl`n zV1FXH7U+m|xCc6D%@}u&#{I~ZE2b~#nsTBTYIVAU1AS%zU@N*A8uoLg2!{{iQ^i?v zc6@{3(Z|OL5%Fo?5SWRaz-eM-<8r;&OBqzfze}Q<3_4PSSHx)hKfqqZ9{}h$Ln-rV z!0UK;R9nK9E2KsTq3XD5K}&+O3A|b0_SXR$BF9#mAA?}yg2p@2o2Mj|({ zNgWkuRz;P-JH>F~i(%8ePt8b9A}|agg`vFfpxn+P_1n$F#cd)c?`J7&@HFm57TT_9 zX4xk6=M?$l?q7k8A1?(sj$KNc`s@!{qEBg0TfNc0x8~HGQTKPiMNGjnq#KP1buH|g zAYZ!2GP__7eI;5OQ}V0Wj3^_n)Y4x>?TuzE`!GDjwe2}k`{1^xgwEd>0}0%dfA`T8 zbEfJBu7+^l7l9CR3`$Qbx?b6W{xB@`Iu^vdon+DZC@N!47MzzC{9ZgCpW8-Gy8=Be zfb||Z=Ez@#S5|RXTV7nOUaT@{IN%z#cpX&{+?1lt#JMqS7)NEvV!D zofh-N`JWLsr2JZ2tk-OgCuo-v5~ryvy9>4i^Mk=Oq~dSA#8E84MLp7hWTVA`2+VCs zmj`?BcyT5v3-H;%<<-lzu1Sog0tc-?z4bI|B~NV88p7d7Ed&_jo=q{SB#Uk3UPIjJ ztXJY5fa9g)B1xAGW2By2OX7&Qq4ytv>Isgj&rLey2U-}t%jMP)@9U7OR~L+#{Xdof zon7{5wE#3>sAtTQzoY=Fu8L*$C9qZ$+0~`!t8&;30^=0G1Pp$ib$6*h#5t8lnxNjB z*N2+GRpP;TQuHzr`Zg#ro^Mr%?Z$<_IG$q~>gMz=gC6Rq0YPHwCkhpwv{BS(n;`sG_@|oJ(G_%Mg_m9}F#>7U&vu#*|eVmY9&W)6{ zbswaM1M5CH0VQOR*tD4@5D0+MVh(*pyCQ6r9KBx#ntl7$wQdy4|CS3MXngFU(w-z? zik-v9=W@jN@N5uLh@sQ-k1M!4A8h{dWQpTZ61;ChwBC3VEM_NJpheLBj}5U~?F zALc7nzblmL#NuPV2v~Fsjoz;>X*_ZD=0jNiClzp3V&=UH+E8)H9;o4(vgF*gfmzPn zzDd@hc_Ey(ADh|wlSh}~SINocr?ZMpD-v#^Vw8|!+7{5I0-%Ot6#3;H+tcjyVZ%Fe z%!}V~U9JZboU?vu#(Crm{u20h30(7sy7w(sJmlA4Y9^M6&Oi_L$*Sc?%2zmLDhX2f zn22;tNbzMDs%}Ues+A+ro(4<9mZEs-u3>Kf$$5pLXsmmHSRB9Z7(5|ts^e5O4v>Sm z_w_~;A2}=#6ZlVve4QJ!|9E-}4kpZawAcQKwq2IYK@HjFGG2~tBJ0GR7p6@0$BY#b z!`+5J%{O=^X29XrtoSwaV8W6gBJiy4#7@27_|Hhuns347)dc-l=!PXN$mpwh_3^W^ z#`p!m+jWKUd6mEMWnb4*MKeFo|Int{+7NG0{yK~@!^eMJ1YOaKi@1jG07-iwP>$fHt>;bxhuSuOc~Z zSNf0=RQUkcam_&JV3j?TCpP0lSMU^YY!`4(Q<{SC;V+z8l3t^TNePOfS0)^8Jrvh9 zvqdE4sr}A#O-dNe1ueL0$Ldxdgy~)$mW@rr&BliEUZ~#5-l>}=4*)lwyaDW#L<>H= zNBPLh=~$IYRELNL{$Ski(^41ru|zj0R%9rRh8#FLh7*{?RbNBqq9bqSp*NUN$2LrO z`bJERga#uL)G#22Z@Yu}beO^{dxdL$qvb)Asg@a#;tOqJdB8w!c<$MlyLG-F{YXeY z45@A$eZjiQpD4N_J;bJzY?L5kz>g$;Awu?2a-8zH9`%a7sn& zAf5YshcT-y2XC-rVXG6DbnF|Ub(G+6MF+Xw&sphQx68-GtV(lxg$$3T70R3V^=W>} ze*%zZ;!f{$I7xecOXA0h5-o^>$#}ZPr5iOVkiW^`-;YJB&?-c9s^Sgero*%Edd_-W%>cuE3#^NLXLRk-FX zKa_NQ(2QrGFJM!cCO20(8|HR<$Vt*ry6{ykx*3F+%tBCqUL_k$_IH2i5R3S0ANZfc zYzGpQjH8TnM}v+r3L}+Q7!MODqr=h109O9q?l$s z{g>XJ#_N)Xz=e*~_0*=b-f0j@kM3vR+NJ3sekM&WYSsEBXTWg_CxPKhJt+_n@4=Y! z+oey6jwcj*;a)y0Zh`xnxzW~yCDw$WA{|rkbw6q_>T`ma*fk{Mm~#BJuy@RRo01l9i*?R4|be2Dp>+;xJFwvNOSC?--0AS# zXH-}9$>P$(%_hI5R~5WdH+>HjogzT|EHssEpTNs|cM5!$Pr08rr_e3!B(^gWJi+f% zzNHE-Zx6CWeO|>Y2{G>~Uh=nyN9&uJdKWl#xDz6tFQ%P|(=n4+TXz5~z;z0fFLixZ zmTp#?lx@^^9lapydH=MHxW1dR6Sd+mk`g9TJm;@U-JivkwvvIGw|=Yx3k@r8B<}lN z;Y<#`1#Y;VCR$m9YnXlrypZ_V*oS#5(kAjLD2m%4%9YJ9LD%H-Ved84MO3l`%7&k&n`w-3~@9 zPbe3|_1(S8n=B8re69BK1NcJMtro9uKaj$CVb?(!*n1aPr0Hz%ahnR};>k>|f0+u9RHFza$xp4Tc?XxvLQFxk zOMj8t(BY6x=Po3`)J``rR_{In{RN1H2NYsw5AW;(NiVAps@vs`;OooDwNg|9Z=yVAGF_MyseT`B2mHIsqUAFWqseH6f z&H+AL{l*-jWb?0hL3;%ju7iQ_(cE3w%KS5Gx=g+$$&@nFdf6|#OMi=s$S$?^4EA*i zqf&dP_hA7tEO}|x9-vKIi<{S|;1$}vjG25y3!MVKm6Y;6S)8WPq8yi1g|{UR3vF<1 zeXM1B4{yOrAg?=O)AQ$Oy*<2BW!#f}+V|?~Ds>g9<-!j=M~Q5yZ%n58Q789x=%&dC z#@Z@z2geSR`W5iPw~Y!C_!+LT*)m=Xq!2>FjoMZt<66aEAE4fW$a{Bu-4p*sOPw!= zPB-shpxPJEHWp$r9Sn%Rj2DPmZVhcPC<${-qw_UoQ4POsAdP2OLD_lSvnqGwM{9_O z(X7ye?c+lcm}96Xr~{2z(5X#dLBXqcJB6xhr&8 zu;67k5@IGJQ;##8N~gX2`eefIVZZFp@BKi3PW)?Wy#a}xiHC8O_r9;XWU6o*6z=ye zCB+!7Cd?REm4@O(2>Fy#yq4NlVM_Sb?`Z42EZY6g*obY3cYbwfJNoQ_Ud8bS&s;f* z)d?>g)kNUzpi0~+E5KQcZXCyvmsigrv`aX4d|bon zZ$!*6yaXsEYMv*`C7*CgE*yf>G|=42Qyh$vK=%bI{Ll#wRp-p@!1`M|-i{(+LGcmZNh1N4!4+bXATC8sI(VN5q7y*sV7 zinhD@sZ5fr-#1^!ZNjx+>DjNznWr|I;#1wgu_eRM?b?vWj|D>1iui6_US0MbUW&jJ zgtuc}Y);~;yv;#0YJ?`f8jS!4oBX78$Nc!*St0+g>Z|Qa|6MYI`9e2R#M`atu-%`9 z+4Voa5kZ$^CFK>${hIW$Y;HsbUTL>KwlOtSkc^H`TQdla5Xx1A{xuhsZ4^503+?xw zuE*yECIv{iS_<(oN@OKWjB@ROy}RNO1A@NfOOE`N*G@!H4~?dFlta+udMG$PU)Fo` zJ@oAjl7-g?M*a`}w+qPd=w66u(W$A-AfBlOtU0_BR9#Ami1|waevjru<)UQoKkb}5c$vLl zXteh_Os4jT2@?y1ecoBBuJ5p5p6ne6HQ*D9G`?>ok?dFw-(zeKVR7qVLb5da-rpAgdRMOed71n1la&03A{Jia0wZh>Yz(~ z&g5JK|6+VRTIe)Q4pn*0Nq1LvjUQZp@EJl4mK>=Z^~=sLTL)d5z%-BQvfmNqfAHGJ zekIRsSWAsIsB2-O^{zO>Uf1Z*yEo{dM=QXq4DlEz0IENRjyd-1%wU#t2!B!lMLL6` zOJh348y7B6eY0wl7)o&EHm+{LClV zH6D{`gd+u#2Bku{zU`8rM)8V@LE~v7?$3NlAjyn*#!r}HxP5_?9vGtr`Zu?6mT!rz zAMsEgZ@lmg@zmTjWsaY1XG(11uP4YZ2`Y7qww)HDdvjWnJDo7i z+zLgeyE3u~#L+(JpOI)-xZ|%;Cesq-6Z--t%@Y?_Oh-%zAv$<`uW&=w#>BQ7#QcGG z6)rhM4JaoUDh7ZfJAyzq_RtFqQKCq=L52;b8w7-~{2%@-ns?fA2&Jdb-#~&ko)9UZ z_8`>a1M_s6yfBAp`~SANzdO3Dm$is{&x#yUNlz?aFO(SfXR6_7T3N}NWA(cQoYzd? zxEF(rgERdkDq_N@(76z?L+7)fe2~pu7^2f=xV>VspyhYRbw(=}yzZkG-9|R?nNH-E zq=<0BKtjT1+D6V_@-ty83}Q>u+^?r*G-Pc+huBj25qx1fNey&5B}B(}X#e?$v-6%T zz7rO@XQA60oM&cpi{u67oy)+ww+(+nJYu=|Cf7q4`)arYg$RmGQV>@O_Sos-X&myom@7S`4nyobH`>GH8ugt2_<`cIdknIL4C@xP5_l-my%jwvP zPrU})&jrNje%{67EH|TSld^}NsVALoJ6Evr9?!IzlYXQ<3<=w-=*y)^>sG9(^r{02 zL{CntfoJney!Y0YEHk1=c+u+88xDI!WY;pe!r-i^L3q*dX|8)UM;WXWu{xM2RWTci zx+aJoktnqdD!MrTPcP`x+=S%cey46QPaz58uN)TsZF@YS8@( znSB!Vxg2aX-h=+%AO3nLPfi}R;N0~2Cl>C@H55IH?6AS!IEE`NJCXCjEgz%l9?5%A z&x+?rb+0ZE4cG`UqSkea00e)#5Go#&J|oBUe}(lT{oClO5Ah6AYg@J0D@mzZ3YT8N z%q1b&pr8BRV>s!KR49=c-T3ccZQ7)M_KSopjGh5_Ie7YbYh|vGZyux1qkgq$r3e1_ z;C%@pf48$*@IuX75TFV~eNVQ*eKA~Kq&Pcdn6)!QpL32~~~*=~JY7hGRNBF&C*Mkxhp+tHN2a#RfPfNxkV0YPCN zKz<+mezD{(vfbr=WD)8U!?2XM$4Y9SxR{;YE^)!>ODYHGVo=4xoPt4*|FF4=@^Dhq z={9Jm8IR~LR1O3~F_qEkoXBNdMJcX=CPNcGH!Iq7cV*KdbCaSJ2Lu=fHVa|z-mQ{x z?FvO1BsPzT@hSN9lQ&qBX=Yf;%In!Ge@97EtT#qZKEE;ZdA~PPEQ0_pd?T1JWw?e1 z&`r~kU~;OcJ2u#l}Hr0INbV% zoCEzNor!EyC8lJ;OH9{V4`2Kd?8OO{fwr%R2<%>p>Q^9CXeRyi?6?fs>#Tt9pmk}N#{xk)rNCLzyZps z3trP}EQN>Y=5PNJTxCrvczykWtnt9)@0!P!3D|~^+=+7a6xscy*(8iRN}2I@slzR3 zDW0G|=+4r0xC*!wm&+P>M?pW=4gIPqRL>e6xY~nhRwF|nkIHfkyI+ZregMcus4FVL z=I+NhlMBBUfd&k?PgmZADG(stwk@|OtPUDdiTT=O{v#_wJPlbd;L97*Czna9l5ZgeLj-RZFaoa6LwVxglDb`^eP)rZ8*Agwq8Pj8b|# zS}YKh6Vjl)$8*KYq#BaVZqCz8QgIIM^yt36z2HkrQzlk{$rRXOn+L`5sXDD=28fvE z;pc=FFF3}!(6kQ(ndmYhI)7k}=Z1Np;((!OKOw|=sO3lr2I8>elv>>9))SnPIihRq zW>y?FsPO3psmMZJiuj9XiWU{0=PzdY6MMU7ZYsXw)uW=#Oo>C`3$>8byuRmiLY&F{ z0^5(-gy;|bMgqH7Xs5;Neo;U6xiqCW;qIaAH^$6HL}&~ z5$Zf|x@872Y!yWcg@!=GXl*DVAhBn0osRfvPP4;D4|pD_TF-GNN3himXf~%AvGDZ5 zRj)*ericJ$7RZKncL-*jjx1Q>sz1-sAYv=(%4hVpvkhCrhmtb{=qO`UBveh zP`cXAp}PV!<9r3AaA4o}0E;fD80YLAd3-zABW^dI$6MVVvJXA$AeVZRA*Qz>md1SF z@jN)`yIgwbd3>2Lz@M8HQRnsY7%2Kl+2h!i4~LGgb45QA?|#^ddliI<%RDc`yjij) z>3VG#F?nG=$19}$)aleMwJf?K1Z1nPB&hh@N3_4aOVGyWycKoa2E?{8i}?C42^~ZJ zoVk5>Z>UP!5o8)blCS1|k6OhDSc10Hrhos6aDNhtjrT+_UN@s0&i_<_xKXkNmEkP8 z0#r>}M(ss0B!?`&i?|6_H0Xi6RgW|h^M@t^WBj=&8{5BH(&F;97^?eu^t+7p7t`$}8Zdu!O>Q423R&L8T;wx#l2<=!k-vp3hf0}7M#$gbzkSw* zy4Q8L9DX-vBrB%31OGeX0X}s!0ep^}wYcB7D2wf>T*vMUTz>?KzC9$d?IQfiy3|Ir z&1Wy{bx{u}X2m^6wp)xT4@saii^zg)o1v~>%C3W$3Q3ynYm&92oZx|V09Gjh?-f+q zI%f3W`G^+vzuE61FDVp{m^;Vpd)5*3^vLTOoM<0owu6GV3SJ46`xt}>`Mo{cwovGC(tipO{Bny(Z@e8V@M(~o88n0I zwph-KJ4lV*Qw(AACvDkv*zl7&nCTn<`dpCX$^5(;P|D#aslDzhUrME4r$iRQ&XZfEyv5vjutgxBTm0^{BwzO zVDt==cl{x@JntEewvxx~b;z#12LIB4yajROD6z&9h{5{?y?W$gE@|lkccLgfQ++9q zZ1Ns(I24SkzgC*Q+2i~H{J0n5Zx%Es%GkI1BcmHvo-BpP?2j@YC{W`d0mPyd!c?09PI(o8QFQFCvsmiI$>^XzOK{&-s=k(O##)f^K6bcIB+RxZW=A z;+TzA^jNt9;4I1%+O1;t#*5nz4~wdQ4_>%JY5)I5E(l)zdMT$J&~fdoV29J2Dib zc&}YhtAew5BWoXuTi%NV>h-fHvm9E@wJkiqkfMKmbf|EWXS0Z?8v0EbYgKrjwXhP* zdGsjp4`7apkyS0Y*xZzc=6~)ALoz%{S`F~OI+eSyzteB09Z!!Vi?sAklDs7lQF9Sz zpo-K8^9^wMRRiXuQ6V?_XO$BYU;Sdr?8FQv{5axT^_y*stS1}XB|=rkz%FLc(J{e$ zm`{)|J>d9YM0g81t5f+V&_EcjvOqkK^OaO^ch5fHH*kYXuyzD?fsx40G1PxcD?6Vi zSE*(JPmc!TLh>c~++}3>hy&zM8bf5)1C$Xgk5G#;y%>Tp9F3q0w#(%FYd0p%e-yHo zyLd|VEk$M4PGzM=qy?k>s~0-W&RB5#MZE=!Ntq?$ysNa%8(7U!ht_%g6H`Wh$VxE$ zvJ=xC7lLgT)v6pkSnxQRQaS}hi-z)DO<;)mP7ot3+<)0XJ%)Ue(t?3)R*bDFjMfzg2m!T@OF435o& zP+_9m_q+a4IeG!RJxTT7dQZQbf(;XdH)RX3b1K*FH@<-X8t^%}X&014w&&$qlg}qtRxNS0+A=6y19Ro(pFyhGTUG)-t!Sw zZ>z}F_L@4FDOcg4Ab9dQd0WUE=k+?zRgQl$MulFWs4*ik%ULw!5^F zi6E1pjV9B|muzRI0K=itX@~4aayLsf!!itfYA)|Rau4IPe|&s{Vn#;1I$lfs>WYw; z^ydj6*vQrF+ElxF{0_?~pNXQbIl`DwtMISLR>kXZvrjDFOQ*s6IL|3d!62z&#w3us z;)$fORT6hT&`n76}LU+OrNH#2{lp9f*^jM;T$$K~_z@ zv;kzchV6cJq!I2zAawdaFg{p#?YfyV)387#aOXVN98LBxOcTEfe`WY`T38pp6U+-s zIFW>_xMg%Y%OA|~N)-KARfl*=_#C^<4fWW#yXyq&nz;thI)$7fGR-9uUXVJl>Cltf zP4a$OLH)ZYvsO({cU9BbYN&|$5$S0WT z`t(;wafaijC{|!VA)0Ptbth4#9I55CsaY6`TA?-ItO8x@2f8CJ*p5hyWt~omO{55B zi3x-&i8tP34xav6fOJE3VplK2aPM1~|4xDq4g;0gcpl*b*ATG_aZV*22~Q_Vw@&;~ zt_WX+^MhAPl6+%L`o`w7AC5JxUAiY{mNY}W&k5L=VH$ek2JV$fA9vroV2SY2{XrYU zwy8ZD#ZdQe;UX)8rD2lO+bLYAUY{})Y(8rux${(#wME731m#V~4?13sQ&M8Yk7by7 zkV(s?avVEp@OieBpuG?`ezgQ3>R$^Zk!(0+3>R>}G%n_wvv#GPNN42-@xuYFxMKsq>y;OpW$)qz}o(gr)iL z#h*NZSzXv*JQ_@w$V`g|!Do;ALj9fhv?hHybDFnvAW@_EAJA!z?LMVYaik5+zJ19v z)Mz;3Zh036KT>u42E0JJTw3KZlvvqEt3J~{cy3lqCC;gt#50({(S$M-JFWf-exyGaEnJi%|r z_d8X#Mpz@L6e7vT9t_i$r)}Cn;ztHzUTJy!OMQokC`SySbA(a`IcdE)ps?-;XD+P( zDG`o@fK{jjAt6KiHgk2~!(0o0II}uDbtGKM-pTh0=`a8zYApVLDj&9%QTu7+?>{GS#XryTa4Z6!-KF&0tuULf?XkI zb=8$*GN*yh9Y*~LLfr_)>06RGi#yjBaEp*7&f{_7RzMz zbVI$gT2elN@yFnYy!ZGhiRPt`%H98#!87 z+=h-)Hn~J6+w!-Z%Rl?)gTl(j=)5}9*izRrpCMve9^)XY;%5eZ7b2o~p5u;pJmG&Q zLmoP>#Ps)daw8=UX|FiH9fseoon!C|YO(kMF7Bc(r1})&<@t~+`_7BuBP)0yjgCQ^ z2Xn+F!Rkd(H|KqD^yC1tq!d51tc(1ULJm|nbjh!BV;%{P2D$K(*3drA3R#$UvLNLt zMWB0W>D=91gv7(ggp6yB%q$9(SQq?|fSyEV@&O@Da*qHoMHZS8X=}lvDqhunvcVl@C8-K33tQ97{{z_v@XfbO%#g@l8VL6lT-Uo!ZuSh zm3xW#F1nBisN~gOMfiOk-18TxVAa;dk3_!GzIo31zuQQpG6WvwXv9POPa(eb zLf7^gVRj5l0buV20 ziwyQWN9h$sf&C$KgoLjX%GQE4<H}m&k}ZmgG^VyIdq?cB9^uO7h zQP{!-8CDy=i%JQ}$&*7oD6ALcv&eD(&r~2;EkcojVQD`)t?J-&?rFq&H%&7OE*GDv zGGPKE{|Qm1O{c-lzT#~ja^R){#*yiwx!3wz!W^e%ed8S#JjR$ul8PuNQEj_e za|c|crOnkm(M_$)C*|$jMf##TcBDARgt?l|(Ouilf3}l{`^uV&1i3@FOfwvNt2`UJ z{@)mGRw?eWAyfa+K4eEu2pL^*O8K--m=}>@yUwgqm9rSYJUKrJdJHLZ&%v^Y3!fZ5l-mF4~8$WFg^dPi}665lo$)^y4p&^Vi!YjU>bl zNXg_W2Ec!nL-%2$+i7wBDe;mFLs(*T|5^{O%?!S}s(1guCsRj4Q7tnwob!?=OUs() zL@rSTQ*mXD@PvrWok!e{u!s$A{KE$H^iAtfItGfIoFxpo+`bF_wBqAY6sftoQ=46# zMR`ebKM?S|pAO=7iT1O6o7gp#xce>7ZW|A%;iP1(9#JeYhGy*X`adsp|F;#*ebm*DYxJ* zImxYorfK4r)@31N9t?Q-CzD7|kp&ti=g$ZSORhz0D@8AV) z?pvp})TJ1-$vzm2jS>^Q0`MPAk^JKr3`?SKC}HI6;co8%BhTl9$QntBP|^3|ZR~Nx zUavIz@-CkDBmjtH+hel5;it2r=lE2~2<%vVcUY>j?8 za=Da*jS|hDX?eq3pvHSX)S9MT%-_)*=3mC1q3rY5s;T#Gu2r$i-pSi60*-yb31;7* z&zmQf`{iJ%Z20K%15)_Kq`wum&Bz3LSHgL5&-AoVM44Ty`-H>Zd29i9eWO#0U#?zG zj+Q{)cwqMfHS!Gi|42Fye=6VqkDqhyGdROB!a4S)tYgn|kP!_@93;1`lB|$$&LOK* zrzM(4X`)UgMCHy-M@0EPF>}P%xIsokhh5!8{c=@%Mi4+#9+PqD6{O%4 zd>P)kw=&Ok#C5XY&LVqlzp{|&ih|jU7VKvyn*3eSpo8t<&)^-i_k@*WW5Y|H9<8iv zj;)=?eT2@0J&s^;d03!%ngnAnavWZo7^@RC4Z2`b=0jp;V~4c4CeZB!JI-Q)dk^1w z8gfur=Svs8E-|HPu>-b1ackrXueXTNAHd!D5O_rL8fAki64#r9VM3q9lHgK>=8#o| z7w~%-ui&|cys8S^zxWRIJNs#t$mzYd743Sov;OhxZvWNeFat86_BsiJfh$w84DJUC zeQ>)()uX^r{Vj09z(Wy|li%-XSF5nHn~jN$p~^Efn6x zAlM7%#oha|2J%)B1lbd9%f$r_M+ihg9)uV{U~gE>R)eV#Yk9~_ao`C&I=xNxu0(jc zb6GxsTqMm3*)@+&cJyGMNh<2a$W_RFD+{}VkU_z?Z~u+ygMOf31>vjlRX~+owq`mF z7>ZZyt9gU;e5k^cSC9=hq;Gzz)F(gAM_O_N{ZF(EGTHEeRt|m13r(# z8;;`UnfC^>u(>;nH^H*f3P*%NY8JdJG=3#|_%AH!n4;1|h|eiIGy+1H?8p_YoL&uU zu|Qma|G_#*ZPIzmJ%TH&q)@InzK4?H`g38@WPYVBHwadXc1Tt0o)e&8xR21(b3w+N z@;6&)8CSP5_UmA99dQPqRi?b|8La&xuS+Rxc|h$6H? zknhK{*H&BH0|v6{$js7F{~?!}Lu%rR&)UfIA3VVw$_%^m4rJht2KVlKE^2ZZZi(4q zqeEOeHY9)Rw4~G>u>**9I5#VUqsfII4P`5bWz-C zHU=x>C29!fgoUHOvoTUUng_Z%>pXj!Nrt_S4agv44eY@eNP>Z~S2!|ok1>1fdJNzF z8P4J|6<*{d@ULnpxn~b-bwzSXB?vg+8!dXZ$I!t zkMc5l&Ow%70Payj-rh&$FZ!l)0zWXbTd&Zq;<86P(S6p4))r4#ekfhsuvhIm9&aAj zlHnd;g`V5=p1ruMNb3ECuj!Xyy)&gPm`x(NaH-WWWyKOwDU@1&fy|4)f$bCxaQ%Z~ zE_SYHNviyR3-6EO4WIcWRBt}?{<3NjbRH%Kv4Zq>fKN7&1@jpZ1 zWMk^4f5Lb_Z?NF)taQQ?V}SSi@-Mh4hkL<5q~Fu!d2K}&t$4S=ln;9O;T*Lf*ppN% zM@X1CC(~z$IJksP-kHD(a!Le8OpujDxZ}Zypr@pYeGBk82goI-4RA1b4`r5K?Q;GT zJELkFX)cU!ei#lN7JG-0v!h+{XF3HEp@}2X(O7vw-^fDFXVMZOCSxN)neV!Pf&KoK z5$KBYb)WHn7qEp{pm!JB|B}v#Sh$3YADw4zkcB_zUq$Qk7dd#=X?|5RexJCR(>V5N z8MIL@vMhF5KYvra9X59l_~x}j&v)F_Z-~CF5=rv}%lt2%#;hRMZSE=8^hO z7@*ax6fO8+XyfCoVe;7{?MvX?f9wu?#X}CA^AR<7hb?|dw1Ia4J=>`Yv*9s=LD;(e zYcOAVh6-|753)jAP3npG&7ghiBNp$&@#mR==p0$^8_P3F6byuiW9$i~q(^@*_Cu%> z=c?XR4nNtE9j|FN{ccOA&xR_nd=8$$_m>rOI&kC!-6r^TfrmCr)ZLO7?(_#s z534d*&k>f!hu6SEo1UOP9ta8t^HJEoWN@5|hsW^CNt~QJFy0L*Gpj9pN0X%YB+S0O7UdswB?~*pWiYkHcU}xDEeGLxa0mlZfd2r?{A{S4E$DHOF@PWEz=D zqlGsP0G+2%zo9W=OvoxLS=MpmHA&F#-J-6Rg9u0_tD=tjsIoT7D2Hwk6RKZMaeOjY zfga&tOeILc`r9FcH@b5Sjumpyz?ni1r&^{O!{x=+A-ZwIGfSj-8`UezhWz|hUOX)S6|WloSw1~Ov$d4I=S=VSJu5j#B6ax%EY15#_^ZWWLIx5VaHdK zlVZe$F}J>I)&8>mCM@;aswG@(rCCP;uMMk;qpxfV*+(!h*Dv%5LF%h$3VN-)Fl2>0 zb%>b5&KHpd0D-IvB7aONdJFB*)#-m&vK{B*;PIw#FrDGI?%6H$4fczxlkjeZ?_>Io zkI*iuEb>+`g077FU*5;nQBATEZR9#%uSIRtA&n_KggqO1dkz0#1XqV}I-8N=`2h1P z+z5G!r5Mq>{aBIKyr)&H%2N%#@P9phwa^V_BC~TwQiR`e>GX}k?8w1>{1_A1xbT`H zP)H01r_jphh=uD^1c#?;-kIuX0;_(HWjbQ6rdnus^_%Eeus&r~`gVy|6|A`9g@|MMn|_?EYoXN6ADna909OH+)L&{7xnSL|O3k=lfvR z7@^7)9ayR@X7I~d-A!2$_#4VUD+a#uK^sh+LU~SL=-N2C1!MtIkig`6UQP-ox;rC2 zI(R`f;5E6Aw3^SJs(hQI;Fe{4i-Le|bwNYUuIg{ZJl!FqwIF_|H;7T!M^X=*UqloJ zfKFFQ2IU({fkSuaiPRpgr-+d!t_*}qV6X^iNPF9@>5r0@YeTrcE<17E|m36wgLQ`hQIS%?Oa;6_XS2l44>Y#Q@3;NN# z6-_lWO5KYh(&n?^zrp}gotfjtt_9_@`J1xs_qRbDw1H0<{Xi1yS{h+?3h>(UhIClW z>+AJABJrp%JeG7r1V+%~Upm|^I7jt`CqhRx;pXZJV8g#ABgm7$V5{*ws2sTe3{yB; z4$Fu+)%(@^c^zrweNJjfQDN@w_G{r?i5Bag1#uQRQYqmRH zM6hB@C4jna3`6R;hco%~=3vDZH1&rNT`e(zBnlwP{h zqpG3OJL6e%XS1v>1BA;_(8`QENwCW&_rcSp)6qKPGW~If9)05m@zXR^mSQnbjVQ2dv zxJ`(Q*;2lilXRc~F}f-pea=i+muS>}-s;_V_1Sf7;}vV2<>J!6|JEra2XBGTuE?YO z@AccEs%Vq}7}A7U&ruI;+7}$~0AoJXr)DeaA zOxE8jRY7wv=Ep~}A1emjn@a3Ff!E=rV<#UVo3dmm#*M!Gjy1E*D-N{PmzdT!fxcyp zXU;oaqQLn|m2Ld2LsiT4Il}vm{V#IZAs#fIejA<|&P+uKq(ifaP}4=(bjM4|2%TJu z1FS_PBN~3e+lx*VF<^HftJjbHKmnC(p&jBEk{x>*ldP@LjX1EGE7$=-_XSQhB-W{? zh>vBWzvgyd9nIK6Slj7fLL8`5=kJr_>I*~ak@?%~p{;S0o(p23pdPZ?>l@R#g>5l= zPLoJ1XJLwM3Ezr>gJRq347~%Wg#x+mMv1KSeDlc0&EbtA(!MRVizYN552qa`7Pf@1 zKs`u8$VTwQUUG5CIU~nYk(*HGMR*1G$lE(hT_%d<2vuN%D;P8B5T2eQPunBI&a_p- zzub4?6}75e=6uu+RNzj?Y3!~x;Ed#-X0j)q;S$bS_oRCSo^M965I454F~s7peCHSR zu-%=|184`R{xsYu+>oS53)>6Gg{M3_qjVzkgT!4v(95^8L^WwiG7_6QD15q}u*?~J z{e@8ROJyZ{;H3NzEGUsMKP}U<5s~bf&Q2b~Vb?;ygU~!WA`Ka{RV_`;#)cMi?Bv`6f~9Wha6$dI>P(f6GHJ=z>GzXq~Ap zCijgbzh0B4xyzq26c|k}f9Qv*KRpjV)uT;iB4z$0aF58d_HRP8Tn82tVJw?@dhXms zjQW$@I(j{WRizZup+Z@ZcnmWolsJ)w+dB!$SIU0weJLUW^*yMhAZy+&-Xe_9*#o9J zfa;Ae1~_3UnK3}_>b~}%MA2vJkQ;VAl7RWu!0DY1iY7=zwT4E~H~Nqnub%_QRfmtA z8htnI%qx09)$l^4b zkX0AYyDbXhex{pk|Eqb@9|25R@lS1o+IP;vYzS!}H^E+&^N?&%2BU&?_^6NPpw7g9 zx>5zx4q7+agRM<8Q$!UuGyN)im}~H@*W|hQPk32Fh2Ke~Rm7qQ6`dzwZ6t)AsOW+~ zLjN4n5L7|d@01h86$3FQszBt~JhcpNi3{I$PuN+7+Ij=6@&X1%_st(8&Q&(3MC`E=Q6#^ct(9 zh7_*P=U#Yvw85#G$f}Rw>8$6EF(IRG(x4+%eCzoY`cVwa;@DT!=y6mbbb)SvJ%?ST zJYAu+boGiH)lec)Q$EhAHP@0E0_G1jC(7EiU$x$LzJ0H@moJ=dTC3+jP*Vi9e7YrG2MiKps54OOKQ3mO2^L?$$n7;Q<}|K5l9k=_ z##8K>cwL-AQd_BcQs_^s=z?`ZqT&C(bOu-9{qL2HTx|^q!5>0drm`$XEd{xI1%E4f zeP?A9=v(h*?AHMgDibP7NZeibMQ_dit8^VZ0P3E|WWCLVu)o6W_lfoZ%?4QTj zO5+b(2rqciD!)7<+|v@h z3iuVQXTV!?nwC$^TVmQ9%9~p5QbY~5C#niIpmWuY<-^0#;a{2Hfj3^ngda-wCq}bwxXU)sI_IR(m(jKDgnCa*d1G6Cm;Xw_}$!ZJAb5R~@j8>1Sv8gCIVINJt zf99d%skf`-m`ls1P7mK>G3$7(^Ef$+Bu5kQ~y>7`-U_Mdz zu?QOV_PjALTu%sTl8+@4F(=fIj?)Jt?~k=a=1T0Q(ZIdTEfi>l|_d=ZM`YmkvT z??N=nR$7d4+!&GD9huao`PE#D`1*Qk)2Ol3Xf`+A6>K?69IMmSG@if~=_WwE@x3P4 z+!Ox`)=1%FKP7pX^F#yoGkCFb(MAoMmS8O7eIkdtURuEMfGhSKTyHHsFJaXs$)Ojy zXk7fxl$12AFb%Et?}FiM+}*X1PONmy1#{t^WCDo9n;#+pR7|f7wkPn$neX($fdj<+ zQP6!o4|MyHsj@lDU!*?jI<%E&5T5x+Elob|A#!yESa&hQ)HsPr{-ol7OSW)yD~9Htt%lM&d(r!39YqnAsSU^R(8{B+l?SF}oS*ypv?WA`9pXfkZ+cC>E?17ODUV_fN%2!KvsrZVY5PIRa zI`0Y`zvbb-y>-`N3}ihB1O|&yTQ@7UW6WbLX5_1gWP|n`NJnvW#7Q&9(_dCk3$JBO9RyRf1F6vi z4(=`U<5XZIQX@HE(c(FUqLZbgz{!*19+hcO!rX>Q14f!OnX8!USnIFI`jSYfw?QQd zWBu}tIjh7+QV@C%bvr~GoR2e1GPp8=BAmZ-ZRaC?XqNc^M9Ol5^B-2uG4<#z_~


iHs=&@RiIf=$O`!SRn|8(w528?%Fgspr=x}-J&zJeuVi?86M{n zsW9gj{V3BEPIP-_ZxX8_7EXKQ*pI6Hl0!B9lHutW5etu!V`cRgsK&AM>=Ta-HVTBLqpr{|nF%|g13`l;WImPDR zsXZWepk>;-6k0mr{(<{l6|{_Ym1WrI0fR@6m9#G$B_EcLah;J#l4dC?=q{wGLABN- zgW0#)vDdH%{~`MpVSTLwO*EN|wX|pDTr-Eg9emn55l*KV2!!dGSyamHtnFJ70hh=M zbfA|QcxFF+VMt%->JRVxsqlwEk(&5~B7gV|i+x1#(?slD+w+YCnfoO~y9gb3NjjA8Ma8J782MyA(n_Lke=3<3L3Wop4B0D(k_eGK3%ZPWju<${~pG zuP%#wa;({4fDQkQ$^FTwdUo3(KwqD~9Si<(m-%EdSHYQR_vA0!vJclo4jemzJh=EJ zC(_jt+cTQtn3*PVi=Feql&iX#{p&40Y0ienN)z9ja+ICZMc}tafsxDer;wR5p7EA0Nlxg`bff8 z|MsMMLVXj=(q3hF!jDe$9_`inP^>ARl>-r@>t!ysN^I3 z8kbjib0IXtoc-rgSZwCZboZYU+uAwF{!y7h*f!wJ|1SIWZ!^FnBuWc=YxYA`XrFo9 zvp_NZ@a@jJDVv7i^Q(cTd|^De^!zO3+wGwrnIzg%>DZmvD-dFk@9~hpRhFWT zgNVL|68*#Mhx7%~3y+iUUCJ80n5$*pE_q3c=A$z37az)68h{0v%OXmCzx3u#B&WSi zHy*}J8ihgcHI@t9!}_q$gL$TV%8Uh^@kIP0n`zyMh1aw7UZ7)QFVSe5JpoM$<8r~Y z)9FI?HP;21;^<)DK+{L)Y=$vTD}Jc_dIe0s2|-J&}vfio58|qdP+uQwuC^VYjd_l z!OFvSNHX*d#L*HTi-iKD8_tX%t7ox{&wbgE#$@;sjjMZdLe{hh6FZC;UWIh*c|S(5I!eym+I7*- z2|_)1Ho6;9m=+V>kI~VPGm^H&%iG4?#VI%dI(q6Ly&ctZ%~Af^+UgER^oN9-(++Us z{#f3{3Gg z7N+1|a9OVf4+N2QcIyNiq>*y2jS!xPDt;CW>q7wF1P4}0^ZV0mb(y56N<7ZaoQPs( zw4n>XQA7@U)U z6;rD-evtd%8A`c+Nm(#Eu&Q$J*edei22+{g*OSC!gQH#?olYPG=up_6LnT?sIT zp)4N=oIbX}#OuvA*fItRmJo!#Uk2gd5(S{0rd+(odvUJ@3Qc9Vqpm4}aMCV#s%kFe z$dD@VEAUg?;xE;JR|KM`lrHF^iK(30jtumpTLk}CS4zo-fryROwVMrD6i5bV-5!yymE8mUZtDMC+p}*5KaqhSUME&NADr98!lv zX^_g)qPs|PTbhxjMmMwsYh0h>D?W=}9xD^qhCxksd%~i*g(w4`w}9EF{)Z3>o3zhp zOyiiy9V5@*c13|xWIq+{U)>o97N5S@srQ!#$e{RM2{%J2F4_`B(MA_ zQ2fk(Z76Qkp?&T$lUKQcn|kDw4+Bv@;(^+%QcnzOnjx)o!%IGKSt!jj9NAxqSoX2I zjymuKK65X^!wv&2c;Ero6lX(rO*5%9t0Ixh44_yRF|Q&}eul1S`I&9mn;ozf>f4Bj z%vIrOiw8MLipT7{Rtqy~=ok~4a}Huv&!c0>hEoFRWS1!e&U1UQxe1Z&amri5wmC)5 zy^?^mYEU^3E}mn>-ZZOy14*K1N40$IC%Ce#y>am2cYQNORbUv5>+8iOkYQRcOwK4W zY2Wx>K23Z1@*!f`L7!X;IOE;$?PXfuX-#&UaE->P{&VfP_)kOL@r^M^1KyN&fNKwm z_+=WCQ;DY4vM@$7J4W#3HSm@Hnv-!3LN)+jVZA~M`~|OMwxg0itRcG^h&@dBZdLId z-T={Rgt%CP+CA_d#kb7R^;^NHLJ`XcU(c?*kGam4wTX6BV2D|y!kyM{Cz3~B0h_(^k9f|sbbFxs zx5BqrM~y%~e)%1G`Rj*bj2ZAs5G}c&!<#}S z?*k0kgT{P=52~9>fq*_l-^z3GyK`zZHx+oA5^Te*{F?_66T}1`0Xe->^rwZqT^1ts zko{L5i`&`}ii?WY)Ti+#@Ru;g(AGnT!yVnZgXxUNxbk;?nkEuoK@-D1zDhYdCK%YM ziC2*0hiC8cXvFmCH(jq2ZdmWBM+L|dQGCipky1D)1p4ByQ$0hl%H2oA z#yqwzptjB*ClX(BcfW^@am8U_6bt~UR)l@j&87VZUuv4Jh^!#gA$gz)4t3+-A>b|f zwNa#8st0#mLv&a1C3j+zKF{kPx^5p{|8E*4)2l|iON@w91+V2tW?zeKL{U%l)Hku7 z>0Ky5#_QX>bdqF@zPXX}5dZ>J7)`DsHyNDHru9Z3AxnVl z7TqnlG!X;}3Wu2*J>CSKlf;Lx#+nMWP$v_-Er-;(QNzz*P0d_miSKN_!hV%6*!mW| zadDX1ya(V9<}#(?v3;$eAV*@rHuv#J27+pkol!zZ}$Q@i1|&Q)&ZCaCDTJ@4af zU~FjsEti@S*X8OwPko6@&htamuj#WUKVlU40qf1F%5G-s!^HhB%Ehb<@xoUnD{K$;TlB7`BeV5NRC zcBDpqB{%8oiq@kmU%T+n3h640#;*}7sLZ^Rr8Fs%R@X{xNs*WVW|^>_VZDttOdvL0 z<|SU@M8ZjL5Mi9Td0|qq>_knT{QI;a^wWInGe#Al(}qB9J#k&FS`CTGitw7<7}IWu z+U~w!k4{!sZHz>VpZuqG_2b<9Cf-mQ{MA8L`5g_fHhU5_1! ze7|K20Cfba3YN~V6!=ad2vX|)?d*$8&NOL3_+4aO$AlJs0J%?VU_@*qv%Tfsbyi3U z)A+wtt&!B%*nMZREssAz>1UgDN^Z2}W-G8|`CrznU-HJ5!HsL#GaJl1_P_nJ8&pk?K|McGGn+?Wn;rKwb|iZQDJG!;5T zn=wR`=qI<@Yr5)Wv4>tB&dIJ}x7P}^jZBLyG2zfph4z)0GG zZ};BWxmzeiZw@+(Ow8zV5Iz%${<=-Tbw3dM0VCBcH)YI?#bQ_*z>nNujV6_($pKYY zl;I99eSDg>&2ul@?=!f_y_F4{b)b6Ai^Hc&2?ZD84u39I&#sZ#V;H{~$L&6EAD$Ev2w32>f<&caKC@U?QhzfL>kcMipeYc`vV6M19 z#oNh9FdA{`Z-MV09K1pl!HazEK!ic*C{IJGv`_UsWr~YyPPw`5Nk5F)Iz+ zigixm^zfV|2qAizE%d$?G*i88jV5!6gu3*)F}LS@UL-9RT=10X&p;+y?9JZp%g<`Q z^)}dk|9@f&jGTwV+Guph`!upnRn8QKVj0ZZ7p19r!wI9@9E?F}+W6#d# zHFuH^6cm^ZIgoZEip|0LZLG>W5BXhVF?^Q>Q>f@Hg0q@ig8Wx~J(!bAab~vPt0c4a4;wnaJ>MkMd4-zdt!1l z#v0kGL}$g7Y@LCJ{CHZZE_F@jv#iUeM~;AkY9Z%=6ycE}ici|YM<#9oT}@g(ZODZ6 z;TJFoS$e&40w#88yy4j4L&wy^4&kBR3d={M41VmVH@c~&!%ShV7MVJ+gc&aws>_y1 zn;{8%l;yzUP1x!V@_it13*3C#$)|3~1h08xlI6cyWevqqUqj+A+F)WyiBHk^MDW*5?r4wQWXY4A7;e#jK z*Zp_ous2kIqsqLn{SG_%XW84HWwOx&DQe4Nob6icKzKUbX6!!L-N4^`L!3IG9od^T z16RUKEMYZ*DucpP2>}_4u(AX@0~s7|yIkP-;U`!8c$gP6VQ9S%Yvdt2BvD*2_~;c3 zaNBK*D^rxfau54JakMav8&KBUr58Y@Th^(kTWG`k&LGSdok6bYs>Y3Xe=mAau|>a_ zjvw@9Cy?g@PnV7+kCl}%`PNE&<4K!1A1IyQF>QS6ku6yIcA1difxYLvr~QbR62n?n z&x9tNfT1eH*{1E>7slY=eznWSFe(otuy{@m&>|W@jlf1`$}Sp<^^EXRq(n>2vKW)FjCa94##UV_uT(1<{arB3JpREc=Iy znFMb7lMKFzP!}aN_Vyv)L_LP!#*Q7G4FmbQ+F_``$fDkPA zD+dsPdHAyMjyAY;$6DHCGRhMfPGm7udUgZ8mdCjk2cX8A3Jhch`gTnTWdH9~f{*_@_}Qt4+Q* z&N30CF0;DUCBOuC?X0;QvdBdp9loIg4-V1~iGX`KstB3IZQrsu1HF>^!0&y9RTT5g zHkQDJ_4225{@2y)i6PNxy9bMGwjrWo^B3nfBOk_=XlpfKyBNF z^GzG;JeIBSAn&Y^1|0Xk@N_37(kso>vAGD)aF=-5!|aK+Wo)4X2>am(>)gO&fijmK zAxi+;jw~R-?4L`dlDNNgsDv)hW)CW6Q~j#z{lj1-9NIUiIvJ%00iDn792`#*7Tro6 zLhf98zaM?Q1o_gD5`#;VmlN22o`b75L_Me-4FZCN*QdXDVa#Fgy3f2{8R^r!A~Tzi75VqapZJ7n;A zO26|ZvrYO3Hx+02?g+cT3&-BqE0LCFv{GTB_7#1M3@9!_a+5p3bY=RSF5yOv<_Pg# zsy{dER6~i{#SB*U2y5bII#4)c%dfUnR*P&k>z9~z;2qsSjM>}oz*f4jZQX*4;}--R zG#pluqtOqu&XkE8aMq5P(R^5Mn{y|=MIC#fJWLFMS%a3lTR<~8=2W;G7@{R^dt8=b zHi+((cA)=2XtGC*wN9sF7TBdnllFYwYH z0`W;^^rT4SusK!nel~e0#?LZk9vGD`zWBIB4flf`nV!RXQ8do|yw{8E!fjjxHY~7y zHAzMHu8B6!K_NB1XFj}#*N)`^BGCJRXk-|idu(8QL;P}pe2cWFbFM+LmnoXz&_5L( zeUWHuI&XZ9y@!C!bqCcdfNEkaaA1=SS9uj`m%;Ta5!i+~{-4w>#G`*_G$`_36Yb*0 zh`c{X3fLhwUfni)lNu#rPYJ1Xz9WrsJHk!j8_f4nKVPeWb;WoFOo42QMFN`N~V2n`g-ekH2j}j2Hj}c?-yOLqWY}*c{db?6ga()attUF z>pzNtclx2u4k_`hrj?6}0$KYSb}2AIfXjP(Ayhjm2`w}I`UX>d^vOlXwk5>J^@!0D zspj13_a*F!rIu2q{#S3aBXx$e!?i5GQSHG$9q1#!<cZY3w;W{@?ngt?V zJ33?v3nePlBmlEc{YZS1{O8jS>xsHv>Ia*kp{@@4z&p!_Y*{_pE>E@q@_n}vl>d#> zXBX+3U^Xaf#To%0BE@+vhelsRUclIm^;xGL*mOZWsU#)y_bm-s+z0~X`}afTf9L}X zasz$LHiaw*LsY-Nu|~(ul#Vi)?AQzxarx_ej{ob6+}2mg*j&s5j@d&9A$a;7TW!9L zb@JHZnD8G5zj?^~|AD%WQB@nOtiTx)y=&%eHm~(W*|tbBMwlqyd1F>3?Vl#p4n#gk zq1VJC>fucWi#a>Bm*h^c#+qwL;CFpsmM%HY`nf+@RW(ritp0Q7uGLnh09jv=8a$*> z>snHL{1W>-hEyy`WjcOBm<;c`5A53uTK{IhrJ>~bSKbndIgnAcTtzvG`0x<4^)Z3o7R^D7#`~!WL`vUu{ zf69W#SxSKw0PAO0lAf~!Az0UFZJ_R0DhzGI^JFiOJibCjACi)VvN3k*zaJo1C&}iY z_QLoHvR-#P?nN$%QCo?tt{6b8^~=9Hao_O;o?HLvKhi{Js2etylL6~&*PPoF&m6Vj zp4(XbI_9aVi6k?E0>9NtObj@8SgU`Vj9D4U@j?Zu*?1&>){Zuk%4XLQd&aOjoN_4g zpEGy@k`$o#66=n3on@;_n%^RL6FOt^SZ7Q{y5X3AAG$f?_ApWrAlR63RWi8Ge(pPh zQ?JD9^M4_sW(vB(f0wn|D6zX%EsR>z1&8+Rr^6B;rE*0KZY~z7m%!w_`<1L=OvMG} z6US3qU2U$(-#tyxn*{2fOiIzNu##D~Kh;bi<=~E(8$92gS=HpcTayt}1aJLSP72W0 zHuG$!UqXr=={KYJp*y*v#rzkEQpFP}R!HKAHBS;@C?BHkt-9XXfM0q_w;PV9L^lur z3&UK4yIEP%T-o{te>MhqiVpT+Uz3{57c=)pb`m_ zp*Fa@ovh%8$a*O1^KLl_jQlY0LM>b!|5xN%qF5Yld=-X?e}^aX+1I9+bS%O#@9ios zlG_i(elbNxzs@LP4>zK_r_v57+0e)Qo_pFqCv~b{>U{1<$kmFn=+(yV;vJsiR|a0u&A0=5LRp*uXCN=btu=RWjbREGbHI%20H zRFqf@&|b1JzA!nocIQ=O{KQ8bcA=nw`!tmscEaXS*K^eP=tu;2MJGCXQY_YLDdxZX zA$+QQ)V|4I6Kw&t!p+nTBafY`k85fTrhiHujG~X-#<@xgxemMd@stLy16!sRCt$_N zYa|xC)2j<-AUt(|-m2&LCnR;dnVN4eGnD(~h-Z&M1(+kwKQut^l(0pigat3_n%K8PGy|G0!@HOtEvjzk244O{l~s>lAq9? zc-^D3PbzzcoB_nRrKvM!W@br)vejb8V*AdcI|u~&+iB#%!bsHN6UBT}Am%cTw(DLl z48{Q>eE$vAaHp>7TVkbDC|JE zRx7X_Wr88D{BGR~&)$U~!jJD9BM8RhmC^yW5J1%1V@+ArC@9sv(`SRZEVHy6hvZ7KX_M6hI8>f-NIVF(& zQbzE2aXpJhs8X{-2?A2G58eTMH%-U5ARUGQm6^BxBzf;KPCEZ&H#Wdz<2?x4}jKAPn zTm7BhQVfTQlAtROQT>3(R4=)Mc$VQ$VkG{7g*rJLcefpdR1 z1kV$;EMokbNgH3q6>~FKf!2ckd?a@VLzEXG@LR9FM=8&>zn0~<$C@{|Jn!Hh@@Eyi zd0eFh@su&5Z-B}okAS8(G==v{76_l+{wt*#l^UI^oa8mgu~$c@5T{vb59q~aB&iEd zPAZ%vCCz__3F~Na7ht*S%u%A^#h?HPq8Nde5M+p_?DQ4f%FaL`K7fl4nQe7=G(F&v zr0Ny1SV=gKcfGxi@#iz35QKr5vgnMMbD2VE?x)KR2QB{?t3;IlF0B+~-v|qmv2&y3 z%`pp+@5@EK{TRz5&-W!~ZJ7*;h+2ZUj~{>J8yr|>8MdkD5bnw|p6Nkj&!}w@3M`jX zh*Sfq#BJF68jO3qhL`s7hHA|MuqC@`Cjd)si2WxmQG9@?UqaNvDfsS%LQY)5d;zZ;H55v}0iU;+?WVEn{6(Ge z1lVDlIhxq^@iMUv5H2bqIIGe64RQfAbdh(k%bv=y} z;6A1mdZ6DRp=?w9?n)^SMk%uXmywij)adWCuY=kNzSs%Wob**+QUgrGJZjLM)gfg+ zJ;{**Q~XthS&4VYG2=e?bV<9?2$|tPO#9(rXG&#Xv>M57>c7Y3Ik{cI-nPZZP^yVJ z+3lQs>o3&2UHA+oKjalzB&IfTa+)-R(7BmorvKVTZrkW!BfD^a_Y^fpFjBn7aW&`0 z&j6DV#JRN+o=;lkh3E5#k^7BtVYR#UyL?vSu^fE~Vf2*rKClFP^<`S^pX*BdOS8YO zh=nFwC=Cd1(2$caF|uAtp!yi)asA=_m}$(1bO@GMoYAZC2WY1uDfaIom)Cs|%wEQo zjB5+V4Ege!?0WQE@Ua3<|1ir*l4l=&oD!>xv4HV#tK3__cS-u_LJ+uUkCyZzO^m98 z#<=!cTx0hpUe>+gBI91`ZBTuS^ZL1zhWXV%FTB~g%n)R&Mwl^#%Nt&QMYziv!bb&Y z2k&G4m@Z@M@)6eW_9XD=IbSM0$qVfk?Z@TkNntAWhEc26Zjd#bnJx2!`oGOvVn=5? z-}~Ly6Uf0+YH)g3n0BJ~Z))gbhO$ug9U<(>o>R!d6)-;@*QHEoV=N*qOHw20M%dR^ zvHGQ2&nOcSy*7~K#m^MfjO-tjQOlGWRcBy4JR`d(l)<&kSzxI#JFx?^%Hux9!d}F) zMuEWAI>k%R;g=VLv|*j^rzn1M(ng;5ozlM!wY`>5QQJ;PzxwSMm@QTM&;XcT##ip# zg#tb7c~R%k>X#=-ZbpL!xLJwrc7Zo#23FbnqCR3njQ>jcoQ6Rs_$OCZ=x_*I3Hmsb zKe!;c?O2gM0N&X5mpnKyhWBPT-F-MZ=%iHhn)zfBSFIP##POy!QB0Lc-) zIvyvTSH#>$#wv6Y`Fj#E3j^O1ne&S{-bpCKwl{Tjpml{ zSb~^E{9?sRd(d>qye~)cuK$)66HmJAk+qH<4%AQrHEv#pPTIrko_d9JzQqa717N}{ zUna4a6*%&pFR91RvmkTz>8wF)6NS^8fWt$!!)gX$cmv}mCdOIG$rxi^CR zEp?gO)@327pu|Y!73}CFZXk36cyI8#A$XKI--y z7QE+y$wLycTj7)Jwfbf$ZJR$30T8IUSPf(l4XqfU^vm$(Y%Xm&@|u`bbAn*R%f&H)NxdJ~0j$R)mkQlgumXA{6;l=@@tg?D&nWZ^pHieltM7<=vQp z&EV{yF{AGQQ5C_riO}Mc)do3`j*?LCH@0%X8&|_#_~&%>)n53WC=O#9Mez5wD#Vvl zIIZNiJ-^1Bn-VBj=qPHgY-PI^dj$LXPrb~?1>BYZL0^s&ub-8s0>|v6PAzzYxcitG zt#7!rIJ)e;ynMNC*9kvIu>LMkV1}n%n}vQXsR+Az@afH)1=}sO3CR8CrpL^f0pz}| z;CUfbc3T%gJs3?!TtA_T0Eh5{! zbS{AEWh_^0y1&3HE2u+oNo2BIw!VC{W!5e5ujdJP5^d_WPl) zg~o=BC7g6lE>7rzW!Oj+0@mV&ET=S0BiivSh7?M}---bOr5B}yp$AFkv(G`MQ3GIg z#|dLGX#zIc)+69g>_|M;UxARMB5Ms{6vm$)qD&~PWDZ{}aj=o_TP5iS?8Su(F@yUO z1Rj7#PMuI^I6?+c_+gy{4to($0~+^L^A6#feFr$b%IPw_iM)z4%wGPb(PnEbnXcVl zPy9TxG;2^Cv=?XeY-gZ$x2q>R`Zb13_hg};jqyBf)pjw6V%LO!Ek`qc5 zl1FrR1L<#L1WX6D4(|R{liHYWVQqO0o2-Wy)?;BBu^<|obl zybi2hnkFM_8EPW>W^%=3`8Lwk_cB>;HzLXI>UqnTzl&7AYgdf zAK}8pl9}`N#XlGE?6_{+jOK^T)miyEAyHbqOK~a;<8A|PkeL-0oh=7xG%rjdiKee? zf_X?UwtGP=J&sWloNZ&-u|1$~1<4 zlA4Pt{D{z8)_B%gF)o}=O1aXD3GXz-9(k+=tJ!4a89sNnDb%QVP862CE(ba@KkXF_Cbk}Ss4|K4ME**APZJzgBty*Gb+1u`gZi-mQDLRC^}%#3+%|F z)x1i7URc~gm$60QfM^M4RN|q|GuG5@m#Se&bUshs9b$t2vW-|;e*jwioWTpM*(bR7 z1v+Ri6BsZ^u4%(`1<#Qw?<4pt?!KhUeoBB)O!onLGhNoAyq{(S+fC-tWskpGMc|*3 z%KNbbnl$xzl`1bVMz21u%Jin$WrdbLe%9Yp*qCw~$0Xo=xgy+aj@e~`5^#jOiN+iOsd@K6>aa?!d@uDQ5M2uJqU#>BP0~DEwDhU`ldC+T z=cgHhO95XXB|02YyPy~vH0;86xQwUEmNy!?r_D#WpH{ZNO&;kw0K}?1=A~U0XsIi# zhrQL{;CQ0PLV2xrzMJX4M{pZy_{9PdCsL>F1(v@QD$(`Qbt3LF-#;7TyQ78d$yq%s z?&}Oa(9XVzn6G9;zcmHI;=lou3Sy-?lR#u_-?>6r(}Uf#6*P#8W<@0EnqR@gTJ&b= zK%Qj5k4db4G^pIUAP*CcT!n&E81J5?ACSZafkn>%wwfEt?kGeW{?o|gUBmsf;+2`~ z@^&}e=yDyZSIRqlE9dPgx9|hW9qNYg<4}NG-oD3ZOG21?-XhR7$)Um}XKP6RmGFY9D2g+PbCgBNbmKO@dD`ySc`f>b2&ioOEfJX`HI<#g!;fDk@1(Il zE|YFI-eKAe@y6<2rtZ6v_pc8+{RH%qxAP0m0u^U5LD3|d1b@Q~zJdpo??Di%f}yUe zb~&jPM1jS~ArLnw5s8Mq+Vf?qyCk^kj8&5-UC8VDzuXI2=)9&Ivt2^^8PG0El%9z| zLn^xCt-#a;?_~{dW)wHG4i6c>-YVcN9m-r1L*Ypn|DA^*<0Vy4)z;DTP9Mto&s+uPnC`-a zRoz0yihvR|Ae-U}-u@N5evM};zpUu25ExM)PvJb+N4Me4B_umj5>^{l9+!#QtIY}a zu1pV@!5sRmQ}|LsW%DGVGW^-HOW6L{fQ&0^j;XN~_M}~_m+`CVBTU`wq!!oj1A`aP zM(^*~{}v>=0AkmfqSpvW`4V$B;qpogzfozA;0mM&<3Z090C04hbku(Qhr0?DVkYy$YDdNej%}X0@h;Jwv3FpOp zzip~+*^ z_$2O!Ldzq(r_ZP*%(GdYxs%bt>XvS6D?nSCMJ@hdzI22f%Yd-#Lbf_w1UMA@nnkOE z^v+NytC7x~b(m0?t=8FvtE{z$^5ZRXmU>a=ql}%tuAv^+Y*Xb)GgkFp*GNj$xbY&{ z#wqLMxy);ZAGF%+@oyTFy)ivfo+yGxe6B;aYgc5O6ooRzJZg^Zn|rgZWxKG&=N;t# zyiMRdBnbqE*!~s}eg_r`s*a-&RfDPGJ+7!zM~)kLZUply0=*gLqJ5T8y?cnoF2{4K z!;xgp6BH%Gn3AooLsZ8#ENB*rD%u8Cd}aG2e#&csVl&CoLOzdw9Z;Cj7jCzRWc%Uy z_@fGL1Gt!7)6h>0Syv~yW5ia(UsYoDGC?KpTnJ>m1}oZlxlWsqQGtikpG%KSK4Oh+ z--+`+?O2yHf~T!>KJQo2U|U{{cli`&X@bF&QK^U&zUDVHD%XwOM!p$ z$XQ&(*9Yd5OvCzOZDZ=;22xng^V7V)xRPJ8qAsgM4m5+kJCR}S|cmC%dH2+c4w+6u(;)Ldf^F$88j&O2H%X0aZL)nydF|+B|O$$q7o=Feps$P>v z*70R$6O+B6D{~U}xV*n?@1+Bf$^B_n$b^_`)Njli93OWirhn7QQCREQR01kB>Di|_ zl?vh!Ue)$Yxp(G3_S2;xP5d!6)Z?2T5;U!)YV(!dbO8LdZN;P*@Ykt(xFKB;82Vs7 z&wjhdy@-a1G*;|Ry)p%>Sn~c73W~IXH!5Bc;F~j7&r;b6n;x;+31I;wzRdvQ{IlQC zy$Q&cGn>4j(TZwTMxtKUa52wnL>-6X zfI~~7%beA)k-OE*QH~+jc8BEoPwqkBDuL2VZOkgN(3QT3ek1w}rJp@k!9+REE;oLe}7@v}Nf# z|FIha%VNs3vOu~4nF|d>LFJQ5377HV@1-jz<_xX(?6 zTZdj+*r)|*lxn~a*(9ud!_&p(vDO8hn?-Y-82{~W*%Oq`zUt$$c??B$onJ3fr082f z{4j2`ZYmo)yQLA1N*?F5ANO|GA^UC9lkwyKy$1a-S>;mBtRV@~$|8m4G?qnV?G|gE4r{IQ@(zZ9J&tPL>FJTM*u2+#AZf0Ika?);H_4PKI zc}rQF3;d<=_tKCCmSmUJ0EX5uh&V?mSSln2-`3ue5X{peZ?T*M?!uJAS6?F%ufBfg z{YXSmEW(w%Q;OW5of;_aQ!P}m*{%Nj>@#w z#nv31M8=Ah7+bGl`etNgAK-7#o`rG0Er4E$fqJjQXjlRE%OE%uO4h=^4Q6oC+`Z@2 z$iW^xa`48blX7mx_i)A)hbakuzcGLMOx<1kG5VN4{zwy9P`U>V``Ri41JA$leXYQv zOHxB&wW$~P^VLF-viiA-MpZG`w@ zn@e{Sc9e+gZX+8VE@xU3fze@LL;a!DFZGi^#pWxL(M!T4^qWovgY=K*qJ9)kq>RW> zPqZG(*;`3sxO4&SPbJn=@eX!1Nk}RLglvr4XS4BhDWy!k0-a2~Pz$IjB?QX)qVY4G z5`6@r{NS0pMEm1bd)qb*JsnMJLkk$NPIpjgrUf%L%pvk7j z7DzE$mB8EL7X`M~0I>-j5yTf}y%cxj3h0@O0($6uIC)bz!jkdSH)`L_oko(KWh{lX*vnYQc4Y&xCjLJ76W8dV!-fF^@yd)msgFbj#v=W8A!N;eU zwYVOV)%FlJ{(#CIv`rjOs(^!2xIueHu(Vht_ly<%bN>-Fp(;jW3t;3?!5cV*@sFnl z{lGqtAq03b-zxiE2U&U9xI9eaOZgKta^MvHef+U?7pFv497upiA>+HS&jGnnz zA=4ggdPoez>=%6)vjbhfL1iCtr;Go@RVyH=@v7|BwQO@n9JcW1172AU1H?%Xr9}X_ z{sDroqUE{!t3;hWhdhXHKCc7175>KhJNq8YAv~auEs~@9g8BX?+S2EfrTCZC1yPo4 zLrBGsvzS`3j!oCO?9Q|PiBlLKKox@{ys=toTek>QC5Yk|+~eo=#DGwO3>!qdC6h<- zbD5TWnHSZFcSNE5l*h50JS`Lf$8cG~(GoJC<(edd?HG>RO4?9zoz)km{RB%s@k9gN zQZx5iKyMnI$GmZ)wTW3!5W#b4bN_wG6&-xrmTiyR%lC_-=E!E(+@`i0^T}%uKQ3j4 zVH?9c$?kKjR?B_{FxjN#m*;YqUHY!bW$e~>-y0@b+W&z!dHAHHxd++SUCuFNb8zk7 zWeNIsLzHzwK$(vz_p3br3ZfYH?2_6JOupM$G=*&FH52*;GrgOlPM zXkx)VWNxBM{;2@>^rw_%5Yy-KkrJH38t86Jm9OAZYtQ{jx01bWl+4Dz!lE=~!JF>} zXkrceZH73HcJ68+h6(+yQjG*@e?6an=PkOy>*ooN1xhF;e#J%p(QchBk#IW7>r1(B z>(qj>-ixV$JVpHQ2=tDRjjXSK<4Xz*m2E`?c%!)4-_A5+Rj8krHXsGwzalX;Ylkgn zCG@`{3qN|#g&!A|7GlrsSE$zCDzj9ns_#x7a}aE4X70;|{Enbkmz)bN&x3nw`GDqQsutCQH|8Seujo@%um6LL&5O)LCB1@QgjrbQ{cS`&y?Yx-S8y z0o4|AoJw_gVirGt=ryQr`mcPXYH&eY;}_1Aq0Dd|0o9H{DltxW9cyJHdE|n;S~Kv? zT61kBJd$p7as1w#< zf3KS0{#7>>`pjbLm33H6iS)-9u)BGQlIc(YN9hV{w}>u<51n#PQ;IfB!v@!7S;NCv zN?{xed@`;lNaW}5u|nLA=zMs@Y?H@j@2Rg8Vm{_!@4gt%DZlFIkTI29d%<=Q6S4Pd z#!fb+>V9U(dX4a^Kg36oGDg!{@m-cM_+8pan}%5uQ{#8jwH~I-F~wCFa*YbJyw45!H36f|OO^f<(kwSJeS*S4JH?vN2FW*8v%-xIZn z>53wyfDXjN7$y4tMx0Yt5ZMt}g@w;3#BTF4t6*wXzLf1uO29lcB$E4vB5>_Hl+!3x->SE) z`lw1aN4% zvb{9WXI@kE-*KwQ$cS-dIgm^Nz&Ycf6(K2>Q;Yf+M|ZRgPCNNQ6#Nce+#2p3B#D33 zmYK9loU&6YiPaa3(5zNCOPR2~3}X7Ps<}G5p0C z7rDfn$;0*iB#G9)(^{{{PHoBuBc3wts9+S;2qJtY!go@s|Adxms|;0_-JC5^u95zZ z=+5v{f_NX|xT_5~5Ci#{AC*0ctElL)PzY{IRiOWk=a)a(siU%s$c?dQyi>*bZ)ry^ zCP`dg5^ryo_<)XO9juy3F}RxTtCR8gW!XNkYMI(`9NdZ0@nQ%=tlGbL?v*nD;3CIq zk`+B^hE*KtvS@2(8m%C!PUqf7*6_pShpeBDy7IObFkDf8R8u~ZOocD)2ZmfFUGBF? zxj5aVDb7fV4Ad7xkSj0o zzioKNzobwkfG4{ix4Fg+rPTt4*CR?PRY!xh&t6zI@SKBLYf3_p3iTH6C*=Fj#rnTjYz8cpo?0*AlexM^z$>)`&T;`{IK*Mv3 zZ=Uc7sS1`Zix9=J6&vAuM1ZzN-T!E+{@C1etDNxlDpKYB(x{lnNiKfAU7vWd*!Jg= zA68I2M(V||Vr7y};<9A@;j~S4YLfCnQ77adT`E?)L`%H8Vd&Z3Nf}3imt2DSuj7T+K7AUz$yEj@XGEdCggqBGI z+=)QE@P;qdqsNWC-CsT_DCLL9hs(Nifpz>%q&%HIpwhFtG^ZK0a|B;eWSj$PU>Rnc zSh=Q9yaZKTK~&r+cQ;VfxWAICC563@Cia0Xxd2hgDcoAA(nh79 z*8eEN|N8MWX`hx5v)DPRm&9*dz+xmqbk2!y<;TCdb(z^v=z!Opz3xE z`^N-Ho){cYq1-|`4^T>C7N~cBN}jr2p5u^5=5>$wq&6LA^Mi<`J+&v&yd|7+{I+Jf zUpkAnGwKS?Zs1#>`oi6D^nCh{SJqZ0cz^ghmVB8i&XLj&&1!cVaUUF^9;Cd%2|DSb zb&+dhOTN{oqB%%?y#IQwD2 z+G`lQe?pDh(mTEb@sJwn)Q@&Ps=w%#RSw+`I6|U@{zh}8Vc?ttF8!Sif3Z+Z>3<-d zyjv01)fGyv&ch4kXFBke0q4eJQx7iqNmh*88Lqs48K`-12O8UtrCFvYMHQd5L-lRJ zcr$PCK=Wo!P&;*hRDDgmz|2ygpKr~JRVRAuhr96t`)ZI0T;&KCI=GO8H(#DEP;sM& z3}0Ju{pyTH5+GY?C0dQ@P+{t+$LQ#NW2#}h7q(;<;L)?dI}qptHp%0{TphuC#nFb0 z9WXQYj>4!@f5h7)7YnDzbD9j+0ig0!FsnQbNj!Vo22LkxMEtYYtH3{H3E_iSg_~<( z**-uY-X>7ZO_MgaTQSmvc;ea?U)Q;-mq5GE5^M+Z{+N!@bVTx!8@IPKywMWim{Ou7 zC4L}!%(Y?^w<7rCa>4`UsY5@o1*pE8vlr#LSqMyb@B-H+@%NZRi3O5V%|?QC$_$Hd z*x1ki(a-rk;&ht`@_nA#K{GguZ+HP_t-v@O$t~7pnBdvxnxt6-TR7%2A#!^H=Hs%u zz~Yx!JIzTn`8ip}mGM$En}VnD?uZ%MVb|V0NnrU`i+N=aQv7-0 zpS3S_p?UllJi;AAn16f$owu#ka>tD9fMu)g?&aC^Rs;TVpU0oIBmOYUsz`|U^lbgQ z%@Y6beW83~9reW7;o3rXK>29TYz2w7`#*%{?EtU6K1Yo)m-X4~iq8`i^U7{1f-jP4 zq%6snchk6r=A5s6a-IoyRsb%R44sD(%rl4o#*ojy+ncRIAK_IHOSCci#oLexc@&CE z5v1KjvKks+Fqb=WlJNaJ$sJ?p$fpAjmtc)9spNwi$XaHjn!hNT3z{{UolY(LJuk! zmFdc%XgV$*Wi{oxXmC3JsKX=JYBx2uHZdhkO68m%eu09Fl|u1O7F-fL zslX5RCpS$du_MkB&eyXnB9b0%xrQOgT$veX_$(mop2A4qC;l_GqJ=gMr?mRRp32KtNeri#4Js zqMhKcS%hbd{`Hb7yFr}=jT@v7x<=m#zKIimOd&bwdQKZ!XY!ebn-1L#f^$-BFFx_| z=TzaeN)ns7A7kM}g>@$h-M98NBcKd-IDlV$$B;)@!5C@e5%#TA(=Bf#?`JEkajww? z8o7s{FbGAY)F(f47Mq5~3RFc8zB%RWP^`thM{5kp1Ew4YI($$6OyYf;6~&l9T$|uH z_{E!_*Ddv|Ti$9ZWduJzXf9aP;GZlp&3Pqvct1X-MpdNf3ntK`m}}NF-JwusdB&Zu zzUy56oGhQ8YYm$N^FKADxsdx+6wT^@}Y0 zlw=ffJWT)9=bHhR>`MjK))gmvAU;^MrAJ;7Z}-x?Ec^DOhwyKzyuUb|mz^!q#aXf4 zU+KW2#45V%&&B73Hh~qHY)Kkq_-OWFjm4j^qraXfxtv@4d2>fc#sNxCenl9NqTXj+ z>rv1UTJ2bVYW@Uq`s>@b7csmed?e_Ao9xx{Sx@xjzGQ+OS?EMr)8H{d7`z5RL zD(}k1A}vx)a>|2&U%Bq>VJE*@&5uo-;RfGudz@Lj%X|>|Y&9QpFxpkqT$sIFi0F4g`LepB~<&ocS%f;`Sd$y%GNJFnw?X}m3A zY}tFw0K=cY@ATxlr(8*sYu5!~zuVU;&2Mu3WwbBgqKy9d_tE(8cG!MSq}6$wzu!{U zN=oe9`>N=q>zAw7`$t2TYu0gEVlQd0omMS%fTo6?B^{w==&7zOdef4tzBB%#rVMS+ zEfIS0zTs&5RzdX5@bl3(h@XCZ>lU5Gojr=R)Fm4c-M9P^O9G7r9}#l{wDXo$p@*fj z>=Bcbws5QuNq^ZLY&b66O>8hleN5~4A*XC)+nM7D%q9p2AFGLlt1;Vf?+NtBBH58E{IE{XUd`#sY6xeg`K3ilGz zAM!xBVw z2g>^|0XZItaxJyfa$XX=@cGN+yISfUzANztmlwSgk^kagLEm3AoAs(_E2eCm`U2&5Z3KjU_{SVx*?CWp_*KSo!BCVN0zL^g7nrz!y1SOzdFx_&QP`1= z=VhS9ny9}CD{$T^$hxkZYKn8v8eRd%MB};5T8yFx_`SjWFMt1PM`%YW3D#ewZf!$q z8D2*vW4~)huExMGk7;sGTNVMM{50D_0z6FWxI-Fr?8eUjlIJ`eQX-~*MA{OUEh{Ey zDOqI*Ig-{g9lnis32_$s+4^!KHfeTr1J{+OX|KL($M7xMSmZ3`X8CK*QX3E3#sQCi z5rAL~8vH^IQ=mks@Ry<|$)H8piaWJ&lhb>0@;j1{s?sj$$b4WSdQGakSz06yjZRs&V-7HK>(KPKx13FYy0H3BMc>d)&%il*# z+<%QA!Tk2QVx=M>2rZ<#)Z$q6RPKhXPHC#{n(%l3IsN!9H81z>Kib#|KfGM(R8iG- z2}3|fO6q0&?tR5I5?vJ%^7wasQTF==uIA5vv-tA>+q$BF`b;z^xy`FSCar1J5OI^) z51&2U#@?!RGRVy>RS7W~cs=_B71aJUffkz=MBmyMnPPUdt?sK?h@bP0KXDI_47`5U zwOVd_kY?GUOzo0zu{0UyXK^hcmLA+WgGLhv&Y`m zj(fQwzln{7JL3PS@s)nP&3U*s^yi)__ZXvwxrexYSCWQ&!_T++4jN6y7kb3C*j}1@ zK2+j%>FvWwfOMW4a^lHUg;p25OvtioJpQDGJ>Fz;Dr;TU#}nV5<@%@|?kcF@R&L|} z2_^`>Y$fhd+Ho>2to+-mo9O{9RT$obA4a;E+YMh=&lm%TpP*%swSRgv9uRbYhFnpv ze!Obzf;h)#hwMxO)y6NpAIh>g+ZpMt-StsrQ{7*S;4Mcn(>DdDr(>k~r{Bk3_j7LF zz#Z#&`daX5HAGQf_pac6<*5fQZe-s+(&?geOVO6wTB7*>l{|Nfd~hAdd7;dPDbfxv zdlccGb&u7eJs4^$Dtt`{Z2%&%BxCDL=WaAq^Fo$b2ZB?^V%* zqO$_gf*}`+Q@ASW8EFS=`@KrFv{)pWtL-PZ=~Tk4W-Fm?lykLvnqXVgBHbPbmy5nT z*Q|q8+)aotY`zFw29SIyVMlN-q}h<(x`%I8Zv1c2+P43$bm)%Zt~KE8c_32d8m9Wv zIr4cMCtfvI*cq&o(Vc`lDbE=wBO@MD*ysj)!tiPB4!Y*Z`7Wm%f6hf{CIUlNmj*Yl zP6OVZxI_p7?|me%4Z~B$uRAkuuiz?wb}D8poiU*J>ZEj7TLb>}^T=4OHQ+*H)H)_W zrLTgCwD>W>Pn!4;u4bCwsi0`j;h$C;s|?X5I#tI0K)OBQ_Gu&V5JJfF&a2ber76}nF1P0I#*+F|Wn6cQY{X(glc_KN zf@=n^I~=qP*z&6E%M@DrW@#i?(fA5*r)+(wPGaY zz@KxNz_V$lU!jvea-45>P|LxIaORWkwjepsa7Ia}8i<){_KR&fNz|@OSU$ZXh~TugFlGGzG_nu*=oWKu?OR}(R|UxuxL z8=iz#l;4gL3-7<*fyzE;a9+10hmczw!~gT1??Of&0Tuq(zrPLb+RU<^amxH0ww46m z0;G0tZ-F%vPz|9trw%vd3K*VsL1+cqo@eiy!^yZumspn!M4PO_-2LY96=);n@ndSD z>stibACgEX_7m{v?>$nT3YV7#UACXlmaa12{_t?<`qy~AS|QY3&7`d~12?<=pytvf zr^5fLA!ChU0;vNo-~~;#a1e;ty2G_)nH2^a-^Y5y6|$NMz+W7nf}MJcb_q^TxV3B_ zN=kGg-VF+|F+ymS2EmM}c?9y==Gq01hO-JqI z1DNUWU8nOz<8un_HqLP6Hn2!fD$6be406sfaS&zmdjlv<5x(_7vSRp*_6sVNHz=}aB+#%m*;kNF8!&8!N=nE1mHk)5q)$+1 z_g`_6`_DuF3^*a|mSGPtFlC#2pmB7#Jo^*6BXM6)Hgjo`GCC~Jia47XvVV)2cJ_0g z_em{ctP&(SErmA28<;buCRh>PdGlI1er%=8(>c_IeW@ZUK;v%a5x$*C5?r0C(@FVw zjsNlIT&@KsqPrZIQy(f=Gw~5oomYcntp}114(a_2QxwU)8Hc(H`-}u;4#KHoMyS?0 zZwI99&!7}6_=q{wm%2ys_a9UliUgaKNtLT~p|m+`FzlO*Lq5hEnSjnz61(LjjCx!u ziVV)HGOZr~HCAV9U267TJhS%^vtejSxkdW(cZl$NqhNIj%E+_hC^^Ez19KO!M~oHB zG1;3PQ$~K_W5iX^HKDpN=Pr1w*z|V`@s00#QC?UB;(hZ6iHGA zD=Dy*tsFgUGc;dUUbOch=bmyxk))v8KQCuZOr=iV6Vw$Vq*mWFShdw1=Vr5ZP7Mf# z(TByRkKj{Kn9$0ps55(VEIt9u=GNsVvoY*WFnsG>c5_7N|hB4&VlZY`3w z({Erv)+@CZB&b9#O-S@^iv?$!)V~k|T-{su%fXlCu)<9e^QY-9$FMK% z@zP!>^R*OBM8(d{`_K5RF-P&*_5#Rqn^GM!do`dn%3btJY0V-6WpTVHur zD|)M=LV;c0XZ`|R@~N^VuNGA)%mr_bA@IWNWeJBY7kejkpOIOpx4v%EHK4;8OO0>5 z|71H9_WHU2Iuy9dpZ*`s<~MY5y}adxKcu_&@MXqglsEpA9GR;HR2K6F#1GMJ%nS-I zO;VA#etk;oXnGu`%W+D z^m$f!rfyEm5dyQ)lHtLHf?|}}C60$t6)2@_uDNpqF}6l?3~!<{ANcnP+wrJLAy6%r9z7M<#k192sjx zdWm?Ev6~Yy>9A5h$^7Qy@I0N6z0Gee7#EW=Xa`jV4MTb# zr1^tK1O!EdmO2x|_X9t9D|{a;YEe-a{(PEQ(Z-{4YGq))H9kFMVvo^5(kQAL&l2s5 z4Q*!SffSSw|AbUo-3eICsl~RiJ0Bf&ALb?M=&QfcK;wo!`@qa|Znkm|!CbZ$Z@Emm z_0T~eiFN^iKA&Uoz#K! zo2DEMmD=)Z(4Yf1y}YLt@f;;hX=!n@zb4_LFwdihKgG1jXJ{F@cRzD#w10rQX*m#~ zY&1uipWq2S=LPiZ%BxAr~=4NHX!E6N!Fc<;v8Q# zS&N%w)~ z>(CEHQJVq(OYMnd5BNX+*)L`~X#|VnTU_bP%jK-_SWP`*y1f+`AsHJWwbm*5ehrCc zKc2?F1i;#HQj`a>g4c(`QXRy3p3S5n|7*(_e-xsU*8hcr9%+$+8Zs*ywQR`3=htDv z?_wJ5Pbya*Jjsk0GJrQHkSyb5jMhIbinWMx88VFfDrSm;f?w>Q!_1~K8ArnWImu|s zSu4|i*N9|{<#7_XbzTX~Ax=Yi-jaT54be_HwM2?UPAW<3A`1U>07(~0Jk-)TGl zZM}k3jqkgY;8xW(6(nMBEntBpfUF!Ka)Z4zpkFPE4l;r=27l}lBlNo3XDcmov0B_A z3&2H5+(F)#sE!bB22 zJDaW{D1l$l1#m0g_d}J&>W3-b8^md6o+m)YuK0pkK=?WW|KD-U>u#h$sL2V_RzA}p z6?^g30b=?Vv|B!7n|9iu7XO$Q6ki+C3c~F@Xy&XBEll^dGbvV55ah~g!4W?(+`eB5 zCnicNboP%xjgU6R&k`+)Myxy~wV_q?*uD*!FOw&U0ByzSL zq>T`k=Z}JDcJCk(-(I3nMh_yL8X~`AU|2Y=^3?qlzfI1v3xAJ2%re$7V0a!?U<(|Q z-zYZJgu<&GG>gZyB2B+;TOT3u%sWNHOQ?aC%xy)IH~%p8I;~uxCn<7M;D0vd8?O*J zI&D=CImC~08uSEx@->=S8KK-yEV57ia5c|(6eFkWGcV4V%cin>nz4Nc3m82l_5-i>9phTPGN(+^L!QeBV!k|V7?0_6K|na6+HXj_~+1jSAy-&f5DGUU?5;aWVGc>F(A8NAB(e(qJT$a-1ypv?sj4ye?23FK zGmR%Hz;XYW!d8zI@}AyT^!>6aKxkzMRLtid+L7hS@OcrtJ^Mbx%>v z3yHNPC1IHjDKsF7WbcNcV=HylOCXLk^aN%Cwz#}aAU{U}uot5J}F-{gm zd5e?}*atBK_sx*Y41sDl`HE4AmM6Sx>xOgy2Pdgk%vxnZ9g311zJj{VbPc=WRz3{0 z_gbSUvNaUUeN_VEG%!tahN1wY0RQv!sHzOfNAOMww%K8McHgM=Ozd4C{Ih5DDxIx?P(>g4}l&mO!b4QI1~Cws}BllU%TVc1(xy9zD}I*sy1BnHG+Xl`jz5*GIb9Zj&zL7uR$4 zQWjAv`z7XZ`53l+U2?+o7wQYp_Www_7JsI{|G({R8{6EOxz0Uhn0q4I5Q{~LDimGZQl?ZYY6=xy-Y!WlHIkN6lxj&riDffNs^tY4c`cB-OAb&tX~VK+uVnX} zO+l-5?foi&27T?s4j&#i?_vh57QW0~fsD)KA2_xT1at+bWnSaFIL`dw0$T^M36a7p zn>WJx3fP+K-Q0pdLIBWdd+-OKb>g}btcrmW?m|9a-GkrzIZ4mVIy3hLqfC7pg<^*o z6DdH!m*iRP9G-MhN$iv;+(-)s26viO6pB+8=gAW)a5t`4Nbx3c%p0-$f;MuBi1Mq3 zju;70PlqAeut}D^uVIpe8}o#Uxp>us)AxQi)4t;x7cYhTRp+K&^(*fq)Q;m3L*y>Qk64W<9 zVVQLT^UDGXhRomuNC%O1rd&^#C=NV$c0vP03TL3# zm+k{B75v0kQ@Cy8Ukjau;cU`AX~ti|S8)2k{%;e2UY{`CVaf2{C5D{hB%xp(%+ zcQxUKy{*W&dd*4@5O?icE0Rs3Wg2+~_{cW~=@wW>%=f??C#2_Y}u&x3x<`POQ4vm?alHMonR!dp`Y0ju?YsJQKHK2bB! z^%dh8Ci22;rHkC@l)9f7!6Z7ef ze&Lpj9vWAZqxRy$9#0~fUzW@E=`K*?x24Fi`Ez~PxUZbPi$sa;?&EdSfGFA!re2aq zW$(dQj^`_xhCCkXM8iSYkfh{KPybHYU+&Qte>9O14<)JNaNFUGaDw=hxp3wiGV&eL z4IGyrFt^6vOJbVoz+Pfo!R>zvHXmk+Pc%t%?sd*s2u$N=?SPvle*w>_cwoOHd;7L& zhw#$V5U5bIoS+m%flu6QE3?ykoB_8*)dQonL@k4uVf7^u%4QZ;YTYrUki?F_C)b?Z z@A76akXN6$P5&n9{BqXr)|4LRj1|3ceL;=W;*2dE@Gyl1uF#(zLe{=^MWbdt#_K~D)5NNk9tc%U25Dk}Q0#j$}?_Vk~j;As=MJ|BhPNm6BvzPb{SZC@@fFE3sk2 zPpF($Zom_pNYlS-f8L|BYM0lohw2Ws3u9R=zp_L*Us7Z$vN89P&ebVuRZ&MtP-Bp= zU~3p~>y77N<%QZ&h=b?PRNb3qgqx}2q=69R`3!+Zx z)w3s~wxWpxcW-bOom&z5TaYTYO2GF!ki`wc2#YoVC!n1e|MVba^zed$s3AB#ChzXC z5avfgV?Fc3^Yh+p!fxHe#F!V~U6I2Wp6x{tE>sDq(oGv^v!9q6rTw)Ow&x{iHfj-^ zoJ78Vqr@^Z614uVwFsu?-Z206S8gA(nrdLu#WXD{C|M@Wv05RBtrS}i8sEGtaQ(e# z5c^J$6_2tWw}A5w^jX1eO&7Q&%yZoOOvODY>hjF`Z$#6HRVKL7N?o`^@(mMmlU#NI z|L4@Ad)0B){ksyO|2~GE%^A?D83Vp<@M~ZEC3ga?%+48x+1SiK+Yrnjya2TW74yza zm@ql45YJt&^Q7g}`G@M2YPvCa?`+H*UYx89oBkouN&4ADdw6QnQE;tR?!=A76s`M^ z#v7>b(sZ$Kg;Io{SrjnQ8&nbdnZbVpQ+h|{@Q=L4iWLmM zrrbFw<Zj_lDIzvDwz22;#DO9JDJ=(Kd`v=99G^HIDA9+R*^yE=vLA z1`7lp>!`P_!Jx|q%e{HmlXT6Bt^$#vqsch)hVf&Vx!RuapN3r|X1gH2+=}nz?bt*~HT9uZZT@n1hwX0W_WSHH&BxLQb zvev8h?e~_^e#Q+j#nv@VFb)yeSPpS1TI6=11 z%$03ocsx+pOM@eBY^5ADs8Yx#>wlx@h|53JNk}KQ+lw9k^t+N+Z7c6s>yAW)cnlF5 z{Vofl?C$0?Pr4I%B})P#&tj)~>G#eJR-(8ot`SFfZZb%gIhpm?T-q19&$9Y1591a` z6?I?d1|{?l9XR@FCi9_z&ak%z5uKV3enD$}@P_hqU)+z6(L{nfX8m z99MLF&6;oaGHI$Yu45j;jrPb$^*~a~g;5-{oVZGv6!++s$|n0U`?HQ9vb81iM6T>Z z&Mr62&zjn%8uQ>dVe>Le@+(Y*+r7gOzrTutn--*Y!(&y$bJiu*Hj8>ek?Pb0M9txE zzh68=JGO|a+i;YV*7eICxrY~1y+C%)=WH(=)8oT2{3*ucdPMMDQ=q{4eRS?rOF94k z+b4igzk(Z}|LPEM!%t#n4di%fVi?v>~w~o-ohJol)0|mvHX{CoRMNL?_oKUSTFJhlsE4 zA}%9lJ{$UyT4s2ZFzb?;KWdGm?+@AU0QG)nX=KR@z8*d3z(q^?#K$mrhUzqUcVE-)V&wtoOA}O=f%92;)B^j0)@RN+z=-qgFT>dQ^{?hZ>(@fae5V0S4xN4kj(_rqhu>zz>B(+uE zyZtHu_UnC?^rCI)JJ+w1MF_uxQPx98U_fcjpE(_X>G^_?rsTS{k2U~<<9r#cA&{?P z)hQv|0$2Bo-1>8x2aTLoRtAQ_`BJOiB0yd3uXT1~T98LF@@hRk?0O&)9iZ(qY&fw= zdeqrT=&PwK`RiDkd`|n8e76!x`&GQo(0cyPXxOxg8YAwv2>%X|Ggk!izj{yqYhn?x z`_<&*Zqg1C6?>vEwi}x$3OCq`Y5CevlLOPz!oB61bpJziz~K_KgKFc53FNv=Ap9gp zYKv#iXPr#NWN^{3b+3|?n*R$jTMfVKR*ZS2CO{aCnT9WTgKCi#7NVS$?WC(I11T~~ zViNzAgbwCeq-U!>ae_^O+w^7G=Z?c6x=D>I?#&1Sm6IB^Wcc1%&aP40TAG@NF|&AmdKfPOnp&NS@P%m$&3}Ogju*efL$#^Kgr&ECOFTRtR zeU4(t4e0iRBVJ&{adJW8J&N7lk6X-)1~;$Zn|@)8fM4K9KRN_JYD!AUAEB3TeVKao zgCjvqT+%+8MNH$>?`Lo2ksdcR>r*~sh&VXR8F&8B7vAL$ZYww)9JIf?+7zIgqRI61 z!_rk0VD4yJb?wlMmV(61Y@Wics2pSn$Bg0n1~D9mMm||U*Df%qA^X*L_@J$& zl?9<+hMt4cyS;x0lI#1?4t_uo-+BFvzxQEDpOb8va{>+*#eeqbnSm-3aRD6RKxaYP zKa2>Yf+fRFNc*f@Ox(18%TFnMzOXe;l<>QFf79|v#T8r3)CBy4&=|MEF+n$R-r4Ps z1>Clc+}#U3QjMv+WeS9>6Ya-tg~kXwdim7>e9JqM?5oKI+~^>G*PxrY-l?WAgdX!;x@imz<+(b|7jc&lL} zvtf@Y6SQwlTj+nox4WqQPvqtwo^a&Hz7}jTqqFdLCZMDol4WP3iRvTIvKK^#cyLxn(l zt1=8c2(Y&jN2wdb7t?DB7mI<0Yl_%=4LfwQRSl#tmy3^D$)~fygv!qEuzcQ_7zDP|5GE8<~YIf zqCHyuLNK=5V=GFN6J8MO$1PcZU_56X{eTr`{xJ*S5{&t_I*P9W=UG99a zMp6j;5NG8DVu!WZ+2S|57|UCpAfubGmrNo^-tv4iCm^~s#?*BebH|QgKn~cVe(l2s zBs%U1EhhIL1!@fD&yvY2Zv1z;(Fvu!B0v`QY6e~Zqs&(4<3g9OXL?xmw`Y; z(W(6*urw5hl@h8cG`woY#Z~uw!o_KVJKnA?zQ2|nGPPDLQg*y@u}yB6ae*sWICt7; zvz2Sjj7b?pl--Tvu?PQNUp%WgfM8y31zcW8{9_wGV=XHpkJI=1&lzCf4K+?F#~>Oz z!qIgivaVw}Hx43B{zAN;%5_h^!H#nWXi`H5H9&f{4!CU;O}YYf@K?l-7O)P{T(iw< z;h<0R>Xb;Jl#mwQwJ&xdviI6e{x#h4yVWxIz#9|3`BLbJy_fX5{YZP* z%LdK@-#Pp7a{7T+p>SDm3qoI=%Pmo|6A|lVz60{Zaai_)oJvxQ_X{RR=N_I~mR7Tc z`Hu+IRn=s&znd<)SY0^_wwYyuYoVjX4J5SoXG|7!1ZjQ?6Fu#rx;O;0$H6ov62}fd zU;^Bm=}f}5FkOQ=1s_KST^t4A>qXX?tLTcmnAQ#3P*x5%V-_4A?SW!$P{ogr3KP$L z<_9B~UMY4$sFfwZ+B?^Rvg!0(?B|qF2AhIw1pB3E(W8FhMGO`>BCF$V0xAqInH3a@&vE^fF$G?oq z;|Jr^Q=V-(_$R3}TKAxf0+yVKxMWT`4BlbsB*O2MMQNK-%}xM4FhIvIa^@LK`%322 zFK%wUAc6(DrBfZxkyHKmQFNz3rYIA+e*UzIVC!NE+WEtbJe%_D`rxW~kz2<;+Sq(< zP>jOmB{M4Uy>*VC2!^jOO7fUSxZm!T|^o%<&2Z^IU z)py+j2i(31`?~Ze4hV8Xa1&gy9oioSl;sARP6r4vr@WHzd}#Ge5u4?6go?g9qhe z&F3&!`Yo?!Q(51=U4~co^ImN_brNM+p8Y(Q`%?|h`=t@GT`ErzN?Cwc$te3da95aG zigQ*G_}qlS`?Wi*={v9i>+9DAKW{wY>EDli%AoA0o(2JKs&<4j{L)$d+XxK1ZXw=A z3wg{d-!MJ1K(+T>_&iY(Z%uE<77V zr#xo3Sq5QBUld9u4p8s|D3aKngbA{(rRLQB|2vfF3MYy2rOjI~m%hwgJdug5QR?p>@OaM};Io$B5L&{lS3oXhf zESn%x#_Qv&Fha|mUwFWg^{}E>ZEW-gE|GtGG;zER=D$2;5@{0YxhZgfQk1V%T#=9_ zG43d__-!yO0QoN23+a9*Bv>;055o$kQg*v!9EHD0@2r2_!dx7y zkx+a#?9#>#LZCKRT6Q{FUsIGC?*qg80k=Xh=i&E#Ht7s^z)EoUTe)mEan(hce6gua zs6y$8{7S`Vx|O$ybv>s<1)joElwiNZJc`}IiAK-52Ca1Q&-}=oOIT!)eHNkIu8S-z z`3XTI{MQ84pg@)+%IZQIYN+Dl=E8hK!R=-LlsDujH)x6$0_B2_0>2r(_>_{}zn|d> zW6u_&52Z1nDkJQQ7UoggXPa)0r&U)yJ45zU0hb*75-vTiu(ykkIuFAwQe~jWQ2&`8ZHniZ!7W6i)Cg!?ib1rXtp-#DudJ9~bj~qFRn0HVK z1X@swJM0X*C)8u#>(1+lYG>1bH}Bp{-oiihfjg~g(k|H~)J%^1{0Z_u<;58oRTJ%9 z6@(k^!T7h;i*kK9ghHSMI%7_!mrDo1Vpg#XZ)g8l8vaNEZgzj82d+JIS+~}k`SoGw zhkqpE-UP~$1OB33zo$4CUsD`Xb&w--u93h-ai-k?JyIG1ue<4eykRK7{-Vj#v|ATJ zgzDO0!$vun3xL}9@tC^wr^rZ)+kI8}}rRap6~KlPPoiqy1p*Xoa?VV!1FT_vumJYv@x zmBXVaF>t;mROf>`qfY5zO%XupbJSFz+Y($N5MZKyqE(L|xw`L?hQC=qXJO1@3b+A- z3NSlzt5n=R(BtcK13t{{=A_jULoO;A+&kipPu_P?_8!{tbULh9Q$h5dv3JY9}@L?>f}O3JZJd7g)FSccjO(G zbLmIZnVt^VeP|Qkf@E|9`&WGvl8mND-`RsEYT;-BRMA6e+SR^Y@I$$nMtR$o&J`_2 z%?#u2%m(SmJx|D?u)=~@2yT~qmxQ{JnX;Y%^KQ;y9g4EkPPS$|F7ZES!})#?YUJVB zV}|*<>-RQ)_w|J{R*B`K{~ll9M1I7*_@T+;sw+tFQ5AtVe1AH-EIoA6g-@2@aX&1+ z4DLL;caLz3lW5{@MKgN#&Z3H+Po{XRUGk+B*?M}@g-3_6Nke8|g}0_NHd zje()sH_+<86i->WY!f`gYp*Vq2}nxsp!pEJP@d>-&lnyn%Eg!D`4wAutA#OEwHMM? zg2JBW8bm}BL*_%53F2bEH(Q=gGwrOeU3&xUgo&v|_NNbSqXKWb=w}R-?l{5Q0l~$@ z_c?w{6+`jn8|oBa4DmV>Zab|U)?U^S+%=QaDp+^1+f9oClHc z?#Uz%)5GecqaObsF?~=4C;lD;E6)n#pp-^NyW4`iiG*l_EK;}=QYA8tWyC~C=fYr2o`QE1_#ykCiYds&=2iDd3VB5e|8)os{@_SYC8 zkz&C-NK@n|l8;)ufFfj)H&6cK>wi=%cQ{Nk>$n-})|8PfAkU0B)5oL68| z_M+-`t?DF^{)^21d;b&qCxag#H4&M^d_jNDT@1rFzujuklDW*l&;IiBWNv+N8d^d# z7ng2g{3soo1aMQN)D1-b4NmA$DkU`DB?7Us5w+jzj5J6iCS8v>6lRMgM z=@b)U$ebb%F7{g@-wsA!BlHI7wj#Q+G}?1cg`I2kDmAueK)moYClMI9n`^i%GZ5aD z%(}ufm6$CXRzQs_B40|gDLVMLirwh90YS`#}Ch=*f5sv)*{y=+wb(rd1}@c=4H}&$5tEg__J-o6T;~!mf3kgZm zq1Zi8rwp+gPY4>0^P7)&;Jv=h8n%a$>tl;~|iB>33&TV@XaI zZ0MH{zXew4=B^eZ6fd4e?^{5}-F>i-9+I|^&H<$yTUyoan2>~7wKq2wr9G>5w)5#1 z#}|ZJ=~29CZ!S|5>y2o1ONnlII(H!SNyhZb_*t7@H{W;ZKM;5@aM-E=jr@VTlH`w~ zOiO>Kd^@{F-!e(^b(Xl-NEmAPj)Q$?>(C_@ZD!gX;aTC(BjK1Ue2P^s( z4o?A`>W4=-MSGGCRf#A|Ta;VJu(S)YJi;?z`4RiAESZhT+<`mlm+xprUa&QH@cA7H zg52=5?nMl-VlPg7L`y^(2-HwD$%NS9_0bOB*W6dDy*$)z3UBk`aaqxH>Mvr(pFi1T z`NO1f;udeTmHG4ejCDWU8*-fwThdjyBXy~Lky69Q3zI;lk0G$NV;$VjR;Dpq=zt;i(r59{MLf^j+?t|Tv+ffnA-#H{^D<^`}N-+Sg ze-*nay=IF!T_~Zj;f{Y}hESuvHq2eKR+dos3KteEi6@bYnMBjKC1x)+FfH!D@Jih9 zPNxu;Hk*_K-Wk~p7Pj1~%5hp7VCTJ?r}7RcsQE@__bhxkinJ2q_TWtVUg~beJd@G7 z*wMs!g#E4tZJ*op-hrQ|js6V~4!C9ID}cUDxH)a{*tsU6rOHRZ7e^7)Jn5EqfmQ-lG_DV znl5$?M2WuTj-1^jRzm$*ZoN);lAUl~1J^Pu*vNEUI{F4fa6VB+?VLwA9tbijbU#pl(FQnXU47oC1W zmdA<{eUnAVZ|{S#RstlGp;iK;@F*QFLrx>&aeOIj*VYc*fYm^8xXpB z_$Pfb&h42ZyT5M?JT;{dSfT!Vd>9BeqTz-E<}qei?lnbW`%PPV0OoCF5pbCZ)d={t zcJu$Po2=s`B#m9*I)A`(PM+PWVU_A0*M_s(K8p8X0_5Z29Qttvev@ue2){2<-C&b_ zR-u_$_UEROU5*lIH@d)QZUG#f(!w*%DJ23-#Z)iAW0b(xB6Y90UXImdXWgFJ{1kvw zjneG>(w21T#Wklk^S7TV2Ick_775RJ{s#=@`lM({UnrDSLQ^ZBprkbJfJqq>Un5=} z)F-TC0)K5ADtiT>DlfaFI8zTIX!^~d#;x$XSjAuAk6f(5U4+QHE~LlF{)|1PFAk@` z`eV8`P~1-w^ZOZ~peDk>n}cB?iy@NDUnvO$U^X=Dl8BAkah!WCZHd%LTlt=KBM=kO zBQMdr@0ny`R#n#7l^hza9R8TDboYR65xdv{FZZm-Oq-m7h7}KgepqRK0(H=8LuyI};kN9SfHZnDr?k)W{BiuAYyZ1>+ z>=nH6z0Zi>n7-hG(PUm`GCHOU5-9XOlRM$cQHR*m%NN`UQZS}Z)+Xsb&c{JM1UF_e zyv0kj2hXfY1rEwmLWco4#2o9h0q@hXb=`}q<6w+jkP{HjojBn7;;YVC-jvK$EnK?U~2=v8osjLhz_pi_RlsV|GHh&@_dj_z{(J^r?QSaJYc7-F+pOs+rzmrhy zL7mzgfAI9*0gB1KLL|34Doi?&D%$!H`(*j48JR}0(XId8?*&0x>>gMqTX8wpM;S$I z+J-b$)n{_+GgN;*R1N0J3Ve^XBKu{+U+saXzS4-B1_<7KZ=}$dC`u+^jGcYqzdPmg z>3^VzE+U|?G3W;fXn+0A2b?TR{_!<%5NOkXP~|CQ_fgVYL_`-W)JtH>>FU4Sn^d~t ztt6ejkjGuGsm?#ASeD9GI9>Q#scw(4JSd0ZFyPEV?`6+Y`&PP``}QDS_Q!hS$1HtQop%( z@g6<%a?~0x@Fj1#O@ce3*Wj9N7Ia7NK`VE}!`x1^H?0edoXDl^FH)3D1m6tz5NK5# zv+@iibuL5Zrs*|Y%u`TnucX)CUraSV21m3FI#yw2ywCRn1MKZW68h6--0*PeMTbY>juME#?6GTyOA-eF9lq`g*lN<_i)OEMW`CnbMzs7LxWtK3zAps9r+4QP|f6Rwuw?iks^ zb#!_v#(yO(NiXe%WH*WQfVs<&Y0Ka)ep!^a)u}DZ8DPlvBqsZ9*D_S`y$duMR|eC6 zSo0ZoGlVx0Ok8C=w|3*ir%%|`nmRGJ<(vW*xhi}6z1HfW>vE8)F{K*Qelw|JH1C(X zY)k&n&z;{0&=Qyu_M&&uqgAxUPSfVYJGrJVBy%++!km?4Z27g2ykp~{{LIA0L4!lA z5zEva#;prd2fH(+g+*4d*Dq80fpv?nn~2_m*Ie5_+qS~MF>&R0fi+CC3k@n6?|;n@ zuhg}CaI?i)S1!R|D$+u{mTdERy>x&%s`qX}5%FN}vSaCJNNn- zMw?8@%zW_|Jvl2hxGFhaQ-;GYaZA>R3k+11wq%YfA89l6!=q$U4y532T!Jlm(hIW-1~YEq}z2#KLWnvx)U zSJ>6yUE>U>wc_Q`4 z%84l-x#sme?|N7jqVd1$dKx0-8@>?6|D<77N;;|(vj-F2?4u;oz>r}OlOTWk*|fh( zKPwi8v7KeKJS5)*pb4JRqENRVXR94O9k+n92d}QS`~YNxF~<#2%j9YFMNpNq zBbpHSM-!rO_%fF@pa?atf%U$$a2)R|KOvw&KiIcNdN^+HV*RFYRGs~qR~Su~s)tXe z7^e^1lz5uC?L~##=il1J7FPQfGk3eAJHp<-MuhU^JL2x^J(^?4Ng9*}{Y@_Js!~wL zdV<#?-UQe0wKOlyMH?txESS$6lPE)tmA2rdy9_oyB)M&r!A;J|$;@C&`b|=~X1sS6 z{2%3SRveVdCgg$%$jQOv&qCj2)peW)TvfaPb2Arzs6*{jLbeY#VpM!n)^3|M5a@ zP^GlZrzE}VD@+gV$0;#tmR&NC-xTS?WE7NeuEXSi+YnUqTjqQ}bM9W1!dI=)<1#l3 zltgc*Wy_y$2*d^ZdZG`n1;Nh#dE9!Q%-J!?=C2yDqE178mLMI(XQKQaZ>-#tzYo5j zyMokz4i&z=;PK2$PJY)7_m9cscQ{M>d0H5JScY`t(nrq=N~TptlZ6e*aaBT%1n{_V zF~!EiqR{oY?nw(qi%Au*1vPzIY(+0WifT7R%>AoXp%e?#3B=YNH*Q|YiRxKEkWm>n z1sKQ0hCy}7bE0tn6C9$)p|axe>YG}Ah(Oq|05HfqDX+5dzFH25;NmRhi2jcx+z_cS zi|l8xNu&8g9D3xvo6Z=V4+umLWz1sxHyp8p>FfMhH%?_rc; zh8dT~^7oNQjJsC!BJ7u^4@e|?<~v5^o0V5Nwc)d*?Eh*&;Hcv$NrQ5uW`>Vh#DH0d zP{%{K*r5uBY0M8WTbFqBX74p^vnqEvfLgL*m70@Elcj35WuMJ@&cCBCn5&Twf934M zI$XqM4HF<&lvp1vY^N{;+X%JaDET+HuuFnT*}kauUWPqqW<&{Uoc+PHK1`uFAEfKz zkUbz*tuMrfr&3ax$XC{MD=R9~v7&#(@$zor1pdcCFBbq1nJ<8U3(DVkGb$9j!3Ng% z`I(tK00B74N2f34ahK*yzpWMiTYUkvYH?x+)U7npJ=C5Pl(zq1B8kmEa17tG$p2f< zRX;_H!F)5t@hp&@4lkg^!PRWc^ZOBKsicUzTVhr&$sRE=t4qDRMYqEJR?$hUHhcGj z5O@?%_H(J@e$TL?YoUF0ypdC$xnLw?Y33J0iYfDSuSWphqcO2NtDa!)L+m$GZBL1{ z4Z<@mSsI7(Mnk1U<+`@>n5Y>p@!21sOkvM^+|wL%7kaM{Tgmvvx=LKun@l${)1q>` zvkxDE_Eus{(RxDM0iaRe!~G|5QphXt+~x z7nA)w`O((PUzkI6 z-3?3$L9*tQX=y{=#uHpFwNy?NpfU;y18ZDO%Rc}cf<(R-i>&WE%6ba5v5mwfzRSn2 zFL7pj@DW#1FFhr!fT#7;m7Sn9eZ|i2#E^Y25qq z?S9bBCwNy=1t}H2`WV_z&%L6-N7|3_6y|@5)D#xI1|8d~=M+lmTMDH+Cte0smtoKH zQW~UCs!qz{VRQ$6#psEf0KX*&>P#S7l`&Lz@*08}7bbx^;xt~yq?r67?hIyj>5X;W z2p3rU#MzRoiKfQk8S9Fg(ZpY2n+^#I74H>&tp|y+x9UJil;ghzA?trO!0~QxQ>UC3 z7!Y1c1g<05ud`?6Xww|*$E|Q$L$h0O!mPYf90tJ{XKSc-28KVs-h_G@87wSS-BIDP zI0>CK^jjJ-ph&LVGDqP$+=*q&&mt__hXvw)Q{TUF!9I6Jn z9s;Bc8+f1%TJ7uC#pCL!h;GdtkQeo0v^V@l5Cyr~1)OMaf*y$`&&2E9b0OI8G>5&0 zsRd&%rB7}?0IO|P5np~Mxk*{i->m(a)Zjvl+cPYFs2K!mURtT)^}v)E8V4ji-u+B1 zO?@r*pY*;QtI4GBh6|}Rsf$lMm${wbcX=rG3nme3b8#!s74DS5sY3{(s zO*mfWN*u4?7;_h2St}D69UM&GftAcjL5qeTHTY~w)KiI_w4b@aHc>bJaWIFbWL)(tScOgp#=1{2L>qrY zBFX7nQufgq3W#L0;Z*3pAQN;iMH8wLspwOi*Q!>px5HEW3xSYOF5F)^(%~dU0li>);5=7PQ{{!QI&vTPNHil9UTL`` z6&J+z=s!nvH$;~_C!8!cgd;|5grCl3+DPBt0QrPl>TSKw|C)cHzzu{lTj^tS+ZMY0cuUe7zp^P-NDW^oT-{CG zkWC17Z%FH5z-XIz8M--GJCU3V7mS3Y?eMnWO>5y=PW4WSel;0jP$lcplYP0@_{c0? zW?M8IabilXYRyE(sh{5&P~y9Y`i38lHVYC?&9SUK?341_mcFFC1D+^o@(D2GcrHT| z9M>~4H1@1OT|-Wu&~_+6D|6{SXRL%D6UxJL{_(i2>BXBL!EQs_WuF$;>@Bo*@i$c~ z`G@*?IR#&KT14k=rL=rK>77k(IjSpJV3a-T-Wl(4=D#&GEgBhiATP7`l@aR z4?X`OznZ=Zm+Ri{EySjXr>RPd8L1@!hVEka>49zGA>7ez;iSFE_sjt z*9OVJ1HW$VWF9^ zp*45UH+A7h3DVF`YT4FcpW!ZdJ+uv1vxj}Nse7aZktIl!oLkk-?`)N;}WKpyS3Wq?!lI2b-W*ob5k zZYwNpnJyqQ74t^-D;SRzOi?Ja+xI9GGs1-nkZM1bKk!$zS5@7tv<#f>C)Q}hbD!h9 zX-<-t-qJ|>=$PZCsh>twzYt7^F5+8s58yU2EcnRbD91`FVuj^~2AWNGsGvSNq}0f8`mS{J7Ovewpe=p3gnc~b*r}nV^%UKI z!VRv_l`c0+YYEuUh$>h?QrR*y&a3(6xY6D~t?%eM`dLK7 zYaEbGUIxJ2wBt@=R9u(Zy3?5R&kI))&BAcqn$hf%iu)d5{f}@1{sZ2}eoZ#q61<8t zHf;P-xz8L6Sn2@EALRd8>lBG}3+%V+(PJunZHqmL480@QUP%95^_exkl(v_NwsdZj z-wJ)Ovn7NBZqbP}&8d}mM+2-(zp7y41kf)>L3VGTsQQx_B9$oYY~-hWT2#!rnH^Tm zyn%JQ09MyLibAHi%=)HFf*o4{-7*Fp} zq;@v{fp=vdQ&<>olG9`Eehnz~p?}_&qHXZ{+fqt8foi17dIqQ>_HqE;NX^b*WO)yj9 zvX)2`J&?>m8tYFPNcCY93IM-pEluI4HADe>T}j8C{pT>K=k= zP3G#!gv+bZg05I}!mNBF;4R#SdRvI8Gg^V%X)R0|U03|rWlo}CR>iy^+KuZzx1peN zcO0Ok?<3JB-g5#bzuwBetl!yB47q~cY0^UHU*6B!vu%pHseWAE!z-#vsP4D)sUEHb z*QuCJfKpVA%xL^aj|FSE{Nv6sg?WlCKhDCi531*rgrn}NVDnINrV5jhhP{cJf6h5# zMw##p(q`azWLq4rOEeL zGEMnYR}+r0mq71<7y8oe)5A#b>8zqmsdbTo+zT4O2~xV3H=2X~HsP^T7H*R*#dT{> z1j8-H)peM51qn>_0#@Yz^3l%=xP8j_Kpb!H0iZyRY1{yFXZ%z|Z>G~5FG)t#GWi8- z4@7`ct|Gfm`m=sszd&7oqMg)kqL19kF$(8RS~QUy;DKfMm@`ztb%|8%I#M?Q;#qt} z!^9B)EZF$c%4{r-2&I`wg-wWFtL) z&=*tUh{de8Nb_p-lcS~#!^ibUcTz1gFI%nB0vKUX6nJIYj&$V%XZ`<5ctz`Hur_AL z&Y~;h32e^Kl;-B+EF}Z$@I8%;E`q7g^_d1vXgeYNawc#oR29Ts;ALf?tQ$jp9-M;? zJzq?MBYv29p2q$p9g0jN)5k6@;j>T}|64oTO1tnG>&1fKA=h_&J?iT=+|OI%^|d)# z+3j~HfP2f473CQj5x?b^2RQJH+1vpt!RAe>jMW2yIwkG}Gh@HxTuvS9TIes%J{}2u40|9z+(fzV)s`jP!^zPqbhW8Xw|EmFg@{-=}8!6WtB*u1E zcs@LgTCIoam_|pQlB7kEd3G}e9NSxR;g?=nSoxzq>lMta$lDy)=`ysAVo5hd`&(HF z8s|A_s5*6lE$3;y>Z+`TL@?!76Z~Qm&r3Rpk4_UJ(5O|b@S@;*7&2Q^y>oprVs*4h zisWbu2fZFwu24;PK8`t?hY1s1MR?8jEmG={nR1`Blg)lir;7^R1*XJZkSX5Uo)1o> z-4U@|mQ>YWP2sEE2z`~b3ImgQLsK%=WN>gLYNi1!<24ATK>p_QdFrj)LH7ab2}$S# z_d@#t9B&nTY+;S_@z9vGX%l40+v6nU_rYKn{NF87X+cX=5I0u`yd{RuX6d*Q1;N{r zU?67jjJCcc(}kTICmno*e%Lm83H?;6u|r8z$TU}3+_hXGD2*hf^}I%QHtqo=B2dlL zaBCDT2VSy)#XnZS-|BifbRHjt)!hZNbW2(g7z(&QK@?9-WgaF_VBXemn9V1&(d}hv zQqPs|xZ*j!qv7qEe@fFrl^F#$w&_D?$fd@m3# zZUSN_Oz)1&+h5@H>v*5Mt>^xargM*H^8f$;wrdA$&dla~rVMikInE)egiacg%OMmA zNs`!{l{6hB!jyWe&?+Q~i;!xvB#j73twKmnVc)CI?f3iFf4eT%>-9W59`^@RgOFX9 zqS=AtTv_=c4m_8QeNjv^Ylaf==*IUlp(FYv%+f#MSa$GBHbf-!IEaZ6!LMtpGS+b& zz|I-Sg^Y<*nH5$*L;DGY57Q_-#RA)Y_&kk^ixdw}v=?i)E#mhp3pHXv) zS2tWyw(U@@Jg7?2+xU@8iY2S#@N7B#A;QeQ$-A`QC}DZVxBY?zuP=`txJ`zp67|zM zTVfw0*U)w(8P#c_tDo;p1Pq9ldX3`}#2S}&XIw=3@uVBsHyzpjW*(sBh7lH1BmLsv zd0OZ&9Qh%^bs4$FHnhaLxk~KT3L4v>KaTX<(-eioZvb8Yig2R?95H3qyl^mce9aSF z#m0q=_^?kv?k7~em(FZC(y_)Yoahoie3>DpMm(Ssk z=yJAvLFM9#%Xn>$B6mKDQE2<*7$m;0*uYEJNprbHq-m#OVtX{IGIq(C%|Q&nL%h1R ztWy0IwL4b;znizkW{WTCSDeWDqTb8(wGjy-Lm7_8lc2h>pKxkHnWd!#a^qgoMtM>=&fpzfpJq?0hOx zEDZ!nfUO!CPkGYM{-9JY6E7nakq;5(N`Fba8$@UU`3GHW-Rmh(|8@$eU%tFz6I!ulS;XgeKu!VPC);ex9G;LYJ+kJ!)Hs|xRdZG_JLj$c4^BU zC&(y#(g78SnqS26{`jdkBifC*w8y6aKT3SNDtl3mAC393Oi&v6i;20U$c4)d9{M~4>Cb1F$!q5 zH~(b3o2Q5ooVKB%5XvSFG_;1 z1tuKu)bb^y{gYNKWv}c&Hpw%!)Mr7GZ#sx6;UN?X)p5YP^h+>Z06{gJDS^3C0Fm@~ z5$X3IZE*t8l)b_UE@x)^RImvDnMdwD>yNA=t+Rc$U2P<}Srqq58EvA538bIc8`buadPg zLntJh^Fr@VJMX9H1Am_9RUkua7Eh{2WP^C~3C6W|t7x*;1$2CdgfK^V?E~lR)+7Cu@;=);E%vlug z3=#6qB$))F45g+Ke}=Q-V;uJ1X5eNHd!R{`&q37};wPDg zJJpZk;{tl%n{PKSR`isBu12+cx%%QA=Ghx48cpZTqVSuVgy9*IjY68gGkfr-W_ZT? zli2onr453Hk?_DxrvI*rr>;i>*O0nANg4E!uPwZ%=~JO24lL$@4WQ-^RD4s?jLs)Z zh}b3zo^;0Ffr3)GH73pQ=TReO$e|5u!=E-l>1u9$qlv3W4K=9mKPSD1&yYC#H!sOT zp$bA3%}zLo!ou;UwpY0pHE6}($a>dy^2;tOuj4+O*~2?LM7sZ$WutJUllP;v&f6jJ zN?X{QDea~2*Ae_`puXs!Fnuq=x|u)_rwL=6nleVc6*x*!cW~`E#;`MJ8NG?4WF<|q zykJWv^)jPI(M083w}@eQHmJNb38zbE7Q?p)GbREZXQo$c!_9rVjf|a!HzBU2XVOcG zF~^lC$}Hm!66U!Qd~2N3KC&ZSzR)LK^n3}N)}jQZ8@{|S;j&vm6y239*v%ALaS`>g z%|jue73_l?Wi%i0&tp3JVfiI!@zqo@C9pb4r7~WHx@%oJHN@gDTM-D_Z_w7C(b{Q4 z3pTpJn^vISe6^|(L=pGpL<3V{8HOTfxQKwE4G8m|oa@JnNg7daF~rVdhGFlB7%O+f zj;`x1tm*et_gVdvE;~&~tnFOBn@tMhd;{L@xSBvu2bRea!N+HR>lKKJRCMMsH6%#; zkqU6F2+R(CClGc~SXvx zP<|P)?5J8eTG)WqjXUap2B@#JOv}YwLn@L^zhIm)eG&fpNC?~&IDMB%ee*U{BYGCC zZ-SKVJq1?3?r|GsY)PuVXTb6?@dnS|OFhK9J_{G=qP(xZUb?6$QMBBLy|d|`Q= z9COnqlWGI_E-~SiFOsCIH)8pwi1=_r;Z>_gG((Y@CB?4mVT74(h;8)tSgd3baK57i zXQ9e4d@~ZDVc7TLA)z7cDf{uORcyS9F^PsNcJ#LfU(+)R2?Uj6iRitjw~%B?CzDvF z%3-$a8MUiR2gsyU=9LUAH93{J1c%80y#q8c2`|V3)vU(YRB_QUR;L(S5Bo*VHgt|Nc#0JN6A;ct`Y%NF!eiKZub1 zyG;d)^^3Lm?iBVo*&i-&*dkMNfxDiID1N*c1bPAWUm9o_jMwBZHMe`IJ}QM8baHla zTWES;zbEtBq;aH09&i}EdaVn2GJJ{i(0K?oT`JJ!)iTewx-|vQ~W9k*;_(TJa(vo;>$QHDs9w%am zE#Fr1jzBmC)f`plTyzuS2V$SEs*O!MRI#EABzf1%Xl4|EX=v)D4E!kt;$!Z^yTk-Z z^Ixd>Xj*iv%cnzBF<02Gs9vz>Mwa3K;EhAa;dh?~M6?zkQ}@r={*aPh%d3I4a5yN+ zO(JjlO@Iz~RgzB6S!d?Hw>=|97Bc*I-2#eJ^-R-Dwa(|azLCrt&gE`td(h0TnMmQI zcJk?x94Xn)TEE*Q1pS)4j+?GfZi{4Gh&gB94fjev2|`O2l(6)QaJZb=b|oh#yG^0- z_7uPYpJL+{1Hcc(@^n5i)%*ImwWpfJPdIRnx@HY45MoTNY z&Qprl@xUX@c+pcPR4#5VME1<3vK_oG?8XmtVIsW?TUc;@ulI%COdA z_@=XEDXbz=5~&ov@xnx}q#bnV+;(T-j_MR$2c5oV>EJXyKUzX;pxMP2eA8doX zSQK0)VD(e*XAnA4Xs4Xv%s@L}{SkRf=JnNYO}9m-xZ)gNslQyZNn*O))wFVzMx`y zRLw2aL02Mj7o-0EYUe>}GHzIeOCakdq9|Zqt3>=?z~}PvIezOQ!FO>R3`t7CZ%%7S zQy2M!d!_OLl~*qfzW!=iWg})&eFY!8lRSV3AyO%S;Nh4T*t#;N);>_RwE9QB_Xpx} za;bCMB%XIcZAk-n^fS6eqx$NlB$igR9CJ2YH*0(~SYWnXuzehEj}aXsE-=_Vi{y`Z>{ zh^d4Sx`TcYvDLs3#CDvhz;o245U>2wLh~8q5Ddqn8E>=gvqHr=+GIhU5XMr*CbQB9 zQ^>uaQ>U8#rs^CUHCSy%nGG0QxLqYYT@bZaUn{+c!hb=4KRZ^mIS$LE<4T+Ub4F&<#ku z@p?P@O~s2VISz;`!1HgH(X}>MyFqL%8ht;29T+Q5$~-pD zFjN8qwIv1}X_fn8MfvAGbwa!qwJDP7ycu8U*w!u=Cxf2!rSkkU4!GHnY2zp0c*Y+o z6-3J#DpI8f@cqz%v~!oK&6ZCjNQM(<6*xh%mT)iMjVYPOlo-91*udMvSZGFe7;;`O zFTen_nqu0Cp_}3yjekP5LkgFXckfg)#P$5KlkaV6GyihMwpCU+K`zT>?e3zAl^CMa z))uGEqwS)1aG^DLF7Mv=K)eBSIt0-5vbX(kF3oq}=&%*koNvxkt$9Hp7}N7j$y#ka zY*e-&MvAxe(yxoJxQdMsJn@#f?1HNOc|ujtZ%GqwhkbxJQR|~9l70$&VCcCb#=jbf zn90=tEQ9Ag>H@lw6RcC0v+9AOK?lIu#%maW{m_A3-Hk#XO6*zFj>@3rtTUtY4%Bru z#B=X!$iu?s6_6NUZ7@%Y=QYhlzP( z?6#b?$B;U<`b<1>5SUu)L~oFT&NVZl4vj$%WRwSwtR#!`Czf#6Ji<`7;|)REfhTWVeWFCv?FLItPY7@I(#zN&j zFJWqIgKIfu^Ws-;82l;5kTn|4>xSo;2S7aa8m{N|Y%k&*dbs~wjD znK|FSd%8I-y%rrX*s=ShRN(^;ll)V8E$P@pKZ{L=qj*ibX+wCGTokD++2r!eay*?14(gWXmF z2}8DUHFK}BD-C|pkQpq(J3l;;S+g^>u;VZ|Sx5{it^^u($L?2=DYp$Nj4bh^EK*0jX-=nN1)k1EMV_;IP>L zdRS-OB*Q<1f;vrCPsNFLu>Y~+=G06if^|{uGbz#9pL$r1Z>DTn1;p_bQ&S!4>_|rc zWUg>l8xZ1l91@*w_APpb2<#;dwyivT}Am!c2=i#?(oIRjuK_Hybw}?{~~V@t*x^rmX?p zN$w~pcW31mM63;m_BK!}$VVjZd;30KW@zMLC#lyW4%9Q8G~x#tF}$F2(U|?`0EAkg zNU1%6hdgM3@hD^Y(7j)aEw}hlsJ;dGHha^TpuU-bi{q!$q<&%5zP%HnGJ-A<5?+p4u9IKG!LevkoHsEMPPW~ha8r)XyP4Rx zlw(yY5~3=oaRrO4UcX{k^{A3Q+-Ld^ZDL1Ft6($t!J~gAURe}0L(C5Zy|6^~BI8Q; zHm{}EPH?ta7loNrFaqa%v;Ty$YgEHMH{vT0>19L6SToL_I*GVg5&V3^QQp3zsszHo zYnaWL#H_8&R;=CnL!80T)Y7(N-jocVe*X6R_PKwz7tN0Vi|aq20+6Uk`;jF4=GP)g zm&G<%-lB#5_X(l3rfghMhrT;8Ym|p3ng+BuHrcteY7moj@&gfDvs`*(xo3dpiNp4$ z>eFT+X+sZxmyjR!DnI>$E!2g;Mh@b&q2r8f z5_VCVozQ}HOLl5O2{PGMVyP>{MAO1u)=WeK|9BC`2?0yA-CVyc&WW;eOo6V{acSS< zbGVDDZ!{D@Ar&`w%*<@zJfrUO)-HE;nQK4+qteW4kCrCc zIM56M-sVXty?Yk#`2jfn)%0ydvCYPq-?=$gC8RCv9f*$(VFeWToFb78q&W82jbDDP_^qrp*%RIrl5^SuO_ zYf866JkCX?1}0qS&(}Q}ZYkRrbZHMN{*X8fe?_Z@U3(zr^dcc&51@VgR7EDC7?=PA z^^60mb6_i2lq6oIej$yxM+jI_wj%BSH6OcK+|K8j|Gh^|jXaulYC?t7szOb3j!G=% zf=L>Y==2{e+iBWt`C8NLHkoBU%!3A@z& z)WsOb_#p%$+seW+bhjIYkBL}Ao~Wdk_#>L=etSvy@VO;)+qCgc)_AM;S3vf+C!g_Q zPX$p~zx*&lR_wuJbee?RQ^}*Eup0hAM2F*OsPb+x5wBZNc8cw^8pme#t3wb(RNaHpN%}L#4g^O zV|Da%k6MM%(2k0NUbU`MK~`7)V~DBm0GZe-^kd|EbWDfib-D|?lYWe)x$?A9-stPBCJ1 z$@4%f?niwjhu1KyW8KM#}O4}+T%o-n-S8IoqqwzJZQ zS4FFT3gt)+4s?;dG%jl?C|!KcOk=gZ3PVXKeB0?-{@0=fc)prXr!v0YT{^?YB+g)l(g97X`%@jkMaPz^!tRG&9Ac<-OVC0Tu}SlN1W!vbdCE@wzZ@(-TqPGX;jE?x=Dx zx`3#&Xr_5yU{trJN<7_t2#2d=M+bq~X!*S@F_B53skM9WAJ-hdCsH)K7xr>9W0vH) zgyM2xI>FA?vMbFV;a4w}$X4$VImfv`Z-cd8vAqd3&$dQ5ww!Kd*v-VKaJ$m`uB>E* zT2hazo#}$2as0uz%Q>(piMaI8sq^dT*R_ne-=~;SA3E#Tr_DKdk@;iy976gPZEcp3 zyLsUPm}s@G_mJb%5;?9Ek|ZBzsHFE$iK$Ek@%?kUUU`RWp*93u0HRbH);!dor}|n8 z-^z;RkGr)nhwms|=cT_v4OHNx)@ZVtX0bt-dQDZ7?EI!(6-J{5eK8U@nie6K7`5=F zV|>Qn)8V?yz~Q&qk4&Tj=Gwfkq;1!iPA5squDc@Nm}o|Rh0&i4YaQhalG?|Izid&A z#Q9PrBgyWV`s5D!OMRGCn)_4{I=g0BWYHv|{z$M6 zbK7Uh0;3);ci65r7GFD}+@bJAhdW<+DsShM(ih`U#63(QjQ zMR28ji(HB%t-U1a03N#is70&uC|yLQXi@Fqw#7&41p|7NYRBssRo)vRR5P-P$5=e$ zwv(_ia{i0@ zRNrZ0&(JgE;PTi=gn#0(l82mU5QCGybk3gw!yHp?E-?eU&z*P;R znDx@h;hk_Opg#N4MAgkha$^5V8(z2ZLx|gzsWoom*txBl+3&?{bc$uU=yh!$eJDon zOc398IVGb!?J{KKPKY~UW;3U59hdu+%mD|uSe2&KVaKi0H>|yWO4Oe#liz)ux%L|$ zYj{;^!}V#&iI`~O^%$t=z=a_#Q^(uPMlEcn!D13`uVES`ULr!&*4k0fU_`?*h6hJml zo3}<85#8Gl5qss?{y&qQ)@Idqi+nIY2j&~A@92sfjz*>oeAqY zgH0jn`@w@fxRYg~{1Nmm``e=Sb-q11dMSO#=r^XZ?yG|d87ORBw+#aMsNt8AQI5!N znKjYPCYQt{0(`-d@++Xp(DWAKM{+s_~V(w&M z{5Rak+guRa@7nkZ;t=#(P8d+DQFcTer2(~W!fG}VZ_NDW5P8BTd^d{Drkh-Xr;4He zmL^npY@?P)Wx%>|DOa3J>lx9Mm*Wz$H-)=r6J^6M#pa3>e2kfq^A!%{i&eEGY}A4M zsCmZCjKfpYTfhjZGjj-)F6U$4l5wqUyTZ&Nx2#=G?>-CR5u6fZ{h1;5{LlT_2jQlI zI+XK$1-15PO93)jTY|r4%Sy0?+2d*)5S+;v!25mzBA?tr7&snfHyuR1U&m%1{{mM* zn&B#&#s1MUXpeNCvY|G!QHfXVGMZ%x)mclT_Q0sIw(1j67t&2qT>PxA_Ur~!gx>5= zNCY6Nwrz*H9Gi3HNKYbY21S8;de|{(j|kb#dbI^Qi7eU5f}i-1jY`+w`mj3XX~BW* zs^kjExfzAIx5c5iIWw4OM#B8{D28SSg>o&Q<} z6E7zKqT5xpd0rf0<4pv1~`-xqbcvfdws0c5H5&R z1?gBV-orw8_Iz&V>@udX^q_(Bu_5fWQ~AgT`{x}BV6VJOp=j-2GwMx;)(;L@2TSiI zN`+4vchyD893&9lukySVxbS2M(`}=}UVn0aPz9q>?i}5%P>aBhY(v=Dlp#CUF(yA{ z=g>g7tUa~NjZvSD9M%%?r>?D`q=PH8U_7tXIddRQPl2hj0J+DDRDSKiwKIMNIE_#9 z2&dKPn`~33k?2jg#2t6Wf-WMzHA{>TW+H`-v}vYZ&3`=9IdYv;kgmlgVCp!w=mAT< zaU@rK%z~F}dLwL!L7vR9rjqFDhPu(`#KJSxIgIxmADocY4<0b`mLtOu#)0d}@RnCb zkQpjFQ)M+Wm3X4SdSJeE=hjnJomFIey$7vY!@B|{*_iDm z5#GF_HdMXC%Pc^ZBvGT@Bu^easgaB$s_J;Mt963#6t+(kl=|i@ZtxY-8!If#p~RK1 zCrNNgJ?v_9s$clJbDLP4{goRc6b(31LLY`{<9c+08pBf;3r zV7PvC;-3F_oj$qch5GVVr~+`m=`bt)6!0^7%Er8g7<&-)roqBsi3Q&Hn4WfjcLm+y|F1Mfz;u%A|XO?;&2Dg#$z5nc893 z984inn<4tgg8KXA!yE6FDN>JiGBU=cLtQ>+s`=n|M7QJ|C`Ofh55mUV1T&(Zh?xGC z0FqjDinYJAi{5~aRcvFlWD|RgdAmB;<9lbOTG91)qaa-g_P2?&V{6Me|MB#vTgKJC zeA$_sDyLXl#xSfn*}@*zfW&By*SeNP1S^)RK7w|DN6-ju!&p?NCSrU~owv>Wa}HT| zO1z|R3FziK(SFO*fKGQS-)Ld^y*B3DK`7M&AZ<;8(mAunaX*PQ4PSzwly*Zi?c9S5 z)6IINmuTxwVh7J`W?QQp&f&H>$X0lr`mJ&}{4>xCll$yaC6MUO%}UHVtULQCoDj=H z7yTg+d&;CS=7O*4!Jw?oF6l%)t#JBR$uCPsLn}(VcNz)ULVd=+V!8E0FQZ_NJs`%* zi;Lc$u5=NZ;8U&ujP~Lh2gr1*XJA%sB`seYV|{(f8hDNb9qh?=&dnK<`G2|R`c_@b zj_4kGyKyo*K>dtJ{o<5XZH9=h8tETAIg7KsOhMRWF@QsN_wdJ=~~*o`V`qQ9BiJk zqrRI;tGSO-$@#}>KbRyh@6~fJ3rbbxn=WMz>4c@n{|XER%yjUU^KcvQ$7Nuoo1e=& zhV9&JM*Y(2>#g3@#;oQ!69|`>8PAWn+CJY$LF*bYrk4?oJ4FPRJCk6+hC#R}Wzuk6 zb6J7Zv&a~`5r>aWUkoO?oK!fl(%P%7$Ao>q-{t6)v%Uzw=rG0=3xh8Us!P~p(Do6I zN}Wf%Q@4zJKA&O;dg4xb>YIO>p1JVpSvCsZZ)8BBeqQPUhcXWSme>);`?J-(K}Mc~ znUUfb`5@i-DVXw|r+C;SU>h+S<*F&$+q{L|h9T;nX5?E7Eq8Y=rEP`{m$bIFUEvCo zwz%38e{h3_XdPV%TZ~a`IY6oMJj+zD)gl@>$r@{!7}|nAES8WZ7DCL?4GakuKn!{Y zxzDK=G0VjhSH+d8PGlrsM3_(d9CVWOLWgem(3$z1a05Smf%bR^bK?9lDr5hufnb!> zm6>Pq4DfF#m*DcRvcvdVCx-*^QRTx)UXEPmb)TG=%15TWGA{)ejiET0 zuSRz$g(J;5VDnuBg|q%SN5Na*2K279I4r^xGQ{|o9~cBEHp zy>KQsjRr(H=*4M$b--S>nSuXsm4k|S@QvbzkukXOeogI}7h>UO*_n(3ALcJe-+@w% zZaDk=2IN|>+=ia^m?6dU_6ul=`IEq_60|T*AtUOy>vY~2`Pj1ftEb9tbv9K{5cA7^ z*Q9D#gFUfu3Y+QcAy6zA@{F)t2Z&pD+JRFCP)?JMxu!^W zMf#n(*X-nH6Uj<-V)1=Ezyv#Vy&xH$p>|#8{d^d$+`cGjo`+I|*L<}OO!78alEe#; z{2h1kKW2=gnE4)Z;meZ|b(6M&b zX{{v=R`}zP&{k`TG_k^^rQB+aJ$WH() zGTEt>e_~3lRwPZAo+z3(tjd};h}=7$!DZ&Rq6a?zR=cg1$mNQGt_iPRFz8#vw<|A_ zs5>xV&?(75cf&2PT<$1Wk?LrN{d|Q}Giu;+8*BJsG+2{& z7BO7>AL4uRfsDG#w0nKW9J_II4*sw@cOotBR%+nS^R&klH4rRBPI{~L;zEPqYp*gH z&|D#%P6bD^y%1LxX?;NA36$A^ zv-v`)bYZVL3w(mh8oF$aR@B-@kPvFjl=ZAsdh6Iq>~9eO2ldBa zR}X0$wNja(XvZ|f2aV}bbUppXQqI%56@@P%mn}KZvRkCg1-%|N-nP}|EGv%M=2?l5 zUfEl>)ER?8t728B)YOk%MfShK!kdBN-(=E_9z;_fZP8wK%PS7;lwKg}g{gBBRk%Q< za#sl|yv$M~{ExY3D56n{1C6M^KE4(1+xy>Vop{MKWNHPFrA|`=V4K9i1KI`BrPPDh z#cIC+0?i98Fx{2C$C|maZ^R5#dyuvBz|+pRPqPlXomS0mIW8OX7MnPsD^@b)68|oO zYeccs%765TO+^xyen6bxXCwz&k%MfB5#=uQhWQlD{Z!g2u`tXU%tGd=nxu)rCohJ7 zw&<@^5yCNo-KTD-Y7d$P0CcMZ87B@k!!kDB?L|M(a7LzZHe{VGPlrM${b7x!4sLfh z+CT0^R~w`8Ju0sL-BE0&4@8JV`yE{=M*hCI(-pR+iK@(;INtaQW&Bgm!ifoSpDbEMSB^($kvwd?^{ubJF>ARN@voW1Tv#BB#B?V3zR^YpPf*R zqB9XV|A>qn#!tSfn|1U(&1qV&T2@zjd@V(a6p726G6d;U%$;k;HBHC7*?&LFGht`MOj5VJUwp)OcA(7E>cdh3bje zar-96(A7Nw8+?uI_H^P98dk)i?jOwRPUUEJp0Y;*f92+?2$VVYyCYQ~isl~~z|~q_ zI1d$yYE>BgOoVAYdOY_-Fuk6@;{)Z_HnC1KFbf{+j~=ZBbVx?J(gv2(#Inb)%yz?( zsu>Y9yZvt^vsY5R5zfcXwOZ}Lf5FCpQ-sOPSmg6HC8gRX!d|@yF-ljmH0d#lXwkyJ zOjFn&1D;D5eQ~a3wNAVUKwZMIXRD6!W-W=~QE{JdKw;|4~PMvDFIW_M)wmw~Q-1i>xXXxx_a)J=ESvRIV0W7MpAy z33d?^`h^F_^H8$}`bQf~spvCW53gV-T{^k{_)xMiI=eZ-v@x74*=X;*uE>75DX7R zb4E=#*0#6sk%z`!H>g{FH=`y1_}4EW?tKi!)%AMgOmyfGTfy?RX(QlyNMOa56yBDGQO5(d1oaT~(L3xW6+!4G_USf8e(k7v1rPTf zQ{sO1aY5FnjXI8uE8qq_DnoGZTT3sxlj9FbaO zpQ@|3#I{jjT-zi4Y|FB;sh~xrD|r?7w!|gsf$3xz$Zvxa8=A+Pq%l8?k_;)lM2m}= zZ`s9tYAuct18ZBix44nNzB>f!;3J)Uv!$QBLtHKpXAUQT$`vSlYXE>>t5DXakzcHkklgzsAy`Wi=R+T#K1;Y5%r-VF!>t;8E^o837Da@kTQ=|LzY~HF;YX zEGuu_?1t22t$#3F)$ON_GT z{5BQ3dY6WHl4h`A<0;;*{*nB5lGd)xNM*_OMYVV9E$O|cuvyE~{1NT@mTaUnT_`=g zCTpXfyuNi3Ve6*Gx$zn`{16?n?}28o+4g2KrUBmPn!J5`1NH`VxPva&0Q|;d;86>C z3tpaKj4AuipL~-_DdZFeAR>C`Ic}5`Qua?R8m^)Ri7&*^&N#5)m21x;3j{e20zr{A zrXJOCD}?B_!cs!kkJB0zL2eGI9eeU*pk)U_mSbXBKz5)<$kUhn?Zi&tB-@p}$#9g9 z`%;r;3o~$aO+0RN$@s{Usmvv+D#GspB=*gW`wSkoabR(%;tp9@|Fe-uARrs&6wTd7 zgjvM?8-y=qns}uGbwl z@%AW{w1f0Ew7bJK0$oChaiYoLolUTW$-j0T0LkI6pQnVPG6<1UWf**Bl!yKX2;K69R z-5A86xg>>?KuhK!EI!ktBj2xJzH62VMGV>HSymT^#e=jN%;_cZ$l)lUezmzRUJ5bZ zEiq#8u#EN2{LLBH+Mu!F#T0m8m()e7J&vSb})$QW6Nm{^}+1JVt zJMK;h%XgTv_74Jhc?<}%g?V?%*^lZ;bqo|p>)E9F9DTh*F-Rq^9(19bNJQW(u%IiJOV*6V4UHRxY{Y( z3{jt*QJ^=hr`G#6Q>9L^?DJ-H(#u~URi1UgWU2FTv<)e$<;je48ah^s^Z6qpa%a>^ zUf)W_*nM8Vq4l~rS-});!z4HVXs|POboR<7TI68B*C8a!v5O0RhK9dGVp?C?m9s>7E zUe00qx7N|tcY0&{47lZ(Zz)97)`v7RLE2ve!Dju&`5miUl_b;72V^6LbBuSP%sRFT zNa}7a7iEAnr2aSz-c1LFSN=$z_=%E35NrZL*HCohZ{cZy6){fAS0wH*pwUFuApg== zh#OUXd4Rg)?sR_Jt0HZlA`MiwMHlr{X2%!lH~ytP-iU09_e=TgQ}L3^u>0(|h(Wv? zmN|;Wp7a67ukwQ(%RlC~`;$qpEa0@+)wH4Xd9`QqvnjPn|6355_Sr${`spw+r$!u# z;$nFnNGO#dNe^B}$CpJihK)A!N=~tB@Lub1Vy{1nNRea?GceHfV3QW>d^-gi6q+db zp2U7P{h-#vNMzLAw2ibx6tw#DwlAEZnWn;WyM#eo$o~_n_h$jrg$|km1 z2N=H%i8t^TT=Qg4ZMtsNuh{zL1+Jc!(&o#W!w55ffhxE?-r~06 zSUkccdn_AEXAlGe&6wKM9O;XJt(T;ZWmBs7yyJ#?{3-w&Li-=>73}{Z5{fM_&+_O z)hTTKZ31alAyCidAZ&j4qja%#3#0p_KUh8s!x5jX;EN8kBf4wWwY)w86KYW~ZHF<9 zrN+Ym<)YZW(+*Z*1Q0*Afx|j;T+%n; z2bH??z!bW$)LKLaF%&`UE?(khdb#EshSgiya5XhErq&HuyqEIOmUXWZT7YF~dnXv= z^Cj8uKk+6uI5-4v3+EZSj-K!kikh#{lHX^<-O4@)VhllAlL3F$Fh8LkidGb~s5LAY zr>+`s5iO3mfkUAJrx_)0sz&pxPSI!2ED z&lXLdR!tWlN|TU5_gDPAMwU7$d(n!Iw>~_lTxqkwVvcd7sQF0Q%3+%>jEX1o&QYOp z*TwTTf~yAXo{cEkjsM{Q=Vn{#FCpYWTK#Ee{*F;^f%H48x#VHBh8|GrTEm#`iBH8O z>W!yVILRiq=OPSB$4l}}KNx#q(^NumA4|~`&UI#B*HT(`bXv_)7zArrvcHBg0!lDt z8=Q^gXy)O}2^#@l;z6|(4;26|`UDUU_iRj@z~;Wt`?o>V?UE6%LhN-*E0_WdBr@L< zX6h33@psF!HX5PouU9bxwa(r1!hE-Z5xfeO3(~ygFusf==&0OS%p9}gsg!N?^I3wg z%Lln~7c?KI>ze?BsmrHQnSr|0ckBPQ1bz9p|bTIIvJtnpX)`ltBz$N**-CdHcEQYEXsF!h6KhZ0F-ZJGHTS0T{)GSiZY**D^MMR9W53GQD!oeW zQwet2o}2R4R8dg7?KCzM#=CA2vFa9LH=miiji^vZ8Gl~QYT1Ic*58G%9Xka8&PwwX zvxJCLr44PT)W4ZXk9&qnbd9BrIsxIjkKoHmNh(69tC>p+&W2J~9}s*Meo}zXgOvGc zb<@l`aEZhTU~dPxJ9aDb#n#hqb(~SAzf=Z60W$Nsfv$4(lv;STBMBiNbf0Z#AIjCY z;#^~vcV1zZUsrm)fdre=a2&5ojz*KTN7cRrLNm>B>X5P>N75K(yS+u7OLaPj>1j}y ztOe%J#LyOu!OY-87pazv15a{Q>egP@m_qr=4cAPYhYlx~;(f zygq|Fsv|PbO+F+c2pZqTfPFcaB`P zuVkais&Qn*YaI5n?A~p6m7C4LD3?wQBRQlu5S$4*L^C9_{Y_`y$EQ3f?8xrJXm_ge znaHOsBD?|s>Yo}TJ04$y`<(K}XC1TVP0HUN7phlNjMybXW!YAYXl=MSR}?R1Zm6|K z`1#!03g@CEqlxa1$s~_tn(v-J@Yu`Pj5-N2ukXUz%c}9uFGc4u0|CzaKa;q5k&zLW z`>H}`=$2gRwPkVnv}!x&PZw~$dsoUDQd6blB(-sUk!&SfCS72hpTQ02at*m$z3oe` zMw>-%TKyl!!h+~{s}rlAWZ+P+C!8Ht{TtE!Rw(gotF($x_w9KOiY{Vpj(7;fz3Y|- z@jrFuDVs=$Yt<0Mevug;vEqdwtdQaODB!>c z5-FFl+1)Fj>9}eRJTHO4W?6wdNbF6Er$OC&Hkin`t$-S=|xJe1-U)+4f-?`F!dVd?1uS zRyjemqB~zYV>JyZQH&$E0!(v1T|fJevl!0Y1TiJ;B7Q1q4QgZx{v)g3NfDcx@OL#j zKpVd0Pau3-R^OrXX7uM*#PRPtz#UVi@Q16|+x?M*;O}nWK8FRUaS}x8?+0iE9wk#C z&sebA=trfR!%UGE0YEn@^y_0GTihNr7=IlgbFvh&2r!c|;`GrTqt@DXH87%VK4->> zlX=gjmY*hs8%W2+J-~XE%@}vB*zxbkxzlEQIK?GkAU8*(2VS|9I^v{i!JibsoA-CbcmPpHsotvuW}nCDO8jr*K9Qp~(@q z1vT{%L(wlqbPIvK!Ys0b zp_mm2#yaqkA+T5LDfcFZ=mGaCH!=M)Kaa@lX;?!NUHur{wfot{F^rv?EWk{2R7uM% zm?Mgu3_6?E@AV7>8zg_c5~Ycu;(1f;6%?7&68{bqdA|(kq8ujan^Yw7s^UJwe-FFl znB%<`CT5%nj>XTYeQEbQFrJcN=?M&PsCfxVrT&V;vhDt3E~yw4`CX$9_YfL6XnOc@ zrwiu_TngYQEnd3Bm$dq&>#MN50{oWepVi)RhAo^{gR>lHDeoEmpQH0dXeWX<>VJvGzZ@;dc6s%7M9GY3G9=mwVT&uV8AW=Er*QZ$T|d)7k=o#WEXPRlYo>!rBe7EVd*?$I@CL!Uy`r&|DPb0VR7k&vHwuTC({=F>o-WVkl-<84R( zZ;LG&VOg#C4I0>XQ+amr<^ZSZPw3&pP})7}v(A6T$ogx+?vq3R*VEZ}#gxWzeCFPH zN%v-&UZxpj%3Gw-^s;2oD-z1+bhwJTR!AZ$Ns*aiREpWPb}Ol|rP{SkgX9=a-KHdY z8%m9ot&rNvDKr|r%szwN=P$VD_j{i6e4lf_zwZb9?O2WcwNXTt(PyP>YO7r`nG!I| z)4lPthlyGDF`9%<$lK=8W^4-)+U_`Y8TXn=hgkgj(KG2}lgbCqvYB0~seAgI@TJsW z&Z~<@tEJw7>#fqnJsuR@>!ol)(~6^&40dw6JBsEI{5QI+l4W`twu?MC##dI5zg7X- zYti}8Q}%F_8Egu82N*iQ`jZqA8foKh1$Y0LQ5~dU2=Wij$kur%x(_fuHJc0(%jD2$ z^L<(7&Tfwg#RQo6k?12j)=Z%_^z?*f$A4f>GpAUl5SzNrnR~vg4XB!^5xJFV9g8kR z+Cou9leX%8G*&UR9JJx?UfN?O5<}Yjo1CeuK1Ngd&0U zWA{AY@T?+umg+Krz%6*Q%@R@Zr$t?RhDn2#BNXm*$rjDA)F`aL$Ucny)Obek-Pszz zpFuea?t;{IUL)`^lyyEUgRwwk6Wi|tWVPC)rN>R$^yq-DawlFV)}Tj3gv zXF z8D8dtJPDpBy*Nu_rh^;$u5p2>+)Z!Wu@OBeYip&gw5X6#3y72uBW!+EHPz&bqwsU2 zkFn#OvW1!(hY42CTSv4gdfPO=?|Z_Nh3^oBJwDL4$n@`14$^UkpedAv$xEI2aU_H{ zml;uW@~D9Pg>4yE-pRmQ|Cud^kod?{qkNW81P7`#c`N)FW&|S^Q5}!U(d?HJ8MP%= z{X!>O*fiPAKvv82y!QZcv(xLEd2?hE1!efkEY~L}AFER-(SXU@)L!U^eA7{TErfHK z;(%%R$xK@SzFf`)Rx%41aX7MsCP@y(kyK`2#KaCse#JN=#<-4aGcmp5Zm%uBwo1IU zD=jt?Y5vf8_uR1FM{Ow!0pJt7^WBu{eu0}KK-i-i@6=5_eusxQV4 z6uC99a(+QyXdaN2=eM9S0T$+pABL^xPXg-|xLVf_$h?u#cPpwmw;00Ak|b>-E4xp; zEs*u*zDO@^$(chBfA*1+=w+1aDXa@+PtY5q>G7@5=~>DJ;l{nVGuq%1qih!Rg938%pRURiSkplNeMK1?VR`$AWzv+G0_lw`|K<| zQ_GLt{^A9^a7`5-f9&qPd+n3Z-8e>fb;%_kUzk3iny@bF*TsEua8P~U2mHL?_yRGn z63peb%Wo1aOULjoJ3E&*%|VaPN^8BZvKA|{o@4v(>S~c&mAu>>@fVi__3QKH#C|I- z)$q76)Ckuc^7XRwBDv`I;Sb&}j>^mc?>SN+EKv5?!CG<>ZkqsWF0UwgLf|LLlYH$( zWf^pqWIR)P{iE+@S5x3QH5`nT?Ne~OAU^ZyASL~HT~i{ouio8&Uk-2Scj9B?mk58w zmbJaAXZp<`C-1svz;nf*Ut)X zoqBpXoZ17tKxG@)?6$XTcJg>_>~qb0PeZINC~i#}UC$ok#xNpvcT!7_7z(=qit_@DeBVO4nD%z-mVatXVJ&1s%Vc0S2i<~(#|NjKI>)-;7>KPYB@7*Ma ztg1)RGlV?PRdHiyeOk~^O>m^*>LSxISU6pAXv7-B%Jk4Jh8C8(yCqsU_x;xqI)VKjul}2(7)fTRcUa2R6<+mzerQ91&`4{v78{+@~ literal 0 HcmV?d00001 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