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}