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