001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029 package org.hd.d.pg2k.webSvr.util;
030
031 import java.io.IOException;
032 import java.io.PrintWriter;
033 import java.io.Writer;
034 import java.util.concurrent.atomic.AtomicBoolean;
035
036 import javax.servlet.ServletOutputStream;
037 import javax.servlet.http.HttpServletResponse;
038 import javax.servlet.http.HttpServletResponseWrapper;
039
040 import ORG.hd.d.IsDebug;
041
042 /**Allows conservative compaction of (removal of redundant characters from) an XML response.
043 * This implementation is only prepared to try to compact the text
044 * when it can be parsed unambiguously and in a light-weight way.
045 * <p>
046 * The meaning of the XML should be unchanged,
047 * just represented in fewer bytes on the wire.
048 * <p>
049 * Thus it will only attempt to filter a (char-based) Writer response,
050 * not a (byte-based) Stream response, for now.
051 * <p>
052 * We call the Writer of the underlying response
053 * to preserve the char-based nature of the text as we are processing it.
054 * <p>
055 * This implementation is not thread-safe.
056 * <p>
057 * This class does not need to be public nor derived from.
058 */
059 final class XMLCompressionServletResponseWrapper extends HttpServletResponseWrapper
060 {
061 public XMLCompressionServletResponseWrapper(final HttpServletResponse response)
062 {
063 super(response);
064 if(null == response) { throw new IllegalArgumentException(); }
065 origResponse = response;
066 }
067
068 /**Original response; never null. */
069 private final HttpServletResponse origResponse;
070
071 /**Any ServletOutputStream that has been returned, else null. */
072 private ServletOutputStream stream;
073
074 /**Any PrintWriter that has been returned, else null. */
075 private PrintWriter writer;
076
077 /**The PrintWriter from the underlying response, else null.
078 * Note that underlyingWriter is only non-null if writer is non-null,
079 * and the writers and stream cannot not be non-null.
080 */
081 private PrintWriter underlyingWriter;
082
083 /**Get a ServletOutputStream to write the content associated for this response.
084 * This will prevent us from filtering the content.
085 * <p>
086 * We get the stream from the underlying response directly.
087 */
088 @Override public ServletOutputStream getOutputStream() throws IOException
089 {
090 if(writer != null)
091 { throw new IllegalStateException("Writer already created for this response"); }
092 if(stream != null)
093 { return(stream); }
094 if(IsDebug.isDebug) { System.out.println("WARNING: XML compression disabled because stream requested."); }
095 stream = origResponse.getOutputStream();
096 return(stream);
097 }
098
099 /**If true then we are removing whitespace.
100 * We start off (after creation or reset)
101 * by removing any leading whitespace in the output,
102 * which is technically not allowed anyway in XML.
103 */
104 private final AtomicBoolean removingWhitespace = new AtomicBoolean(true);
105
106 // Inherit javadoc.
107 @Override public PrintWriter getWriter() throws IOException
108 {
109 if(writer != null)
110 { return(writer); }
111 if(stream != null)
112 { throw new IllegalStateException("Stream already created for this response"); }
113
114 // We cannot apply any encoding/compression if there is a Content-MD5 header,
115 // since it should be applied/recomputed after this compression.
116 if(origResponse.containsHeader("Content-MD5"))
117 {
118 writer = origResponse.getWriter();
119 return(writer);
120 } // Already encoded.
121
122 // Record the underlying writer.
123 underlyingWriter = origResponse.getWriter();
124
125 // We don't provide any internal buffering,
126 // but assume that it is provided by the wrapped response object.
127 final Writer out = underlyingWriter;
128
129 //if(IsDebug.isDebug) { System.out.println("INFO: XML compression on."); }
130
131 // Wrap the output in a compacting and buffering [Print]Writer instance.
132 // We choose to override methods at the Writer level for robustness
133 // (we cannot control how the internal implmentation of PrintWriter may change).
134 // We buffer our output for efficiency on the wire.
135 // The basic aim is to preserve XML semantics and linebreaks,
136 // and be fast, while removing most common redundant whitespace,
137 // so we remove whitespace after a \n until a char > 32.
138 // This also removes blank lines.
139 //
140 // Since \n should never appear in the middle of attribute values, etc,
141 // this should preserve essentially all significant XML whitespace.
142 //
143 // TODO: MAY HAVE TO MAKE SURE THAT WE HANDLE reset[Buffer]() correctly.
144 writer = new PrintWriter(new Writer(){
145
146 @Override
147 public void close() throws IOException
148 { out.close(); }
149
150 @Override
151 public void flush() throws IOException
152 { out.flush(); }
153
154 /**Write character data to the underlying stream. */
155 @Override
156 public void write(final char[] cbuf, int off, int len) throws IOException
157 {
158 synchronized(lock) // Have lock, so will use it, even though this code never gets multi-threaded.
159 {
160 // Ensure only one read and one write of volatile value is needed.
161 boolean rWS = removingWhitespace.get();
162 try
163 {
164 // We process the output characters one at a time.
165 outer: while(len > 0)
166 {
167 final char c = cbuf[off];
168 // Skip any ASCII chars <= 32 while removing whitespace.
169 if(rWS && (c <= ' ')) { --len; ++off; continue; }
170
171 // Assume that we now have a non-whitespace character
172 // or are in a context where we are not stripping whitespace,
173 // so write the next character(s) as-is.
174 rWS = false;
175
176 // For efficiency, try to write chars up to next \n in one go.
177 for(int o = off, l = len; l > 0; --l)
178 {
179 if(cbuf[o++] == '\n')
180 {
181 final int nChars = o - off;
182 out.write(cbuf, off, nChars);
183 off += nChars;
184 len -= nChars;
185 rWS = true; // Back to stripping whitespace after \n.
186 continue outer;
187 }
188 }
189
190 // No \n in tail of buffer,
191 // so write the rest of the chars out in one go.
192 out.write(cbuf, off, len);
193 break;
194 }
195 }
196 finally
197 { removingWhitespace.set(rWS); }
198 }
199 }
200 });
201 return(writer);
202 }
203
204 @Override
205 public void reset()
206 {
207 super.reset();
208 removingWhitespace.set(true); // If reset was allowed then be prepared to zap whitespace again.
209 }
210
211 @Override
212 public void resetBuffer()
213 {
214 super.resetBuffer();
215 removingWhitespace.set(true); // If reset was allowed then be prepared to zap whitespace again.
216 }
217
218 /**Prevent the Content-Length header from being set.
219 * Since we aim to compact the text,
220 * then any content length that the caller is aware of
221 * is hopefully going to be wrong (too large).
222 */
223 @Override public void setContentLength(final int length)
224 {
225 // Do not allow this value/header to be set.
226 }
227
228 /**Finish up. */
229 public void finishResponse()
230 {
231 try
232 {
233 if(writer != null) { writer.close(); }
234 else if(stream != null) { stream.close(); }
235 }
236 catch(final IOException e)
237 {
238 // Absorb error but log it.
239 e.printStackTrace();
240 }
241 }
242 }