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 net.sf.ehcache.Cache;
022import net.sf.ehcache.CacheManager;
023import net.sf.ehcache.Element;
024import org.apache.logging.log4j.LogManager;
025import org.apache.logging.log4j.Logger;
026import org.apache.wiki.api.core.Attachment;
027import org.apache.wiki.api.core.Engine;
028import org.apache.wiki.api.core.Page;
029import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
030import org.apache.wiki.api.exceptions.ProviderException;
031import org.apache.wiki.api.providers.AttachmentProvider;
032import org.apache.wiki.api.providers.WikiProvider;
033import org.apache.wiki.api.search.QueryItem;
034import org.apache.wiki.attachment.AttachmentManager;
035import org.apache.wiki.util.ClassUtil;
036import org.apache.wiki.util.TextUtil;
037
038import java.io.IOException;
039import java.io.InputStream;
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.Date;
043import java.util.List;
044import java.util.NoSuchElementException;
045import java.util.Properties;
046
047
048/**
049 * Provides a caching attachment provider.  This class rests on top of a real provider class and provides a cache to speed things up.
050 * Only the Attachment objects are cached; the actual attachment contents are fetched always from the provider.
051 *
052 *  @since 2.1.64.
053 */
054//        EntryRefreshPolicy for that.
055public class CachingAttachmentProvider implements AttachmentProvider {
056
057    private static final Logger log = LogManager.getLogger(CachingAttachmentProvider.class);
058
059    private AttachmentProvider m_provider;
060
061    private final CacheManager m_cacheManager = CacheManager.getInstance();
062
063    /** Default cache capacity for now. */
064    public static final int m_capacity = 1000;
065
066    /** The cache contains Collection objects which contain Attachment objects. The key is the parent wiki page name (String). */
067    private Cache m_cache;
068
069    /** Name of the attachment cache. */
070    public static final String ATTCACHE_NAME = "jspwiki.attachmentsCache";
071    /** Name of the attachment cache. */
072    public static final String ATTCOLLCACHE_NAME = "jspwiki.attachmentCollectionsCache";
073
074    /**
075     * This cache contains Attachment objects and is keyed by attachment name.
076     * This provides for quickly giving recently changed attachments (for the RecentChanges plugin)
077     */
078    private Cache m_attCache;
079
080    private final long m_cacheMisses = 0;
081    private final long m_cacheHits = 0;
082
083    /** The extension to append to directory names to denote an attachment directory. */
084    public static final String DIR_EXTENSION   = "-att";
085
086
087    private boolean m_gotall;
088
089    /**
090     * {@inheritDoc}
091     */
092    @Override
093    public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException {
094        log.info( "Initing CachingAttachmentProvider" );
095        final String attCollCacheName = engine.getApplicationName() + "." + ATTCOLLCACHE_NAME;
096        if( m_cacheManager.cacheExists( attCollCacheName ) ) {
097            m_cache = m_cacheManager.getCache( attCollCacheName );
098        } else {
099            m_cache = new Cache( attCollCacheName, m_capacity, false, false, 0, 0 );
100            m_cacheManager.addCache( m_cache );
101        }
102
103        //
104        // cache for the individual Attachment objects, attachment name is key, the Attachment object is the cached object
105        //
106        final String attCacheName = engine.getApplicationName() + "." + ATTCACHE_NAME;
107        if( m_cacheManager.cacheExists( attCacheName ) ) {
108            m_attCache = m_cacheManager.getCache( attCacheName );
109        } else {
110            m_attCache = new Cache( attCacheName, m_capacity, false, false, 0, 0 );
111            m_cacheManager.addCache( m_attCache );
112        }
113        //
114        //  Find and initialize real provider.
115        //
116        final String classname;
117        try {
118            classname = TextUtil.getRequiredProperty( properties, AttachmentManager.PROP_PROVIDER );
119        } catch( final NoSuchElementException e ) {
120            throw new NoRequiredPropertyException( e.getMessage(), AttachmentManager.PROP_PROVIDER );
121        }
122
123        try {
124            final Class< ? > providerclass = ClassUtil.findClass( "org.apache.wiki.providers", classname );
125            m_provider = ( AttachmentProvider )providerclass.newInstance();
126
127            log.debug( "Initializing real provider class " + m_provider );
128            m_provider.initialize( engine, properties );
129        } catch( final ClassNotFoundException e ) {
130            log.error( "Unable to locate provider class " + classname, e );
131            throw new IllegalArgumentException( "no provider class", e );
132        } catch( final InstantiationException e ) {
133            log.error( "Unable to create provider class " + classname, e );
134            throw new IllegalArgumentException( "faulty provider class", e );
135        } catch( final IllegalAccessException e ) {
136            log.error( "Illegal access to provider class " + classname, e );
137            throw new IllegalArgumentException( "illegal provider class", e );
138        }
139    }
140
141    /**
142     * {@inheritDoc}
143     */
144    @Override
145    public void putAttachmentData( final Attachment att, final InputStream data )
146        throws ProviderException, IOException {
147        m_provider.putAttachmentData( att, data );
148
149        m_cache.remove(att.getParentName());
150        att.setLastModified(new Date());
151        m_attCache.put(new Element(att.getName(), att));
152    }
153
154    /**
155     * {@inheritDoc}
156     */
157    @Override
158    public InputStream getAttachmentData( final Attachment att )
159        throws ProviderException, IOException {
160        return m_provider.getAttachmentData( att );
161    }
162
163    /**
164     * {@inheritDoc}
165     */
166    @Override
167    public List< Attachment > listAttachments( final Page page) throws ProviderException {
168        log.debug("Listing attachments for " + page);
169        final Element element = m_cache.get(page.getName());
170
171        if (element != null) {
172            @SuppressWarnings("unchecked") final List< Attachment > c = ( List< Attachment > )element.getObjectValue();
173            log.debug("LIST from cache, " + page.getName() + ", size=" + c.size());
174            return cloneCollection(c);
175        }
176
177        log.debug("list NOT in cache, " + page.getName());
178
179        return refresh(page);
180    }
181
182    private < T > List< T > cloneCollection( final Collection< T > c ) {
183        return new ArrayList<>( c );
184    }
185
186    /**
187     * {@inheritDoc}
188     */
189    @Override
190    public Collection< Attachment > findAttachments( final QueryItem[] query )
191    {
192        return m_provider.findAttachments( query );
193    }
194
195    /**
196     * {@inheritDoc}
197     */
198    @Override
199    public List<Attachment> listAllChanged( final Date timestamp ) throws ProviderException {
200        final List< Attachment > all;
201        // we do a one-time build up of the cache, after this the cache is updated for every attachment add/delete
202        if ( !m_gotall ) {
203            all = m_provider.listAllChanged(timestamp);
204
205            // Put all pages in the cache :
206            synchronized (this) {
207                for( final Attachment att : all ) {
208                    m_attCache.put( new Element( att.getName(), att ) );
209                }
210                m_gotall = true;
211            }
212        } else {
213            @SuppressWarnings("unchecked") final List< String > keys = m_attCache.getKeysWithExpiryCheck();
214            all = new ArrayList<>();
215            for ( final String key : keys) {
216                final Element element = m_attCache.get(key);
217                final Attachment cachedAttachment = ( Attachment )element.getObjectValue();
218                if (cachedAttachment != null) {
219                    all.add(cachedAttachment);
220                }
221            }
222        }
223
224        return all;
225    }
226
227    /**
228     *  Simply goes through the collection and attempts to locate the
229     *  given attachment of that name.
230     *
231     *  @return null, if no such attachment was in this collection.
232     */
233    private Attachment findAttachmentFromCollection( final Collection< Attachment > c, final String name ) {
234        for( final Attachment att : new ArrayList< >( c ) ) {
235            if( name.equals( att.getFileName() ) ) {
236                return att;
237            }
238        }
239
240        return null;
241    }
242
243    /**
244     *  Refreshes the cache content and updates counters.
245     *
246     *  @return The newly fetched object from the provider.
247     */
248    private List< Attachment > refresh( final Page page ) throws ProviderException {
249        final List< Attachment > c = m_provider.listAttachments( page );
250        m_cache.put( new Element( page.getName(), c ) );
251        return c;
252    }
253
254    /**
255     * {@inheritDoc}
256     */
257    @SuppressWarnings("unchecked")
258    @Override
259    public Attachment getAttachmentInfo( final Page page, final String name, final int version) throws ProviderException {
260        if( log.isDebugEnabled() ) {
261            log.debug( "Getting attachments for " + page + ", name=" + name + ", version=" + version );
262        }
263
264        //  We don't cache previous versions
265        if( version != WikiProvider.LATEST_VERSION ) {
266            log.debug( "...we don't cache old versions" );
267            return m_provider.getAttachmentInfo( page, name, version );
268        }
269
270        final Collection< Attachment > c;
271        final Element element = m_cache.get( page.getName() );
272
273        if( element == null ) {
274            log.debug( page.getName() + " wasn't in the cache" );
275            c = refresh( page );
276
277            if( c == null ) {
278                return null; // No such attachment
279            }
280        } else {
281            log.debug( page.getName() + " FOUND in the cache" );
282            c = ( Collection< Attachment > )element.getObjectValue();
283        }
284
285        return findAttachmentFromCollection( c, name );
286    }
287
288    /**
289     * {@inheritDoc}
290     */
291    @Override
292    public List<Attachment> getVersionHistory( final Attachment att ) {
293        return m_provider.getVersionHistory( att );
294    }
295
296    /**
297     * {@inheritDoc}
298     */
299    @Override
300    public void deleteVersion( final Attachment att ) throws ProviderException {
301        // This isn't strictly speaking correct, but it does not really matter
302        m_cache.remove( att.getParentName() );
303        m_provider.deleteVersion( att );
304    }
305
306    /**
307     * {@inheritDoc}
308     */
309    @Override
310    public void deleteAttachment( final Attachment att ) throws ProviderException {
311        m_cache.remove( att.getParentName() );
312        m_attCache.remove( att.getName() );
313        m_provider.deleteAttachment( att );
314    }
315
316    /**
317     * Gets the provider class name, and cache statistics (misscount and,hitcount of the attachment cache).
318     *
319     * @return A plain string with all the above mentioned values.
320     */
321    @Override
322    public synchronized String getProviderInfo() {
323        return "Real provider: " + m_provider.getClass().getName() +
324                ".  Cache misses: " + m_cacheMisses +
325                ".  Cache hits: " + m_cacheHits;
326    }
327
328    /**
329     *  Returns the WikiAttachmentProvider that this caching provider delegates to.
330     *
331     *  @return The real provider underneath this one.
332     */
333    public AttachmentProvider getRealProvider() {
334        return m_provider;
335    }
336
337    /**
338     * {@inheritDoc}
339     */
340    @Override
341    public void moveAttachmentsForPage( final String oldParent, final String newParent ) throws ProviderException {
342        m_provider.moveAttachmentsForPage( oldParent, newParent );
343        m_cache.remove( newParent );
344        m_cache.remove( oldParent );
345
346        // This is a kludge to make sure that the pages are removed from the other cache as well.
347        final String checkName = oldParent + "/";
348
349        @SuppressWarnings("unchecked") final List< String > names = m_cache.getKeysWithExpiryCheck();
350        for( final String name : names ) {
351            if( name.startsWith( checkName ) ) {
352                m_attCache.remove( name );
353            }
354        }
355    }
356
357}