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     */
019    package org.apache.wiki.providers;
020    
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.util.*;
024    
025    import net.sf.ehcache.Cache;
026    import net.sf.ehcache.CacheManager;
027    import net.sf.ehcache.Element;
028    
029    import org.apache.log4j.Logger;
030    import org.apache.wiki.*;
031    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
032    import org.apache.wiki.api.exceptions.ProviderException;
033    import org.apache.wiki.attachment.Attachment;
034    import org.apache.wiki.attachment.AttachmentManager;
035    import org.apache.wiki.search.QueryItem;
036    import org.apache.wiki.util.ClassUtil;
037    import 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.
049    public 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    }