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