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