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 * <wiki:IncludeResources type="script"/> 051 * </pre> 052 * Any code that requires special resources must register a resource request with the TemplateManager. For example: 053 * <pre> 054 * <wiki:RequestResource type="script" path="scripts/custom.js" /> 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}