]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add pluggable mechanism for loading classes for a design (#16583)
authorLeif Åstrand <leif@vaadin.com>
Wed, 25 Feb 2015 14:24:29 +0000 (16:24 +0200)
committerVaadin Code Review <review@vaadin.com>
Thu, 26 Feb 2015 11:26:24 +0000 (11:26 +0000)
Change-Id: I2ac17e3c5a7c36492567238af8f4cf6723b0ec69

server/src/com/vaadin/ui/declarative/Design.java
server/src/com/vaadin/ui/declarative/DesignContext.java
server/tests/src/com/vaadin/tests/design/ComponentFactoryTest.java [new file with mode: 0644]

index dc96e789bf78a0ed918c09b3d3dbb5ea2fffaea1..1b8585e6f60379498c39551cbcbdb346b2f16cc8 100644 (file)
@@ -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 <code>Class.forName(className).newInstance()</code>, which might not
+     * be suitable e.g. in an OSGi environment or if the Component instances
+     * should be created as managed CDI beans.
+     * <p>
+     * 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
+     * <code>Class.forName(className).newInstance()</code> 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<? extends Component> 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<? extends Component> 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.
+     * <p>
+     * Please note that this setting is global, so care should be taken to avoid
+     * conflicting changes.
+     * 
+     * @param componentFactory
+     *            the component factory to set; not <code>null</code>
+     * 
+     * @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
      * 
index 5f160d6f26680c48656d75abee44d3ff65de857b..09fefd0a6be6cc4c494836c4f1dabf246b0e1c50 100644 (file)
@@ -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<? extends Component> 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<? extends Component> 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<? extends Component>) 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 (file)
index 0000000..a5f1d28
--- /dev/null
@@ -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<ComponentFactory> currentComponentFactory = new ThreadLocal<ComponentFactory>();
+
+    // 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<String> messages = new ArrayList<String>();
+        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("<v-label />".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("<v-label />".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("<v-label />".getBytes()));
+    }
+
+    @After
+    public void cleanup() {
+        currentComponentFactory.remove();
+    }
+
+}