/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.markup; import java.io.IOException; import org.apache.wicket.Application; import org.apache.wicket.MarkupContainer; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.loader.DefaultMarkupLoader; import org.apache.wicket.markup.loader.IMarkupLoader; import org.apache.wicket.markup.parser.IMarkupFilter; import org.apache.wicket.markup.parser.IXmlPullParser; import org.apache.wicket.markup.parser.XmlPullParser; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Factory to load markup either from cache or from a resource. * <p> * This class is the main entry point to load markup. Nothing else should be required by Components. * It manages caching markup as well as loading and merging (inheritance) of markup. * <p> * The markup returned is immutable as it gets re-used across multiple Component instances. * * @author Juergen Donnerstag */ public class MarkupFactory { /** Log for reporting. */ private static final Logger log = LoggerFactory.getLogger(MarkupFactory.class); /** A markup cache */ private IMarkupCache markupCache = null; /** The markup resource stream provider used by MarkupCache */ private IMarkupResourceStreamProvider markupResourceStreamProvider = null; /** * @return Gets the markup factory registered with the Wicket application */ public static MarkupFactory get() { return Application.get().getMarkupSettings().getMarkupFactory(); } /** * Construct. */ public MarkupFactory() { } /** * MarkupLoaders are responsible to find and load the markup for a component. That may be a * single file, but e.g. like in markup inheritance it could also be that the markup from * different sources must be merged. * * @return By default an instance of {@link DefaultMarkupLoader} will be returned. Via * subclassing you may return your own markup loader (chain). */ public IMarkupLoader getMarkupLoader() { return new DefaultMarkupLoader(); } /** * Create a new markup parser. Markup parsers read the markup and dissect it in Wicket relevant * pieces {@link MarkupElement}'s (kind of Wicket's DOM). * <p> * MarkupParser's can be extended by means of {@link IMarkupFilter}. You can add your own filter * as follows: * * <pre> * public MyMarkupFactory { * ... * public MarkupParser newMarkupParser(final MarkupResourceStream resource) { * MarkupParser parser = super.newMarkupParser(resource); * parser.add(new MyFilter()); * return parser; * } * } * </pre> * * @see #onAppendMarkupFilter(IMarkupFilter) * * @param resource * The resource containing the markup * @return A fresh instance of {@link MarkupParser} */ public MarkupParser newMarkupParser(final MarkupResourceStream resource) { // Markup parsers can not be re-used return new MarkupParser(newXmlPullParser(), resource) { @Override protected IMarkupFilter onAppendMarkupFilter(final IMarkupFilter filter) { return MarkupFactory.this.onAppendMarkupFilter(filter); } }; } /** * Subclasses can override this to use custom parsers. * * @return parser instance used by {@link MarkupParser} to parse markup. */ protected IXmlPullParser newXmlPullParser() { return new XmlPullParser(); } /** * A callback method that is invoked prior to any {@link IMarkupFilter} being registered with * {@link MarkupParser}. Hence it allows to: * <ul> * <li>tweak the default configuration of a filter</li> * <li>replace a filter with another one</li> * <li>avoid filters being used by returning null</li> * </ul> * Note that a new {@link MarkupParser} instance is created for each markup resources being * loaded. * <p> * * @param filter * The filter to be registered with the MarkupParser * @return The filter to be added. Null to ignore. */ protected IMarkupFilter onAppendMarkupFilter(final IMarkupFilter filter) { return filter; } /** * Get the markup cache which is registered with the factory. Since the factory is registered * with the application, only one cache per application exists. * <p> * Please note that markup cache is a pull through cache. It'll invoke a factory method * {@link #getMarkupResourceStream(MarkupContainer, Class)} to load the markup if not yet * available in the cache. * * @return Null, to disable caching. */ public IMarkupCache getMarkupCache() { if (markupCache == null) { markupCache = new MarkupCache(); } return markupCache; } /** * @return <code>true</code> if markup cache is available. Make sure you called * {@link #getMarkupCache()} at least once before to initialize the cache. */ public boolean hasMarkupCache() { return markupCache != null; } /** * Get the markup associated with the container. * * @param container * The container to find the markup for * @param enforceReload * If true, the cache will be ignored and all, including inherited markup files, will * be reloaded. Whatever is in the cache, it will be ignored * @return The markup associated with the container. Null, if the markup was not found or could * not yet be loaded (e.g. getMarkupType() == null). Wicket Exception in case of errors. */ public final Markup getMarkup(final MarkupContainer container, final boolean enforceReload) { return getMarkup(container, container.getClass(), enforceReload); } /** * Get the markup associated with the container. Check the cache first. If not found, than load * the markup and update the cache. * <p> * The clazz parameter usually can be null, except for base (inherited) markup. * <p> * There are several means to disable markup caching. Caching can be disabled alltogether - * getMarkupCache() return null -, or individually (cacheKey == null). * * @param container * The container to find the markup for * @param clazz * Must be the container class or any of its super classes. May be null. * @param enforceReload * The cache will be ignored and all, including inherited markup files, will be * reloaded. Whatever is in the cache, it will be ignored * @return The markup associated with the container. Null, if the markup was not found or could * not yet be loaded (e.g. getMarkupType() == null). Wicket Exception in case of errors. */ public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz, final boolean enforceReload) { Args.notNull(container, "container"); if (checkMarkupType(container) == false) { // TODO improve: Result { boolean success, enum FailureReason {not found, not yet // available}, Markup markup } return null; } Class<?> containerClass = getContainerClass(container, clazz); IMarkupCache cache = getMarkupCache(); if (cache != null) { // MarkupCache acts as pull-through cache. It'll call the same loadMarkup() method as // below, if needed. // @TODO may be that can be changed. I don't like it too much. return cache.getMarkup(container, containerClass, enforceReload); } // Get the markup resource stream for the container (and super class) MarkupResourceStream markupResourceStream = getMarkupResourceStream(container, containerClass); return loadMarkup(container, markupResourceStream, enforceReload); } /** * Without a markup type we can not search for a file and we can not construct the cacheKey. We * can not even load associated markup as required for Panels. Though every MarkupContainer can * provide it's own type, by default they refer to the Page. Hence, no markup type is an * indicator, that the component or any of its parents, has not yet been added. * * @param container * The MarkupContainer which markup type has to checked * @return true, if container.getMarkupType() != null */ protected final boolean checkMarkupType(final MarkupContainer container) { if (container.getMarkupType() == null) { log.debug("Markup file not loaded, since the markup type is not yet available: {}", container); return false; } return true; } /** * Get the markup resource stream provider registered with the factory. * <p> * If the 'container' implements {@link IMarkupResourceStreamProvider}, the container itself * will be asked to provide the resource stream. Else Wicket's default implementation will be * used. * * @param container * The MarkupContainer requesting the markup resource stream * @return IMarkupResourceStreamProvider */ protected final IMarkupResourceStreamProvider getMarkupResourceStreamProvider( final MarkupContainer container) { if (container instanceof IMarkupResourceStreamProvider) { return (IMarkupResourceStreamProvider)container; } if (markupResourceStreamProvider == null) { markupResourceStreamProvider = new DefaultMarkupResourceStreamProvider(); } return markupResourceStreamProvider; } /** * Create a new markup resource stream for the container and optionally the Class. The Class * must be provided in case of base (inherited) markup. Else it might be null (standard use * case). * * @param container * The MarkupContainer which requests to load the Markup resource stream * @param clazz * Either the container class or any super class. Might be null. * @return A IResourceStream if the resource was found */ public final MarkupResourceStream getMarkupResourceStream(final MarkupContainer container, Class<?> clazz) { Args.notNull(container, "container"); if (checkMarkupType(container) == false) { // TODO improve: Result { boolean success, enum FailureReason {not found, not yet // available}, Markup markup } return null; } Class<?> containerClass = getContainerClass(container, clazz); // Who is going to provide the markup resource stream? // And ask the provider to locate the markup resource stream final IResourceStream resourceStream = getMarkupResourceStreamProvider(container).getMarkupResourceStream( container, containerClass); // Found markup? if (resourceStream == null) { // TODO improve: Result { boolean success, enum FailureReason {not found, not yet // available}, Markup markup } return null; } if (resourceStream instanceof MarkupResourceStream) { return (MarkupResourceStream)resourceStream; } return new MarkupResourceStream(resourceStream, new ContainerInfo(container), containerClass); } /** * Gets and checks the container class * * @param container * The MarkupContainer which requests to load the Markup resource stream * @param clazz * Either null, or a super class of container * @return The container class to be used */ public final Class<?> getContainerClass(final MarkupContainer container, final Class<?> clazz) { Args.notNull(container, "container"); Class<?> containerClass = clazz; if (clazz == null) { containerClass = container.getClass(); } else if (!clazz.isAssignableFrom(container.getClass())) { throw new IllegalArgumentException("Parameter clazz must be an instance of " + container.getClass().getName() + ", but is a " + clazz.getName()); } return containerClass; } /** * Loads markup from a resource stream. It'll call the registered markup loader to load the * markup. * <p> * Though the 'enforceReload' attribute seem to imply that the cache is consulted to retrieve * the markup, the cache in fact is only checked for retrieving the base (inherited) markup. * Please see {@link #getMarkup(MarkupContainer, boolean)} as well. * * @param container * The original requesting markup container * @param markupResourceStream * The markup resource stream to load, if already known. * @param enforceReload * The cache will be ignored and all, including inherited markup files, will be * reloaded. Whatever is in the cache, it will be ignored * @return The markup. Null, if the markup was not found. Wicket Exception in case of errors. */ public final Markup loadMarkup(final MarkupContainer container, final MarkupResourceStream markupResourceStream, final boolean enforceReload) { // @TODO can markupResourceStream be replace with clazz??? Args.notNull(container, "container"); Args.notNull(markupResourceStream, "markupResourceStream"); if (checkMarkupType(container) == false) { // TODO improve: Result { boolean success, enum FailureReason {not found, not yet // available}, Markup markup } return null; } try { // The InheritedMarkupMarkupLoader needs to load the base markup. It'll do it via // MarkupFactory.getMarkup() as main entry point, which in turn allows to choose between // use or ignore the cache. That's why we need to propagate enforceReload to the markup // loader as well. // Markup loader is responsible to load the full markup for the container. In case of // markup inheritance, the markup must be merged from different markup files. It is the // merged markup which eventually will be cached, thus avoiding repetitive merge // operations, which always result in the same outcome. // The base markup will still be cached though, in order to avoid any unnecessary // reloads. The base markup itself might be merged as it might inherit from its base // class. return getMarkupLoader().loadMarkup(container, markupResourceStream, null, enforceReload); } catch (MarkupNotFoundException e) { // InheritedMarkupMarkupLoader will throw a MarkupNotFoundException in case the // <b>base</b> markup can not be found. log.error("Markup not found: " + e.getMessage(), e); // Catch exception and ignore => return null (markup not found) } catch (ResourceStreamNotFoundException e) { log.error("Markup not found: " + markupResourceStream, e); // Catch exception and ignore => return null (markup not found) } catch (IOException e) { log.error("Error while reading the markup " + markupResourceStream, e); // Wrap with wicket exception and re-throw throw new MarkupException(markupResourceStream, "IO error while reading markup: " + e.getMessage(), e); } catch (WicketRuntimeException e) { log.error("Error while reading the markup " + markupResourceStream, e); // re-throw throw e; } catch (RuntimeException e) { log.error("Error while reading the markup " + markupResourceStream, e); // Wrap with wicket exception and re-throw throw new MarkupException(markupResourceStream, "Error while reading the markup: " + e.getMessage(), e); } // Markup not found. Errors should throw a Wicket exception return null; } }