001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.ui;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.logging.log4j.LogManager;
023import org.apache.logging.log4j.Logger;
024import org.apache.logging.log4j.ThreadContext;
025import org.apache.wiki.WatchDog;
026import org.apache.wiki.api.core.Context;
027import org.apache.wiki.api.core.Engine;
028import org.apache.wiki.event.WikiEventManager;
029import org.apache.wiki.event.WikiPageEvent;
030import org.apache.wiki.url.URLConstructor;
031import org.apache.wiki.util.TextUtil;
032
033import javax.servlet.FilterChain;
034import javax.servlet.FilterConfig;
035import javax.servlet.ServletException;
036import javax.servlet.ServletOutputStream;
037import javax.servlet.ServletRequest;
038import javax.servlet.ServletResponse;
039import javax.servlet.WriteListener;
040import javax.servlet.http.HttpServletRequest;
041import javax.servlet.http.HttpServletResponse;
042import javax.servlet.http.HttpServletResponseWrapper;
043import java.io.ByteArrayOutputStream;
044import java.io.IOException;
045import java.io.OutputStreamWriter;
046import java.io.PrintWriter;
047import java.io.UnsupportedEncodingException;
048import java.nio.charset.Charset;
049
050
051/**
052 * This filter goes through the generated page response prior and places requested resources at the appropriate inclusion markers.
053 * This is done to let dynamic content (e.g. plugins, editors) include custom resources, even after the HTML head section is
054 * in fact built. This filter is typically the last filter to execute, and it <em>must</em> run after servlet or JSP code that performs
055 * redirections or sends error codes (such as access control methods).
056 * <p>
057 * Inclusion markers are placed by the IncludeResourcesTag; the default content templates (see .../templates/default/commonheader.jsp)
058 * are configured to do this. As an example, a JavaScript resource marker is added like this:
059 * <pre>
060 * &lt;wiki:IncludeResources type="script"/&gt;
061 * </pre>
062 * Any code that requires special resources must register a resource request with the TemplateManager. For example:
063 * <pre>
064 * &lt;wiki:RequestResource type="script" path="scripts/custom.js" /&gt;
065 * </pre>
066 * or programmatically,
067 * <pre>
068 * TemplateManager.addResourceRequest( context, TemplateManager.RESOURCE_SCRIPT, "scripts/customresource.js" );
069 * </pre>
070 *
071 * @see TemplateManager
072 * @see org.apache.wiki.tags.RequestResourceTag
073 */
074public class WikiJSPFilter extends WikiServletFilter {
075
076    private static final Logger LOG = LogManager.getLogger( WikiJSPFilter.class );
077    private String m_wiki_encoding;
078    private boolean useEncoding;
079
080    /** {@inheritDoc} */
081    @Override
082    public void init( final FilterConfig config ) throws ServletException {
083        super.init( config );
084        m_wiki_encoding = m_engine.getWikiProperties().getProperty( Engine.PROP_ENCODING );
085
086        useEncoding = !Boolean.parseBoolean( m_engine.getWikiProperties().getProperty( Engine.PROP_NO_FILTER_ENCODING, "false" ).trim() );
087    }
088
089    @Override
090    public void doFilter( final ServletRequest  request, final ServletResponse response, final FilterChain chain ) throws ServletException, IOException {
091        final WatchDog w = WatchDog.getCurrentWatchDog( m_engine );
092        try {
093            ThreadContext.push( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
094            w.enterState("Filtering for URL "+((HttpServletRequest)request).getRequestURI(), 90 );
095            final HttpServletResponseWrapper responseWrapper = new JSPWikiServletResponseWrapper( ( HttpServletResponse )response, m_wiki_encoding, useEncoding );
096
097            // fire PAGE_REQUESTED event
098            final String pagename = URLConstructor.parsePageFromURL( ( HttpServletRequest )request, Charset.forName( response.getCharacterEncoding() ) );
099            fireEvent( WikiPageEvent.PAGE_REQUESTED, pagename );
100            super.doFilter( request, responseWrapper, chain );
101
102            // The response is now complete. Lets replace the markers now.
103
104            // WikiContext is only available after doFilter! (That is after interpreting the jsp)
105
106            try {
107                w.enterState( "Delivering response", 30 );
108                final Context wikiContext = getWikiContext( request );
109                final String r = filter( wikiContext, responseWrapper );
110
111                if( useEncoding ) {
112                    final OutputStreamWriter out = new OutputStreamWriter( response.getOutputStream(), response.getCharacterEncoding() );
113                    out.write( r );
114                    out.flush();
115                    out.close();
116                } else {
117                    response.getWriter().write(r);
118                }
119
120                // Clean up the UI messages and loggers
121                if( wikiContext != null ) {
122                    wikiContext.getWikiSession().clearMessages();
123                }
124
125                // fire PAGE_DELIVERED event
126                fireEvent( WikiPageEvent.PAGE_DELIVERED, pagename );
127
128            } finally {
129                w.exitState();
130            }
131        } finally {
132            w.exitState();
133            ThreadContext.pop();
134            ThreadContext.remove( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
135        }
136    }
137
138    /**
139     * Goes through all types and writes the appropriate response.
140     *
141     * @param wikiContext The usual processing context
142     * @param response The source string
143     * @return The modified string with all the insertions in place.
144     */
145    private String filter( final Context wikiContext, final HttpServletResponse response ) {
146        String string = response.toString();
147
148        if( wikiContext != null ) {
149            final String[] resourceTypes = TemplateManager.getResourceTypes( wikiContext );
150            for( final String resourceType : resourceTypes ) {
151                string = insertResources( wikiContext, string, resourceType );
152            }
153
154            //  Add HTTP header Resource Requests
155            final String[] headers = TemplateManager.getResourceRequests( wikiContext, TemplateManager.RESOURCE_HTTPHEADER );
156
157            for( final String header : headers ) {
158                String key = header;
159                String value = "";
160                final int split = header.indexOf( ':' );
161                if( split > 0 && split < header.length() - 1 ) {
162                    key = header.substring( 0, split );
163                    value = header.substring( split + 1 );
164                }
165
166                response.addHeader( key.trim(), value.trim() );
167            }
168        }
169
170        return string;
171    }
172
173    /**
174     *  Inserts whatever resources were requested by any plugins or other components for this particular type.
175     *
176     *  @param wikiContext The usual processing context
177     *  @param string The source string
178     *  @param type Type identifier for insertion
179     *  @return The filtered string.
180     */
181    private String insertResources( final Context wikiContext, final String string, final String type ) {
182        if( wikiContext == null ) {
183            return string;
184        }
185
186        final String marker = TemplateManager.getMarker( wikiContext, type );
187        final int idx = string.indexOf( marker );
188        if( idx == -1 ) {
189            return string;
190        }
191
192        LOG.debug("...Inserting...");
193
194        final String[] resources = TemplateManager.getResourceRequests( wikiContext, type );
195        final StringBuilder concat = new StringBuilder( resources.length * 40 );
196
197        for( final String resource : resources ) {
198            LOG.debug( "...:::" + resource );
199            concat.append( resource );
200        }
201
202        return TextUtil.replaceString( string, idx, idx + marker.length(), concat.toString() );
203    }
204
205    /**
206     *  Simple response wrapper that just allows us to gobble through the entire
207     *  response before it's output.
208     */
209    private static class JSPWikiServletResponseWrapper extends HttpServletResponseWrapper {
210
211        final ByteArrayOutputStream m_output;
212        private final ByteArrayServletOutputStream m_servletOut;
213        private final PrintWriter m_writer;
214        private final HttpServletResponse m_response;
215        private final boolean useEncoding;
216
217        /** How large the initial buffer should be.  This should be tuned to achieve a balance in speed and memory consumption. */
218        private static final int INIT_BUFFER_SIZE = 0x8000;
219
220        public JSPWikiServletResponseWrapper( final HttpServletResponse r, final String wikiEncoding, final boolean useEncoding ) throws UnsupportedEncodingException {
221            super( r );
222            m_output = new ByteArrayOutputStream( INIT_BUFFER_SIZE );
223            m_servletOut = new ByteArrayServletOutputStream( m_output );
224            m_writer = new PrintWriter( new OutputStreamWriter( m_servletOut, wikiEncoding ), true );
225            this.useEncoding = useEncoding;
226
227            m_response = r;
228        }
229
230        /** Returns a writer for output; this wraps the internal buffer into a PrintWriter. */
231        @Override
232        public PrintWriter getWriter() {
233            return m_writer;
234        }
235
236        @Override
237        public ServletOutputStream getOutputStream() {
238            return m_servletOut;
239        }
240
241        @Override
242        public void flushBuffer() throws IOException {
243            m_writer.flush();
244            super.flushBuffer();
245        }
246
247        class ByteArrayServletOutputStream extends ServletOutputStream {
248
249            final ByteArrayOutputStream m_buffer;
250
251            public ByteArrayServletOutputStream( final ByteArrayOutputStream byteArrayOutputStream ) {
252                super();
253                m_buffer = byteArrayOutputStream;
254            }
255
256            //
257            /**{@inheritDoc} */
258            @Override
259            public void write( final int aInt ) {
260                m_buffer.write( aInt );
261            }
262
263            /**{@inheritDoc} */
264            @Override
265            public boolean isReady() {
266                return false;
267            }
268
269            /**{@inheritDoc} */
270            @Override
271            public void setWriteListener( final WriteListener writeListener ) {
272            }
273            
274        }
275
276        /** Returns whatever was written so far into the Writer. */
277        @Override
278        public String toString() {
279            try {
280                flushBuffer();
281            } catch( final IOException e ) {
282                LOG.error( e );
283                return StringUtils.EMPTY;
284            }
285
286            try {
287                if( useEncoding ) {
288                    return m_output.toString( m_response.getCharacterEncoding() );
289                }
290
291                return m_output.toString();
292            } catch( final UnsupportedEncodingException e ) {
293                LOG.error( e );
294                return StringUtils.EMPTY;
295             }
296        }
297
298    }
299
300    // events processing .......................................................
301
302    /**
303     *  Fires a WikiPageEvent of the provided type and page name
304     *  to all registered listeners of the current Engine.
305     *
306     * @see org.apache.wiki.event.WikiPageEvent
307     * @param type       the event type to be fired
308     * @param pagename   the wiki page name as a String
309     */
310    protected final void fireEvent( final int type, final String pagename ) {
311        if( WikiEventManager.isListening( m_engine ) ) {
312            WikiEventManager.fireEvent( m_engine, new WikiPageEvent( m_engine, type, pagename ) );
313        }
314    }
315
316}