Change-Id: Ic6f201a45d66aefe9ec93ba3be5a75b6532bf014tags/7.4.0.beta1
/* | |||||
* 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 ""; | |||||
} |
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.io.Serializable; | import java.io.Serializable; | ||||
import java.lang.annotation.Annotation; | |||||
import java.util.Collection; | import java.util.Collection; | ||||
import org.jsoup.Jsoup; | import org.jsoup.Jsoup; | ||||
import org.jsoup.parser.Parser; | import org.jsoup.parser.Parser; | ||||
import org.jsoup.select.Elements; | import org.jsoup.select.Elements; | ||||
import com.vaadin.annotations.DesignRoot; | |||||
import com.vaadin.ui.Component; | import com.vaadin.ui.Component; | ||||
import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent; | import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent; | ||||
import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; | import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; | ||||
* @param doc | * @param doc | ||||
* the html tree | * the html tree | ||||
* @param componentRoot | * @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, | private static DesignContext designToComponentTree(Document doc, | ||||
Component componentRoot) { | 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 designContext = new DesignContext(doc); | ||||
designContext.getPrefixes(doc); | designContext.getPrefixes(doc); | ||||
// No special handling for a document without a body element - should be | // No special handling for a document without a body element - should be | ||||
if (componentRoot != null) { | if (componentRoot != null) { | ||||
// user has specified root instance that may have member fields that | // user has specified root instance that may have member fields that | ||||
// should be bound | // should be bound | ||||
FieldBinder binder = null; | |||||
final FieldBinder binder; | |||||
try { | try { | ||||
binder = new FieldBinder(componentRoot); | |||||
binder = new FieldBinder(componentRoot, classWithFields); | |||||
} catch (IntrospectionException e) { | } catch (IntrospectionException e) { | ||||
throw new DesignException( | throw new DesignException( | ||||
"Could not bind fields of the root component", e); | "Could not bind fields of the root component", e); | ||||
} | } | ||||
final FieldBinder fBinder = binder; | |||||
// create listener for component creations that binds the created | // create listener for component creations that binds the created | ||||
// components to the componentRoot instance fields | // components to the componentRoot instance fields | ||||
ComponentCreationListener creationListener = new ComponentCreationListener() { | ComponentCreationListener creationListener = new ComponentCreationListener() { | ||||
@Override | @Override | ||||
public void componentCreated(ComponentCreatedEvent event) { | public void componentCreated(ComponentCreatedEvent event) { | ||||
fBinder.bindField(event.getComponent(), event.getLocalId()); | |||||
binder.bindField(event.getComponent(), event.getLocalId()); | |||||
} | } | ||||
}; | }; | ||||
designContext.addComponentCreationListener(creationListener); | designContext.addComponentCreationListener(creationListener); | ||||
writer.write(docAsString); | 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. | * Loads a design from the given file name using the given root component. | ||||
* <p> | * <p> | ||||
* The file name to load. Loaded from the same package as the | * The file name to load. Loaded from the same package as the | ||||
* root component | * root component | ||||
* @param rootComponent | * @param rootComponent | ||||
* The root component of the layout. | |||||
* The root component of the layout | |||||
* @return The design context used in the load operation | * @return The design context used in the load operation | ||||
* @throws DesignException | * @throws DesignException | ||||
* If the design could not be loaded | * If the design could not be loaded | ||||
* @param stream | * @param stream | ||||
* The stream to read the design from | * The stream to read the design from | ||||
* @param rootComponent | * @param rootComponent | ||||
* The root component of the layout. | |||||
* The root component of the layout | |||||
* @return The design context used in the load operation | * @return The design context used in the load operation | ||||
* @throws DesignException | * @throws DesignException | ||||
* If the design could not be loaded | * If the design could not be loaded |
Locale.ENGLISH) + tagName.substring(i + 2); | Locale.ENGLISH) + tagName.substring(i + 2); | ||||
} else { | } else { | ||||
// Ends with "-", WTF? | |||||
// Ends with "-" | |||||
System.out.println("A tag name should not end with '-'."); | System.out.println("A tag name should not end with '-'."); | ||||
} | } | ||||
} | } |
*/ | */ | ||||
public class FieldBinder implements Serializable { | 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 | // mapping between field names and Fields | ||||
private Map<String, Field> fieldMap = new HashMap<String, Field>(); | private Map<String, Field> fieldMap = new HashMap<String, Field>(); | ||||
* Creates a new instance of LayoutFieldBinder | * Creates a new instance of LayoutFieldBinder | ||||
* | * | ||||
* @param design | * @param design | ||||
* the design class instance containing the bound fields | |||||
* the design class instance containing the fields to bind | |||||
* @throws IntrospectionException | * @throws IntrospectionException | ||||
* if the given design class can not be introspected | * 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) { | if (design == null) { | ||||
throw new IllegalArgumentException("The design must not be null"); | throw new IllegalArgumentException("The design must not be null"); | ||||
} | } | ||||
bindTarget = design; | bindTarget = design; | ||||
resolveFields(); | |||||
resolveFields(classWithFields); | |||||
} | } | ||||
/** | /** | ||||
/** | /** | ||||
* Resolves the fields of the design class instance | * 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())) { | if (Component.class.isAssignableFrom(memberField.getType())) { | ||||
fieldMap.put(memberField.getName().toLowerCase(Locale.ENGLISH), | fieldMap.put(memberField.getName().toLowerCase(Locale.ENGLISH), | ||||
memberField); | memberField); |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
<v-vertical-layout> | |||||
<v-button>OK</v-button> | |||||
<v-button>Cancel</v-button> | |||||
</v-vertical-layout> |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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"); | |||||
} | |||||
}); | |||||
} | |||||
} |
/* | |||||
* 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()); | |||||
} | |||||
} |