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