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