/* * 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.resolver; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Page; import org.apache.wicket.application.IClassResolver; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.IMarkupFragment; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.DisabledAttributeLinkBehavior; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.protocol.http.RequestUtils; import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.PackageResource; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; import org.apache.wicket.util.lang.Packages; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The AutoLinkResolver is responsible to handle automatic link resolution. Tags are marked * "autolink" by the MarkupParser for all tags with href attribute, such as anchor and link tags * with no explicit wicket id. E.g. <a href="Home.html"> * <p> * If href points to a *.html file, a BookmarkablePageLink<?> will automatically be created, except * for absolute paths, where an ExternalLink is created. * <p> * If href points to a *.html file, it resolves the given URL by searching for a page class, either * relative or absolute, specified by the href attribute of the tag. If relative the href URL must * be relative to the package containing the associated page. An exception is thrown if no Page * class was found. * <p> * If href is no *.html file a static reference to the resource is created. * * @see org.apache.wicket.markup.parser.filter.WicketLinkTagHandler * * @author Juergen Donnerstag * @author Eelco Hillenius */ public final class AutoLinkResolver implements IComponentResolver { /** * Abstract implementation that has a helper method for creating a resource reference. */ public static abstract class AbstractAutolinkResolverDelegate implements IAutolinkResolverDelegate { /** * Creates a new auto component that references a package resource. * * @param autoId * the automatically generated id for the auto component * @param pathInfo * the path info object that contains information about the link reference * @param attribute * the attribute to replace the value of * @return a new auto component or null if the path was absolute */ protected final Component newPackageResourceReferenceAutoComponent( final String autoId, final PathInfo pathInfo, final String attribute) { final MarkupContainer container = pathInfo.getContainer(); if (!pathInfo.absolute && (pathInfo.path != null) && (pathInfo.path.length() > 0)) { // Href is relative. Create a resource reference pointing at this file // <wicket:head> components are handled differently. We can // not use the container, because it is the container the // header has been added to (e.g. the Page). What we need // however, is the component (e.g. a Panel) which // contributed it. MarkupStream markupStream = pathInfo.getMarkupStream(); Class<? extends Component> clazz = markupStream.getContainerClass(); // However if the markup stream is a merged markup stream (inheritance), than we // need the class of the markup file which contained the tag. if ((markupStream.get() instanceof ComponentTag) && (markupStream.getTag().getMarkupClass() != null)) { clazz = markupStream.getTag().getMarkupClass(); } // Create the component implementing the link ResourceReferenceAutolink autoLink = new ResourceReferenceAutolink(autoId, clazz, pathInfo.reference, attribute, container); if (autoLink.resourceReference != null) { // if the resource reference is null, it means that it the // reference was not found as a package resource return autoLink; } } // else we can't have absolute resource references, at least not at // this time // fall back on default processing return null; } } /** * Autolink components delegate component resolution to their parent components. Reason: * autolink tags don't have wicket:id and users wouldn't know where to add the component to. * * @author Juergen Donnerstag * @param <T> * type of model object */ public final static class AutolinkBookmarkablePageLink<T> extends BookmarkablePageLink<T> implements IComponentResolver { private static final long serialVersionUID = 1L; private final String anchor; /** * When using <wicket:link> to let Wicket lookup for pages and create the related links, * it's not possible to change the "setAutoEnable" property, which defaults to true. This * affects the prototype because, sometimes designers _want_ links to be enabled. */ public static boolean autoEnable = true; /** * Construct * * @param <C> * * @see BookmarkablePageLink#BookmarkablePageLink(String, Class, PageParameters) * * @param id * @param pageClass * @param parameters * @param anchor */ public <C extends Page> AutolinkBookmarkablePageLink(final String id, final Class<C> pageClass, final PageParameters parameters, final String anchor) { super(id, pageClass, parameters); this.anchor = anchor; setAutoEnable(autoEnable); add(new DisabledAttributeLinkBehavior()); } /** * * @see org.apache.wicket.markup.html.link.BookmarkablePageLink#getURL() */ @Override protected CharSequence getURL() { CharSequence url = super.getURL(); if (anchor != null) { url = url + anchor; } return url; } /** * @see org.apache.wicket.markup.resolver.IComponentResolver#resolve(org.apache.wicket.MarkupContainer, * org.apache.wicket.markup.MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override public Component resolve(final MarkupContainer container, final MarkupStream markupStream, ComponentTag tag) { return getParent().get(tag.getId()); } } /** * Interface to delegate the actual resolving of auto components to. */ public interface IAutolinkResolverDelegate { /** * Returns a new auto component based on the pathInfo object. The auto component must have * the autoId assigned as it's id. Should return null in case the component could not be * created as expected and the default resolving should take place. * * @param autoId * the automatically generated id for the auto component * @param pathInfo * the path info object that contains information about the link reference * @return a new auto component or null in case this method couldn't resolve to a proper * auto component */ Component newAutoComponent(final String autoId, final PathInfo pathInfo); } /** * Encapsulates different aspects of a path. For instance, the path * <code>org.apache.wicket.markup.html.tree.Tree/tree.css</code> has extension <code>css</code>, * is relative (absolute == true) and has no page parameters. */ public static final class PathInfo { /** whether the reference is absolute. */ private final boolean absolute; /** An optional anchor like #top */ private final String anchor; /** The extension if any. */ private final String extension; /** The optional page parameters. */ private final PageParameters pageParameters; /** The path excluding any parameters. */ private final String path; /** The original reference (e.g the full value of a href attribute). */ private final String reference; /** The container for this path */ private final MarkupContainer container; /** Parent markup stream */ private final MarkupStream markupStream; /** * Construct. * * @param reference * the original reference (e.g the full value of a href attribute) */ public PathInfo(final String reference, MarkupContainer container, MarkupStream markupStream) { this.reference = reference; this.container = container; this.markupStream = markupStream; // If href contains URL query parameters .. String infoPath; // get the query string int queryStringPos = reference.indexOf("?"); if (queryStringPos != -1) { final String queryString = reference.substring(queryStringPos + 1); pageParameters = new PageParameters(); RequestUtils.decodeParameters(queryString, pageParameters); infoPath = reference.substring(0, queryStringPos); } else { pageParameters = null; infoPath = reference; } absolute = (infoPath.startsWith("/") || infoPath.startsWith("\\")); // remove file extension, but remember it String extension = null; int pos = infoPath.lastIndexOf("."); if (pos != -1) { extension = infoPath.substring(pos + 1); infoPath = infoPath.substring(0, pos); } String anchor = null; if (extension != null) { pos = extension.indexOf('#'); if (pos != -1) { anchor = extension.substring(pos); extension = extension.substring(0, pos); } } // Anchors without path, e.g. "#link" if (anchor == null) { pos = infoPath.indexOf("#"); if (pos != -1) { anchor = infoPath.substring(pos); infoPath = infoPath.substring(0, pos); } } path = infoPath; this.extension = extension; this.anchor = anchor; } /** * Gets the anchor (e.g. #top) * * @return anchor */ public final String getAnchor() { return anchor; } /** * Gets extension. * * @return extension */ public final String getExtension() { return extension; } /** * Gets pageParameters. * * @return pageParameters */ public final PageParameters getPageParameters() { return pageParameters; } /** * Gets path. * * @return path */ public final String getPath() { return path; } /** * Gets reference. * * @return reference */ public final String getReference() { return reference; } /** * Gets absolute. * * @return absolute */ public final boolean isAbsolute() { return absolute; } /** * Gets container. * * @return container */ public MarkupContainer getContainer() { return container; } /** * Gets markup stream * * @return markup stream */ public MarkupStream getMarkupStream() { return markupStream; } } /** * Resolves to anchor/ link components. */ private static final class AnchorResolverDelegate extends AbstractAutolinkResolverDelegate { /** the attribute to fetch. */ private static final String attribute = "href"; /** * Set of supported extensions for creating bookmarkable page links. Anything that is not in * this list will be handled as a resource reference. */ private final Set<String> supportedPageExtensions = new HashSet<>(4); /** * Construct. */ public AnchorResolverDelegate() { // Initialize supported list of file name extension which'll create // bookmarkable pages supportedPageExtensions.add("html"); supportedPageExtensions.add("xml"); supportedPageExtensions.add("wml"); supportedPageExtensions.add("svg"); } /** * @see org.apache.wicket.markup.resolver.AutoLinkResolver.IAutolinkResolverDelegate#newAutoComponent(java.lang.String, * org.apache.wicket.markup.resolver.AutoLinkResolver.PathInfo) */ @Override @SuppressWarnings("unchecked") public Component newAutoComponent(final String autoId, PathInfo pathInfo) { final MarkupContainer container = pathInfo.getContainer(); if ((pathInfo.extension != null) && supportedPageExtensions.contains(pathInfo.extension)) { // Obviously a href like href="myPkg.MyLabel.html" will do as // well. Wicket will not throw an exception. It accepts it. Page page = container.getPage(); final IClassResolver defaultClassResolver = page.getApplication() .getApplicationSettings() .getClassResolver(); String className = Packages.absolutePath(page.getClass(), pathInfo.path); className = Strings.replaceAll(className, "/", ".").toString(); if (className.startsWith(".")) { className = className.substring(1); } try { final Class<? extends Page> clazz = (Class<? extends Page>)defaultClassResolver.resolveClass(className); return new AutolinkBookmarkablePageLink<Void>(autoId, clazz, pathInfo.pageParameters, pathInfo.anchor); } catch (ClassNotFoundException ex) { log.warn("Did not find corresponding java class: " + className); // fall through } // Make sure base markup pages (inheritance) are handled correct MarkupContainer parentWithContainer = container; if (container.getParent() != null) { parentWithContainer = container.findParentWithAssociatedMarkup(); } if ((parentWithContainer instanceof Page) && !pathInfo.path.startsWith("/") && new MarkupStream(page.getMarkup()).isMergedMarkup()) { IMarkupFragment containerMarkup = container.getMarkup(); MarkupStream containerMarkupStream = new MarkupStream(containerMarkup); if (containerMarkupStream.atTag()) { ComponentTag tag = containerMarkupStream.getTag(); Class<? extends Page> clazz = (Class<? extends Page>)tag.getMarkupClass(); if (clazz != null) { // Href is relative. Resolve the url given relative to // the current page className = Packages.absolutePath(clazz, pathInfo.path); className = Strings.replaceAll(className, "/", ".").toString(); if (className.startsWith(".")) { className = className.substring(1); } try { clazz = (Class<? extends Page>)defaultClassResolver.resolveClass(className); return new AutolinkBookmarkablePageLink<Void>(autoId, clazz, pathInfo.getPageParameters(), pathInfo.anchor); } catch (ClassNotFoundException ex) { log.warn("Did not find corresponding java class: " + className); // fall through } } } } } else { // not a registered type for bookmarkable pages; create a link // to a resource instead return newPackageResourceReferenceAutoComponent(autoId, pathInfo, attribute); } // fallthrough return null; } } /** * Autolink components delegate component resolution to their parent components. Reason: * autolink tags don't have wicket:id and users wouldn't know where to add the component to. * * @author Juergen Donnerstag */ private final static class AutolinkExternalLink extends ExternalLink implements IComponentResolver { private static final long serialVersionUID = 1L; /** * Construct * * @param id * @param href */ public AutolinkExternalLink(final String id, final String href) { super(id, href); } /** * @see org.apache.wicket.markup.resolver.IComponentResolver#resolve(org.apache.wicket.MarkupContainer, * org.apache.wicket.markup.MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override public Component resolve(MarkupContainer container, MarkupStream markupStream, ComponentTag tag) { return getParent().get(tag.getId()); } } /** * Resolver that returns the proper attribute value from a component tag reflecting a URL * reference such as src or href. */ private interface ITagReferenceResolver { /** * Gets the reference attribute value of the tag depending on the type of the tag. For * instance, anchors use the <code>href</code> attribute but script and image references use * the <code>src</code> attribute. * * @param tag * The component tag. Not for modification. * @return the tag value that constitutes the reference */ String getReference(final ComponentTag tag); } /** * Autolink component that points to a {@link ResourceReference}. Autolink component delegate * component resolution to their parent components. Reason: autolink tags don't have wicket:id * and users wouldn't know where to add the component to. */ private final static class ResourceReferenceAutolink extends WebMarkupContainer implements IComponentResolver { private static final long serialVersionUID = 1L; private final String attribute; /** Resource reference */ private final ResourceReference resourceReference; private final MarkupContainer parent; /** * @param id * @param clazz * @param href * @param attribute * @param parent */ public ResourceReferenceAutolink(final String id, final Class<?> clazz, final String href, final String attribute, MarkupContainer parent) { super(id); this.parent = parent; this.attribute = attribute; // Check whether it is a valid resource reference if (PackageResource.exists(clazz, href, getLocale(), getStyle(), getVariation())) { // Create the component implementing the link resourceReference = new PackageResourceReference(clazz, href, null, null, null); } else { // The resource does not exist. Set to null and ignore when // rendering. resourceReference = null; } } /** * @see org.apache.wicket.Component#getVariation() */ @Override public String getVariation() { if (parent != null) { return parent.getVariation(); } return super.getVariation(); } /** * Handles this link's tag. * * @param tag * the component tag * @see org.apache.wicket.Component#onComponentTag(ComponentTag) */ @Override protected final void onComponentTag(final ComponentTag tag) { // Default handling for tag super.onComponentTag(tag); // only set the href attribute when the resource exists if (resourceReference != null) { // Set href to link to this link's linkClicked method ResourceReferenceRequestHandler handler = new ResourceReferenceRequestHandler( resourceReference); CharSequence url = getRequestCycle().urlFor(handler); // generate the href attribute tag.put(attribute, url); } } /** * @see org.apache.wicket.markup.resolver.IComponentResolver#resolve(org.apache.wicket.MarkupContainer, * org.apache.wicket.markup.MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override public Component resolve(MarkupContainer container, MarkupStream markupStream, ComponentTag tag) { return getParent().get(tag.getId()); } } /** * Resolves to {@link ResourceReference} link components. Typically used for header * contributions like javascript and css files. */ private static final class ResourceReferenceResolverDelegate extends AbstractAutolinkResolverDelegate { private final String attribute; /** * Construct. * * @param attribute */ public ResourceReferenceResolverDelegate(final String attribute) { this.attribute = attribute; } /** * @see org.apache.wicket.markup.resolver.AutoLinkResolver.IAutolinkResolverDelegate#newAutoComponent(java.lang.String, * org.apache.wicket.markup.resolver.AutoLinkResolver.PathInfo) */ @Override public Component newAutoComponent(final String autoId, final PathInfo pathInfo) { return newPackageResourceReferenceAutoComponent(autoId, pathInfo, attribute); } } /** * Resolver object that returns the proper attribute value from component tags. */ private static final class TagReferenceResolver implements ITagReferenceResolver { /** the attribute to fetch. */ private final String attribute; /** * Construct. * * @param attribute * the attribute to fetch */ public TagReferenceResolver(final String attribute) { this.attribute = attribute; } /** * Gets the reference attribute value of the tag depending on the type of the tag. For * instance, anchors use the <code>href</code> attribute but script and image references use * the <code>src</code> attribute. * * @param tag * The component tag. Not for modification. * @return the tag value that constitutes the reference */ @Override public String getReference(final ComponentTag tag) { return tag.getAttributes().getString(attribute); } } /** * If no specific resolver is found, always use the href attribute for references. */ private static final TagReferenceResolver DEFAULT_ATTRIBUTE_RESOLVER = new TagReferenceResolver( "href"); /** Logging */ private static final Logger log = LoggerFactory.getLogger(AutoLinkResolver.class); private static final long serialVersionUID = 1L; /** * Autolink resolver delegates for constructing new autolinks reference keyed on tag name (such * as <script> or <a>. */ private final Map<String, IAutolinkResolverDelegate> tagNameToAutolinkResolverDelegates = new HashMap<>(); /** * Resolver objects that know what attribute to read for getting the reference keyed on tag name * (such as <script> or <a>. */ private final Map<String, ITagReferenceResolver> tagNameToTagReferenceResolvers = new HashMap<>(); /** * Construct. */ public AutoLinkResolver() { // register tag reference resolvers TagReferenceResolver hrefTagReferenceResolver = new TagReferenceResolver("href"); TagReferenceResolver srcTagReferenceResolver = new TagReferenceResolver("src"); tagNameToTagReferenceResolvers.put("a", hrefTagReferenceResolver); tagNameToTagReferenceResolvers.put("link", hrefTagReferenceResolver); tagNameToTagReferenceResolvers.put("script", srcTagReferenceResolver); tagNameToTagReferenceResolvers.put("img", srcTagReferenceResolver); tagNameToTagReferenceResolvers.put("input", srcTagReferenceResolver); tagNameToTagReferenceResolvers.put("embed", srcTagReferenceResolver); // register autolink resolver delegates tagNameToAutolinkResolverDelegates.put("a", new AnchorResolverDelegate()); tagNameToAutolinkResolverDelegates.put("link", new ResourceReferenceResolverDelegate("href")); ResourceReferenceResolverDelegate srcResRefResolver = new ResourceReferenceResolverDelegate( "src"); tagNameToAutolinkResolverDelegates.put("script", srcResRefResolver); tagNameToAutolinkResolverDelegates.put("img", srcResRefResolver); tagNameToAutolinkResolverDelegates.put("input", srcResRefResolver); tagNameToAutolinkResolverDelegates.put("embed", srcResRefResolver); } /** * Register (add or replace) a new resolver with the tagName and attributeName. The resolver * will be invoked each time an appropriate tag and attribute is found. * * @param tagName * The tag name * @param attributeName * The attribute name * @param resolver * Implements what to do based on the tag and the attribute */ public final void addTagReferenceResolver(final String tagName, final String attributeName, final IAutolinkResolverDelegate resolver) { TagReferenceResolver tagReferenceResolver = new TagReferenceResolver(attributeName); tagNameToTagReferenceResolvers.put(tagName, tagReferenceResolver); tagNameToAutolinkResolverDelegates.put(tagName, resolver); } /** * Get the resolver registered for 'tagName' * * @param tagName * The tag's name * @return The resolver found. Null, if none registered */ public final IAutolinkResolverDelegate getAutolinkResolverDelegate(final String tagName) { return tagNameToAutolinkResolverDelegates.get(tagName); } /** * @see org.apache.wicket.markup.resolver.IComponentResolver#resolve(org.apache.wicket.MarkupContainer, * org.apache.wicket.markup.MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override public final Component resolve(final MarkupContainer container, final MarkupStream markupStream, final ComponentTag tag) { // Must be marked as autolink tag if (tag.isAutolinkEnabled()) { // get the reference resolver ITagReferenceResolver referenceResolver = tagNameToTagReferenceResolvers.get(tag.getName()); if (referenceResolver == null) { // fallback on default referenceResolver = DEFAULT_ATTRIBUTE_RESOLVER; } // get the reference, which is typically the value of e.g. a href or src // attribute String reference = referenceResolver.getReference(tag); // create the path info object PathInfo pathInfo = new PathInfo(reference, container, markupStream); // Try to find the Page matching the href // Note: to not use tag.getId() because it will be modified while // resolving the link and hence the 2nd render will fail. Component link = resolveAutomaticLink(pathInfo, tag); if (log.isDebugEnabled()) { log.debug("Added autolink " + link); } // Tell the container, we resolved the id return link; } // We were not able to resolve the id return null; } /** * Resolves the given tag's page class and page parameters by parsing the tag component name and * then searching for a page class at the absolute or relative URL specified by the href * attribute of the tag. * <p> * None html references are treated similar. * * @param pathInfo * The container where the link is * @param tag * the component tag * @return A BookmarkablePageLink<?> to handle the href */ private Component resolveAutomaticLink(final PathInfo pathInfo, final ComponentTag tag) { final String componentId = tag.getId(); // get the tag name, which is something like 'a' or 'script' final String tagName = tag.getName(); // By setting the component name, the tag becomes a Wicket component // tag, which must have a associated Component. if (tag.getId() == null) { tag.setAutoComponentTag(true); } // now get the resolver delegate IAutolinkResolverDelegate autolinkResolverDelegate = tagNameToAutolinkResolverDelegates.get(tagName); Component autoComponent = null; if (autolinkResolverDelegate != null) { autoComponent = autolinkResolverDelegate.newAutoComponent(componentId, pathInfo); } if (autoComponent == null) { // resolving didn't have the desired result or there was no delegate // found; fallback on the default resolving which is a simple // component that leaves the tag unchanged autoComponent = new WebMarkupContainer(componentId); } return autoComponent; } }