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