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