From 5830a1f96b24186a68023258630ef1d89590d31e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Leif=20=C3=85strand?= Date: Wed, 25 Feb 2015 16:24:29 +0200 Subject: [PATCH] Add pluggable mechanism for loading classes for a design (#16583) Change-Id: I2ac17e3c5a7c36492567238af8f4cf6723b0ec69 --- .../src/com/vaadin/ui/declarative/Design.java | 113 +++++++++++++++++ .../vaadin/ui/declarative/DesignContext.java | 51 ++------ .../tests/design/ComponentFactoryTest.java | 117 ++++++++++++++++++ 3 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 server/tests/src/com/vaadin/tests/design/ComponentFactoryTest.java diff --git a/server/src/com/vaadin/ui/declarative/Design.java b/server/src/com/vaadin/ui/declarative/Design.java index dc96e789bf..1b8585e6f6 100644 --- a/server/src/com/vaadin/ui/declarative/Design.java +++ b/server/src/com/vaadin/ui/declarative/Design.java @@ -57,6 +57,119 @@ import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; * @author Vaadin Ltd */ public class Design implements Serializable { + + /** + * Callback for creating instances of a given component class when reading + * designs. The default implementation, {@link DefaultComponentFactory} will + * use Class.forName(className).newInstance(), which might not + * be suitable e.g. in an OSGi environment or if the Component instances + * should be created as managed CDI beans. + *

+ * Use {@link Design#setComponentFactory(ComponentFactory)} to configure + * Vaadin to use a custom component factory. + * + * + * @since 7.4.1 + */ + public interface ComponentFactory extends Serializable { + /** + * Creates a component based on the fully qualified name derived from + * the tag name in the design. + * + * @param fullyQualifiedClassName + * the fully qualified name of the component to create + * @param context + * the design context for which the component is created + * + * @return a newly created component + */ + public Component createComponent(String fullyQualifiedClassName, + DesignContext context); + } + + /** + * Default implementation of {@link ComponentFactory}, using + * Class.forName(className).newInstance() for finding the + * component class and creating a component instance. + * + * @since 7.4.1 + */ + public static class DefaultComponentFactory implements ComponentFactory { + @Override + public Component createComponent(String fullyQualifiedClassName, + DesignContext context) { + Class componentClass = resolveComponentClass( + fullyQualifiedClassName, context); + + assert Component.class.isAssignableFrom(componentClass) : "resolveComponentClass returned " + + componentClass + " which is not a Vaadin Component class"; + + try { + return componentClass.newInstance(); + } catch (Exception e) { + throw new DesignException("Could not create component " + + fullyQualifiedClassName, e); + } + } + + /** + * Resolves a component class based on the fully qualified name of the + * class. + * + * @param qualifiedClassName + * the fully qualified name of the resolved class + * @param context + * the design context for which the class is resolved + * @return a component class object representing the provided class name + */ + protected Class resolveComponentClass( + String qualifiedClassName, DesignContext context) { + try { + Class componentClass = Class.forName(qualifiedClassName); + return componentClass.asSubclass(Component.class); + } catch (ClassNotFoundException e) { + throw new DesignException( + "Unable to load component for design", e); + } + } + + } + + private static volatile ComponentFactory componentFactory = new DefaultComponentFactory(); + + /** + * Sets the component factory that is used for creating component instances + * based on fully qualified class names derived from a design file. + *

+ * Please note that this setting is global, so care should be taken to avoid + * conflicting changes. + * + * @param componentFactory + * the component factory to set; not null + * + * @since 7.4.1 + */ + public static void setComponentFactory(ComponentFactory componentFactory) { + if (componentFactory == null) { + throw new IllegalArgumentException( + "Cannot set null component factory"); + } + Design.componentFactory = componentFactory; + } + + /** + * Gets the currently used component factory. + * + * @see #setComponentFactory(ComponentFactory) + * + * @return the component factory + * + * @since 7.4.1 + */ + public static ComponentFactory getComponentFactory() { + return componentFactory; + } + /** * Parses the given input stream into a jsoup document * diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java index 5f160d6f26..09fefd0a6b 100644 --- a/server/src/com/vaadin/ui/declarative/DesignContext.java +++ b/server/src/com/vaadin/ui/declarative/DesignContext.java @@ -31,6 +31,7 @@ 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; /** * This class contains contextual information that is collected when a component @@ -482,14 +483,17 @@ public class DesignContext implements Serializable { private Component instantiateComponent(Node node) { // Extract the package and class names. String qualifiedClassName = tagNameToClassName(node); - try { - Class componentClass = resolveComponentClass(qualifiedClassName); - Component newComponent = componentClass.newInstance(); - return newComponent; - } catch (Exception e) { - throw new DesignException("No component class could be found for " - + node.nodeName() + ".", e); + + ComponentFactory factory = Design.getComponentFactory(); + Component component = factory.createComponent(qualifiedClassName, this); + + if (component == null) { + throw new DesignException("Got unexpected null component from " + + factory.getClass().getName() + " for class " + + qualifiedClassName); } + + return component; } /** @@ -530,39 +534,6 @@ public class DesignContext implements Serializable { return packageName + "." + className; } - @SuppressWarnings("unchecked") - private Class resolveComponentClass( - String qualifiedClassName) throws ClassNotFoundException { - Class componentClass = null; - componentClass = Class.forName(qualifiedClassName); - - // Check that we're dealing with a Component. - if (isComponent(componentClass)) { - return (Class) componentClass; - } else { - throw new IllegalArgumentException(String.format( - "Resolved class %s is not a %s.", componentClass.getName(), - Component.class.getName())); - } - } - - /** - * Returns {@code true} if the given {@link Class} implements the - * {@link Component} interface of Vaadin Framework otherwise {@code false}. - * - * @param componentClass - * {@link Class} to check against {@link Component} interface. - * @return {@code true} if the given {@link Class} is a {@link Component}, - * {@code false} otherwise. - */ - private static boolean isComponent(Class componentClass) { - if (componentClass != null) { - return Component.class.isAssignableFrom(componentClass); - } else { - return false; - } - } - /** * Returns the root component of a created component hierarchy. * diff --git a/server/tests/src/com/vaadin/tests/design/ComponentFactoryTest.java b/server/tests/src/com/vaadin/tests/design/ComponentFactoryTest.java new file mode 100644 index 0000000000..a5f1d288a2 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/ComponentFactoryTest.java @@ -0,0 +1,117 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import org.junit.After; +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.DesignContext; +import com.vaadin.ui.declarative.DesignException; + +public class ComponentFactoryTest { + + private static final ComponentFactory defaultFactory = Design + .getComponentFactory(); + + private static final ThreadLocal currentComponentFactory = new ThreadLocal(); + + // Set static component factory that delegate to a thread local factory + static { + Design.setComponentFactory(new ComponentFactory() { + @Override + public Component createComponent(String fullyQualifiedClassName, + DesignContext context) { + ComponentFactory componentFactory = currentComponentFactory + .get(); + if (componentFactory == null) { + componentFactory = defaultFactory; + } + return componentFactory.createComponent( + fullyQualifiedClassName, context); + } + }); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullComponentFactory() { + Design.setComponentFactory(null); + } + + @Test + public void testComponentFactoryLogging() { + final List messages = new ArrayList(); + currentComponentFactory.set(new ComponentFactory() { + @Override + public Component createComponent(String fullyQualifiedClassName, + DesignContext context) { + messages.add("Requested class " + fullyQualifiedClassName); + return defaultFactory.createComponent(fullyQualifiedClassName, + context); + } + }); + + Design.read(new ByteArrayInputStream("".getBytes())); + + Assert.assertEquals("There should be one message logged", 1, + messages.size()); + Assert.assertEquals( + "Requested class " + Label.class.getCanonicalName(), + messages.get(0)); + } + + @Test(expected = DesignException.class) + public void testComponentFactoryReturningNull() { + currentComponentFactory.set(new ComponentFactory() { + @Override + public Component createComponent(String fullyQualifiedClassName, + DesignContext context) { + return null; + } + }); + + Design.read(new ByteArrayInputStream("".getBytes())); + } + + @Test(expected = DesignException.class) + public void testComponentFactoryThrowingStuff() { + currentComponentFactory.set(new ComponentFactory() { + @Override + public Component createComponent(String fullyQualifiedClassName, + DesignContext context) { + // Will throw because class is not found + return defaultFactory.createComponent("foobar." + + fullyQualifiedClassName, context); + } + }); + + Design.read(new ByteArrayInputStream("".getBytes())); + } + + @After + public void cleanup() { + currentComponentFactory.remove(); + } + +} -- 2.39.5