diff options
author | Artur Signell <artur@vaadin.com> | 2014-12-14 20:01:52 +0200 |
---|---|---|
committer | Matti Hosio <mhosio@vaadin.com> | 2014-12-15 13:20:12 +0200 |
commit | 898e28d6aebc2b484f59f528a609bd59e5dfa4f5 (patch) | |
tree | cdb0bc9b76af30fb21b631c11e65093c0b5ceeb5 /server | |
parent | a27ea03db9f6ba5f4358c359645cd62617d2d205 (diff) | |
download | vaadin-framework-898e28d6aebc2b484f59f528a609bd59e5dfa4f5.tar.gz vaadin-framework-898e28d6aebc2b484f59f528a609bd59e5dfa4f5.zip |
Add public API for loading design based on @DesignRoot (#7749)
Change-Id: Ic6f201a45d66aefe9ec93ba3be5a75b6532bf014
Diffstat (limited to 'server')
11 files changed, 433 insertions, 19 deletions
diff --git a/server/src/com/vaadin/annotations/DesignRoot.java b/server/src/com/vaadin/annotations/DesignRoot.java new file mode 100644 index 0000000000..a00a00dc0b --- /dev/null +++ b/server/src/com/vaadin/annotations/DesignRoot.java @@ -0,0 +1,41 @@ +/* + * 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.ui.declarative.Design; + +/** + * Marks the component as the root of a design (html) file. + * <p> + * Used together with {@link Design#read(com.vaadin.ui.Component)} to be able + * the load the design without further configuration. The design is loaded from + * the same package as the annotated class and by default the design filename is + * derived from the class name. Using the {@link #value()} parameter you can + * specify another design file name. + * + * @since 7.4 + * @author Vaadin Ltd + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DesignRoot { + String value() default ""; +} diff --git a/server/src/com/vaadin/ui/declarative/Design.java b/server/src/com/vaadin/ui/declarative/Design.java index 433705c632..db656937ef 100644 --- a/server/src/com/vaadin/ui/declarative/Design.java +++ b/server/src/com/vaadin/ui/declarative/Design.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; +import java.lang.annotation.Annotation; import java.util.Collection; import org.jsoup.Jsoup; @@ -32,6 +33,7 @@ import org.jsoup.nodes.Node; import org.jsoup.parser.Parser; import org.jsoup.select.Elements; +import com.vaadin.annotations.DesignRoot; import com.vaadin.ui.Component; import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent; import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; @@ -128,14 +130,43 @@ public class Design implements Serializable { * @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 Component} are set when parsing the component tree - * + * optional component root instance. The type must match the type + * of the root element in the design. Any member fields whose + * type is assignable from {@link Component} are bound to fields + * in the design based on id/local id/caption */ private static DesignContext designToComponentTree(Document doc, Component componentRoot) { + if (componentRoot == null) { + return designToComponentTree(doc, null, null); + } else { + return designToComponentTree(doc, componentRoot, + componentRoot.getClass()); + } + + } + + /** + * Constructs a component hierarchy from the design specified as an html + * tree. + * + * If a component root is given, the component instances created during + * synchronizing the design are assigned to its member fields based on their + * id, local id, and caption + * + * @param doc + * the html tree + * @param componentRoot + * optional component root instance. The type must match the type + * of the root element in the design. + * @param classWithFields + * a class (componentRoot class or a super class) with some + * member fields. The member fields whose type is assignable from + * {@link Component} are bound to fields in the design based on + * id/local id/caption + */ + private static DesignContext designToComponentTree(Document doc, + Component componentRoot, Class<? extends Component> classWithFields) { DesignContext designContext = new DesignContext(doc); designContext.getPrefixes(doc); // No special handling for a document without a body element - should be @@ -151,20 +182,19 @@ public class Design implements Serializable { if (componentRoot != null) { // user has specified root instance that may have member fields that // should be bound - FieldBinder binder = null; + final FieldBinder binder; try { - binder = new FieldBinder(componentRoot); + binder = new FieldBinder(componentRoot, classWithFields); } 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()); + binder.bindField(event.getComponent(), event.getLocalId()); } }; designContext.addComponentCreationListener(creationListener); @@ -233,6 +263,94 @@ public class Design implements Serializable { } /** + * Loads a design for the given root component. + * <p> + * This methods assumes that the component class (or a super class) has been + * marked with an {@link DesignRoot} annotation and will either use the + * value from the annotation to locate the design file, or will fall back to + * using a design with the same same as the annotated class file (with an + * .html extension) + * <p> + * Any {@link Component} type fields in the root component which are not + * assigned (i.e. are null) are mapped to corresponding components in the + * design. Matching is done based on field name in the component class and + * id/local id/caption in the design file. + * <p> + * The type of the root component must match the root element in the design + * + * @param rootComponent + * The root component of the layout + * @return The design context used in the load operation + * @throws DesignException + * If the design could not be loaded + */ + public static DesignContext read(Component rootComponent) + throws DesignException { + // Try to find an @DesignRoot annotation on the class or any parent + // class + Class<? extends Component> annotatedClass = findClassWithAnnotation( + rootComponent.getClass(), DesignRoot.class); + if (annotatedClass == null) { + throw new IllegalArgumentException( + "The class " + + rootComponent.getClass().getName() + + " or any of its superclasses do not have an @DesignRoot annotation"); + } + + DesignRoot designAnnotation = annotatedClass + .getAnnotation(DesignRoot.class); + String filename = designAnnotation.value(); + if (filename.equals("")) { + // No value, assume the html file is named as the class + filename = annotatedClass.getSimpleName() + ".html"; + } + + InputStream stream = annotatedClass.getResourceAsStream(filename); + if (stream == null) { + throw new DesignException("Unable to find design file " + filename + + " in " + annotatedClass.getPackage().getName()); + } + + Document doc = parse(stream); + DesignContext context = designToComponentTree(doc, rootComponent, + annotatedClass); + + return context; + + } + + /** + * Find the first class with the given annotation, starting the search from + * the given class and moving upwards in the class hierarchy. + * + * @param componentClass + * the class to check + * @param annotationClass + * the annotation to look for + * @return the first class with the given annotation or null if no class + * with the annotation was found + */ + private static Class<? extends Component> findClassWithAnnotation( + Class<? extends Component> componentClass, + Class<? extends Annotation> annotationClass) { + if (componentClass == null) { + return null; + } + + if (componentClass.isAnnotationPresent(annotationClass)) { + return componentClass; + } + + Class<?> superClass = componentClass.getSuperclass(); + if (!Component.class.isAssignableFrom(superClass)) { + return null; + } + + return findClassWithAnnotation((Class<? extends Component>) superClass, + annotationClass); + } + + /** * Loads a design from the given file name using the given root component. * <p> * Any {@link Component} type fields in the root component which are not @@ -246,7 +364,7 @@ public class Design implements Serializable { * The file name to load. Loaded from the same package as the * root component * @param rootComponent - * The root component of the layout. + * The root component of the layout * @return The design context used in the load operation * @throws DesignException * If the design could not be loaded @@ -276,7 +394,7 @@ public class Design implements Serializable { * @param stream * The stream to read the design from * @param rootComponent - * The root component of the layout. + * The root component of the layout * @return The design context used in the load operation * @throws DesignException * If the design could not be loaded diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java index a3026fca18..e84faa31ca 100644 --- a/server/src/com/vaadin/ui/declarative/DesignContext.java +++ b/server/src/com/vaadin/ui/declarative/DesignContext.java @@ -509,7 +509,7 @@ public class DesignContext implements Serializable { Locale.ENGLISH) + tagName.substring(i + 2); } else { - // Ends with "-", WTF? + // Ends with "-" System.out.println("A tag name should not end with '-'."); } } diff --git a/server/src/com/vaadin/ui/declarative/FieldBinder.java b/server/src/com/vaadin/ui/declarative/FieldBinder.java index f456b2e29a..4826c2be64 100644 --- a/server/src/com/vaadin/ui/declarative/FieldBinder.java +++ b/server/src/com/vaadin/ui/declarative/FieldBinder.java @@ -39,8 +39,8 @@ import com.vaadin.util.ReflectTools; */ public class FieldBinder implements Serializable { - // the design class instance (the instance containing the bound fields) - private Component bindTarget; + // the instance containing the bound fields + private Object bindTarget; // mapping between field names and Fields private Map<String, Field> fieldMap = new HashMap<String, Field>(); @@ -48,16 +48,31 @@ public class FieldBinder implements Serializable { * Creates a new instance of LayoutFieldBinder * * @param design - * the design class instance containing the bound fields + * the design class instance containing the fields to bind * @throws IntrospectionException * if the given design class can not be introspected */ - public FieldBinder(Component design) throws IntrospectionException { + public FieldBinder(Object design) throws IntrospectionException { + this(design, design.getClass()); + } + + /** + * Creates a new instance of LayoutFieldBinder + * + * @param design + * the instance containing the fields + * @param classWithFields + * the class which defines the fields to bind + * @throws IntrospectionException + * if the given design class can not be introspected + */ + public FieldBinder(Object design, Class<?> classWithFields) + throws IntrospectionException { if (design == null) { throw new IllegalArgumentException("The design must not be null"); } bindTarget = design; - resolveFields(); + resolveFields(classWithFields); } /** @@ -91,8 +106,8 @@ public class FieldBinder implements Serializable { /** * Resolves the fields of the design class instance */ - private void resolveFields() { - for (Field memberField : getFieldsInDeclareOrder(bindTarget.getClass())) { + private void resolveFields(Class<?> classWithFields) { + for (Field memberField : getFieldsInDeclareOrder(classWithFields)) { if (Component.class.isAssignableFrom(memberField.getType())) { fieldMap.put(memberField.getName().toLowerCase(Locale.ENGLISH), memberField); diff --git a/server/tests/src/com/vaadin/tests/design/designroot/DesignRootTest.java b/server/tests/src/com/vaadin/tests/design/designroot/DesignRootTest.java new file mode 100644 index 0000000000..0d390ba184 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/DesignRootTest.java @@ -0,0 +1,53 @@ +/* + * 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.designroot; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +// This test will not pass until default instance creation is changed. Will leave it ignored for now. +@Ignore +public class DesignRootTest { + @Test + public void designAnnotationWithoutFilename() { + DesignWithEmptyAnnotation d = new DesignWithEmptyAnnotation(); + Assert.assertNotNull(d.ok); + Assert.assertNotNull(d.CaNCEL); + } + + @Test + public void designAnnotationWithFilename() { + DesignWithAnnotation d = new DesignWithAnnotation(); + Assert.assertNotNull(d.ok); + Assert.assertNotNull(d.cancel); + } + + @Test + public void extendedDesignAnnotationWithoutFilename() { + DesignWithEmptyAnnotation d = new ExtendedDesignWithEmptyAnnotation(); + Assert.assertNotNull(d.ok); + Assert.assertNotNull(d.CaNCEL); + } + + @Test + public void extendedDesignAnnotationWithFilename() { + DesignWithAnnotation d = new ExtendedDesignWithAnnotation(); + Assert.assertNotNull(d.ok); + Assert.assertNotNull(d.cancel); + } + +} diff --git a/server/tests/src/com/vaadin/tests/design/designroot/DesignWithAnnotation.java b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithAnnotation.java new file mode 100644 index 0000000000..1107d8c00c --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithAnnotation.java @@ -0,0 +1,35 @@ +/* + * 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.designroot; + +import org.junit.Ignore; + +import com.vaadin.annotations.DesignRoot; +import com.vaadin.ui.Button; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.declarative.Design; + +@DesignRoot("DesignWithEmptyAnnotation.html") +@Ignore +public class DesignWithAnnotation extends VerticalLayout { + + public Button ok; + public Button cancel; + + public DesignWithAnnotation() { + Design.read(this); + } +} diff --git a/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.html b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.html new file mode 100644 index 0000000000..27c323dfec --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.html @@ -0,0 +1,4 @@ +<v-vertical-layout> + <v-button>OK</v-button> + <v-button>Cancel</v-button> +</v-vertical-layout>
\ No newline at end of file diff --git a/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.java b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.java new file mode 100644 index 0000000000..9723df9afa --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/DesignWithEmptyAnnotation.java @@ -0,0 +1,35 @@ +/* + * 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.designroot; + +import org.junit.Ignore; + +import com.vaadin.annotations.DesignRoot; +import com.vaadin.ui.Button; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.declarative.Design; + +@DesignRoot +@Ignore +public class DesignWithEmptyAnnotation extends VerticalLayout { + + protected Button ok; + protected Button CaNCEL; + + public DesignWithEmptyAnnotation() { + Design.read(this); + } +} diff --git a/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithAnnotation.java b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithAnnotation.java new file mode 100644 index 0000000000..14e4269e80 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithAnnotation.java @@ -0,0 +1,31 @@ +/* + * 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.designroot; + +import org.junit.Ignore; + +import com.vaadin.ui.TextField; + +@Ignore +public class ExtendedDesignWithAnnotation extends DesignWithAnnotation { + private TextField customField = new TextField(); + + public ExtendedDesignWithAnnotation() { + customField.setInputPrompt("Something"); + addComponent(customField); + + } +} diff --git a/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotation.java b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotation.java new file mode 100644 index 0000000000..22865b098c --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotation.java @@ -0,0 +1,50 @@ +/* + * 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.designroot; + +import org.junit.Ignore; + +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Notification; +import com.vaadin.ui.TextField; + +@Ignore +public class ExtendedDesignWithEmptyAnnotation extends + DesignWithEmptyAnnotation { + + private TextField customField = new TextField(); + + public ExtendedDesignWithEmptyAnnotation() { + super(); + customField.setInputPrompt("Something"); + addComponent(customField); + + ok.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + Notification.show("OK"); + } + }); + + CaNCEL.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + Notification.show("cancel"); + } + }); + } +} diff --git a/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotationUI.java b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotationUI.java new file mode 100644 index 0000000000..34f517d25d --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/designroot/ExtendedDesignWithEmptyAnnotationUI.java @@ -0,0 +1,32 @@ +/* + * 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.designroot; + +import org.junit.Ignore; + +import com.vaadin.server.VaadinRequest; +import com.vaadin.ui.UI; + +@Ignore +public class ExtendedDesignWithEmptyAnnotationUI extends UI { + + @Override + protected void init(VaadinRequest request) { + setContent(new ExtendedDesignWithEmptyAnnotation()); + + } + +} |