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