001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.wicket.markup;
018:
019: import java.io.IOException;
020: import java.util.Collection;
021: import java.util.Iterator;
022:
023: import org.apache.wicket.Application;
024: import org.apache.wicket.MarkupContainer;
025: import org.apache.wicket.WicketRuntimeException;
026: import org.apache.wicket.markup.loader.DefaultMarkupLoader;
027: import org.apache.wicket.markup.loader.IMarkupLoader;
028: import org.apache.wicket.settings.IMarkupSettings;
029: import org.apache.wicket.util.concurrent.ConcurrentHashMap;
030: import org.apache.wicket.util.listener.IChangeListener;
031: import org.apache.wicket.util.resource.IResourceStream;
032: import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
033: import org.apache.wicket.util.watch.IModifiable;
034: import org.apache.wicket.util.watch.ModificationWatcher;
035: import org.slf4j.Logger;
036: import org.slf4j.LoggerFactory;
037:
038: /**
039: * This is Wicket's default IMarkupCache implementation. It will load the markup
040: * and cache it for fast retrieval.
041: * <p>
042: * If the application is in development mode and a markup file changes, it'll
043: * automatically be removed from the cache and reloaded when needed.
044: * <p>
045: * MarkupCache is registered with {@link IMarkupSettings} and thus can be
046: * replaced with a subclassed version.
047: *
048: * @see IMarkupSettings
049: *
050: * @author Jonathan Locke
051: * @author Juergen Donnerstag
052: */
053: public class MarkupCache implements IMarkupCache {
054: /** Log for reporting. */
055: private static final Logger log = LoggerFactory
056: .getLogger(MarkupCache.class);
057:
058: /** Map of markup tags by class (exactly what is in the file). */
059: private final ICache markupCache;
060:
061: /** The markup cache key provider used by MarkupCache */
062: private IMarkupCacheKeyProvider markupCacheKeyProvider;
063:
064: /** The markup resource stream provider used by MarkupCache */
065: private IMarkupResourceStreamProvider markupResourceStreamProvider;
066:
067: /** The markup loader used by MarkupCache */
068: private IMarkupLoader markupLoader;
069:
070: /** The application object */
071: private final Application application;
072:
073: /**
074: * Constructor.
075: *
076: * @param application
077: */
078: public MarkupCache(Application application) {
079: this .application = application;
080:
081: this .markupCache = newCacheImplementation();
082: if (this .markupCache == null) {
083: throw new WicketRuntimeException(
084: "The map used to cache markup must not be null");
085: }
086: }
087:
088: /**
089: * @see org.apache.wicket.markup.IMarkupCache#clear()
090: */
091: public final void clear() {
092: this .markupCache.clear();
093: }
094:
095: /**
096: *
097: * @see org.apache.wicket.markup.IMarkupCache#shutdown()
098: */
099: public void shutdown() {
100: this .markupCache.shutdown();
101: }
102:
103: /**
104: * @see org.apache.wicket.markup.IMarkupCache#removeMarkup(java.lang.String)
105: */
106: public final Markup removeMarkup(final String cacheKey) {
107: if (cacheKey == null) {
108: throw new IllegalArgumentException(
109: "Parameter 'cacheKey' must not be null");
110: }
111:
112: if (log.isDebugEnabled()) {
113: log.debug("Remove from cache: cacheKey=" + cacheKey);
114: }
115:
116: // Remove the markup and any other markup which depends on it
117: // (inheritance)
118: Markup markup = (Markup) markupCache.get(cacheKey);
119: if (markup != null) {
120: markupCache.remove(cacheKey);
121:
122: // In practice markup inheritance has probably not more than 3 or 4
123: // levels. And since markup reloading is only enabled in development
124: // mode, this max 4 iterations of the outer loop shouldn't be a
125: // problem.
126: int count;
127: do {
128: count = 0;
129:
130: // If a base markup file has been removed from the cache, than
131: // the derived markup should be removed as well.
132: Iterator iter = markupCache.getKeys().iterator();
133: while (iter.hasNext()) {
134: Markup cacheMarkup = (Markup) markupCache.get(iter
135: .next());
136: MarkupResourceData resourceData = cacheMarkup
137: .getMarkupResourceData()
138: .getBaseMarkupResourceData();
139: if (resourceData != null) {
140: String baseCacheKey = resourceData
141: .getResource().getCacheKey();
142: if (markupCache.get(baseCacheKey) == null) {
143: if (log.isDebugEnabled()) {
144: log
145: .debug("Remove from cache: cacheKey="
146: + cacheMarkup
147: .getMarkupResourceData()
148: .getResource()
149: .getCacheKey());
150: }
151:
152: iter.remove();
153: count++;
154: }
155: }
156: }
157: } while (count > 0);
158:
159: // And now remove all watcher entries associated with markup
160: // resources no longer in the cache. Note that you can not use
161: // Application.get() since removeMarkup() will be call from a
162: // ModificationWatcher thread which has no associated Application.
163: final ModificationWatcher watcher = application
164: .getResourceSettings().getResourceWatcher(true);
165: if (watcher != null) {
166: Iterator iter = watcher.getEntries().iterator();
167: while (iter.hasNext()) {
168: IModifiable modifiable = (IModifiable) iter.next();
169: if (modifiable instanceof MarkupResourceStream) {
170: MarkupResourceStream resourceStream = (MarkupResourceStream) modifiable;
171: String resourceCacheKey = resourceStream
172: .getCacheKey();
173: if (markupCache.containsKey(resourceCacheKey) == false) {
174: iter.remove();
175: }
176: }
177: }
178: }
179: }
180: return markup;
181: }
182:
183: /**
184: * @see org.apache.wicket.markup.IMarkupCache#getMarkupStream(org.apache.wicket.MarkupContainer,
185: * boolean, boolean)
186: */
187: public final MarkupStream getMarkupStream(
188: final MarkupContainer container,
189: final boolean enforceReload, final boolean throwException) {
190: if (container == null) {
191: throw new IllegalArgumentException(
192: "Parameter 'container' must not be 'null'.");
193: }
194:
195: // Look for associated markup
196: final Markup markup = getMarkup(container,
197: container.getClass(), false);
198:
199: // If we found markup for this container
200: if (markup != Markup.NO_MARKUP) {
201: return new MarkupStream(markup);
202: }
203:
204: if (throwException == true) {
205: // throw exception since there is no associated markup
206: throw new MarkupNotFoundException(
207: "Markup not found. Component class: "
208: + container.getClass().getName()
209: + " Enable debug messages for org.apache.wicket.util.resource to get a list of all filenames tried");
210: }
211:
212: return null;
213: }
214:
215: /**
216: * @see org.apache.wicket.markup.IMarkupCache#hasAssociatedMarkup(org.apache.wicket.MarkupContainer)
217: */
218: public final boolean hasAssociatedMarkup(
219: final MarkupContainer container) {
220: return getMarkup(container, container.getClass(), false) != Markup.NO_MARKUP;
221: }
222:
223: /**
224: * @see org.apache.wicket.markup.IMarkupCache#size()
225: */
226: public final int size() {
227: return markupCache.size();
228: }
229:
230: /**
231: * Get a unmodifiable map which contains the cached data. The map key is of
232: * type String and the value is of type Markup.
233: *
234: * @return
235: */
236: protected final ICache getMarkupCache() {
237: return this .markupCache;
238: }
239:
240: /**
241: * THIS IS NOT PART OF WICKET'S PUBLIC API. DO NOT USE IT.
242: *
243: * I still don't like this method being part of the API but I didn't find a
244: * suitable other solution.
245: *
246: * @see org.apache.wicket.markup.IMarkupCache#getMarkup(org.apache.wicket.MarkupContainer,
247: * java.lang.Class, boolean)
248: */
249: public final Markup getMarkup(final MarkupContainer container,
250: final Class clazz, final boolean enforceReload) {
251: Class containerClass = clazz;
252: if (clazz == null) {
253: containerClass = container.getClass();
254: } else if (!clazz.isAssignableFrom(container.getClass())) {
255: throw new WicketRuntimeException(
256: "Parameter clazz must be an instance of "
257: + container.getClass().getName()
258: + ", but is a " + clazz.getName());
259: }
260:
261: // Get the cache key to be associated with the markup resource stream
262: final String cacheKey = getMarkupCacheKeyProvider(container)
263: .getCacheKey(container, clazz);
264:
265: // Is the markup already in the cache?
266: Markup markup = (enforceReload == false ? getMarkupFromCache(
267: cacheKey, container) : null);
268: if (markup == null) {
269: if (log.isDebugEnabled()) {
270: log.debug("Load markup: cacheKey=" + cacheKey);
271: }
272:
273: // Who is going to provide the markup resource stream?
274: // And ask the provider to locate the markup resource stream
275: final IResourceStream resourceStream = getMarkupResourceStreamProvider(
276: container).getMarkupResourceStream(container,
277: containerClass);
278:
279: // Found markup?
280: if (resourceStream != null) {
281: final MarkupResourceStream markupResourceStream;
282: if (resourceStream instanceof MarkupResourceStream) {
283: markupResourceStream = (MarkupResourceStream) resourceStream;
284: } else {
285: markupResourceStream = new MarkupResourceStream(
286: resourceStream,
287: new ContainerInfo(container),
288: containerClass);
289: }
290:
291: markupResourceStream.setCacheKey(cacheKey);
292:
293: // load the markup and watch for changes
294: markup = loadMarkupAndWatchForChanges(container,
295: markupResourceStream, enforceReload);
296: } else {
297: markup = onMarkupNotFound(cacheKey, container);
298: }
299: }
300: return markup;
301: }
302:
303: /**
304: * Will be called if the markup was not in the cache yet but could not be
305: * found either.
306: * <p>
307: * Subclasses may change the default implementation. E.g. they might choose
308: * not update the cache to enforce reloading of any markup not found. This
309: * might be useful in very dynamic environments.
310: *
311: * @param cacheKey
312: * @param container
313: * @return Markup.NO_MARKUP
314: */
315: protected Markup onMarkupNotFound(final String cacheKey,
316: final MarkupContainer container) {
317: if (log.isDebugEnabled()) {
318: log.debug("Markup not found: " + cacheKey);
319: }
320:
321: // flag markup as non-existent
322: return putIntoCache(cacheKey, Markup.NO_MARKUP);
323: }
324:
325: /**
326: * Put the markup into the cache if cacheKey is not null and the cache does
327: * not yet contain the cacheKey. Return the markup stored in the cache if
328: * cacheKey is present already.
329: *
330: * @param cacheKey
331: * If null, than ignore the cache
332: * @param markup
333: * @return markup The markup provided, except if the cacheKey already
334: * existed in the cache, than the markup from the cache is provided.
335: */
336: protected Markup putIntoCache(final String cacheKey, Markup markup) {
337: if (cacheKey != null) {
338: if (markupCache.containsKey(cacheKey) == false) {
339: markupCache.put(cacheKey, markup);
340: } else {
341: // We don't lock the cache while loading a markup. Thus it may
342: // happen that the very same markup gets loaded twice (the first
343: // markup being loaded, but not yet in the cache, and another
344: // request requesting the very same markup). Since markup
345: // loading in avg takes less than 100ms, it is not really an
346: // issue. For consistency reasons however, we should always use
347: // the markup loaded first which is why it gets returned.
348: markup = (Markup) markupCache.get(cacheKey);
349: }
350: }
351: return markup;
352: }
353:
354: /**
355: * Wicket's default implementation just uses the cacheKey to retrieve the
356: * markup from the cache. More sofisticated implementations may call a
357: * container method to e.g. ignore the cached markup under certain
358: * situations.
359: *
360: * @param cacheKey
361: * If null, than the cache will be ignored
362: * @param container
363: * @return null, if not found or to enforce reloading the markup
364: */
365: protected Markup getMarkupFromCache(final CharSequence cacheKey,
366: final MarkupContainer container) {
367: if (cacheKey != null) {
368: return (Markup) markupCache.get(cacheKey);
369: }
370: return null;
371: }
372:
373: /**
374: * Loads markup from a resource stream.
375: *
376: * @param container
377: * The original requesting markup container
378: * @param markupResourceStream
379: * The markup resource stream to load
380: * @param enforceReload
381: * The cache will be ignored and all, including inherited markup
382: * files, will be reloaded. Whatever is in the cache, it will be
383: * ignored
384: * @return The markup
385: */
386: private final Markup loadMarkup(final MarkupContainer container,
387: final MarkupResourceStream markupResourceStream,
388: final boolean enforceReload) {
389: String cacheKey = markupResourceStream.getCacheKey();
390: try {
391: Markup markup = getMarkupLoader().loadMarkup(container,
392: markupResourceStream, null, enforceReload);
393:
394: // add the markup to the cache.
395: return putIntoCache(cacheKey, markup);
396: } catch (ResourceStreamNotFoundException e) {
397: log.error("Unable to find markup from "
398: + markupResourceStream, e);
399: } catch (IOException e) {
400: log.error("Unable to read markup from "
401: + markupResourceStream, e);
402: }
403:
404: // In case of an error, remove the cache entry
405: if (cacheKey != null) {
406: removeMarkup(cacheKey);
407: }
408:
409: return Markup.NO_MARKUP;
410: }
411:
412: /**
413: * Load markup from an IResourceStream and add an {@link IChangeListener}to
414: * the {@link ModificationWatcher} so that if the resource changes, we can
415: * remove it from the cache automatically and subsequently reload when
416: * needed.
417: *
418: * @param container
419: * The original requesting markup container
420: * @param markupResourceStream
421: * The markup stream to load and begin to watch
422: * @param enforceReload
423: * The cache will be ignored and all, including inherited markup
424: * files, will be reloaded. Whatever is in the cache, it will be
425: * ignored
426: * @return The markup in the stream
427: */
428: private final Markup loadMarkupAndWatchForChanges(
429: final MarkupContainer container,
430: final MarkupResourceStream markupResourceStream,
431: final boolean enforceReload) {
432: final String cacheKey = markupResourceStream.getCacheKey();
433: if (cacheKey != null) {
434: // Watch file in the future
435: final ModificationWatcher watcher = Application.get()
436: .getResourceSettings().getResourceWatcher(true);
437: if (watcher != null) {
438: watcher.add(markupResourceStream,
439: new IChangeListener() {
440: public void onChange() {
441: if (log.isDebugEnabled()) {
442: log
443: .debug("Remove markup from cache: "
444: + markupResourceStream);
445: }
446:
447: // Remove the markup from the cache. It will be reloaded
448: // next time when the markup is requested.
449: watcher.remove(markupResourceStream);
450: removeMarkup(cacheKey);
451: }
452: });
453: }
454: }
455:
456: if (log.isDebugEnabled()) {
457: log.debug("Loading markup from " + markupResourceStream);
458: }
459: return loadMarkup(container, markupResourceStream,
460: enforceReload);
461: }
462:
463: /**
464: * Get the markup cache key provider to be used
465: *
466: * @param container
467: * The MarkupContainer requesting the markup resource stream
468: * @return IMarkupResourceStreamProvider
469: */
470: public IMarkupCacheKeyProvider getMarkupCacheKeyProvider(
471: final MarkupContainer container) {
472: if (container instanceof IMarkupCacheKeyProvider) {
473: return (IMarkupCacheKeyProvider) container;
474: }
475:
476: if (this .markupCacheKeyProvider == null) {
477: this .markupCacheKeyProvider = new DefaultMarkupCacheKeyProvider();
478: }
479: return this .markupCacheKeyProvider;
480: }
481:
482: /**
483: * Get the markup resource stream provider to be used
484: *
485: * @param container
486: * The MarkupContainer requesting the markup resource stream
487: * @return IMarkupResourceStreamProvider
488: */
489: protected IMarkupResourceStreamProvider getMarkupResourceStreamProvider(
490: final MarkupContainer container) {
491: if (container instanceof IMarkupResourceStreamProvider) {
492: return (IMarkupResourceStreamProvider) container;
493: }
494:
495: if (this .markupResourceStreamProvider == null) {
496: this .markupResourceStreamProvider = new DefaultMarkupResourceStreamProvider();
497: }
498: return this .markupResourceStreamProvider;
499: }
500:
501: /**
502: * In case there is a need to extend the default chain of MarkupLoaders
503: *
504: * @return MarkupLoader
505: */
506: protected IMarkupLoader getMarkupLoader() {
507: if (markupLoader == null) {
508: markupLoader = new DefaultMarkupLoader();
509: }
510: return markupLoader;
511: }
512:
513: /**
514: * Allows you to change the map implementation which will hold the cache
515: * data. By default it is a ConcurrentHashMap() in order to allow multiple
516: * thread to access the data in a secure way.
517: *
518: * @return
519: */
520: protected ICache newCacheImplementation() {
521: return new DefaultCacheImplementation();
522: }
523:
524: /**
525: * MarkupCache allows you to implement you own cache implementation. ICache
526: * is the interface the implementation must comply with.
527: *
528: * @see MarkupCache
529: */
530: public interface ICache {
531: /**
532: * Clear the cache
533: */
534: void clear();
535:
536: /**
537: * Remove an entry from the cache.
538: *
539: * @param key
540: * @return true, if found and removed
541: */
542: boolean remove(Object key);
543:
544: /**
545: * Get the cache element associated with the key
546: *
547: * @param key
548: * @return
549: */
550: Object get(Object key);
551:
552: /**
553: * Get all the keys referencing cache entries
554: *
555: * @return
556: */
557: Collection getKeys();
558:
559: /**
560: * Check if key is in the cache
561: *
562: * @param key
563: * @return
564: */
565: boolean containsKey(Object key);
566:
567: /**
568: * Get the number of cache entries
569: *
570: * @return
571: */
572: int size();
573:
574: /**
575: * Put an entry into the cache
576: *
577: * @param key The reference key to find the element
578: * @param value The element to be cached
579: */
580: void put(Object key, Object value);
581:
582: /**
583: * Cleanup and shutdown
584: */
585: void shutdown();
586: }
587:
588: /**
589: *
590: */
591: public class DefaultCacheImplementation implements ICache {
592: private static final long serialVersionUID = 1L;
593:
594: private ConcurrentHashMap cache = new ConcurrentHashMap();
595:
596: /**
597: * Construct.
598: */
599: public DefaultCacheImplementation() {
600: }
601:
602: /**
603: * @see org.apache.wicket.markup.MarkupCache.ICache#clear()
604: */
605: public void clear() {
606: this .cache.clear();
607: }
608:
609: /**
610: * @see org.apache.wicket.markup.MarkupCache.ICache#containsKey(java.lang.Object)
611: */
612: public boolean containsKey(Object key) {
613: return this .cache.containsKey(key);
614: }
615:
616: /**
617: * @see org.apache.wicket.markup.MarkupCache.ICache#get(java.lang.Object)
618: */
619: public Object get(Object key) {
620: return this .cache.get(key);
621: }
622:
623: /**
624: * @see org.apache.wicket.markup.MarkupCache.ICache#getKeys()
625: */
626: public Collection getKeys() {
627: return this .cache.keySet();
628: }
629:
630: /**
631: * @see org.apache.wicket.markup.MarkupCache.ICache#put(java.lang.Object, java.lang.Object)
632: */
633: public void put(Object key, Object value) {
634: this .cache.put(key, value);
635: }
636:
637: /**
638: * @see org.apache.wicket.markup.MarkupCache.ICache#remove(java.lang.Object)
639: */
640: public boolean remove(Object key) {
641: return this .cache.remove(key) == null;
642: }
643:
644: /**
645: * @see org.apache.wicket.markup.MarkupCache.ICache#size()
646: */
647: public int size() {
648: return this .cache.size();
649: }
650:
651: /**
652: * @see org.apache.wicket.markup.MarkupCache.ICache#shutdown()
653: */
654: public void shutdown() {
655: clear();
656: }
657: }
658: }
|