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.io.IOException;
020
021import org.apache.wicket.Application;
022import org.apache.wicket.MarkupContainer;
023import org.apache.wicket.WicketRuntimeException;
024import org.apache.wicket.markup.loader.DefaultMarkupLoader;
025import org.apache.wicket.markup.loader.IMarkupLoader;
026import org.apache.wicket.markup.parser.IMarkupFilter;
027import org.apache.wicket.markup.parser.IXmlPullParser;
028import org.apache.wicket.markup.parser.XmlPullParser;
029import org.apache.wicket.util.lang.Args;
030import org.apache.wicket.util.resource.IResourceStream;
031import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * Factory to load markup either from cache or from a resource.
037 * <p>
038 * This class is the main entry point to load markup. Nothing else should be required by Components.
039 * It manages caching markup as well as loading and merging (inheritance) of markup.
040 * <p>
041 * The markup returned is immutable as it gets re-used across multiple Component instances.
042 * 
043 * @author Juergen Donnerstag
044 */
045public class MarkupFactory
046{
047        /** Log for reporting. */
048        private static final Logger log = LoggerFactory.getLogger(MarkupFactory.class);
049
050        /** A markup cache */
051        private IMarkupCache markupCache = null;
052
053        /** The markup resource stream provider used by MarkupCache */
054        private IMarkupResourceStreamProvider markupResourceStreamProvider = null;
055
056        /**
057         * @return Gets the markup factory registered with the Wicket application
058         */
059        public static MarkupFactory get()
060        {
061                return Application.get().getMarkupSettings().getMarkupFactory();
062        }
063
064        /**
065         * Construct.
066         */
067        public MarkupFactory()
068        {
069        }
070
071        /**
072         * MarkupLoaders are responsible to find and load the markup for a component. That may be a
073         * single file, but e.g. like in markup inheritance it could also be that the markup from
074         * different sources must be merged.
075         * 
076         * @return By default an instance of {@link DefaultMarkupLoader} will be returned. Via
077         *         subclassing you may return your own markup loader (chain).
078         */
079        public IMarkupLoader getMarkupLoader()
080        {
081                return new DefaultMarkupLoader();
082        }
083
084        /**
085         * Create a new markup parser. Markup parsers read the markup and dissect it in Wicket relevant
086         * pieces {@link MarkupElement}'s (kind of Wicket's DOM).
087         * <p>
088         * MarkupParser's can be extended by means of {@link IMarkupFilter}. You can add your own filter
089         * as follows:
090         * 
091         * <pre>
092         *    public MyMarkupFactory {
093         *      ...
094         *      public MarkupParser newMarkupParser(final MarkupResourceStream resource) {
095         *         MarkupParser parser = super.newMarkupParser(resource);
096         *         parser.add(new MyFilter());
097         *         return parser;
098         *      }
099         *    }
100         * </pre>
101         * 
102         * @see #onAppendMarkupFilter(IMarkupFilter)
103         * 
104         * @param resource
105         *            The resource containing the markup
106         * @return A fresh instance of {@link MarkupParser}
107         */
108        public MarkupParser newMarkupParser(final MarkupResourceStream resource)
109        {
110                // Markup parsers can not be re-used
111                return new MarkupParser(newXmlPullParser(), resource)
112                {
113                        @Override
114                        protected IMarkupFilter onAppendMarkupFilter(final IMarkupFilter filter)
115                        {
116                                return MarkupFactory.this.onAppendMarkupFilter(filter);
117                        }
118                };
119        }
120
121        /**
122         * Subclasses can override this to use custom parsers.
123         * 
124         * @return parser instance used by {@link MarkupParser} to parse markup.
125         */
126        protected IXmlPullParser newXmlPullParser()
127        {
128                return new XmlPullParser();
129        }
130
131        /**
132         * A callback method that is invoked prior to any {@link IMarkupFilter} being registered with
133         * {@link MarkupParser}. Hence it allows to:
134         * <ul>
135         * <li>tweak the default configuration of a filter</li>
136         * <li>replace a filter with another one</li>
137         * <li>avoid filters being used by returning null</li>
138         * </ul>
139         * Note that a new {@link MarkupParser} instance is created for each markup resources being
140         * loaded.
141         * <p>
142         * 
143         * @param filter
144         *            The filter to be registered with the MarkupParser
145         * @return The filter to be added. Null to ignore.
146         */
147        protected IMarkupFilter onAppendMarkupFilter(final IMarkupFilter filter)
148        {
149                return filter;
150        }
151
152        /**
153         * Get the markup cache which is registered with the factory. Since the factory is registered
154         * with the application, only one cache per application exists.
155         * <p>
156         * Please note that markup cache is a pull through cache. It'll invoke a factory method
157         * {@link #getMarkupResourceStream(MarkupContainer, Class)} to load the markup if not yet
158         * available in the cache.
159         * 
160         * @return Null, to disable caching.
161         */
162        public IMarkupCache getMarkupCache()
163        {
164                if (markupCache == null)
165                {
166                        markupCache = new MarkupCache();
167                }
168
169                return markupCache;
170        }
171
172        /**
173         * @return <code>true</code> if markup cache is available. Make sure you called
174         *         {@link #getMarkupCache()} at least once before to initialize the cache.
175         */
176        public boolean hasMarkupCache()
177        {
178                return markupCache != null;
179        }
180
181        /**
182         * Get the markup associated with the container.
183         * 
184         * @param container
185         *            The container to find the markup for
186         * @param enforceReload
187         *            If true, the cache will be ignored and all, including inherited markup files, will
188         *            be reloaded. Whatever is in the cache, it will be ignored
189         * @return The markup associated with the container. Null, if the markup was not found or could
190         *         not yet be loaded (e.g. getMarkupType() == null). Wicket Exception in case of errors.
191         */
192        public final Markup getMarkup(final MarkupContainer container, final boolean enforceReload)
193        {
194                return getMarkup(container, container.getClass(), enforceReload);
195        }
196
197        /**
198         * Get the markup associated with the container. Check the cache first. If not found, than load
199         * the markup and update the cache.
200         * <p>
201         * The clazz parameter usually can be null, except for base (inherited) markup.
202         * <p>
203         * There are several means to disable markup caching. Caching can be disabled alltogether -
204         * getMarkupCache() return null -, or individually (cacheKey == null).
205         * 
206         * @param container
207         *            The container to find the markup for
208         * @param clazz
209         *            Must be the container class or any of its super classes. May be null.
210         * @param enforceReload
211         *            The cache will be ignored and all, including inherited markup files, will be
212         *            reloaded. Whatever is in the cache, it will be ignored
213         * @return The markup associated with the container. Null, if the markup was not found or could
214         *         not yet be loaded (e.g. getMarkupType() == null). Wicket Exception in case of errors.
215         */
216        public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz,
217                final boolean enforceReload)
218        {
219                Args.notNull(container, "container");
220
221                if (checkMarkupType(container) == false)
222                {
223                        // TODO improve: Result { boolean success, enum FailureReason {not found, not yet
224                        // available}, Markup markup }
225                        return null;
226                }
227
228                Class<?> containerClass = getContainerClass(container, clazz);
229
230                IMarkupCache cache = getMarkupCache();
231                if (cache != null)
232                {
233                        // MarkupCache acts as pull-through cache. It'll call the same loadMarkup() method as
234                        // below, if needed.
235                        // @TODO may be that can be changed. I don't like it too much.
236                        return cache.getMarkup(container, containerClass, enforceReload);
237                }
238
239                // Get the markup resource stream for the container (and super class)
240                MarkupResourceStream markupResourceStream = getMarkupResourceStream(container,
241                        containerClass);
242
243                return loadMarkup(container, markupResourceStream, enforceReload);
244        }
245
246        /**
247         * Without a markup type we can not search for a file and we can not construct the cacheKey. We
248         * can not even load associated markup as required for Panels. Though every MarkupContainer can
249         * provide it's own type, by default they refer to the Page. Hence, no markup type is an
250         * indicator, that the component or any of its parents, has not yet been added.
251         * 
252         * @param container
253         *          The MarkupContainer which markup type has to checked
254         * @return true, if container.getMarkupType() != null
255         */
256        protected final boolean checkMarkupType(final MarkupContainer container)
257        {
258                if (container.getMarkupType() == null)
259                {
260                        log.debug("Markup file not loaded, since the markup type is not yet available: {}", container);
261                        return false;
262                }
263
264                return true;
265        }
266
267        /**
268         * Get the markup resource stream provider registered with the factory.
269         * <p>
270         * If the 'container' implements {@link IMarkupResourceStreamProvider}, the container itself
271         * will be asked to provide the resource stream. Else Wicket's default implementation will be
272         * used.
273         * 
274         * @param container
275         *            The MarkupContainer requesting the markup resource stream
276         * @return IMarkupResourceStreamProvider
277         */
278        protected final IMarkupResourceStreamProvider getMarkupResourceStreamProvider(
279                final MarkupContainer container)
280        {
281                if (container instanceof IMarkupResourceStreamProvider)
282                {
283                        return (IMarkupResourceStreamProvider)container;
284                }
285
286                if (markupResourceStreamProvider == null)
287                {
288                        markupResourceStreamProvider = new DefaultMarkupResourceStreamProvider();
289                }
290                return markupResourceStreamProvider;
291        }
292
293        /**
294         * Create a new markup resource stream for the container and optionally the Class. The Class
295         * must be provided in case of base (inherited) markup. Else it might be null (standard use
296         * case).
297         * 
298         * @param container
299         *            The MarkupContainer which requests to load the Markup resource stream
300         * @param clazz
301         *            Either the container class or any super class. Might be null.
302         * @return A IResourceStream if the resource was found
303         */
304        public final MarkupResourceStream getMarkupResourceStream(final MarkupContainer container,
305                Class<?> clazz)
306        {
307                Args.notNull(container, "container");
308
309                if (checkMarkupType(container) == false)
310                {
311                        // TODO improve: Result { boolean success, enum FailureReason {not found, not yet
312                        // available}, Markup markup }
313                        return null;
314                }
315
316                Class<?> containerClass = getContainerClass(container, clazz);
317
318                // Who is going to provide the markup resource stream?
319                // And ask the provider to locate the markup resource stream
320                final IResourceStream resourceStream = getMarkupResourceStreamProvider(container).getMarkupResourceStream(
321                        container, containerClass);
322
323                // Found markup?
324                if (resourceStream == null)
325                {
326                        // TODO improve: Result { boolean success, enum FailureReason {not found, not yet
327                        // available}, Markup markup }
328                        return null;
329                }
330
331                if (resourceStream instanceof MarkupResourceStream)
332                {
333                        return (MarkupResourceStream)resourceStream;
334                }
335
336                return new MarkupResourceStream(resourceStream, new ContainerInfo(container),
337                        containerClass);
338        }
339
340        /**
341         * Gets and checks the container class
342         * 
343         * @param container
344         *            The MarkupContainer which requests to load the Markup resource stream
345         * @param clazz
346         *            Either null, or a super class of container
347         * @return The container class to be used
348         */
349        public final Class<?> getContainerClass(final MarkupContainer container, final Class<?> clazz)
350        {
351                Args.notNull(container, "container");
352
353                Class<?> containerClass = clazz;
354                if (clazz == null)
355                {
356                        containerClass = container.getClass();
357                }
358                else if (!clazz.isAssignableFrom(container.getClass()))
359                {
360                        throw new IllegalArgumentException("Parameter clazz must be an instance of " +
361                                container.getClass().getName() + ", but is a " + clazz.getName());
362                }
363                return containerClass;
364        }
365
366        /**
367         * Loads markup from a resource stream. It'll call the registered markup loader to load the
368         * markup.
369         * <p>
370         * Though the 'enforceReload' attribute seem to imply that the cache is consulted to retrieve
371         * the markup, the cache in fact is only checked for retrieving the base (inherited) markup.
372         * Please see {@link #getMarkup(MarkupContainer, boolean)} as well.
373         * 
374         * @param container
375         *            The original requesting markup container
376         * @param markupResourceStream
377         *            The markup resource stream to load, if already known.
378         * @param enforceReload
379         *            The cache will be ignored and all, including inherited markup files, will be
380         *            reloaded. Whatever is in the cache, it will be ignored
381         * @return The markup. Null, if the markup was not found. Wicket Exception in case of errors.
382         */
383        public final Markup loadMarkup(final MarkupContainer container,
384                final MarkupResourceStream markupResourceStream, final boolean enforceReload)
385        {
386                // @TODO can markupResourceStream be replace with clazz???
387                Args.notNull(container, "container");
388                Args.notNull(markupResourceStream, "markupResourceStream");
389
390                if (checkMarkupType(container) == false)
391                {
392                        // TODO improve: Result { boolean success, enum FailureReason {not found, not yet
393                        // available}, Markup markup }
394                        return null;
395                }
396
397                try
398                {
399                        // The InheritedMarkupMarkupLoader needs to load the base markup. It'll do it via
400                        // MarkupFactory.getMarkup() as main entry point, which in turn allows to choose between
401                        // use or ignore the cache. That's why we need to propagate enforceReload to the markup
402                        // loader as well.
403
404                        // Markup loader is responsible to load the full markup for the container. In case of
405                        // markup inheritance, the markup must be merged from different markup files. It is the
406                        // merged markup which eventually will be cached, thus avoiding repetitive merge
407                        // operations, which always result in the same outcome.
408                        // The base markup will still be cached though, in order to avoid any unnecessary
409                        // reloads. The base markup itself might be merged as it might inherit from its base
410                        // class.
411
412                        return getMarkupLoader().loadMarkup(container, markupResourceStream, null,
413                                enforceReload);
414                }
415                catch (MarkupNotFoundException e)
416                {
417                        // InheritedMarkupMarkupLoader will throw a MarkupNotFoundException in case the
418                        // <b>base</b> markup can not be found.
419
420                        log.error("Markup not found: " + e.getMessage(), e);
421
422                        // Catch exception and ignore => return null (markup not found)
423                }
424                catch (ResourceStreamNotFoundException e)
425                {
426                        log.error("Markup not found: " + markupResourceStream, e);
427
428                        // Catch exception and ignore => return null (markup not found)
429                }
430                catch (IOException e)
431                {
432                        log.error("Error while reading the markup " + markupResourceStream, e);
433
434                        // Wrap with wicket exception and re-throw
435                        throw new MarkupException(markupResourceStream, "IO error while reading markup: " +
436                                e.getMessage(), e);
437                }
438                catch (WicketRuntimeException e)
439                {
440                        log.error("Error while reading the markup " + markupResourceStream, e);
441
442                        // re-throw
443                        throw e;
444                }
445                catch (RuntimeException e)
446                {
447                        log.error("Error while reading the markup " + markupResourceStream, e);
448
449                        // Wrap with wicket exception and re-throw
450                        throw new MarkupException(markupResourceStream, "Error while reading the markup: " +
451                                e.getMessage(), e);
452                }
453
454                // Markup not found. Errors should throw a Wicket exception
455                return null;
456        }
457}