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, "&nbsp;" );
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}