diff options
author | Leif Åstrand <leif@vaadin.com> | 2015-03-23 16:58:17 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2015-03-31 09:54:33 +0000 |
commit | 015cfe537fcbf1e31e57dd0c04a70d6408981eae (patch) | |
tree | d813a59426d6d03d1b2d1b70b5a0c2aba48acffa /server | |
parent | 103b329d328ab0dde95da9426462491be510a8be (diff) | |
download | vaadin-framework-015cfe537fcbf1e31e57dd0c04a70d6408981eae.tar.gz vaadin-framework-015cfe537fcbf1e31e57dd0c04a70d6408981eae.zip |
Allow customizing declarative tag names (#16933)
Change-Id: Icadaaab9166763e8e2086c6c114efd799ab580d6
Diffstat (limited to 'server')
3 files changed, 376 insertions, 95 deletions
diff --git a/server/src/com/vaadin/ui/declarative/Design.java b/server/src/com/vaadin/ui/declarative/Design.java index 1b8585e6f6..6634e5248a 100644 --- a/server/src/com/vaadin/ui/declarative/Design.java +++ b/server/src/com/vaadin/ui/declarative/Design.java @@ -33,6 +33,7 @@ import org.jsoup.parser.Parser; import org.jsoup.select.Elements; import com.vaadin.annotations.DesignRoot; +import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Component; import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent; import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; @@ -68,7 +69,6 @@ public class Design implements Serializable { * Use {@link Design#setComponentFactory(ComponentFactory)} to configure * Vaadin to use a custom component factory. * - * * @since 7.4.1 */ public interface ComponentFactory extends Serializable { @@ -88,6 +88,50 @@ public class Design implements Serializable { } /** + * Delegate for handling the mapping between tag names and component + * instances. + * <p> + * Use {@link Design#setComponentMapper(ComponentMapper)} to configure + * Vaadin to use a custom component mapper. + * + * @since + * @author Vaadin Ltd + */ + public interface ComponentMapper extends Serializable { + /** + * Resolves and creates a component using the provided component factory + * based on a tag name. + * <p> + * This method should be in sync with + * {@link #componentToTag(Component, DesignContext)} so that the + * resolved tag for a created component is the same as the tag for which + * the component was created. + * + * @param tag + * the tag name to create a component for + * @param componentFactory + * the component factory that actually creates a component + * based on a fully qualified class name + * @param context + * the design context for which the component is created + * @return a newly created component + */ + public Component tagToComponent(String tag, + ComponentFactory componentFactory, DesignContext context); + + /** + * Resolves a tag name from a component. + * + * @param component + * the component to get a tag name for + * @param context + * the design context for which the tag name is needed + * @return the tag name corresponding to the component + */ + public String componentToTag(Component component, DesignContext context); + } + + /** * Default implementation of {@link ComponentFactory}, using * <code>Class.forName(className).newInstance()</code> for finding the * component class and creating a component instance. @@ -135,7 +179,100 @@ public class Design implements Serializable { } + /** + * Default implementation of {@link ComponentMapper}, + * + * @since + */ + public static class DefaultComponentMapper implements ComponentMapper { + + @Override + public Component tagToComponent(String tagName, + ComponentFactory componentFactory, DesignContext context) { + // Extract the package and class names. + // Otherwise, get the full class name using the prefix to package + // mapping. Example: "v-vertical-layout" -> + // "com.vaadin.ui.VerticalLayout" + String[] parts = tagName.split("-", 2); + if (parts.length < 2) { + throw new DesignException("The tagname '" + tagName + + "' is invalid: missing prefix."); + } + String prefixName = parts[0]; + String packageName = context.getPackage(prefixName); + if (packageName == null) { + throw new DesignException("Unknown tag: " + tagName); + } + String[] classNameParts = parts[1].split("-"); + String className = ""; + for (String classNamePart : classNameParts) { + // Split will ignore trailing and multiple dashes but that + // should be + // ok + // <v-button--> will be resolved to <v-button> + // <v--button> will be resolved to <v-button> + className += SharedUtil.capitalize(classNamePart); + } + String qualifiedClassName = packageName + "." + className; + + Component component = componentFactory.createComponent( + qualifiedClassName, context); + + if (component == null) { + throw new DesignException("Got unexpected null component from " + + componentFactory.getClass().getName() + " for class " + + qualifiedClassName); + } + + return component; + } + + @Override + public String componentToTag(Component component, DesignContext context) { + Class<?> componentClass = component.getClass(); + String packageName = componentClass.getPackage().getName(); + String prefix = context.getPackagePrefix(packageName); + if (prefix == null) { + prefix = packageName.replace('.', '_'); + context.addPackagePrefix(prefix, packageName); + } + prefix = prefix + "-"; + String className = classNameToElementName(componentClass + .getSimpleName()); + String tagName = prefix + className; + + return tagName; + } + + /** + * Creates the name of the html tag corresponding to the given class + * name. The name is derived by converting each uppercase letter to + * lowercase and inserting a dash before the letter. No dash is inserted + * before the first letter of the class name. + * + * @param className + * the name of the class without a package name + * @return the html tag name corresponding to className + */ + private String classNameToElementName(String className) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < className.length(); i++) { + Character c = className.charAt(i); + if (Character.isUpperCase(c)) { + if (i > 0) { + result.append("-"); + } + result.append(Character.toLowerCase(c)); + } else { + result.append(c); + } + } + return result.toString(); + } + } + private static volatile ComponentFactory componentFactory = new DefaultComponentFactory(); + private static volatile ComponentMapper componentMapper = new DefaultComponentMapper(); /** * Sets the component factory that is used for creating component instances @@ -171,6 +308,39 @@ public class Design implements Serializable { } /** + * Sets the component mapper that is used for resolving between tag names + * and component instances. + * <p> + * Please note that this setting is global, so care should be taken to avoid + * conflicting changes. + * + * @param componentMapper + * the component mapper to set; not <code>null</code> + * + * @since + */ + public static void setComponentMapper(ComponentMapper componentMapper) { + if (componentMapper == null) { + throw new IllegalArgumentException( + "Cannot set null component mapper"); + } + Design.componentMapper = componentMapper; + } + + /** + * Gets the currently used component mapper. + * + * @see #setComponentMapper(ComponentMapper) + * + * @return the component mapper + * + * @since + */ + public static ComponentMapper getComponentMapper() { + return componentMapper; + } + + /** * Parses the given input stream into a jsoup document * * @param html diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java index 218774c72d..f991b3013a 100644 --- a/server/src/com/vaadin/ui/declarative/DesignContext.java +++ b/server/src/com/vaadin/ui/declarative/DesignContext.java @@ -17,6 +17,8 @@ package com.vaadin.ui.declarative; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,10 +30,10 @@ import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import com.vaadin.annotations.DesignRoot; -import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Component; import com.vaadin.ui.HasComponents; import com.vaadin.ui.declarative.Design.ComponentFactory; +import com.vaadin.ui.declarative.Design.ComponentMapper; /** * This class contains contextual information that is collected when a component @@ -79,7 +81,7 @@ public class DesignContext implements Serializable { defaultPrefixes.put("v", "com.vaadin.ui"); for (String prefix : defaultPrefixes.keySet()) { String packageName = defaultPrefixes.get(prefix); - mapPrefixToPackage(prefix, packageName); + addPackagePrefix(prefix, packageName); } } @@ -236,20 +238,65 @@ public class DesignContext implements Serializable { } /** - * Creates a two-way mapping between a prefix and a package name. Return - * true if prefix was already mapped to some package name or packageName to - * some prefix. + * Creates a two-way mapping between a prefix and a package name. * * @param prefix * the prefix name without an ending dash (for instance, "v" is - * always used for "com.vaadin.ui") + * by default used for "com.vaadin.ui") * @param packageName * the name of the package corresponding to prefix - * @return whether there was a mapping from prefix to some package name or - * from packageName to some prefix. + * + * @see #getPackagePrefixes() + * @see #getPackagePrefix(String) + * @see #getPackage(String) + * @since + */ + public void addPackagePrefix(String prefix, String packageName) { + twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix); + } + + /** + * Gets the prefix mapping for a given package, or <code>null</code> if + * there is no mapping for the package. + * + * @see #addPackagePrefix(String, String) + * @see #getPackagePrefixes() + * + * @since + * @param packageName + * the package name to get a prefix for + * @return the prefix for the package, or <code>null</code> if no prefix is + * registered + */ + public String getPackagePrefix(String packageName) { + return packageToPrefix.get(packageName); + } + + /** + * Gets all registered package prefixes. + * + * + * @since + * @see #getPackage(String) + * @return a collection of package prefixes */ - private boolean mapPrefixToPackage(String prefix, String packageName) { - return twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix); + public Collection<String> getPackagePrefixes() { + return Collections.unmodifiableCollection(prefixToPackage.keySet()); + } + + /** + * Gets the package corresponding to the give prefix, or <code>null</code> + * no package has been registered for the prefix + * + * @since + * @see #addPackagePrefix(String, String) + * @param prefix + * the prefix to find a package for + * @return the package prefix, or <code>null</code> if no package is + * registered for the provided prefix + */ + public String getPackage(String prefix) { + return prefixToPackage.get(prefix); } /** @@ -309,8 +356,7 @@ public class DesignContext implements Serializable { } String prefixName = parts[0]; String packageName = parts[1]; - twoWayMap(prefixName, packageName, prefixToPackage, - packageToPrefix); + addPackagePrefix(prefixName, packageName); } } } @@ -328,14 +374,13 @@ public class DesignContext implements Serializable { */ public void writePackageMappings(Document doc) { Element head = doc.head(); - for (String prefix : prefixToPackage.keySet()) { + for (String prefix : getPackagePrefixes()) { // Only store the prefix-name mapping if it is not a default mapping // (such as "v" -> "com.vaadin.ui") if (defaultPrefixes.get(prefix) == null) { Node newNode = doc.createElement("meta"); newNode.attr("name", "package-mapping"); - String prefixToPackageName = prefix + ":" - + prefixToPackage.get(prefix); + String prefixToPackageName = prefix + ":" + getPackage(prefix); newNode.attr("content", prefixToPackageName); head.appendChild(newNode); } @@ -355,17 +400,11 @@ public class DesignContext implements Serializable { * childComponent. */ public Element createElement(Component childComponent) { - Class<?> componentClass = childComponent.getClass(); - String packageName = componentClass.getPackage().getName(); - String prefix = packageToPrefix.get(packageName); - if (prefix == null) { - prefix = packageName.replace('.', '_'); - twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix); - } - prefix = prefix + "-"; - String className = classNameToElementName(componentClass - .getSimpleName()); - Element newElement = doc.createElement(prefix + className); + ComponentMapper componentMapper = Design.getComponentMapper(); + + String tagName = componentMapper.componentToTag(childComponent, this); + + Element newElement = doc.createElement(tagName); childComponent.writeDesign(newElement, this); // Handle the local id. Global id and caption should have been taken // care of by writeDesign. @@ -377,32 +416,6 @@ public class DesignContext implements Serializable { } /** - * Creates the name of the html tag corresponding to the given class name. - * The name is derived by converting each uppercase letter to lowercase and - * inserting a dash before the letter. No dash is inserted before the first - * letter of the class name. - * - * @param className - * the name of the class without a package name - * @return the html tag name corresponding to className - */ - private String classNameToElementName(String className) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < className.length(); i++) { - Character c = className.charAt(i); - if (Character.isUpperCase(c)) { - if (i > 0) { - result.append("-"); - } - result.append(Character.toLowerCase(c)); - } else { - result.append(c); - } - } - return result.toString(); - } - - /** * Reads the given design node and creates the corresponding component tree * * @param componentDesign @@ -473,15 +486,22 @@ public class DesignContext implements Serializable { * @return a Component corresponding to node, with no attributes set. */ private Component instantiateComponent(Node node) { - // Extract the package and class names. - String qualifiedClassName = tagNameToClassName(node); + String tag = node.nodeName(); + + ComponentMapper componentMapper = Design.getComponentMapper(); + Component component = componentMapper.tagToComponent(tag, + Design.getComponentFactory(), this); - return instantiateClass(qualifiedClassName); + assert tag.equals(componentMapper.componentToTag(component, this)); + + return component; } /** * Instantiates given class via ComponentFactory. - * @param qualifiedClassName class name to instantiate + * + * @param qualifiedClassName + * class name to instantiate * @return instance of a given class */ private Component instantiateClass(String qualifiedClassName) { @@ -498,44 +518,6 @@ public class DesignContext implements Serializable { } /** - * Returns the qualified class name corresponding to the given html tree - * node. The class name is extracted from the tag name of node. - * - * @param node - * an html tree node - * @return The qualified class name corresponding to the given node. - */ - private String tagNameToClassName(Node node) { - String tagName = node.nodeName(); - if (tagName.equals("v-addon")) { - return node.attr("class"); - } - // Otherwise, get the full class name using the prefix to package - // mapping. Example: "v-vertical-layout" -> - // "com.vaadin.ui.VerticalLayout" - String[] parts = tagName.split("-", 2); - if (parts.length < 2) { - throw new DesignException("The tagname '" + tagName - + "' is invalid: missing prefix."); - } - String prefixName = parts[0]; - String packageName = prefixToPackage.get(prefixName); - if (packageName == null) { - throw new DesignException("Unknown tag: " + tagName); - } - String[] classNameParts = parts[1].split("-"); - String className = ""; - for (String classNamePart : classNameParts) { - // Split will ignore trailing and multiple dashes but that should be - // ok - // <v-button--> will be resolved to <v-button> - // <v--button> will be resolved to <v-button> - className += SharedUtil.capitalize(classNamePart); - } - return packageName + "." + className; - } - - /** * Returns the root component of a created component hierarchy. * * @return the root component of the hierarchy diff --git a/server/tests/src/com/vaadin/tests/design/ComponentMapperTest.java b/server/tests/src/com/vaadin/tests/design/ComponentMapperTest.java new file mode 100644 index 0000000000..c6e8c15109 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/ComponentMapperTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed 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 com.vaadin.tests.design; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.ui.Component; +import com.vaadin.ui.Label; +import com.vaadin.ui.declarative.Design; +import com.vaadin.ui.declarative.Design.ComponentFactory; +import com.vaadin.ui.declarative.Design.ComponentMapper; +import com.vaadin.ui.declarative.DesignContext; + +public class ComponentMapperTest { + private static final ComponentMapper defaultMapper = Design + .getComponentMapper(); + + private static final ThreadLocal<ComponentMapper> currentMapper = new ThreadLocal<ComponentMapper>(); + + static { + Design.setComponentMapper(new ComponentMapper() { + @Override + public Component tagToComponent(String tag, + ComponentFactory componentFactory, DesignContext context) { + return getActualMapper().tagToComponent(tag, componentFactory, + context); + } + + @Override + public String componentToTag(Component component, + DesignContext context) { + return getActualMapper().componentToTag(component, context); + } + + private ComponentMapper getActualMapper() { + ComponentMapper mapper = currentMapper.get(); + if (mapper == null) { + mapper = defaultMapper; + } + return mapper; + } + }); + } + + private final class CustomComponentMapper extends + Design.DefaultComponentMapper { + @Override + public Component tagToComponent(String tag, + ComponentFactory componentFactory, DesignContext context) { + if (tag.startsWith("custom-")) { + ComponentWithCustomTagName component = (ComponentWithCustomTagName) componentFactory + .createComponent( + ComponentWithCustomTagName.class.getName(), + context); + component.tagName = tag; + return component; + } else { + return super.tagToComponent(tag, componentFactory, context); + } + } + + @Override + public String componentToTag(Component component, DesignContext context) { + if (component instanceof ComponentWithCustomTagName) { + ComponentWithCustomTagName withCustomTagName = (ComponentWithCustomTagName) component; + return withCustomTagName.tagName; + } else { + return super.componentToTag(component, context); + } + } + } + + public static class ComponentWithCustomTagName extends Label { + private String tagName; + } + + @Test + public void testCustomComponentMapperRead() { + currentMapper.set(new CustomComponentMapper()); + + Component component = Design.read(new ByteArrayInputStream( + "<custom-foobar />".getBytes())); + + Assert.assertTrue("<custom-foobar> should resolve " + + ComponentWithCustomTagName.class.getSimpleName(), + component instanceof ComponentWithCustomTagName); + Assert.assertEquals("custom-foobar", + ((ComponentWithCustomTagName) component).tagName); + } + + @Test + public void testCustomComponentMapperWrite() throws IOException { + currentMapper.set(new CustomComponentMapper()); + + ComponentWithCustomTagName component = new ComponentWithCustomTagName(); + component.tagName = "custom-special"; + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Design.write(component, bos); + String writtenDesign = new String(bos.toByteArray()); + + Assert.assertTrue( + "Written design should contain \"<custom-special\", but instead got " + + writtenDesign, + writtenDesign.contains("<custom-special")); + } + + public void cleanup() { + currentMapper.remove(); + } +} |