Change-Id: Ic6f201a45d66aefe9ec93ba3be5a75b6532bf014tags/7.4.0.beta1
@@ -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 ""; | |||
} |
@@ -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); | |||
@@ -232,6 +262,94 @@ public class Design implements Serializable { | |||
writer.write(docAsString); | |||
} | |||
/** | |||
* 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> | |||
@@ -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 |
@@ -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 '-'."); | |||
} | |||
} |
@@ -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); |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,4 @@ | |||
<v-vertical-layout> | |||
<v-button>OK</v-button> | |||
<v-button>Cancel</v-button> | |||
</v-vertical-layout> |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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"); | |||
} | |||
}); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |