001: /*
002: * $Id: MarkupCache.java 4639 2006-02-26 01:44:07 -0800 (Sun, 26 Feb 2006)
003: * jdonnerstag $ $Revision: 463947 $ $Date: 2006-02-26 01:44:07 -0800 (Sun, 26 Feb
004: * 2006) $
005: *
006: * ==============================================================================
007: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
008: * use this file except in compliance with the License. You may obtain a copy of
009: * the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016: * License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package wicket.markup;
020:
021: import java.io.IOException;
022: import java.util.Iterator;
023: import java.util.Locale;
024: import java.util.Map;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import wicket.Application;
030: import wicket.MarkupContainer;
031: import wicket.WicketRuntimeException;
032: import wicket.util.concurrent.ConcurrentHashMap;
033: import wicket.util.listener.IChangeListener;
034: import wicket.util.resource.IResourceStream;
035: import wicket.util.resource.ResourceStreamNotFoundException;
036: import wicket.util.string.AppendingStringBuffer;
037: import wicket.util.watch.ModificationWatcher;
038: import wicket.util.watch.Watcher;
039:
040: /**
041: * Load markup and cache it for fast retrieval. If markup file changes, it'll be
042: * removed and subsequently reloaded when needed.
043: *
044: * @author Jonathan Locke
045: * @author Juergen Donnerstag
046: */
047: public class MarkupCache {
048: /** Log for reporting. */
049: private static final Log log = LogFactory.getLog(MarkupCache.class);
050:
051: /** Map of markup tags by class (exactly what is in the file). */
052: private final Map markupCache = new ConcurrentHashMap();
053:
054: /**
055: * Markup inheritance requires that merged markup gets re-merged either
056: * AFTER the base markup or the derived markup has been reloaded.
057: */
058: private final Watcher afterLoadListeners = new Watcher();
059:
060: /** The Wicket application */
061: private final Application application;
062:
063: /**
064: * Constructor.
065: *
066: * @param application
067: */
068: public MarkupCache(final Application application) {
069: this .application = application;
070: }
071:
072: /**
073: * Gets a fresh markup stream that contains the (immutable) markup resource
074: * for this class.
075: *
076: * @param container
077: * The container the markup should be associated with
078: * @return A stream of MarkupElement elements
079: */
080: public final MarkupStream getMarkupStream(
081: final MarkupContainer container) {
082: return getMarkupStream(container, true);
083: }
084:
085: /**
086: * Gets a fresh markup stream that contains the (immutable) markup resource
087: * for this class.
088: *
089: * @param container
090: * The container the markup should be associated with
091: * @param throwException
092: * If true, throw an exception, if markup could not be found
093: * @return A stream of MarkupElement elements
094: */
095: public final MarkupStream getMarkupStream(
096: final MarkupContainer container,
097: final boolean throwException) {
098: if (container == null) {
099: throw new IllegalArgumentException(
100: "Parameter 'container' must not be 'null'.");
101: }
102:
103: // Look for associated markup
104: final Markup markup = getMarkup(container, container.getClass());
105:
106: // If we found markup for this container
107: if (markup != Markup.NO_MARKUP) {
108: return new MarkupStream(markup);
109: }
110:
111: if (throwException == true) {
112: // throw exception since there is no associated markup
113: throw new MarkupNotFoundException(
114: "Markup not found. Component class: "
115: + container.getClass().getName()
116: + " Enable debug messages for wicket.util.resource to get a list of all filenames tried");
117: }
118:
119: return null;
120: }
121:
122: /**
123: * Check if container has associated markup
124: *
125: * @param container
126: * The container the markup should be associated with
127: * @return True if this markup container has associated markup
128: */
129: public final boolean hasAssociatedMarkup(
130: final MarkupContainer container) {
131: return getMarkup(container, container.getClass()) != Markup.NO_MARKUP;
132: }
133:
134: /**
135: * Gets any (immutable) markup resource for the container or any of its
136: * parent classes (markup inheritance)
137: *
138: * @param container
139: * The original requesting markup container
140: * @param clazz
141: * The class to get the associated markup for. If null, the
142: * container's class is used, but it can be a parent class of the
143: * container as well (markup inheritance)
144: * @return Markup resource
145: */
146: private final Markup getMarkup(final MarkupContainer container,
147: final Class clazz) {
148: Class containerClass = clazz;
149: if (clazz == null) {
150: containerClass = container.getClass();
151: } else {
152: if (!clazz.isAssignableFrom(container.getClass())) {
153: throw new WicketRuntimeException(
154: "Parameter clazz must be instance of container");
155: }
156: }
157:
158: // Look up markup tag list by class, locale, style and markup type
159: final CharSequence key = markupKey(container, clazz);
160: Markup markup = (Markup) markupCache.get(key);
161:
162: // If no markup in the cache
163: if (markup == null) {
164: synchronized (markupCache) {
165: markup = (Markup) markupCache.get(key);
166:
167: // If no markup is in the cache
168: if (markup == null) {
169: // Ask the container to locate its associated markup
170: final IResourceStream resourceStream = container
171: .newMarkupResourceStream(containerClass);
172:
173: // Found markup?
174: if (resourceStream != null) {
175: final MarkupResourceStream markupResource;
176: if (resourceStream instanceof MarkupResourceStream) {
177: markupResource = (MarkupResourceStream) resourceStream;
178: } else {
179: markupResource = new MarkupResourceStream(
180: resourceStream, new ContainerInfo(
181: container), containerClass);
182: }
183:
184: // load the markup and watch for changes
185: markup = loadMarkupAndWatchForChanges(
186: container, key, markupResource);
187: } else {
188: // flag markup as non-existent (as opposed to null,
189: // which might mean that it's simply not loaded into
190: // the cache)
191: markup = Markup.NO_MARKUP;
192:
193: // Save any markup list (or absence of one) for next
194: // time
195: markupCache.put(key, markup);
196: }
197: }
198: }
199: }
200: return markup;
201: }
202:
203: /**
204: * Remove the markup from the cache and trigger all associated listeners
205: *
206: * @param key
207: * The cache key
208: * @param markupResourceStream
209: * The resource stream
210: */
211: private void removeMarkup(final CharSequence key,
212: final MarkupResourceStream markupResourceStream) {
213: markupCache.remove(key);
214:
215: // trigger all listeners registered on the markup that is removed
216: afterLoadListeners.notifyListeners(markupResourceStream);
217: afterLoadListeners.remove(markupResourceStream);
218: }
219:
220: /**
221: * Remove the markup from the cache and trigger all associated listeners
222: *
223: * @since 1.2.3
224: * @param markupResourceStream
225: * The resource stream
226: */
227: public void removeMarkup(
228: final MarkupResourceStream markupResourceStream) {
229: CharSequence key = null;
230: Iterator iter = this .markupCache.entrySet().iterator();
231: while (iter.hasNext()) {
232: Map.Entry entry = (Map.Entry) iter.next();
233: if (entry.getValue() == markupResourceStream) {
234: key = (CharSequence) entry.getKey();
235: break;
236: }
237: }
238:
239: if (key != null) {
240: removeMarkup(key, markupResourceStream);
241: }
242: }
243:
244: /**
245: * Loads markup from a resource stream.
246: *
247: * @param container
248: * The original requesting markup container
249: * @param key
250: * Key under which markup should be cached
251: * @param markupResourceStream
252: * The markup resource stream to load
253: * @return The markup
254: */
255: private final Markup loadMarkup(final MarkupContainer container,
256: final CharSequence key,
257: final MarkupResourceStream markupResourceStream) {
258: try {
259: // read and parse the markup
260: Markup markup = application.getMarkupSettings()
261: .getMarkupParserFactory().newMarkupParser()
262: .readAndParse(markupResourceStream);
263:
264: // Check for markup inheritance. If it contains <wicket:extend>
265: // the two markups get merged.
266: markup = checkForMarkupInheritance(container, key, markup);
267:
268: // add the markup to the cache
269: markupCache.put(key, markup);
270:
271: // trigger all listeners registered on the markup just loaded
272: afterLoadListeners.notifyListeners(markupResourceStream);
273:
274: return markup;
275: } catch (ResourceStreamNotFoundException e) {
276: log.error("Unable to find markup from "
277: + markupResourceStream, e);
278: } catch (IOException e) {
279: log.error("Unable to read markup from "
280: + markupResourceStream, e);
281: }
282:
283: synchronized (markupCache) {
284: markupCache.remove(key);
285: afterLoadListeners.remove(markupResourceStream);
286: }
287:
288: return Markup.NO_MARKUP;
289: }
290:
291: /**
292: * Load markup from an IResourceStream and add an {@link IChangeListener}to
293: * the {@link ModificationWatcher} so that if the resource changes, we can
294: * remove it from the cache automatically and subsequently reload when
295: * needed.
296: *
297: * @param container
298: * The original requesting markup container
299: * @param key
300: * The key for the resource
301: * @param markupResourceStream
302: * The markup stream to load and begin to watch
303: * @return The markup in the stream
304: */
305: private final Markup loadMarkupAndWatchForChanges(
306: final MarkupContainer container, final CharSequence key,
307: final MarkupResourceStream markupResourceStream) {
308: // Watch file in the future
309: final ModificationWatcher watcher = application
310: .getResourceSettings().getResourceWatcher();
311: if (watcher != null) {
312: watcher.add(markupResourceStream, new IChangeListener() {
313: public void onChange() {
314: if (log.isDebugEnabled()) {
315: log.debug("Remove markup from cache: "
316: + markupResourceStream);
317: }
318:
319: // Remove the markup from the cache. It will be reloaded
320: // next time it the markup is requested.
321: removeMarkup(key, markupResourceStream);
322: watcher.remove(markupResourceStream);
323: }
324: });
325: }
326:
327: if (log.isDebugEnabled()) {
328: log.debug("Loading markup from " + markupResourceStream);
329: }
330: return loadMarkup(container, key, markupResourceStream);
331: }
332:
333: /**
334: * Construct a proper key value for the cache
335: *
336: * @param container
337: * The container requesting the markup
338: * @param clazz
339: * The clazz to get the key for
340: * @return Key that uniquely identifies any markup that might be associated
341: * with this markup container.
342: */
343: private final CharSequence markupKey(
344: final MarkupContainer container, final Class clazz) {
345: final String classname = clazz.getName();
346: final Locale locale = container.getLocale();
347: final String style = container.getStyle();
348: final String markupType = container.getMarkupType();
349:
350: final AppendingStringBuffer buffer = new AppendingStringBuffer(
351: classname.length() + 32);
352: buffer.append(classname);
353:
354: if (locale != null) {
355: boolean l = locale.getLanguage().length() != 0;
356: boolean c = locale.getCountry().length() != 0;
357: boolean v = locale.getVariant().length() != 0;
358: buffer.append(locale.getLanguage());
359: if (c || (l && v)) {
360: buffer.append('_').append(locale.getCountry()); // This may just
361: // append '_'
362: }
363: if (v && (l || c)) {
364: buffer.append('_').append(locale.getVariant());
365: }
366: }
367: if (style != null) {
368: buffer.append(style);
369: }
370:
371: buffer.append(markupType);
372: return buffer;
373: }
374:
375: /**
376: * Clear markup cache and force reload of all markup data
377: */
378: public void clear() {
379: this .afterLoadListeners.clear();
380: this .markupCache.clear();
381: }
382:
383: /**
384: * @return the number of elements currently in the cache.
385: */
386: public int size() {
387: return markupCache.size();
388: }
389:
390: /**
391: * The markup has just been loaded and now we check if markup inheritance
392: * applies, which is if <wicket:extend> is found in the markup. If yes, than
393: * load the base markups and merge the markup elements to create an updated
394: * (merged) list of markup elements.
395: *
396: * @param container
397: * The original requesting markup container
398: * @param key
399: * Key under which markup should be cached
400: * @param markup
401: * The markup to checked for inheritance
402: * @return A markup object with the the base markup elements resolved.
403: */
404: private Markup checkForMarkupInheritance(
405: final MarkupContainer container, final CharSequence key,
406: final Markup markup) {
407: // Check if markup contains <wicket:extend> which tells us that
408: // we need to read the inherited markup as well.
409: int extendIndex = requiresBaseMarkup(markup);
410: if (extendIndex == -1) {
411: // return a MarkupStream for the markup
412: return markup;
413: }
414:
415: // get the base markup
416: final Markup baseMarkup = getMarkup(container, markup
417: .getResource().getMarkupClass().getSuperclass());
418:
419: if (baseMarkup == Markup.NO_MARKUP) {
420: throw new MarkupNotFoundException(
421: "Parent markup of inherited markup not found. Component class: "
422: + markup.getResource().getContainerInfo()
423: .getContainerClass().getName()
424: + " Enable debug messages for wicket.util.resource.Resource to get a list of all filenames tried.");
425: }
426:
427: // register an after-load listener for base markup. The listener
428: // implementation will remove the derived markup which must be merged
429: // with the base markup
430: afterLoadListeners.add(baseMarkup.getResource(),
431: new IChangeListener() {
432: public void onChange() {
433: if (log.isDebugEnabled()) {
434: log
435: .debug("Remove derived markup from cache: "
436: + markup.getResource());
437: }
438: removeMarkup(key, markup.getResource());
439: }
440:
441: /**
442: * Make sure there is only one listener per derived markup
443: *
444: * @see java.lang.Object#equals(java.lang.Object)
445: */
446: public boolean equals(Object obj) {
447: return true;
448: }
449:
450: /**
451: * Make sure there is only one listener per derived markup
452: *
453: * @see java.lang.Object#hashCode()
454: */
455: public int hashCode() {
456: return key.hashCode();
457: }
458: });
459:
460: // Merge base and derived markup
461: Markup mergedMarkup = new MergedMarkup(markup, baseMarkup,
462: extendIndex);
463: return mergedMarkup;
464: }
465:
466: /**
467: * Check if markup contains <wicket:extend> which tells us that we
468: * need to read the inherited markup as well. <wicket:extend> MUST BE
469: * the first wicket tag in the markup. Skip raw markup
470: *
471: * @param markup
472: * @return == 0, if no wicket:extend was found
473: */
474: private int requiresBaseMarkup(final Markup markup) {
475: for (int i = 0; i < markup.size(); i++) {
476: MarkupElement elem = (MarkupElement) markup.get(i);
477: if (elem instanceof WicketTag) {
478: WicketTag wtag = (WicketTag) elem;
479: if (wtag.isExtendTag()) {
480: // Ok, inheritance is on and we must get the
481: // inherited markup as well.
482: return i;
483: }
484: }
485: }
486: return -1;
487: }
488: }
|