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 c, String name )
244    {
245        for( Iterator i = c.iterator(); i.hasNext(); )
246        {
247            Attachment att = (Attachment) i.next();
248
249            if( name.equals( att.getFileName() ) )
250            {
251                return att;
252            }
253        }
254
255        return null;
256    }
257
258    /**
259     *  Refreshes the cache content and updates counters.
260     *
261     *  @return The newly fetched object from the provider.
262     */
263    private Collection<Attachment> refresh( WikiPage page ) throws ProviderException
264    {
265        Collection<Attachment> c = m_provider.listAttachments( page );
266        m_cache.put(new Element(page.getName(), c));
267
268        return c;
269    }
270
271    /**
272     * {@inheritDoc}
273     */
274    public Attachment getAttachmentInfo(WikiPage page, String name, int version) throws ProviderException {
275        if (log.isDebugEnabled()) {
276            log.debug("Getting attachments for " + page + ", name=" + name + ", version=" + version);
277        }
278
279        //
280        //  We don't cache previous versions
281        //
282        if (version != WikiProvider.LATEST_VERSION) {
283            log.debug("...we don't cache old versions");
284            return m_provider.getAttachmentInfo(page, name, version);
285        }
286
287        Collection<Attachment> c = null;
288        Element element =   m_cache.get(page.getName());
289
290        if (element == null) {
291            log.debug(page.getName() + " wasn't in the cache");
292            c = refresh(page);
293
294            if (c == null) return null; // No such attachment
295        } else {
296            log.debug(page.getName() + " FOUND in the cache");
297            c = (Collection<Attachment>) element.getObjectValue();
298        }
299
300        return findAttachmentFromCollection(c, name);
301    }
302
303    /**
304     * {@inheritDoc}
305     */
306    public List getVersionHistory( Attachment att )
307    {
308        return m_provider.getVersionHistory( att );
309    }
310
311    /**
312     * {@inheritDoc}
313     */
314    public void deleteVersion( Attachment att ) throws ProviderException
315    {
316        // This isn't strictly speaking correct, but it does not really matter
317        m_cache.remove(att.getParentName());
318        m_provider.deleteVersion( att );
319    }
320
321    /**
322     * {@inheritDoc}
323     */
324    public void deleteAttachment( Attachment att ) throws ProviderException
325    {
326        m_cache.remove(att.getParentName());
327        m_attCache.remove(att.getName());
328        m_provider.deleteAttachment( att );
329    }
330
331
332    /**
333     * Gets the provider class name, and cache statistics (misscount and,hitcount of the attachment cache).
334     *
335     * @return A plain string with all the above mentioned values.
336     */
337    public synchronized String getProviderInfo()
338    {
339        return "Real provider: "+m_provider.getClass().getName()+
340                ".  Cache misses: "+m_cacheMisses+
341                ".  Cache hits: "+m_cacheHits;
342    }
343
344    /**
345     *  Returns the WikiAttachmentProvider that this caching provider delegates to.
346     * 
347     *  @return The real provider underneath this one.
348     */
349    public WikiAttachmentProvider getRealProvider()
350    {
351        return m_provider;
352    }
353    
354    /**
355     * {@inheritDoc}
356     */
357    public void moveAttachmentsForPage( String oldParent, String newParent ) throws ProviderException
358    {
359        m_provider.moveAttachmentsForPage(oldParent, newParent);
360        m_cache.remove(newParent);
361        m_cache.remove(oldParent);
362        
363        //
364        //  This is a kludge to make sure that the pages are removed
365        //  from the other cache as well.
366        //
367        String checkName = oldParent + "/";
368        
369        Collection<String> names = m_cache.getKeysWithExpiryCheck();
370        for( String name : names )
371        {
372            if( name.startsWith( checkName ) )
373            {
374                m_attCache.remove(name);
375            }
376        }
377    }
378
379}