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 }