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