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