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 java.io.IOException;
022import java.io.StringReader;
023import java.lang.reflect.Constructor;
024import java.util.Collection;
025import java.util.Properties;
026
027import org.apache.log4j.Logger;
028import org.apache.wiki.WikiContext;
029import org.apache.wiki.WikiEngine;
030import org.apache.wiki.api.exceptions.WikiException;
031import org.apache.wiki.event.WikiEvent;
032import org.apache.wiki.event.WikiEventListener;
033import org.apache.wiki.event.WikiEventUtils;
034import org.apache.wiki.event.WikiPageEvent;
035import org.apache.wiki.modules.InternalModule;
036import org.apache.wiki.pages.PageManager;
037import org.apache.wiki.parser.JSPWikiMarkupParser;
038import org.apache.wiki.parser.MarkupParser;
039import org.apache.wiki.parser.WikiDocument;
040import org.apache.wiki.providers.WikiPageProvider;
041import org.apache.wiki.util.ClassUtil;
042
043import net.sf.ehcache.Cache;
044import net.sf.ehcache.CacheManager;
045import net.sf.ehcache.Element;
046
047
048/**
049 *  This class provides a facade towards the differing rendering routines.  You should
050 *  use the routines in this manager instead of the ones in WikiEngine, if you don't
051 *  want the different side effects to occur - such as WikiFilters.
052 *  <p>
053 *  This class also manages a rendering cache, i.e. documents are stored between calls.
054 *  You may control the cache by tweaking the ehcache.xml file.
055 *  <p>
056 *
057 *  @since  2.4
058 */
059public class RenderingManager implements WikiEventListener, InternalModule
060{
061    private static Logger log = Logger.getLogger( RenderingManager.class );
062
063    private int        m_cacheExpiryPeriod = 24*60*60; // This can be relatively long
064
065    private WikiEngine m_engine;
066
067    private boolean m_useCache = true;
068
069    private CacheManager m_cacheManager = CacheManager.getInstance();
070
071    /** The capacity of the caches, if you want something else, tweak ehcache.xml. */
072    private static final int    DEFAULT_CACHESIZE     = 1000;
073    private static final String VERSION_DELIMITER     = "::";
074    private static final String PROP_PARSER           = "jspwiki.renderingManager.markupParser";
075    private static final String PROP_RENDERER         = "jspwiki.renderingManager.renderer";
076    private static final String PROP_WYSIWYG_RENDERER = "jspwiki.renderingManager.renderer.wysiwyg";
077
078    /** The name of the default renderer. */
079    public static final String DEFAULT_PARSER = JSPWikiMarkupParser.class.getName();
080
081    /** The name of the default renderer. */
082    public  static final String DEFAULT_RENDERER  = XHTMLRenderer.class.getName();
083
084    /** The name of the default WYSIWYG renderer. */
085    public  static final String DEFAULT_WYSIWYG_RENDERER  = WysiwygEditingRenderer.class.getName();
086
087    /** Stores the WikiDocuments that have been cached. */
088    private Cache m_documentCache;
089
090    /** Name of the regular page cache. */
091    public static final String DOCUMENTCACHE_NAME = "jspwiki.renderingCache";
092
093    private Constructor< ? > m_rendererConstructor;
094    private Constructor< ? > m_rendererWysiwygConstructor;
095    private String m_markupParserClass = DEFAULT_PARSER;
096
097    /**
098     *  Name of the WikiContext variable which is set to Boolean.TRUE or Boolean.FALSE
099     *  depending on whether WYSIWYG is currently in effect.
100     */
101    public static final String WYSIWYG_EDITOR_MODE = "WYSIWYG_EDITOR_MODE";
102
103    /**
104     *  Variable name which tells whether plugins should be executed or not.  Value can be either
105     *  {@code Boolean.TRUE} or {@code Boolean.FALSE}. While not set it's value is {@code null}
106     */
107    public static final String VAR_EXECUTE_PLUGINS = "_PluginContent.execute";
108
109    /**
110     *  Initializes the RenderingManager.
111     *  Checks for cache size settings, initializes the document cache.
112     *  Looks for alternative WikiRenderers, initializes one, or the default
113     *  XHTMLRenderer, for use.
114     *
115     *  @param engine A WikiEngine instance.
116     *  @param properties A list of properties to get parameters from.
117     *  @throws WikiException If the manager could not be initialized.
118     */
119    public void initialize( WikiEngine engine, Properties properties )
120        throws WikiException
121    {
122        m_engine = engine;
123
124        m_markupParserClass = properties.getProperty( PROP_PARSER, DEFAULT_PARSER );
125        if( !ClassUtil.assignable( m_markupParserClass, MarkupParser.class.getName() ) ) {
126            log.warn( m_markupParserClass + " does not subclass " + MarkupParser.class.getName() + " reverting to default markup parser." );
127            m_markupParserClass = DEFAULT_PARSER;
128        }
129        log.info( "Using " + m_markupParserClass + " as markup parser." );
130
131        m_useCache = "true".equals(properties.getProperty(PageManager.PROP_USECACHE));
132
133        if (m_useCache) {
134            String documentCacheName = engine.getApplicationName() + "." + DOCUMENTCACHE_NAME;
135
136            if (m_cacheManager.cacheExists(documentCacheName)) {
137                m_documentCache = m_cacheManager.getCache(documentCacheName);
138            } else {
139                log.info("cache with name " + documentCacheName + " not found in ehcache.xml, creating it with defaults.");
140                m_documentCache = new Cache(documentCacheName, DEFAULT_CACHESIZE, false, false, m_cacheExpiryPeriod, m_cacheExpiryPeriod);
141                m_cacheManager.addCache(m_documentCache);
142            }
143        }
144
145        final String renderImplName = properties.getProperty( PROP_RENDERER, DEFAULT_RENDERER );
146        final String renderWysiwygImplName = properties.getProperty( PROP_WYSIWYG_RENDERER, DEFAULT_WYSIWYG_RENDERER );
147
148        final Class< ? >[] rendererParams = { WikiContext.class, WikiDocument.class };
149        m_rendererConstructor = initRenderer( renderImplName, rendererParams );
150        m_rendererWysiwygConstructor = initRenderer( renderWysiwygImplName, rendererParams );
151
152        log.info( "Rendering content with " + renderImplName + "." );
153
154        WikiEventUtils.addWikiEventListener(m_engine, WikiPageEvent.POST_SAVE_BEGIN, this);
155    }
156
157    private Constructor< ? > initRenderer( final String renderImplName, final Class< ? >[] rendererParams ) throws WikiException {
158        Constructor< ? > c = null;
159        try {
160            final Class< ? > clazz = Class.forName( renderImplName );
161            c = clazz.getConstructor( rendererParams );
162        } catch( final ClassNotFoundException e ) {
163            log.error( "Unable to find WikiRenderer implementation " + renderImplName );
164        } catch( final SecurityException e ) {
165            log.error( "Unable to access the WikiRenderer(WikiContext,WikiDocument) constructor for "  + renderImplName );
166        } catch( final NoSuchMethodException e ) {
167            log.error( "Unable to locate the WikiRenderer(WikiContext,WikiDocument) constructor for "  + renderImplName );
168        }
169        if( c == null ) {
170            throw new WikiException( "Failed to get WikiRenderer '" + renderImplName + "'." );
171        }
172        return c;
173    }
174
175    /**
176     *  Returns the wiki Parser
177     *  @param pagedata the page data
178     *  @return A MarkupParser instance.
179     */
180    public MarkupParser getParser( WikiContext context, String pagedata ) {
181        try {
182            return ClassUtil.getMappedObject( m_markupParserClass, context, new StringReader( pagedata ) );
183        } catch( ReflectiveOperationException | IllegalArgumentException e ) {
184            log.error( "unable to get an instance of " + m_markupParserClass + " (" + e.getMessage() + "), returning default markup parser.", e );
185            return new JSPWikiMarkupParser( context, new StringReader( pagedata ) );
186        }
187    }
188
189    /**
190     *  Returns a cached document, if one is found.
191     *
192     * @param context the wiki context
193     * @param pagedata the page data
194     * @return the rendered wiki document
195     * @throws IOException If rendering cannot be accomplished
196     */
197    // FIXME: The cache management policy is not very good: deleted/changed pages should be detected better.
198    protected WikiDocument getRenderedDocument( WikiContext context, String pagedata ) throws IOException {
199        String pageid = context.getRealPage().getName() + VERSION_DELIMITER +
200                        context.getRealPage().getVersion() + VERSION_DELIMITER +
201                        context.getVariable( RenderingManager.VAR_EXECUTE_PLUGINS );
202
203        if( useCache( context ) ) {
204            Element element = m_documentCache.get( pageid );
205            if ( element != null ) {
206                WikiDocument doc = (WikiDocument) element.getObjectValue();
207
208                //
209                //  This check is needed in case the different filters have actually changed the page data.
210                //  FIXME: Figure out a faster method
211                if( pagedata.equals( doc.getPageData() ) ) {
212                    if( log.isDebugEnabled() ) {
213                        log.debug( "Using cached HTML for page " + pageid );
214                    }
215                    return doc;
216                }
217            } else if( log.isDebugEnabled() ) {
218                log.debug( "Re-rendering and storing " + pageid );
219            }
220        }
221
222        //  Refresh the data content
223        //
224        try {
225            MarkupParser parser = getParser( context, pagedata );
226            WikiDocument doc = parser.parse();
227            doc.setPageData( pagedata );
228            if( useCache( context ) ) {
229                m_documentCache.put( new Element( pageid, doc ) );
230            }
231            return doc;
232        } catch( IOException ex ) {
233            log.error( "Unable to parse", ex );
234        }
235
236        return null;
237    }
238
239    boolean useCache( WikiContext context ) {
240        return m_useCache && WikiContext.VIEW.equals( context.getRequestContext() );
241    }
242
243    /**
244     *  Simply renders a WikiDocument to a String.  This version does not get the document
245     *  from the cache - in fact, it does not cache the document at all.  This is
246     *  very useful, if you have something that you want to render outside the caching
247     *  routines.  Because the cache is based on full pages, and the cache keys are
248     *  based on names, use this routine if you're rendering anything for yourself.
249     *
250     *  @param context The WikiContext to render in
251     *  @param doc A proper WikiDocument
252     *  @return Rendered HTML.
253     *  @throws IOException If the WikiDocument is poorly formed.
254     */
255    public String getHTML( WikiContext context, WikiDocument doc ) throws IOException
256    {
257        final Boolean wysiwygVariable = ( Boolean )context.getVariable( WYSIWYG_EDITOR_MODE );
258        final boolean wysiwygEditorMode;
259        if( wysiwygVariable != null ) {
260            wysiwygEditorMode = wysiwygVariable.booleanValue();
261        } else {
262            wysiwygEditorMode = false;
263        }
264        WikiRenderer rend;
265        if( wysiwygEditorMode ) {
266            rend = getWysiwygRenderer( context, doc );
267        } else {
268            rend = getRenderer( context, doc );
269        }
270
271        return rend.getString();
272    }
273
274    /**
275     * Returns a WikiRenderer instance, initialized with the given
276     * context and doc. The object is an XHTMLRenderer, unless overridden
277     * in jspwiki.properties with PROP_RENDERER.
278     *
279     * @param context The WikiContext
280     * @param doc The document to render
281     * @return A WikiRenderer for this document, or null, if no such renderer could be instantiated.
282     */
283    public WikiRenderer getRenderer( WikiContext context, WikiDocument doc ) {
284        final Object[] params = { context, doc };
285        return getRenderer( params, m_rendererConstructor );
286    }
287
288    /**
289     * Returns a WikiRenderer instance meant for WYSIWYG editing, initialized with the given
290     * context and doc. The object is an WysiwygEditingRenderer, unless overridden
291     * in jspwiki.properties with PROP_WYSIWYG_RENDERER.
292     *
293     * @param context The WikiContext
294     * @param doc The document to render
295     * @return A WikiRenderer instance meant for WYSIWYG editing, for this document, or null, if
296     *         no such renderer could be instantiated.
297     */
298    public WikiRenderer getWysiwygRenderer( WikiContext context, WikiDocument doc ) {
299        final Object[] params = { context, doc };
300        return getRenderer( params, m_rendererWysiwygConstructor );
301    }
302
303    @SuppressWarnings("unchecked")
304    private < T extends WikiRenderer > T getRenderer( Object[] params, Constructor<?> rendererConstructor ) {
305        T rval = null;
306
307        try {
308            rval = (T)rendererConstructor.newInstance( params );
309        } catch( final Exception e ) {
310            log.error( "Unable to create WikiRenderer", e );
311        }
312        return rval;
313    }
314
315    /**
316     *   Convenience method for rendering, using the default parser and renderer.  Note that
317     *   you can't use this method to do any arbitrary rendering, as the pagedata MUST
318     *   be the data from the that the WikiContext refers to - this method caches the HTML
319     *   internally, and will return the cached version.  If the pagedata is different
320     *   from what was cached, will re-render and store the pagedata into the internal cache.
321     *
322     *   @param context the wiki context
323     *   @param pagedata the page data
324     *   @return XHTML data.
325     */
326    public String getHTML( WikiContext context, String pagedata )
327    {
328        try
329        {
330            WikiDocument doc = getRenderedDocument( context, pagedata );
331
332            return getHTML( context, doc );
333        }
334        catch( IOException e )
335        {
336            log.error("Unable to parse",e);
337        }
338
339        return null;
340    }
341
342    /**
343     * {@inheritDoc}
344     *
345     * <p>Flushes the document cache in response to a POST_SAVE_BEGIN event.
346     *
347     * @see org.apache.wiki.event.WikiEventListener#actionPerformed(org.apache.wiki.event.WikiEvent)
348     */
349    @Override
350    public void actionPerformed( WikiEvent event ) {
351        log.debug( "event received: " + event.toString() );
352        if( m_useCache ) {
353            if( ( event instanceof WikiPageEvent ) && ( event.getType() == WikiPageEvent.POST_SAVE_BEGIN ) ) {
354                if( m_documentCache != null ) {
355                    String pageName = ( ( WikiPageEvent ) event ).getPageName();
356                    m_documentCache.remove( pageName );
357                    Collection< String > referringPages = m_engine.getReferenceManager().findReferrers( pageName );
358
359                    //
360                    //  Flush also those pages that refer to this page (if an nonexistent page
361                    //  appears, we need to flush the HTML that refers to the now-existent page)
362                    //
363                    if( referringPages != null ) {
364                        for( String page : referringPages ) {
365                            if( log.isDebugEnabled() ) {
366                                log.debug( "Flushing latest version of " + page );
367                            }
368                            // as there is a new version of the page expire both plugin and pluginless versions of the old page
369                            m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION  + VERSION_DELIMITER + Boolean.FALSE );
370                            m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION  + VERSION_DELIMITER + Boolean.TRUE );
371                            m_documentCache.remove( page + VERSION_DELIMITER + WikiPageProvider.LATEST_VERSION  + VERSION_DELIMITER + null );
372                        }
373                    }
374                }
375            }
376        }
377    }
378
379}