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        }