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