aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatti Hosio <mhosio@vaadin.com>2014-12-05 11:27:29 +0200
committerVaadin Code Review <review@vaadin.com>2014-12-08 18:19:47 +0000
commit18b333ee3dfd171956829e97a32baa8069346c65 (patch)
treed02139df7b7843b06b70629c35c7954d689826af
parentbd78a4c8b2eb84cbba3bc686bcb40b19fd847640 (diff)
downloadvaadin-framework-18b333ee3dfd171956829e97a32baa8069346c65.tar.gz
vaadin-framework-18b333ee3dfd171956829e97a32baa8069346c65.zip
Support for automatic binding of the fields of the root layout (#7749)
The fields are bound to the components generated when parsing the design. The binding is done based on localId, id or caption. Change-Id: I32ecac3cb76737c9d9d05a123db85fe65d55369c
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignContext.java131
-rw-r--r--server/src/com/vaadin/ui/declarative/FieldBinder.java247
-rw-r--r--server/src/com/vaadin/ui/declarative/FieldBindingException.java34
-rw-r--r--server/src/com/vaadin/ui/declarative/LayoutHandler.java134
-rw-r--r--server/tests/src/com/vaadin/tests/layoutparser/InvalidLayoutTemplate.java55
-rw-r--r--server/tests/src/com/vaadin/tests/layoutparser/LayoutTemplate.java49
-rw-r--r--server/tests/src/com/vaadin/tests/layoutparser/ParseLayoutTest.java40
7 files changed, 670 insertions, 20 deletions
diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java
index a846edff2c..36d0162910 100644
--- a/server/src/com/vaadin/ui/declarative/DesignContext.java
+++ b/server/src/com/vaadin/ui/declarative/DesignContext.java
@@ -15,8 +15,10 @@
*/
package com.vaadin.ui.declarative;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -67,6 +69,9 @@ public class DesignContext {
// to "com.vaadin.ui".
private Map<String, String> defaultPrefixes = new HashMap<String, String>();
+ // component creation listeners
+ private List<ComponentCreationListener> listeners = new ArrayList<ComponentCreationListener>();
+
public DesignContext(Document doc) {
this.doc = doc;
// Initialize the mapping between prefixes and package names.
@@ -393,12 +398,31 @@ public class DesignContext {
* @param componentDesign
* The html tree node containing the description of the component
* to be created.
- * @return a DesignSynchronizable object corresponding to componentDesign,
- * with no attributes set.
+ * @return a DesignSynchronizable object corresponding to componentDesign
*/
public DesignSynchronizable createChild(Element componentDesign) {
// Create the component.
DesignSynchronizable component = instantiateComponent(componentDesign);
+ synchronizeAndRegister(component, componentDesign);
+ fireComponentCreatedEvent(componentToLocalId.get(component), component);
+ return component;
+ }
+
+ /**
+ * Calls synchronizeFromDesign() for the given component and passes the
+ * given component design as a parameter. This creates the entire component
+ * hierarchy rooted at the given component. Also registers the componentid,
+ * localId and caption of the given component and all its children to the
+ * context
+ *
+ *
+ * @param component
+ * The component to be synchronized from design
+ * @param componentDesign
+ * The html tree node containing the description of the component
+ */
+ public void synchronizeAndRegister(DesignSynchronizable component,
+ Element componentDesign) {
component.synchronizeFromDesign(componentDesign, this);
// Get the ids and the caption of the component and store them in the
// maps of this design context.
@@ -431,7 +455,6 @@ public class DesignContext {
if (caption != null) {
mapCaption(caption, component);
}
- return component;
}
/**
@@ -550,4 +573,106 @@ public class DesignContext {
public void setComponentRoot(DesignSynchronizable componentRoot) {
this.componentRoot = componentRoot;
}
+
+ /**
+ * Adds a component creation listener. The listener will be notified when
+ * components are created while parsing a design template
+ *
+ * @param listener
+ * the component creation listener to be added
+ */
+ public void addComponentCreationListener(ComponentCreationListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a component creation listener.
+ *
+ * @param listener
+ * the component creation listener to be removed
+ */
+ public void removeComponentCreationListener(
+ ComponentCreationListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Fires component creation event
+ *
+ * @param localId
+ * localId of the component
+ * @param component
+ * the component that was created
+ */
+ private void fireComponentCreatedEvent(String localId,
+ DesignSynchronizable component) {
+ ComponentCreatedEvent event = new ComponentCreatedEvent(localId,
+ component);
+ for (ComponentCreationListener listener : listeners) {
+ listener.componentCreated(event);
+ }
+ }
+
+ /**
+ * Interface to be implemented by component creation listeners
+ *
+ * @author Vaadin Ltd
+ */
+ public interface ComponentCreationListener {
+
+ /**
+ * Called when component has been created in the design context
+ *
+ * @param event
+ * the component creation event containing information on the
+ * created component
+ */
+ public void componentCreated(ComponentCreatedEvent event);
+ }
+
+ /**
+ * Component creation event that is fired when a component is created in the
+ * context
+ *
+ * @author Vaadin Ltd
+ */
+ public class ComponentCreatedEvent {
+ private String localId;
+ private DesignSynchronizable component;
+ private DesignContext context;
+
+ /**
+ * Creates a new instance of ComponentCreatedEvent
+ *
+ * @param localId
+ * the local id of the created component
+ * @param component
+ * the created component
+ */
+ private ComponentCreatedEvent(String localId,
+ DesignSynchronizable component) {
+ this.localId = localId;
+ this.component = component;
+ context = DesignContext.this;
+ }
+
+ /**
+ * Returns the local id of the created component or null if not exist
+ *
+ * @return the localId
+ */
+ public String getLocalId() {
+ return localId;
+ }
+
+ /**
+ * Returns the created component
+ *
+ * @return the component
+ */
+ public DesignSynchronizable getComponent() {
+ return component;
+ }
+ }
+
}
diff --git a/server/src/com/vaadin/ui/declarative/FieldBinder.java b/server/src/com/vaadin/ui/declarative/FieldBinder.java
new file mode 100644
index 0000000000..c533af15bd
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/FieldBinder.java
@@ -0,0 +1,247 @@
+/*
+ * 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.ui.declarative;
+
+import java.beans.IntrospectionException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.vaadin.ui.Component;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * Binder utility that binds member fields of a design class instance to given
+ * component instances. Only fields of type {@link Component} are bound
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FieldBinder {
+
+ // the design class instance (the instance containing the bound fields)
+ private Component bindTarget;
+ // mapping between field names and Fields
+ private Map<String, Field> fieldMap = new HashMap<String, Field>();
+
+ /**
+ * Creates a new instance of LayoutFieldBinder
+ *
+ * @param design
+ * the design class instance containing the bound fields
+ * @throws IntrospectionException
+ * if the given design class can not be introspected
+ */
+ public FieldBinder(Component design) throws IntrospectionException {
+ if (design == null) {
+ throw new IllegalArgumentException("The design must not be null");
+ }
+ bindTarget = design;
+ resolveFields();
+ }
+
+ /**
+ * Returns true if all the fields assignable to Component are bound
+ *
+ * @return true if all the fields assignable to Component are bound
+ */
+ public boolean isAllFieldsBound() throws FieldBindingException {
+ boolean success = true;
+ for (Field f : fieldMap.values()) {
+ try {
+ Object value = ReflectTools.getJavaFieldValue(bindTarget, f);
+ if (value == null) {
+ success = false;
+ getLogger().severe("Found unbound field :" + f.getName());
+ }
+ } catch (IllegalArgumentException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ } catch (IllegalAccessException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Resolves the fields of the design class instance
+ */
+ private void resolveFields() {
+ for (Field memberField : getFieldsInDeclareOrder(bindTarget.getClass())) {
+ if (Component.class.isAssignableFrom(memberField.getType())) {
+ fieldMap.put(memberField.getName(), memberField);
+ }
+ }
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The name of the bound field is constructed based on the
+ * id or caption of the instance, depending on which one is defined. If a
+ * field is already bound (not null), {@link FieldBindingException} is
+ * thrown.
+ *
+ * @param instance
+ * the instance to be bound to a field
+ * @return true on success, otherwise false
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ public boolean bindField(Component instance) {
+ return bindField(instance, null);
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The fields are matched based on localId, id and caption.
+ * If a field is already bound (not null), {@link FieldBindingException} is
+ * thrown.
+ *
+ * @param instance
+ * the instance to be bound to a field
+ * @param localId
+ * the localId used for mapping the field to an instance field
+ * @return true on success
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ public boolean bindField(Component instance, String localId) {
+ // check that the field exists, is correct type and is null
+ boolean success = bindFieldByIdentifier(localId, instance);
+ if (!success) {
+ success = bindFieldByIdentifier(instance.getId(), instance);
+ }
+ if (!success) {
+ success = bindFieldByIdentifier(instance.getCaption(), instance);
+ }
+ if (!success) {
+ String idInfo = "localId: " + localId + " id: " + instance.getId()
+ + " caption: " + instance.getCaption();
+ getLogger().info(
+ "Could not bind component to a field "
+ + instance.getClass().getName() + " " + idInfo);
+ }
+ return success;
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The field is matched based on the given identifier. If a
+ * field is already bound (not null), {@link FieldBindingException} is
+ * thrown.
+ *
+ * @param identifier
+ * the identifier for the field.
+ * @param instance
+ * the instance to be bound to a field
+ * @return true on success
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ private boolean bindFieldByIdentifier(String identifier, Component instance) {
+ try {
+ // create and validate field name
+ String fieldName = asFieldName(identifier);
+ if (fieldName.length() == 0) {
+ return false;
+ }
+ // validate that the field can be found
+ Field field = fieldMap.get(fieldName);
+ if (field == null) {
+ getLogger().fine(
+ "No field was found by identifier " + identifier);
+ return false;
+ }
+ // validate that the field is not set
+ Object fieldValue = ReflectTools.getJavaFieldValue(bindTarget,
+ field);
+ if (fieldValue != null) {
+ getLogger().severe(
+ "The field with identifier \"" + identifier
+ + "\" already mapped");
+ throw new FieldBindingException(
+ "Duplicate identifier found for a field");
+ }
+ // set the field value
+ ReflectTools.setJavaFieldValue(bindTarget, field, instance);
+ return true;
+ } catch (IllegalAccessException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ } catch (IllegalArgumentException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ }
+ }
+
+ /**
+ * Converts the given identifier to a valid field name by stripping away
+ * illegal character and setting the first letter of the name to lowercase
+ *
+ * @param identifier
+ * the identifier to be converted to field name
+ * @return the field name corresponding the identifier
+ */
+ private static String asFieldName(String identifier) {
+ if (identifier == null) {
+ return "";
+ }
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < identifier.length(); i++) {
+ char character = identifier.charAt(i);
+ if (Character.isJavaIdentifierPart(character)) {
+ result.append(character);
+ }
+ }
+ // lowercase first letter
+ if (result.length() > 0 && Character.isLetter(result.charAt(0))) {
+ result.setCharAt(0, Character.toLowerCase(result.charAt(0)));
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns an array containing Field objects reflecting all the fields of
+ * the class or interface represented by this Class object. The elements in
+ * the array returned are sorted in declare order. The fields in
+ * superclasses are excluded.
+ *
+ * @param searchClass
+ * the class to be scanned for fields
+ * @return the list of fields in this class
+ */
+ protected static List<java.lang.reflect.Field> getFieldsInDeclareOrder(
+ Class<?> searchClass) {
+ ArrayList<java.lang.reflect.Field> memberFieldsInOrder = new ArrayList<java.lang.reflect.Field>();
+
+ for (java.lang.reflect.Field memberField : searchClass
+ .getDeclaredFields()) {
+ memberFieldsInOrder.add(memberField);
+ }
+ return memberFieldsInOrder;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(FieldBinder.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/FieldBindingException.java b/server/src/com/vaadin/ui/declarative/FieldBindingException.java
new file mode 100644
index 0000000000..d8b587a14c
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/FieldBindingException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.ui.declarative;
+
+/**
+ * Exception that is thrown when an error occurs during field binding when
+ * reading a design template
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FieldBindingException extends RuntimeException {
+
+ public FieldBindingException(String message) {
+ super(message);
+ }
+
+ public FieldBindingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/server/src/com/vaadin/ui/declarative/LayoutHandler.java b/server/src/com/vaadin/ui/declarative/LayoutHandler.java
index 84d454bb01..c2efe8eb32 100644
--- a/server/src/com/vaadin/ui/declarative/LayoutHandler.java
+++ b/server/src/com/vaadin/ui/declarative/LayoutHandler.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.ui.declarative;
+import java.beans.IntrospectionException;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -25,8 +26,11 @@ import org.jsoup.nodes.DocumentType;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.parser.Parser;
+import org.jsoup.select.Elements;
import com.vaadin.ui.DesignSynchronizable;
+import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent;
+import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener;
/**
* LayoutHandler is used for parsing a component hierarchy from an html file
@@ -61,7 +65,39 @@ public class LayoutHandler {
} catch (IOException e) {
throw new DesignException("The html document cannot be parsed.");
}
- return parse(doc);
+ return parse(doc, null);
+ }
+
+ /**
+ * Constructs a component hierarchy from the design specified as an html
+ * document. The component hierarchy must contain exactly one top-level
+ * Component. The component should be located under <body>, but also invalid
+ * html containing the hierarchy without <html>, <head> and <body> tags is
+ * accepted. You can optionally pass instance for the root component with
+ * some uninitialized instance fields. The fields will be automatically
+ * populated when parsing the design based on the component ids, local ids,
+ * and captions of the components in the design.
+ *
+ * @param html
+ * the html document describing the component design
+ * @param rootInstance
+ * the root instance with fields to be mapped to components in
+ * the design
+ * @return the DesignContext created while traversing the tree. The
+ * top-level component of the created component hierarchy can be
+ * accessed using result.getRootComponent(), where result is the
+ * object returned by this method.
+ * @throws IOException
+ */
+ public static DesignContext parse(InputStream html,
+ DesignSynchronizable rootInstance) {
+ Document doc;
+ try {
+ doc = Jsoup.parse(html, "UTF-8", "", Parser.htmlParser());
+ } catch (IOException e) {
+ throw new DesignException("The html document cannot be parsed.");
+ }
+ return parse(doc, rootInstance);
}
/**
@@ -81,36 +117,100 @@ public class LayoutHandler {
*/
public static DesignContext parse(String html) {
Document doc = Jsoup.parse(html);
- return parse(doc);
+ return parse(doc, null);
}
/**
* Constructs a component hierarchy from the design specified as an html
- * tree.
+ * document given as a string. The component hierarchy must contain exactly
+ * one top-level Component. The component should be located under <body>,
+ * but also invalid html containing the hierarchy without <html>, <head> and
+ * <body> tags is accepted. You can optionally pass instance for the root
+ * component with some uninitialized instance fields. The fields will be
+ * automatically populated when parsing the design based on the component
+ * ids, local ids, and captions of the components in the design.
*
+ * @param html
+ * the html document describing the component design
+ * @param rootInstance
+ * the root instance with fields to be mapped to components in
+ * the design
+ * @return the DesignContext created while traversing the tree. The
+ * top-level component of the created component hierarchy can be
+ * accessed using result.getRootComponent(), where result is the
+ * object returned by this method.
+ * @throws IOException
*/
- private static DesignContext parse(Document doc) {
+ public static DesignContext parse(String html,
+ DesignSynchronizable rootInstance) {
+ Document doc = Jsoup.parse(html);
+ return parse(doc, rootInstance);
+ }
+
+ /**
+ * Constructs a component hierarchy from the design specified as an html
+ * tree. If componentRoot is not null, the component instances created
+ * during synchronizing the design are assigned to its member fields based
+ * on their id, localId, and caption
+ *
+ * @param doc
+ * the html tree
+ * @param componentRoot
+ * optional component root instance with some member fields. The
+ * type must match the type of the root element in the design.
+ * The member fields whose type is assignable from
+ * {@link DesignSynchronizable} are set when parsing the
+ * component tree
+ *
+ */
+ private static DesignContext parse(Document doc,
+ DesignSynchronizable componentRoot) {
DesignContext designContext = new DesignContext(doc);
designContext.getPrefixes(doc);
// No special handling for a document without a body element - should be
// taken care of by jsoup.
Element root = doc.body();
- DesignSynchronizable componentRoot = null;
- for (Node element : root.childNodes()) {
- if (element instanceof Element) {
- if (componentRoot != null) {
- throw new DesignException(
- "The first level of a component hierarchy should contain a single root component, but found "
- + "two: "
- + componentRoot
- + " and "
- + element + ".");
+ Elements children = root.children();
+ if (children.size() != 1) {
+ throw new DesignException(
+ "The first level of a component hierarchy should contain exactly one root component, but found "
+ + children.size());
+ }
+ Element element = children.first();
+ if (componentRoot != null) {
+ // user has specified root instance that may have member fields that
+ // should be bound
+ FieldBinder binder = null;
+ try {
+ binder = new FieldBinder(componentRoot);
+ } catch (IntrospectionException e) {
+ throw new DesignException(
+ "Could not bind fields of the root component", e);
+ }
+ final FieldBinder fBinder = binder;
+ // create listener for component creations that binds the created
+ // components to the componentRoot instance fields
+ ComponentCreationListener creationListener = new ComponentCreationListener() {
+ @Override
+ public void componentCreated(ComponentCreatedEvent event) {
+ fBinder.bindField(event.getComponent(), event.getLocalId());
}
- // createChild creates the entire component hierarchy
- componentRoot = designContext.createChild((Element) element);
- designContext.setComponentRoot(componentRoot);
+ };
+ designContext.addComponentCreationListener(creationListener);
+ // create subtree
+ designContext.synchronizeAndRegister(componentRoot, element);
+ // make sure that all the member fields are bound
+ if (!binder.isAllFieldsBound()) {
+ throw new DesignException(
+ "Found unbound fields from component root");
}
+ // no need to listen anymore
+ designContext.removeComponentCreationListener(creationListener);
+ } else {
+ // createChild creates the entire component hierarchy
+ componentRoot = designContext.createChild(element);
}
+ designContext.setComponentRoot(componentRoot);
return designContext;
}
diff --git a/server/tests/src/com/vaadin/tests/layoutparser/InvalidLayoutTemplate.java b/server/tests/src/com/vaadin/tests/layoutparser/InvalidLayoutTemplate.java
new file mode 100644
index 0000000000..9447acb37e
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/layoutparser/InvalidLayoutTemplate.java
@@ -0,0 +1,55 @@
+/*
+ * 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.layoutparser;
+
+import com.vaadin.ui.Button;
+import com.vaadin.ui.NativeButton;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.VerticalLayout;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class InvalidLayoutTemplate extends VerticalLayout {
+ private NativeButton firstButton;
+ private NativeButton secondButton;
+ private NativeButton yetanotherbutton; // generated based on caption
+ private Button clickme; // generated based on caption
+ private TextField shouldNotBeMapped;
+
+ public NativeButton getFirstButton() {
+ return firstButton;
+ }
+
+ public NativeButton getSecondButton() {
+ return secondButton;
+ }
+
+ public NativeButton getYetanotherbutton() {
+ return yetanotherbutton;
+ }
+
+ public Button getClickme() {
+ return clickme;
+ }
+
+ public TextField getShouldNotBeMapped() {
+ return shouldNotBeMapped;
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/tests/layoutparser/LayoutTemplate.java b/server/tests/src/com/vaadin/tests/layoutparser/LayoutTemplate.java
new file mode 100644
index 0000000000..72131826ba
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/layoutparser/LayoutTemplate.java
@@ -0,0 +1,49 @@
+/*
+ * 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.layoutparser;
+
+import com.vaadin.ui.Button;
+import com.vaadin.ui.NativeButton;
+import com.vaadin.ui.VerticalLayout;
+
+/**
+ * Template to be populated in the tests
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class LayoutTemplate extends VerticalLayout {
+ private NativeButton firstButton; // assigned based on local id
+ private NativeButton secondButton; // assigned based on id
+ private NativeButton yetanotherbutton; // assigned based on caption
+ private Button clickme; // assigned based on caption
+
+ public NativeButton getFirstButton() {
+ return firstButton;
+ }
+
+ public NativeButton getSecondButton() {
+ return secondButton;
+ }
+
+ public NativeButton getYetanotherbutton() {
+ return yetanotherbutton;
+ }
+
+ public Button getClickme() {
+ return clickme;
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/layoutparser/ParseLayoutTest.java b/server/tests/src/com/vaadin/tests/layoutparser/ParseLayoutTest.java
index adba5e57b9..1c4d50cc55 100644
--- a/server/tests/src/com/vaadin/tests/layoutparser/ParseLayoutTest.java
+++ b/server/tests/src/com/vaadin/tests/layoutparser/ParseLayoutTest.java
@@ -18,6 +18,7 @@ package com.vaadin.tests.layoutparser;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStream;
import junit.framework.TestCase;
@@ -35,6 +36,7 @@ import com.vaadin.ui.TextArea;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
import com.vaadin.ui.declarative.LayoutHandler;
/**
@@ -106,6 +108,43 @@ public class ParseLayoutTest extends TestCase {
}
/*
+ * Check that the field binding works if root instance with member fields is
+ * passed to the LayoutHandler
+ *
+ * @throws IOException
+ */
+ public void testFieldBinding() throws IOException {
+ LayoutTemplate template = new LayoutTemplate();
+ InputStream htmlFile = new FileInputStream(
+ "server/tests/src/com/vaadin/tests/layoutparser/testFile.html");
+ LayoutHandler.parse(htmlFile, template);
+ assertNotNull(template.getFirstButton());
+ assertNotNull(template.getSecondButton());
+ assertNotNull(template.getYetanotherbutton());
+ assertNotNull(template.getClickme());
+ assertEquals("Native click me", template.getFirstButton().getCaption());
+ }
+
+ /*
+ * Check that the field binding fails if some of the fields in the root
+ * instance were not bound
+ *
+ * @throws IOException
+ */
+ public void testUnboundFields() throws IOException {
+ InvalidLayoutTemplate template = new InvalidLayoutTemplate();
+ InputStream htmlFile = new FileInputStream(
+ "server/tests/src/com/vaadin/tests/layoutparser/testFile.html");
+ try {
+ LayoutHandler.parse(htmlFile, template);
+ // we are expecting an exception
+ fail();
+ } catch (DesignException e) {
+ // expected
+ }
+ }
+
+ /*
* Checks that the correct components occur in the correct order in the
* component hierarchy rooted at context.getComponentRoot().
*/
@@ -173,4 +212,5 @@ public class ParseLayoutTest extends TestCase {
assertTrue("The found button is incorrect.", thirdButton.getCaption()
.equals("Yet another button"));
}
+
}