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.providers;
020
021import org.apache.logging.log4j.LogManager;
022import org.apache.logging.log4j.Logger;
023import org.apache.wiki.api.core.Context;
024import org.apache.wiki.api.core.Engine;
025import org.apache.wiki.api.core.Page;
026import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
027import org.apache.wiki.api.exceptions.ProviderException;
028import org.apache.wiki.api.providers.PageProvider;
029import org.apache.wiki.api.search.QueryItem;
030import org.apache.wiki.api.search.SearchResult;
031import org.apache.wiki.api.spi.Wiki;
032import org.apache.wiki.cache.CacheInfo;
033import org.apache.wiki.cache.CachingManager;
034import org.apache.wiki.pages.PageManager;
035import org.apache.wiki.parser.MarkupParser;
036import org.apache.wiki.render.RenderingManager;
037import org.apache.wiki.util.ClassUtil;
038import org.apache.wiki.util.TextUtil;
039
040import java.io.IOException;
041import java.util.Collection;
042import java.util.Date;
043import java.util.List;
044import java.util.NoSuchElementException;
045import java.util.Properties;
046import java.util.TreeSet;
047
048
049/**
050 *  Provides a caching page provider.  This class rests on top of a real provider class and provides a cache to speed things up.  Only
051 *  if the cache copy of the page text has expired, we fetch it from the provider.
052 *  <p>
053 *  This class does not detect if someone has modified the page externally, not through JSPWiki routines.
054 *  <p>
055 *  Heavily based on ideas by Chris Brooking.
056 *  <p>
057 *  Since 2.10 uses the Ehcache library.
058 *
059 *  @since 1.6.4
060 */
061public class CachingProvider implements PageProvider {
062
063    private static final Logger LOG = LogManager.getLogger( CachingProvider.class );
064
065    private CachingManager cachingManager;
066    private PageProvider m_provider;
067    private Engine m_engine;
068
069    // FIXME: This MUST be cached somehow.
070    private boolean m_gotall;
071
072    /**
073     *  {@inheritDoc}
074     */
075    @Override
076    public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException {
077        LOG.debug( "Initing CachingProvider" );
078
079        // engine is used for getting the search engine
080        m_engine = engine;
081        cachingManager = m_engine.getManager( CachingManager.class );
082
083        //  Find and initialize real provider.
084        final String classname;
085        try {
086            classname = TextUtil.getRequiredProperty( properties, PageManager.PROP_PAGEPROVIDER );
087        } catch( final NoSuchElementException e ) {
088            throw new NoRequiredPropertyException( e.getMessage(), PageManager.PROP_PAGEPROVIDER );
089        }
090
091        try {
092            m_provider = ClassUtil.buildInstance( "org.apache.wiki.providers", classname );
093            LOG.debug( "Initializing real provider class {}", m_provider );
094            m_provider.initialize( engine, properties );
095        } catch( final ReflectiveOperationException e ) {
096            LOG.error( "Unable to instantiate provider class {}", classname, e );
097            throw new IllegalArgumentException( "illegal provider class", e );
098        }
099    }
100
101    private Page getPageInfoFromCache( final String name ) throws ProviderException {
102        // Sanity check; seems to occur sometimes
103        if( name == null ) {
104            return null;
105        }
106        return cachingManager.get( CachingManager.CACHE_PAGES, name, () -> m_provider.getPageInfo( name, PageProvider.LATEST_VERSION ) );
107    }
108
109
110    /**
111     *  {@inheritDoc}
112     */
113    @Override
114    public boolean pageExists( final String pageName, final int version ) {
115        if( pageName == null ) {
116            return false;
117        }
118
119        final Page p;
120        try {
121            p = getPageInfoFromCache( pageName );
122        } catch( final ProviderException e ) {
123            LOG.info( "Provider failed while trying to check if page exists: {}", pageName );
124            return false;
125        }
126
127        if( p != null ) {
128            final int latestVersion = p.getVersion();
129            if( version == latestVersion || version == LATEST_VERSION ) {
130                return true;
131            }
132
133            return m_provider.pageExists( pageName, version );
134        }
135
136        try {
137            return getPageInfo( pageName, version ) != null;
138        } catch( final ProviderException e ) {
139        }
140
141        return false;
142    }
143
144    /**
145     *  {@inheritDoc}
146     */
147    @Override
148    public boolean pageExists( final String pageName ) {
149        if( pageName == null ) {
150            return false;
151        }
152
153        final Page p;
154        try {
155            p = getPageInfoFromCache( pageName );
156        } catch( final ProviderException e ) {
157            LOG.info( "Provider failed while trying to check if page exists: {}", pageName );
158            return false;
159        }
160
161        //  A null item means that the page either does not exist, or has not yet been cached; a non-null means that the page does exist.
162        if( p != null ) {
163            return true;
164        }
165
166        //  If we have a list of all pages in memory, then any page not in the cache must be non-existent.
167        if( m_gotall ) {
168            return false;
169        }
170
171        //  We could add the page to the cache here as well, but in order to understand whether that is a good thing or not we would
172        //  need to analyze the JSPWiki calling patterns extensively.  Presumably it would be a good thing if pageExists() is called
173        //  many times before the first getPageText() is called, and the whole page is cached.
174        return m_provider.pageExists( pageName );
175    }
176
177    /**
178     *  {@inheritDoc}
179     */
180    @Override
181    public String getPageText( final String pageName, final int version ) throws ProviderException {
182        if( pageName == null ) {
183            return null;
184        }
185
186        final String result;
187        if( version == PageProvider.LATEST_VERSION ) {
188            result = getTextFromCache( pageName );
189        } else {
190            final Page p = getPageInfoFromCache( pageName );
191
192            //  Or is this the latest version fetched by version number?
193            if( p != null && p.getVersion() == version ) {
194                result = getTextFromCache( pageName );
195            } else {
196                result = m_provider.getPageText( pageName, version );
197            }
198        }
199
200        return result;
201    }
202
203    private String getTextFromCache( final String pageName ) throws ProviderException {
204        if( pageName == null ) {
205            return null;
206        }
207
208        return cachingManager.get( CachingManager.CACHE_PAGES_TEXT, pageName, () -> {
209            if( pageExists( pageName ) ) {
210                return m_provider.getPageText( pageName, PageProvider.LATEST_VERSION );
211            }
212            return null;
213        } );
214    }
215
216    /**
217     *  {@inheritDoc}
218     */
219    @Override
220    public void putPageText( final Page page, final String text ) throws ProviderException {
221        synchronized( this ) {
222            m_provider.putPageText( page, text );
223            page.setLastModified( new Date() );
224
225            // Refresh caches properly
226            cachingManager.remove( CachingManager.CACHE_PAGES, page.getName() );
227            cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, page.getName() );
228            cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, page.getName() );
229
230            getPageInfoFromCache( page.getName() );
231        }
232    }
233
234    /**
235     *  {@inheritDoc}
236     */
237    @Override
238    public Collection< Page > getAllPages() throws ProviderException {
239        final Collection< Page > all;
240        if ( !m_gotall ) {
241            all = m_provider.getAllPages();
242            // Make sure that all pages are in the cache.
243            synchronized( this ) {
244                for( final Page p : all ) {
245                    cachingManager.put( CachingManager.CACHE_PAGES,  p.getName(), p );
246                }
247                m_gotall = true;
248            }
249        } else {
250            final List< String > keys = cachingManager.keys( CachingManager.CACHE_PAGES );
251            all = new TreeSet<>();
252            for( final String key : keys ) {
253                final Page cachedPage = cachingManager.get( CachingManager.CACHE_PAGES, key, () -> null );
254                if( cachedPage != null ) {
255                    all.add( cachedPage );
256                }
257            }
258        }
259
260        if( cachingManager.enabled( CachingManager.CACHE_PAGES )
261                && all.size() >= cachingManager.info( CachingManager.CACHE_PAGES ).getMaxElementsAllowed() ) {
262            LOG.warn( "seems {} can't hold all pages from your page repository, " +
263                    "so we're delegating on the underlying provider instead. Please consider increasing " +
264                    "your cache sizes on the ehcache configuration file to avoid this behaviour", CachingManager.CACHE_PAGES );
265            return m_provider.getAllPages();
266        }
267
268        return all;
269    }
270
271    /**
272     *  {@inheritDoc}
273     */
274    @Override
275    public Collection< Page > getAllChangedSince( final Date date ) {
276        return m_provider.getAllChangedSince( date );
277    }
278
279    /**
280     *  {@inheritDoc}
281     */
282    @Override
283    public int getPageCount() throws ProviderException {
284        return m_provider.getPageCount();
285    }
286
287    /**
288     *  {@inheritDoc}
289     */
290    @Override
291    public Collection< SearchResult > findPages( final QueryItem[] query ) {
292        //  If the provider is a fast searcher, then just pass this request through.
293        return m_provider.findPages( query );
294        // FIXME: Does not implement fast searching
295    }
296
297    //  FIXME: Kludge: make sure that the page is also parsed and it gets all the necessary variables.
298    private void refreshMetadata( final Page page ) {
299        if( page != null && !page.hasMetadata() ) {
300            final RenderingManager mgr = m_engine.getManager( RenderingManager.class );
301            try {
302                final String data = m_provider.getPageText( page.getName(), page.getVersion() );
303                final Context ctx = Wiki.context().create( m_engine, page );
304                final MarkupParser parser = mgr.getParser( ctx, data );
305
306                parser.parse();
307            } catch( final Exception ex ) {
308                LOG.debug( "Failed to retrieve variables for wikipage {}", page );
309            }
310        }
311    }
312
313    /**
314     *  {@inheritDoc}
315     */
316    @Override
317    public Page getPageInfo( final String pageName, final int version ) throws ProviderException {
318        final Page page;
319        final Page cached = getPageInfoFromCache( pageName );
320        final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE;
321        if( version == PageProvider.LATEST_VERSION || version == latestcached ) {
322            page = cached;
323        } else {
324            // We do not cache old versions.
325            page = m_provider.getPageInfo( pageName, version );
326        }
327        refreshMetadata( page );
328        return page;
329    }
330
331    /**
332     *  {@inheritDoc}
333     */
334    @Override
335    public List< Page > getVersionHistory( final String pageName) throws ProviderException {
336        if( pageName == null ) {
337            return null;
338        }
339        return cachingManager.get( CachingManager.CACHE_PAGES_HISTORY, pageName, () -> m_provider.getVersionHistory( pageName ) );
340    }
341
342    /**
343     * Gets the provider class name, and cache statistics (misscount and hitcount of page cache and history cache).
344     *
345     * @return A plain string with all the above-mentioned values.
346     */
347    @Override
348    public synchronized String getProviderInfo() {
349        final CacheInfo pageCacheInfo = cachingManager.info( CachingManager.CACHE_PAGES );
350        final CacheInfo pageHistoryCacheInfo = cachingManager.info( CachingManager.CACHE_PAGES_HISTORY );
351        return "Real provider: " + m_provider.getClass().getName()+
352                ". Page cache hits: " + pageCacheInfo.getHits() +
353                ". Page cache misses: " + pageCacheInfo.getMisses() +
354                ". History cache hits: " + pageHistoryCacheInfo.getHits() +
355                ". History cache misses: " + pageHistoryCacheInfo.getMisses();
356    }
357
358    /**
359     *  {@inheritDoc}
360     */
361    @Override
362    public void deleteVersion( final String pageName, final int version ) throws ProviderException {
363        //  Luckily, this is such a rare operation it is okay to synchronize against the whole thing.
364        synchronized( this ) {
365            final Page cached = getPageInfoFromCache( pageName );
366            final int latestcached = ( cached != null ) ? cached.getVersion() : Integer.MIN_VALUE;
367
368            //  If we have this version cached, remove from cache.
369            if( version == PageProvider.LATEST_VERSION || version == latestcached ) {
370                cachingManager.remove( CachingManager.CACHE_PAGES, pageName );
371                cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, pageName );
372            }
373
374            m_provider.deleteVersion( pageName, version );
375            cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, pageName );
376        }
377    }
378
379    /**
380     *  {@inheritDoc}
381     */
382    @Override
383    public void deletePage( final String pageName ) throws ProviderException {
384        //  See note in deleteVersion().
385        synchronized( this ) {
386            cachingManager.put( CachingManager.CACHE_PAGES, pageName, null );
387            cachingManager.put( CachingManager.CACHE_PAGES_TEXT, pageName, null );
388            cachingManager.put( CachingManager.CACHE_PAGES_HISTORY, pageName, null );
389            m_provider.deletePage( pageName );
390        }
391    }
392
393    /**
394     *  {@inheritDoc}
395     */
396    @Override
397    public void movePage( final String from, final String to ) throws ProviderException {
398        m_provider.movePage( from, to );
399        synchronized( this ) {
400            // Clear any cached version of the old page and new page
401            cachingManager.remove( CachingManager.CACHE_PAGES, from );
402            cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, from );
403            cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, from );
404            LOG.debug( "Removing to page {} from cache", to );
405            cachingManager.remove( CachingManager.CACHE_PAGES, to );
406            cachingManager.remove( CachingManager.CACHE_PAGES_TEXT, to );
407            cachingManager.remove( CachingManager.CACHE_PAGES_HISTORY, to );
408        }
409    }
410
411    /**
412     *  Returns the actual used provider.
413     *
414     *  @since 2.0
415     *  @return The real provider.
416     */
417    public PageProvider getRealProvider() {
418        return m_provider;
419    }
420
421}