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