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 */
017package org.apache.wicket.markup;
018
019import java.util.Collection;
020import java.util.Iterator;
021import java.util.concurrent.ConcurrentHashMap;
022
023import org.apache.wicket.Application;
024import org.apache.wicket.MarkupContainer;
025import org.apache.wicket.WicketRuntimeException;
026import org.apache.wicket.util.lang.Args;
027import org.apache.wicket.util.listener.IChangeListener;
028import org.apache.wicket.util.watch.IModifiable;
029import org.apache.wicket.util.watch.IModificationWatcher;
030import org.apache.wicket.util.watch.ModificationWatcher;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * This is Wicket's default IMarkupCache implementation. It will load the markup and cache it for
036 * fast retrieval.
037 * <p>
038 * If the application is in development mode and a markup file changes, it'll automatically be
039 * removed from the cache and reloaded when needed.
040 * <p>
041 * MarkupCache is registered with {@link MarkupFactory} which in turn is registered with
042 * {@link org.apache.wicket.settings.MarkupSettings} and thus can be replaced with a sub-classed version.
043 * 
044 * @see org.apache.wicket.settings.MarkupSettings
045 * @see MarkupFactory
046 * 
047 * @author Jonathan Locke
048 * @author Juergen Donnerstag
049 */
050public class MarkupCache implements IMarkupCache
051{
052        /** Log for reporting. */
053        private static final Logger log = LoggerFactory.getLogger(MarkupCache.class);
054
055        /** The actual cache: location => Markup */
056        private final ICache<String, Markup> markupCache;
057
058        /**
059         * Add extra indirection to the cache: key => location
060         * <p>
061         * Since ConcurrentHashMap does not allow to store null values, we are using Markup.NO_MARKUP
062         * instead.
063         */
064        private final ICache<String, String> markupKeyCache;
065
066        /** The markup cache key provider used by MarkupCache */
067        private IMarkupCacheKeyProvider markupCacheKeyProvider;
068
069        /**
070         * Note that you can not use Application.get() since removeMarkup() will be called from a
071         * ModificationWatcher thread which has no associated Application.
072         */
073        private final Application application;
074
075        /**
076         * A convenient helper to get the markup cache registered with the application.
077         * 
078         * @see Application#getMarkupSettings()
079         * @see MarkupFactory#getMarkupCache()
080         * 
081         * @return The markup cache registered with the {@link Application}
082         */
083        public static IMarkupCache get()
084        {
085                return Application.get().getMarkupSettings().getMarkupFactory().getMarkupCache();
086        }
087
088        /**
089         * Constructor.
090         */
091        protected MarkupCache()
092        {
093                application = Application.get();
094
095                markupCache = newCacheImplementation();
096                if (markupCache == null)
097                {
098                        throw new WicketRuntimeException("The map used to cache markup must not be null");
099                }
100
101                markupKeyCache = newCacheImplementation();
102        }
103
104        @Override
105        public void clear()
106        {
107                markupCache.clear();
108                markupKeyCache.clear();
109        }
110
111        @Override
112        public void shutdown()
113        {
114                markupCache.shutdown();
115                markupKeyCache.shutdown();
116        }
117
118        /**
119         * Note that this method will be called from a "cleanup" thread which might not have a thread
120         * local application.
121         */
122        @Override
123        public final IMarkupFragment removeMarkup(final String cacheKey)
124        {
125                Args.notNull(cacheKey, "cacheKey");
126
127                if (log.isDebugEnabled())
128                {
129                        log.debug("Removing from cache: " + cacheKey);
130                }
131
132                // Remove the markup from the cache
133                String locationString = markupKeyCache.get(cacheKey);
134                IMarkupFragment markup = (locationString != null ? markupCache.get(locationString) : null);
135                if (markup == null)
136                {
137                        return null;
138                }
139
140                // Found an entry: actual markup or Markup.NO_MARKUP. Null values are not possible
141                // because of ConcurrentHashMap.
142                markupCache.remove(locationString);
143
144                if (log.isDebugEnabled())
145                {
146                        log.debug("Removed from cache: " + locationString);
147                }
148
149                // If a base markup file has been removed from the cache then
150                // the derived markup should be removed as well.
151                removeMarkupWhereBaseMarkupIsNoLongerInTheCache();
152
153                // And now remove all watcher entries associated with markup
154                // resources no longer in the cache.
155
156                // Note that you can not use Application.get() since removeMarkup() will be called from a
157                // ModificationWatcher thread which has no associated Application.
158
159                IModificationWatcher watcher = application.getResourceSettings().getResourceWatcher(false);
160                if (watcher != null)
161                {
162                        Iterator<IModifiable> iter = watcher.getEntries().iterator();
163                        while (iter.hasNext())
164                        {
165                                IModifiable modifiable = iter.next();
166                                if (modifiable instanceof MarkupResourceStream)
167                                {
168                                        if (!isMarkupCached((MarkupResourceStream)modifiable))
169                                        {
170                                                iter.remove();
171
172                                                if (log.isDebugEnabled())
173                                                {
174                                                        log.debug("Removed from watcher: " + modifiable);
175                                                }
176                                        }
177                                }
178                        }
179                }
180
181                return markup;
182        }
183
184        private void removeMarkupWhereBaseMarkupIsNoLongerInTheCache()
185        {
186                // Repeat until all dependent resources have been removed (count == 0)
187                int count = 1;
188                while (count > 0)
189                {
190                        // Reset prior to next round
191                        count = 0;
192
193                        // Iterate though all entries of the cache
194                        Iterator<Markup> iter = markupCache.getValues().iterator();
195                        while (iter.hasNext())
196                        {
197                                Markup markup = iter.next();
198
199                                if ((markup != null) && (markup != Markup.NO_MARKUP))
200                                {
201                                        // Check if the markup associated with key has a base markup. And if yes, test
202                                        // if that is cached. If the base markup has been removed, than remove the
203                                        // derived markup as well.
204
205                                        MarkupResourceStream resourceStream = markup.getMarkupResourceStream();
206                                        if (resourceStream != null)
207                                        {
208                                                resourceStream = resourceStream.getBaseMarkupResourceStream();
209                                        }
210
211                                        // Is the base markup available in the cache?
212                                        if ((resourceStream != null) && !isMarkupCached(resourceStream))
213                                        {
214                                                iter.remove();
215                                                count++;
216
217                                                if (log.isDebugEnabled())
218                                                {
219                                                        log.debug("Removed derived markup from cache: " +
220                                                                markup.getMarkupResourceStream());
221                                                }
222                                        }
223                                }
224                        }
225                }
226        }
227
228        /**
229         * @param resourceStream
230         * @return True if the markup is cached
231         */
232        private boolean isMarkupCached(final MarkupResourceStream resourceStream)
233        {
234                if (resourceStream != null)
235                {
236                        String key = resourceStream.getCacheKey();
237                        if (key != null)
238                        {
239                                String locationString = markupKeyCache.get(key);
240                                if ((locationString != null) && (markupCache.get(locationString) != null))
241                                {
242                                        return true;
243                                }
244                        }
245                }
246                return false;
247        }
248
249        @Override
250        public final int size()
251        {
252                return markupCache.size();
253        }
254
255        /**
256         * Get a unmodifiable map which contains the cached data. The map key is of type String and the
257         * value is of type Markup.
258         * <p>
259         * May be used to debug or iterate the cache content.
260         * 
261         * @return cache implementation
262         */
263        public final ICache<String, Markup> getMarkupCache()
264        {
265                return markupCache;
266        }
267
268        @Override
269        public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz,
270                final boolean enforceReload)
271        {
272                Class<?> containerClass = MarkupFactory.get().getContainerClass(container, clazz);
273
274                // Get the cache key to be associated with the markup resource stream.
275                // If the cacheKey returned == null, than caching is disabled for the resource stream.
276                final String cacheKey = getMarkupCacheKeyProvider(container).getCacheKey(container,
277                        containerClass);
278
279                // Is the markup already in the cache?
280                Markup markup = null;
281                if ((enforceReload == false) && (cacheKey != null))
282                {
283                        markup = getMarkupFromCache(cacheKey, container);
284                }
285
286                // If markup not found in cache or cache disabled, than ...
287                if (markup == null)
288                {
289                        if (log.isDebugEnabled())
290                        {
291                                log.debug("Load markup: cacheKey=" + cacheKey);
292                        }
293
294                        // Get the markup resource stream for the container
295                        final MarkupResourceStream resourceStream = MarkupFactory.get()
296                                .getMarkupResourceStream(container, containerClass);
297
298                        // Found markup?
299                        if (resourceStream != null)
300                        {
301                                resourceStream.setCacheKey(cacheKey);
302
303                                // load the markup and watch for changes
304                                markup = loadMarkupAndWatchForChanges(container, resourceStream, enforceReload);
305                        }
306                        else
307                        {
308                                markup = onMarkupNotFound(cacheKey, container, Markup.NO_MARKUP);
309                        }
310                }
311
312                // NO_MARKUP should only be used inside the Cache.
313                if (markup == Markup.NO_MARKUP)
314                {
315                        markup = null;
316                }
317
318                return markup;
319        }
320
321        /**
322         * Will be called if the markup was not in the cache yet and could not be found either.
323         * <p>
324         * Subclasses may change the default implementation. E.g. they might choose not to update the
325         * cache to enforce reloading of any markup not found. This might be useful in very dynamic
326         * environments. Additionally a non-caching IResourceStreamLocator should be used.
327         * 
328         * @param cacheKey
329         * @param container
330         * @param markup
331         *            Markup.NO_MARKUP
332         * @return Same as parameter "markup"
333         * @see org.apache.wicket.settings.ResourceSettings#setResourceStreamLocator(org.apache.wicket.core.util.resource.locator.IResourceStreamLocator)
334         */
335        protected Markup onMarkupNotFound(final String cacheKey, final MarkupContainer container,
336                final Markup markup)
337        {
338                if (log.isDebugEnabled())
339                {
340                        log.debug("Markup not found: " + cacheKey);
341                }
342
343                // If cacheKey == null then caching is disabled for the component
344                if (cacheKey != null)
345                {
346                        // flag markup as non-existent
347                        markupKeyCache.put(cacheKey, cacheKey);
348                        putIntoCache(cacheKey, container, markup);
349                }
350
351                return markup;
352        }
353
354        /**
355         * Put the markup into the cache if cacheKey is not null and the cache does not yet contain the
356         * cacheKey. Return the markup stored in the cache if cacheKey is present already.
357         * 
358         * More sophisticated implementations may call a container method to e.g. cache it per container
359         * instance.
360         * 
361         * @param locationString
362         *            If {@code null} then ignore the cache
363         * @param container
364         *            The container this markup is for.
365         * @param markup
366         * @return markup The markup provided, except if the cacheKey already existed in the cache, then
367         *         the markup from the cache is provided.
368         */
369        protected Markup putIntoCache(final String locationString, final MarkupContainer container,
370                Markup markup)
371        {
372                if (locationString != null)
373                {
374                        if (markupCache.containsKey(locationString) == false)
375                        {
376                                // The default cache implementation is a ConcurrentHashMap. Thus neither the key nor
377                                // the value can be null.
378                                if (markup == null)
379                                {
380                                        markup = Markup.NO_MARKUP;
381                                }
382
383                                markupCache.put(locationString, markup);
384                        }
385                        else
386                        {
387                                // We don't lock the cache while loading a markup. Thus it may
388                                // happen that the very same markup gets loaded twice (the first
389                                // markup being loaded, but not yet in the cache, and another
390                                // request requesting the very same markup). Since markup
391                                // loading in avg takes less than 100ms, it is not really an
392                                // issue. For consistency reasons however, we should always use
393                                // the markup loaded first which is why it gets returned.
394                                markup = markupCache.get(locationString);
395                        }
396                }
397                return markup;
398        }
399
400        /**
401         * Wicket's default implementation just uses the cacheKey to retrieve the markup from the cache.
402         * More sophisticated implementations may call a container method to e.g. ignore the cached
403         * markup under certain situations.
404         * 
405         * @param cacheKey
406         *            If null, than the cache will be ignored
407         * @param container
408         * @return null, if not found or to enforce reloading the markup
409         */
410        protected Markup getMarkupFromCache(final String cacheKey, final MarkupContainer container)
411        {
412                if (cacheKey != null)
413                {
414                        String locationString = markupKeyCache.get(cacheKey);
415                        if (locationString != null)
416                        {
417                                return markupCache.get(locationString);
418                        }
419                }
420                return null;
421        }
422
423        /**
424         * Loads markup from a resource stream.
425         * 
426         * @param container
427         *            The original requesting markup container
428         * @param markupResourceStream
429         *            The markup resource stream to load
430         * @param enforceReload
431         *            The cache will be ignored and all, including inherited markup files, will be
432         *            reloaded. Whatever is in the cache, it will be ignored
433         * @return The markup. Markup.NO_MARKUP, if not found.
434         */
435        private Markup loadMarkup(final MarkupContainer container,
436                final MarkupResourceStream markupResourceStream, final boolean enforceReload)
437        {
438                String cacheKey = markupResourceStream.getCacheKey();
439                String locationString = markupResourceStream.locationAsString();
440                if (locationString == null)
441                {
442                        // set the cache key as location string, because location string
443                        // couldn't be resolved.
444                        locationString = cacheKey;
445                }
446
447                Markup markup = MarkupFactory.get().loadMarkup(container, markupResourceStream,
448                        enforceReload);
449                if (markup != null)
450                {
451                        if (cacheKey != null)
452                        {
453                                String temp = markup.locationAsString();
454                                if (temp != null)
455                                {
456                                        locationString = temp;
457                                }
458
459                                // add the markup to the cache.
460                                markupKeyCache.put(cacheKey, locationString);
461                                return putIntoCache(locationString, container, markup);
462                        }
463                        return markup;
464                }
465
466                // In case the markup could not be loaded (without exception) then ..
467                if (cacheKey != null)
468                {
469                        removeMarkup(cacheKey);
470                }
471
472                return Markup.NO_MARKUP;
473        }
474
475        /**
476         * Load markup from an IResourceStream and add an {@link IChangeListener}to the
477         * {@link ModificationWatcher} so that if the resource changes, we can remove it from the cache
478         * automatically and subsequently reload when needed.
479         * 
480         * @param container
481         *            The original requesting markup container
482         * @param markupResourceStream
483         *            The markup stream to load and begin to watch
484         * @param enforceReload
485         *            The cache will be ignored and all, including inherited markup files, will be
486         *            reloaded. Whatever is in the cache, it will be ignored
487         * @return The markup in the stream
488         */
489        private Markup loadMarkupAndWatchForChanges(final MarkupContainer container,
490                final MarkupResourceStream markupResourceStream, final boolean enforceReload)
491        {
492                // @TODO the following code sequence looks very much like in loadMarkup. Can it be
493                // optimized?
494                final String cacheKey = markupResourceStream.getCacheKey();
495                if (cacheKey != null)
496                {
497                        if (enforceReload == false)
498                        {
499                                // get the location String
500                                String locationString = markupResourceStream.locationAsString();
501                                if (locationString == null)
502                                {
503                                        // set the cache key as location string, because location string
504                                        // couldn't be resolved.
505                                        locationString = cacheKey;
506                                }
507                                Markup markup = markupCache.get(locationString);
508                                if (markup != null)
509                                {
510                                        markupKeyCache.put(cacheKey, locationString);
511                                        return markup;
512                                }
513                        }
514
515                        // Watch file in the future
516                        final IModificationWatcher watcher = application.getResourceSettings()
517                                .getResourceWatcher(true);
518                        if (watcher != null)
519                        {
520                                watcher.add(markupResourceStream, new IChangeListener<IModifiable>()
521                                {
522                                        @Override
523                                        public void onChange(IModifiable modifiable)
524                                        {
525                                                if (log.isDebugEnabled())
526                                                {
527                                                        log.debug("Remove markup from watcher: " + markupResourceStream);
528                                                }
529
530                                                // Remove the markup from the cache. It will be reloaded
531                                                // next time when the markup is requested.
532                                                watcher.remove(markupResourceStream);
533                                                removeMarkup(cacheKey);
534                                        }
535                                });
536                        }
537                }
538
539                if (log.isDebugEnabled())
540                {
541                        log.debug("Loading markup from " + markupResourceStream);
542                }
543                return loadMarkup(container, markupResourceStream, enforceReload);
544        }
545
546        /**
547         * Get the markup cache key provider to be used
548         * 
549         * @param container
550         *            The MarkupContainer requesting the markup resource stream
551         * @return IMarkupResourceStreamProvider
552         */
553        public IMarkupCacheKeyProvider getMarkupCacheKeyProvider(final MarkupContainer container)
554        {
555                if (container instanceof IMarkupCacheKeyProvider)
556                {
557                        return (IMarkupCacheKeyProvider)container;
558                }
559
560                if (markupCacheKeyProvider == null)
561                {
562                        markupCacheKeyProvider = new DefaultMarkupCacheKeyProvider();
563                }
564                return markupCacheKeyProvider;
565        }
566
567        /**
568         * Allows you to change the map implementation which will hold the cache data. By default it is
569         * a ConcurrentHashMap() in order to allow multiple thread to access the data in a secure way.
570         * 
571         * @param <K>
572         * @param <V>
573         * @return new instance of cache implementation
574         */
575        protected <K, V> ICache<K, V> newCacheImplementation()
576        {
577                return new DefaultCacheImplementation<K, V>();
578        }
579
580        /**
581         * MarkupCache allows you to implement you own cache implementation. ICache is the interface the
582         * implementation must comply with.
583         * 
584         * @param <K>
585         *            The key type
586         * @param <V>
587         *            The value type
588         */
589        public interface ICache<K, V>
590        {
591                /**
592                 * Clear the cache
593                 */
594                void clear();
595
596                /**
597                 * Remove an entry from the cache.
598                 * 
599                 * @param key
600                 * @return true, if found and removed
601                 */
602                boolean remove(K key);
603
604                /**
605                 * Get the cache element associated with the key
606                 * 
607                 * @param key
608                 * @return cached object for key <code>key</code> or null if no matches
609                 */
610                V get(K key);
611
612                /**
613                 * Get all the keys referencing cache entries
614                 * 
615                 * @return collection of cached keys
616                 */
617                Collection<K> getKeys();
618
619                /**
620                 * Get all the values referencing cache entries
621                 * 
622                 * @return collection of cached keys
623                 */
624                Collection<V> getValues();
625
626                /**
627                 * Check if key is in the cache
628                 * 
629                 * @param key
630                 * @return true if cache contains key <code>key</code>
631                 */
632                boolean containsKey(K key);
633
634                /**
635                 * Get the number of cache entries
636                 * 
637                 * @return number of cache entries
638                 */
639                int size();
640
641                /**
642                 * Put an entry into the cache
643                 * 
644                 * @param key
645                 *            The reference key to find the element. Must not be null.
646                 * @param value
647                 *            The element to be cached. Must not be null.
648                 */
649                void put(K key, V value);
650
651                /**
652                 * Cleanup and shutdown
653                 */
654                void shutdown();
655        }
656
657        /**
658         * @param <K>
659         * @param <V>
660         */
661        public static class DefaultCacheImplementation<K, V> implements ICache<K, V>
662        {
663                // Neither key nor value are allowed to be null with ConcurrentHashMap
664                private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<K, V>();
665
666                /**
667                 * Construct.
668                 */
669                public DefaultCacheImplementation()
670                {
671                }
672
673                @Override
674                public void clear()
675                {
676                        cache.clear();
677                }
678
679                @Override
680                public boolean containsKey(final Object key)
681                {
682                        if (key == null)
683                        {
684                                return false;
685                        }
686                        return cache.containsKey(key);
687                }
688
689                @Override
690                public V get(final Object key)
691                {
692                        if (key == null)
693                        {
694                                return null;
695                        }
696                        return cache.get(key);
697                }
698
699                @Override
700                public Collection<K> getKeys()
701                {
702                        return cache.keySet();
703                }
704
705                @Override
706                public Collection<V> getValues()
707                {
708                        return cache.values();
709                }
710
711                @Override
712                public void put(K key, V value)
713                {
714                        // Note that neither key nor value are allowed to be null with ConcurrentHashMap
715                        cache.put(key, value);
716                }
717
718                @Override
719                public boolean remove(K key)
720                {
721                        if (key == null)
722                        {
723                                return false;
724                        }
725                        return cache.remove(key) == null;
726                }
727
728                @Override
729                public int size()
730                {
731                        return cache.size();
732                }
733
734                @Override
735                public void shutdown()
736                {
737                        clear();
738                }
739        }
740}