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