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.render; 020 021import java.io.IOException; 022import java.io.StringReader; 023import java.lang.reflect.Constructor; 024import java.util.Collection; 025import java.util.Properties; 026 027import org.apache.log4j.Logger; 028import org.apache.wiki.WikiContext; 029import org.apache.wiki.WikiEngine; 030import org.apache.wiki.api.exceptions.WikiException; 031import org.apache.wiki.event.WikiEvent; 032import org.apache.wiki.event.WikiEventListener; 033import org.apache.wiki.event.WikiEventUtils; 034import org.apache.wiki.event.WikiPageEvent; 035import org.apache.wiki.modules.InternalModule; 036import org.apache.wiki.pages.PageManager; 037import org.apache.wiki.parser.JSPWikiMarkupParser; 038import org.apache.wiki.parser.MarkupParser; 039import org.apache.wiki.parser.WikiDocument; 040import org.apache.wiki.providers.WikiPageProvider; 041import org.apache.wiki.util.ClassUtil; 042 043import net.sf.ehcache.Cache; 044import net.sf.ehcache.CacheManager; 045import net.sf.ehcache.Element; 046 047 048/** 049 * This class provides a facade towards the differing rendering routines. You should 050 * use the routines in this manager instead of the ones in WikiEngine, if you don't 051 * want the different side effects to occur - such as WikiFilters. 052 * <p> 053 * This class also manages a rendering cache, i.e. documents are stored between calls. 054 * You may control the cache by tweaking the ehcache.xml file. 055 * <p> 056 * 057 * @since 2.4 058 */ 059public class RenderingManager implements WikiEventListener, InternalModule 060{ 061 private static Logger log = Logger.getLogger( RenderingManager.class ); 062 063 private int m_cacheExpiryPeriod = 24*60*60; // This can be relatively long 064 065 private WikiEngine m_engine; 066 067 private boolean m_useCache = true; 068 069 private CacheManager m_cacheManager = CacheManager.getInstance(); 070 071 /** The capacity of the caches, if you want something else, tweak ehcache.xml. */ 072 private static final int DEFAULT_CACHESIZE = 1000; 073 private static final String VERSION_DELIMITER = "::"; 074 private static final String PROP_PARSER = "jspwiki.renderingManager.markupParser"; 075 private static final String PROP_RENDERER = "jspwiki.renderingManager.renderer"; 076 private static final String PROP_WYSIWYG_RENDERER = "jspwiki.renderingManager.renderer.wysiwyg"; 077 078 /** The name of the default renderer. */ 079 public static final String DEFAULT_PARSER = JSPWikiMarkupParser.class.getName(); 080 081 /** The name of the default renderer. */ 082 public static final String DEFAULT_RENDERER = XHTMLRenderer.class.getName(); 083 084 /** The name of the default WYSIWYG renderer. */ 085 public static final String DEFAULT_WYSIWYG_RENDERER = WysiwygEditingRenderer.class.getName(); 086 087 /** Stores the WikiDocuments that have been cached. */ 088 private Cache m_documentCache; 089 090 /** Name of the regular page cache. */ 091 public static final String DOCUMENTCACHE_NAME = "jspwiki.renderingCache"; 092 093 private Constructor< ? > m_rendererConstructor; 094 private Constructor< ? > m_rendererWysiwygConstructor; 095 private String m_markupParserClass = DEFAULT_PARSER; 096 097 /** 098 * Name of the WikiContext variable which is set to Boolean.TRUE or Boolean.FALSE 099 * depending on whether WYSIWYG is currently in effect. 100 */ 101 public static final String WYSIWYG_EDITOR_MODE = "WYSIWYG_EDITOR_MODE"; 102 103 /** 104 * Variable name which tells whether plugins should be executed or not. Value can be either 105 * {@code Boolean.TRUE} or {@code Boolean.FALSE}. While not set it's value is {@code null} 106 */ 107 public static final String VAR_EXECUTE_PLUGINS = "_PluginContent.execute"; 108 109 /** 110 * Initializes the RenderingManager. 111 * Checks for cache size settings, initializes the document cache. 112 * Looks for alternative WikiRenderers, initializes one, or the default 113 * XHTMLRenderer, for use. 114 * 115 * @param engine A WikiEngine instance. 116 * @param properties A list of properties to get parameters from. 117 * @throws WikiException If the manager could not be initialized. 118 */ 119 public void initialize( WikiEngine engine, Properties properties ) 120 throws WikiException 121 { 122 m_engine = engine; 123 124 m_markupParserClass = properties.getProperty( PROP_PARSER, DEFAULT_PARSER ); 125 if( !ClassUtil.assignable( m_markupParserClass, MarkupParser.class.getName() ) ) { 126 log.warn( m_markupParserClass + " does not subclass " + MarkupParser.class.getName() + " reverting to default markup parser." ); 127 m_markupParserClass = DEFAULT_PARSER; 128 } 129 log.info( "Using " + m_markupParserClass + " as markup parser." ); 130 131 m_useCache = "true".equals(properties.getProperty(PageManager.PROP_USECACHE)); 132 133 if (m_useCache) { 134 String documentCacheName = engine.getApplicationName() + "." + DOCUMENTCACHE_NAME; 135 136 if (m_cacheManager.cacheExists(documentCacheName)) { 137 m_documentCache = m_cacheManager.getCache(documentCacheName); 138 } else { 139 log.info("cache with name " + documentCacheName + " not found in ehcache.xml, creating it with defaults."); 140 m_documentCache = new Cache(documentCacheName, DEFAULT_CACHESIZE, false, false, m_cacheExpiryPeriod, m_cacheExpiryPeriod); 141 m_cacheManager.addCache(m_documentCache); 142 } 143 } 144 145 final String renderImplName = properties.getProperty( PROP_RENDERER, DEFAULT_RENDERER ); 146 final String renderWysiwygImplName = properties.getProperty( PROP_WYSIWYG_RENDERER, DEFAULT_WYSIWYG_RENDERER ); 147 148 final Class< ? >[] rendererParams = { WikiContext.class, WikiDocument.class }; 149 m_rendererConstructor = initRenderer( renderImplName, rendererParams ); 150 m_rendererWysiwygConstructor = initRenderer( renderWysiwygImplName, rendererParams ); 151 152 log.info( "Rendering content with " + renderImplName + "." ); 153 154 WikiEventUtils.addWikiEventListener(m_engine, WikiPageEvent.POST_SAVE_BEGIN, this); 155 } 156 157 private Constructor< ? > initRenderer( final String renderImplName, final Class< ? >[] rendererParams ) throws WikiException { 158 Constructor< ? > c = null; 159 try { 160 final Class< ? > clazz = Class.forName( renderImplName ); 161 c = clazz.getConstructor( rendererParams ); 162 } catch( final ClassNotFoundException e ) { 163 log.error( "Unable to find WikiRenderer implementation " + renderImplName ); 164 } catch( final SecurityException e ) { 165 log.error( "Unable to access the WikiRenderer(WikiContext,WikiDocument) constructor for " + renderImplName ); 166 } catch( final NoSuchMethodException e ) { 167 log.error( "Unable to locate the WikiRenderer(WikiContext,WikiDocument) constructor for " + renderImplName ); 168 } 169 if( c == null ) { 170 throw new WikiException( "Failed to get WikiRenderer '" + renderImplName + "'." ); 171 } 172 return c; 173 } 174 175 /** 176 * Returns the wiki Parser 177 * @param pagedata the page data 178 * @return A MarkupParser instance. 179 */ 180 public MarkupParser getParser( WikiContext context, String pagedata ) { 181 try { 182 return ClassUtil.getMappedObject( m_markupParserClass, context, new StringReader( pagedata ) ); 183 } catch( ReflectiveOperationException | IllegalArgumentException e ) { 184 log.error( "unable to get an instance of " + m_markupParserClass + " (" + e.getMessage() + "), returning default markup parser.", e ); 185 return new JSPWikiMarkupParser( context, new StringReader( pagedata ) ); 186 } 187 } 188 189 /** 190 * Returns a cached document, if one is found. 191 * 192 * @param context the wiki context 193 * @param pagedata the page data 194 * @return the rendered wiki document 195 * @throws IOException If rendering cannot be accomplished 196 */ 197 // FIXME: The cache management policy is not very good: deleted/changed pages should be detected better. 198 protected WikiDocument getRenderedDocument( WikiContext context, String pagedata ) throws IOException { 199 String pageid = context.getRealPage().getName() + VERSION_DELIMITER + 200 context.getRealPage().getVersion() + VERSION_DELIMITER + 201 context.getVariable( RenderingManager.VAR_EXECUTE_PLUGINS ); 202 203 if( useCache( context ) ) { 204 Element element = m_documentCache.get( pageid ); 205 if ( element != null ) { 206 WikiDocument doc = (WikiDocument) element.getObjectValue(); 207 208 // 209 // This check is needed in case the different filters have actually changed the page data. 210 // FIXME: Figure out a faster method 211 if( pagedata.equals( doc.getPageData() ) ) { 212 if( log.isDebugEnabled() ) { 213 log.debug( "Using cached HTML for page " + pageid ); 214 } 215 return doc; 216 } 217 } else if( log.isDebugEnabled() ) { 218 log.debug( "Re-rendering and storing " + pageid ); 219 } 220 } 221 222 // Refresh the data content 223 // 224 try { 225 MarkupParser parser = getParser( context, pagedata ); 226 WikiDocument doc = parser.parse(); 227 doc.setPageData( pagedata ); 228 if( useCache( context ) ) { 229 m_documentCache.put( new Element( pageid, doc ) ); 230 } 231 return doc; 232 } catch( IOException ex ) { 233 log.error( "Unable to parse", ex ); 234 } 235 236 return null; 237 } 238 239 boolean useCache( WikiContext context ) { 240 return m_useCache && WikiContext.VIEW.equals( context.getRequestContext() ); 241 } 242 243 /** 244 * Simply renders a WikiDocument to a String. This version does not get the document 245 * from the cache - in fact, it does not cache the document at all. This is 246 * very useful, if you have something that you want to render outside the caching 247 * routines. Because the cache is based on full pages, and the cache keys are 248 * based on names, use this routine if you're rendering anything for yourself. 249 * 250 * @param context The WikiContext to render in 251 * @param doc A proper WikiDocument 252 * @return Rendered HTML. 253 * @throws IOException If the WikiDocument is poorly formed. 254 */ 255 public String getHTML( WikiContext context, WikiDocument doc ) throws IOException 256 { 257 final Boolean wysiwygVariable = ( Boolean )context.getVariable( WYSIWYG_EDITOR_MODE ); 258 final boolean wysiwygEditorMode; 259 if( wysiwygVariable != null ) { 260 wysiwygEditorMode = wysiwygVariable.booleanValue(); 261 } else { 262 wysiwygEditorMode = false; 263 } 264 WikiRenderer rend; 265 if( wysiwygEditorMode ) { 266 rend = getWysiwygRenderer( context, doc ); 267 } else { 268 rend = getRenderer( context, doc ); 269 } 270 271 return rend.getString(); 272 } 273 274 /** 275 * Returns a WikiRenderer instance, initialized with the given 276 * context and doc. The object is an XHTMLRenderer, unless overridden 277 * in jspwiki.properties with PROP_RENDERER. 278 * 279 * @param context The WikiContext 280 * @param doc The document to render 281 * @return A WikiRenderer for this document, or null, if no such renderer could be instantiated. 282 */ 283 public WikiRenderer getRenderer( WikiContext context, WikiDocument doc ) { 284 final Object[] params = { context, doc }; 285 return getRenderer( params, m_rendererConstructor ); 286 } 287 288 /** 289 * Returns a WikiRenderer instance meant for WYSIWYG editing, initialized with the given 290 * context and doc. The object is an WysiwygEditingRenderer, unless overridden 291 * in jspwiki.properties with PROP_WYSIWYG_RENDERER. 292 * 293 * @param context The WikiContext 294 * @param doc The document to render 295 * @return A WikiRenderer instance meant for WYSIWYG editing, for this document, or null, if 296 * no such renderer could be instantiated. 297 */ 298 public WikiRenderer getWysiwygRenderer( WikiContext context, WikiDocument doc ) { 299 final Object[] params = { context, doc }; 300 return getRenderer( params, m_rendererWysiwygConstructor ); 301 } 302 303 @SuppressWarnings("unchecked") 304 private < T extends WikiRenderer > T getRenderer( Object[] params, Constructor<?> rendererConstructor ) { 305 T rval = null; 306 307 try { 308 rval = (T)rendererConstructor.newInstance( params ); 309 } catch( final Exception e ) { 310 log.error( "Unable to create WikiRenderer", e ); 311 } 312 return rval; 313 } 314 315 /** 316 * Convenience method for rendering, using the default parser and renderer. Note that 317 * you can't use this method to do any arbitrary rendering, as the pagedata MUST 318 * be the data from the that the WikiContext refers to - this method caches the HTML 319 * internally, and will return the cached version. If the pagedata is different 320 * from what was cached, will re-render and store the pagedata into the internal cache. 321 * 322 * @param context the wiki context 323 * @param pagedata the page data 324 * @return XHTML data. 325 */ 326 public String getHTML( WikiContext context, String pagedata ) 327 { 328 try 329 { 330 WikiDocument doc = getRenderedDocument( context, pagedata ); 331 332 return getHTML( context, doc ); 333 } 334 catch( IOException e ) 335 { 336 log.error("Unable to parse",e); 337 } 338 339 return null; 340 } 341 342 /** 343 * {@inheritDoc} 344 * 345 * <p>Flushes the document cache in response to a POST_SAVE_BEGIN event. 346 * 347 * @see org.apache.wiki.event.WikiEventListener#actionPerformed(org.apache.wiki.event.WikiEvent) 348 */ 349 @Override 350 public void actionPerformed( WikiEvent event ) { 351 log.debug( "event received: " + event.toString() ); 352 if( m_useCache ) { 353 if( ( event instanceof WikiPageEvent ) && ( event.getType() == WikiPageEvent.POST_SAVE_BEGIN ) ) { 354 if( m_documentCache != null ) { 355 String pageName = ( ( WikiPageEvent ) event ).getPageName(); 356 m_documentCache.remove( pageName ); 357 Collection< String > referringPages = m_engine.getReferenceManager().findReferrers( pageName ); 358 359 // 360 // Flush also those pages that refer to this page (if an nonexistent page 361 // appears, we need to flush the HTML that refers to the now-existent page) 362 // 363 if( referringPages != null ) { 364 for( String page : referringPages ) { 365 if( log.isDebugEnabled() ) { 366 log.debug( "Flushing latest version of " + page ); 367 } 368 // as there is a new version of the page expire both plugin and pluginless versions of the old page 369 m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION + VERSION_DELIMITER + Boolean.FALSE ); 370 m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION + VERSION_DELIMITER + Boolean.TRUE ); 371 m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION + VERSION_DELIMITER + null ); 372 } 373 } 374 } 375 } 376 } 377 } 378 379}