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