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 org.apache.commons.lang3.time.StopWatch; 022import org.apache.logging.log4j.LogManager; 023import org.apache.logging.log4j.Logger; 024import org.apache.wiki.StringTransmutator; 025import org.apache.wiki.api.core.Attachment; 026import org.apache.wiki.api.core.Context; 027import org.apache.wiki.api.core.ContextEnum; 028import org.apache.wiki.api.core.Engine; 029import org.apache.wiki.api.core.Page; 030import org.apache.wiki.api.exceptions.FilterException; 031import org.apache.wiki.api.exceptions.ProviderException; 032import org.apache.wiki.api.exceptions.WikiException; 033import org.apache.wiki.api.providers.PageProvider; 034import org.apache.wiki.api.spi.Wiki; 035import org.apache.wiki.attachment.AttachmentManager; 036import org.apache.wiki.cache.CachingManager; 037import org.apache.wiki.event.WikiEvent; 038import org.apache.wiki.event.WikiEventListener; 039import org.apache.wiki.event.WikiEventManager; 040import org.apache.wiki.event.WikiPageEvent; 041import org.apache.wiki.filters.FilterManager; 042import org.apache.wiki.pages.PageManager; 043import org.apache.wiki.parser.JSPWikiMarkupParser; 044import org.apache.wiki.parser.MarkupParser; 045import org.apache.wiki.parser.WikiDocument; 046import org.apache.wiki.references.ReferenceManager; 047import org.apache.wiki.util.ClassUtil; 048import org.apache.wiki.util.TextUtil; 049import org.apache.wiki.variables.VariableManager; 050 051import java.io.IOException; 052import java.io.StringReader; 053import java.lang.reflect.Constructor; 054import java.util.Collection; 055import java.util.Properties; 056 057 058/** 059 * This class provides a facade towards the differing rendering routines. You should use the routines in this manager 060 * instead of the ones in Engine, if you don't want the different side effects to occur - such as WikiFilters. 061 * <p> 062 * This class also delegates to a rendering cache, i.e. documents are stored between calls. You may control the cache by 063 * tweaking the ehcache configuration file. 064 * <p> 065 * 066 * @since 2.4 067 */ 068public class DefaultRenderingManager implements RenderingManager { 069 070 private static final Logger LOG = LogManager.getLogger( DefaultRenderingManager.class ); 071 private static final String VERSION_DELIMITER = "::"; 072 /** The name of the default renderer. */ 073 private static final String DEFAULT_PARSER = JSPWikiMarkupParser.class.getName(); 074 /** The name of the default renderer. */ 075 private static final String DEFAULT_RENDERER = XHTMLRenderer.class.getName(); 076 /** The name of the default WYSIWYG renderer. */ 077 private static final String DEFAULT_WYSIWYG_RENDERER = WysiwygEditingRenderer.class.getName(); 078 079 private Engine m_engine; 080 private CachingManager cachingManager; 081 082 /** If true, all titles will be cleaned. */ 083 private boolean m_beautifyTitle; 084 085 private Constructor< ? > m_rendererConstructor; 086 private Constructor< ? > m_rendererWysiwygConstructor; 087 private String m_markupParserClass = DEFAULT_PARSER; 088 089 /** 090 * {@inheritDoc} 091 * 092 * Checks for cache size settings, initializes the document cache. Looks for alternative WikiRenderers, initializes one, or the 093 * default XHTMLRenderer, for use. 094 */ 095 @Override 096 public void initialize( final Engine engine, final Properties properties ) throws WikiException { 097 m_engine = engine; 098 cachingManager = m_engine.getManager( CachingManager.class ); 099 m_markupParserClass = properties.getProperty( PROP_PARSER, DEFAULT_PARSER ); 100 if( !ClassUtil.assignable( m_markupParserClass, MarkupParser.class.getName() ) ) { 101 LOG.warn( "{} does not subclass {} reverting to default markup parser.", m_markupParserClass, MarkupParser.class.getName() ); 102 m_markupParserClass = DEFAULT_PARSER; 103 } 104 LOG.info( "Using {} as markup parser.", m_markupParserClass ); 105 106 m_beautifyTitle = TextUtil.getBooleanProperty( properties, PROP_BEAUTIFYTITLE, m_beautifyTitle ); 107 final String renderImplName = properties.getProperty( PROP_RENDERER, DEFAULT_RENDERER ); 108 final String renderWysiwygImplName = properties.getProperty( PROP_WYSIWYG_RENDERER, DEFAULT_WYSIWYG_RENDERER ); 109 110 final Class< ? >[] rendererParams = { Context.class, WikiDocument.class }; 111 m_rendererConstructor = initRenderer( renderImplName, rendererParams ); 112 m_rendererWysiwygConstructor = initRenderer( renderWysiwygImplName, rendererParams ); 113 114 LOG.info( "Rendering content with {}.", renderImplName ); 115 116 WikiEventManager.addWikiEventListener( m_engine.getManager( FilterManager.class ),this ); 117 } 118 119 private Constructor< ? > initRenderer( final String renderImplName, final Class< ? >[] rendererParams ) throws WikiException { 120 Constructor< ? > c = null; 121 try { 122 final Class< ? > clazz = Class.forName( renderImplName ); 123 c = clazz.getConstructor( rendererParams ); 124 } catch( final ClassNotFoundException e ) { 125 LOG.error( "Unable to find WikiRenderer implementation {}", renderImplName ); 126 } catch( final SecurityException e ) { 127 LOG.error( "Unable to access the WikiRenderer(WikiContext,WikiDocument) constructor for {}", renderImplName ); 128 } catch( final NoSuchMethodException e ) { 129 LOG.error( "Unable to locate the WikiRenderer(WikiContext,WikiDocument) constructor for {}", renderImplName ); 130 } 131 if( c == null ) { 132 throw new WikiException( "Failed to get WikiRenderer '" + renderImplName + "'." ); 133 } 134 return c; 135 } 136 137 /** 138 * {@inheritDoc} 139 */ 140 @Override 141 public String beautifyTitle( final String title ) { 142 if( m_beautifyTitle ) { 143 try { 144 final Attachment att = m_engine.getManager( AttachmentManager.class ).getAttachmentInfo( title ); 145 if( att == null ) { 146 return TextUtil.beautifyString( title ); 147 } 148 149 final String parent = TextUtil.beautifyString( att.getParentName() ); 150 return parent + "/" + att.getFileName(); 151 } catch( final ProviderException e ) { 152 return title; 153 } 154 } 155 156 return title; 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override 163 public String beautifyTitleNoBreak( final String title ) { 164 if( m_beautifyTitle ) { 165 return TextUtil.beautifyString( title, " " ); 166 } 167 168 return title; 169 } 170 171 /** 172 * {@inheritDoc} 173 */ 174 @Override 175 public MarkupParser getParser( final Context context, final String pagedata ) { 176 try { 177 return ClassUtil.getMappedObject( m_markupParserClass, context, new StringReader( pagedata ) ); 178 } catch( final ReflectiveOperationException | IllegalArgumentException e ) { 179 LOG.error( "unable to get an instance of {} ({}), returning default markup parser.", m_markupParserClass, e.getMessage(), e ); 180 return new JSPWikiMarkupParser( context, new StringReader( pagedata ) ); 181 } 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 @Override 188 // FIXME: The cache management policy is not very good: deleted/changed pages should be detected better. 189 public WikiDocument getRenderedDocument( final Context context, final String pagedata ) { 190 final String pageid = context.getRealPage().getName() + VERSION_DELIMITER + 191 context.getRealPage().getVersion() + VERSION_DELIMITER + 192 context.getVariable( Context.VAR_EXECUTE_PLUGINS ); 193 194 if( useCache( context ) ) { 195 final WikiDocument doc = cachingManager.get( CachingManager.CACHE_DOCUMENTS, pageid, () -> null ); 196 if ( doc != null ) { 197 // This check is needed in case the different filters have actually changed the page data. 198 // FIXME: Figure out a faster method 199 if( pagedata.equals( doc.getPageData() ) ) { 200 LOG.debug( "Using cached HTML for page {}", pageid ); 201 return doc; 202 } 203 } else { 204 LOG.debug( "Re-rendering and storing {}", pageid ); 205 } 206 } 207 208 // Refresh the data content 209 try { 210 final MarkupParser parser = getParser( context, pagedata ); 211 final WikiDocument doc = parser.parse(); 212 doc.setPageData( pagedata ); 213 if( useCache( context ) ) { 214 cachingManager.put( CachingManager.CACHE_DOCUMENTS, pageid, doc ); 215 } 216 return doc; 217 } catch( final IOException ex ) { 218 LOG.error( "Unable to parse", ex ); 219 } 220 221 return null; 222 } 223 224 boolean useCache( final Context context ) { 225 return cachingManager.enabled( CachingManager.CACHE_DOCUMENTS ) 226 && ContextEnum.PAGE_VIEW.getRequestContext().equals( context.getRequestContext() ); 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 @Override 233 public String getHTML( final Context context, final WikiDocument doc ) throws IOException { 234 final Boolean wysiwygVariable = context.getVariable( Context.VAR_WYSIWYG_EDITOR_MODE ); 235 final boolean wysiwygEditorMode; 236 if( wysiwygVariable != null ) { 237 wysiwygEditorMode = wysiwygVariable; 238 } else { 239 wysiwygEditorMode = false; 240 } 241 final WikiRenderer rend; 242 if( wysiwygEditorMode ) { 243 rend = getWysiwygRenderer( context, doc ); 244 } else { 245 rend = getRenderer( context, doc ); 246 } 247 248 return rend.getString(); 249 } 250 251 /** 252 * {@inheritDoc} 253 */ 254 @Override 255 public String getHTML( final Context context, final Page page ) { 256 final String pagedata = m_engine.getManager( PageManager.class ).getPureText( page.getName(), page.getVersion() ); 257 return textToHTML( context, pagedata ); 258 } 259 260 /** 261 * Returns the converted HTML of the page's specific version. The version must be a positive integer, otherwise the current 262 * version is returned. 263 * 264 * @param pagename WikiName of the page to convert. 265 * @param version Version number to fetch 266 * @return HTML-rendered page text. 267 */ 268 @Override 269 public String getHTML( final String pagename, final int version ) { 270 final Page page = m_engine.getManager( PageManager.class ).getPage( pagename, version ); 271 final Context context = Wiki.context().create( m_engine, page ); 272 context.setRequestContext( ContextEnum.PAGE_NONE.getRequestContext() ); 273 return getHTML( context, page ); 274 } 275 276 /** 277 * {@inheritDoc} 278 */ 279 @Override 280 public String textToHTML( final Context context, String pagedata ) { 281 String result = ""; 282 283 final boolean runFilters = "true".equals( m_engine.getManager( VariableManager.class ).getValue( context,VariableManager.VAR_RUNFILTERS,"true" ) ); 284 285 final StopWatch sw = new StopWatch(); 286 sw.start(); 287 try { 288 if( runFilters ) { 289 pagedata = m_engine.getManager( FilterManager.class ).doPreTranslateFiltering( context, pagedata ); 290 } 291 292 result = getHTML( context, pagedata ); 293 294 if( runFilters ) { 295 result = m_engine.getManager( FilterManager.class ).doPostTranslateFiltering( context, result ); 296 } 297 } catch( final FilterException e ) { 298 LOG.error( "page filter threw exception: ", e ); 299 // FIXME: Don't yet know what to do 300 } 301 sw.stop(); 302 LOG.debug( "Page {} rendered, took {}", context.getRealPage().getName(), sw ); 303 304 return result; 305 } 306 307 /** 308 * {@inheritDoc} 309 */ 310 @Override 311 public String textToHTML( final Context context, 312 String pagedata, 313 final StringTransmutator localLinkHook, 314 final StringTransmutator extLinkHook, 315 final StringTransmutator attLinkHook, 316 final boolean parseAccessRules, 317 final boolean justParse ) { 318 String result = ""; 319 320 if( pagedata == null ) { 321 LOG.error( "NULL pagedata to textToHTML()" ); 322 return null; 323 } 324 325 final boolean runFilters = "true".equals( m_engine.getManager( VariableManager.class ).getValue( context, VariableManager.VAR_RUNFILTERS,"true" ) ); 326 327 try { 328 final StopWatch sw = new StopWatch(); 329 sw.start(); 330 331 if( runFilters && m_engine.getManager( FilterManager.class ) != null ) { 332 pagedata = m_engine.getManager( FilterManager.class ).doPreTranslateFiltering( context, pagedata ); 333 } 334 335 final MarkupParser mp = getParser( context, pagedata ); 336 mp.addLocalLinkHook( localLinkHook ); 337 mp.addExternalLinkHook( extLinkHook ); 338 mp.addAttachmentLinkHook( attLinkHook ); 339 340 if( !parseAccessRules ) { 341 mp.disableAccessRules(); 342 } 343 344 final WikiDocument doc = mp.parse(); 345 // In some cases it's better just to parse, not to render 346 if( !justParse ) { 347 result = getHTML( context, doc ); 348 if( runFilters && m_engine.getManager( FilterManager.class ) != null ) { 349 result = m_engine.getManager( FilterManager.class ).doPostTranslateFiltering( context, result ); 350 } 351 } 352 353 sw.stop(); 354 355 LOG.debug( "Page {} rendered, took {}", context.getRealPage().getName(), sw ); 356 } catch( final IOException e ) { 357 LOG.error( "Failed to scan page data: ", e ); 358 } catch( final FilterException e ) { 359 LOG.error( "page filter threw exception: ", e ); 360 // FIXME: Don't yet know what to do 361 } 362 363 return result; 364 } 365 366 /** 367 * {@inheritDoc} 368 */ 369 @Override 370 public WikiRenderer getRenderer( final Context context, final WikiDocument doc ) { 371 final Object[] params = { context, doc }; 372 return getRenderer( params, m_rendererConstructor ); 373 } 374 375 /** 376 * {@inheritDoc} 377 */ 378 @Override 379 public WikiRenderer getWysiwygRenderer( final Context context, final WikiDocument doc ) { 380 final Object[] params = { context, doc }; 381 return getRenderer( params, m_rendererWysiwygConstructor ); 382 } 383 384 @SuppressWarnings("unchecked") 385 private < T extends WikiRenderer > T getRenderer( final Object[] params, final Constructor<?> rendererConstructor ) { 386 try { 387 return ( T )rendererConstructor.newInstance( params ); 388 } catch( final Exception e ) { 389 LOG.error( "Unable to create WikiRenderer", e ); 390 } 391 return null; 392 } 393 394 /** 395 * {@inheritDoc} 396 * 397 * <p>Flushes the document cache in response to a POST_SAVE_BEGIN event. 398 * 399 * @see WikiEventListener#actionPerformed(WikiEvent) 400 */ 401 @Override 402 public void actionPerformed( final WikiEvent event ) { 403 LOG.debug( "event received: {}", event.toString() ); 404 if( isBeginningAWikiPagePostSaveEventAndDocumentCacheIsEnabled( event ) ) { 405 final String pageName = ( ( WikiPageEvent ) event ).getPageName(); 406 cachingManager.remove( CachingManager.CACHE_DOCUMENTS, pageName ); 407 final Collection< String > referringPages = m_engine.getManager( ReferenceManager.class ).findReferrers( pageName ); 408 409 // Flush also those pages that refer to this page (if a nonexistent page 410 // appears, we need to flush the HTML that refers to the now-existent page) 411 if( referringPages != null ) { 412 for( final String page : referringPages ) { 413 LOG.debug( "Flushing latest version of {}", page ); 414 // as there is a new version of the page expire both plugin and pluginless versions of the old page 415 cachingManager.remove( CachingManager.CACHE_DOCUMENTS, page + VERSION_DELIMITER + PageProvider.LATEST_VERSION + VERSION_DELIMITER + Boolean.FALSE ); 416 cachingManager.remove( CachingManager.CACHE_DOCUMENTS, page + VERSION_DELIMITER + PageProvider.LATEST_VERSION + VERSION_DELIMITER + Boolean.TRUE ); 417 cachingManager.remove( CachingManager.CACHE_DOCUMENTS, page + VERSION_DELIMITER + PageProvider.LATEST_VERSION + VERSION_DELIMITER + null ); 418 } 419 } 420 } 421 } 422 423 boolean isBeginningAWikiPagePostSaveEventAndDocumentCacheIsEnabled( final WikiEvent event ) { 424 return event instanceof WikiPageEvent 425 && event.getType() == WikiPageEvent.POST_SAVE_BEGIN 426 && cachingManager.enabled( CachingManager.CACHE_DOCUMENTS ); 427 } 428 429}