summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorLeif Åstrand <leif@vaadin.com>2015-03-23 16:58:17 +0200
committerVaadin Code Review <review@vaadin.com>2015-03-31 09:54:33 +0000
commit015cfe537fcbf1e31e57dd0c04a70d6408981eae (patch)
treed813a59426d6d03d1b2d1b70b5a0c2aba48acffa /server
parent103b329d328ab0dde95da9426462491be510a8be (diff)
downloadvaadin-framework-015cfe537fcbf1e31e57dd0c04a70d6408981eae.tar.gz
vaadin-framework-015cfe537fcbf1e31e57dd0c04a70d6408981eae.zip
Allow customizing declarative tag names (#16933)
Change-Id: Icadaaab9166763e8e2086c6c114efd799ab580d6
Diffstat (limited to 'server')
-rw-r--r--server/src/com/vaadin/ui/declarative/Design.java172
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignContext.java170
-rw-r--r--server/tests/src/com/vaadin/tests/design/ComponentMapperTest.java129
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();
+ }
+}