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