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