/* * 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.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Document.OutputSettings.Syntax; 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.annotations.DesignRoot; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Component; import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent; import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; /** * Design is used for reading a component hierarchy from an html string or input * stream and, conversely, for writing an html representation corresponding to a * given component hierarchy. * *
* In html form a valid nonempty component hierarchy contains a single root * element located under the <body> tag. A hierarchy of components is * achieved by nesting other elements under the root element. An empty component * hierarchy is represented as no elements under the <body> tag. * *
* For writing a component hierarchy the root element is specified as a
* Component parameter or as a DesignContext object containing the root
* Component. An empty hierarchy can be written by giving a null root Component.
*
* @since 7.4
* @author Vaadin Ltd
*/
public class Design implements Serializable {
private static final String UTF8 = "UTF-8";
/**
* Callback for creating instances of a given component class when reading
* designs. The default implementation, {@link DefaultComponentFactory} will
* use Class.forName(className).newInstance()
, which might not
* be suitable e.g. in an OSGi environment or if the Component instances
* should be created as managed CDI beans.
*
* Use {@link Design#setComponentFactory(ComponentFactory)} to configure * Vaadin to use a custom component factory. * * @since 7.4.1 */ public interface ComponentFactory extends Serializable { /** * Creates a component based on the fully qualified name derived from * the tag name in the design. * * @param fullyQualifiedClassName * the fully qualified name of the component to create * @param context * the design context for which the component is created * * @return a newly created component */ public Component createComponent(String fullyQualifiedClassName, DesignContext context); } /** * Delegate for handling the mapping between tag names and component * instances. *
* Use {@link Design#setComponentMapper(ComponentMapper)} to configure * Vaadin to use a custom component mapper. * * @since 7.5.0 * @author Vaadin Ltd */ public interface ComponentMapper extends Serializable { /** * Resolves and creates a component using the provided component factory * based on a tag name. *
* This method should be in sync with
* {@link #componentToTag(Component, DesignContext)} so that the
* resolved tag for a created component is the same as the tag for which
* the component was created.
*
* @param tag
* the tag name to create a component for
* @param componentFactory
* the component factory that actually creates a component
* based on a fully qualified class name
* @param context
* the design context for which the component is created
* @return a newly created component
*/
public Component tagToComponent(String tag,
ComponentFactory componentFactory, DesignContext context);
/**
* Resolves a tag name from a component.
*
* @param component
* the component to get a tag name for
* @param context
* the design context for which the tag name is needed
* @return the tag name corresponding to the component
*/
public String componentToTag(Component component, DesignContext context);
}
/**
* Default implementation of {@link ComponentFactory}, using
*
* Please note that this setting is global, so care should be taken to avoid
* conflicting changes.
*
* @param componentFactory
* the component factory to set; not
* Please note that this setting is global, so care should be taken to avoid
* conflicting changes.
*
* @param componentMapper
* the component mapper to set; not
* If a component root is given, the component instances created during
* reading 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. 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
* reading 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> classWithFields) {
DesignContext designContext = new DesignContext(doc);
designContext.readPackageMappings(doc);
// No special handling for a document without a body element - should be
// taken care of by jsoup.
Element root = doc.body();
Elements children = root.children();
if (children.size() > 1) {
throw new DesignException(
"The first level of a component hierarchy should contain at most one root component, but found "
+ children.size() + ".");
}
Element element = children.size() == 0 ? null : children.first();
if (componentRoot != null) {
if (element == null) {
throw new DesignException(
"The root element cannot be null when the specified root Component is"
+ " not null.");
}
// user has specified root instance that may have member fields that
// should be bound
final FieldBinder binder;
try {
binder = new FieldBinder(componentRoot, classWithFields);
} catch (IntrospectionException e) {
throw new DesignException(
"Could not bind fields of the root component", e);
}
// 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) {
binder.bindField(event.getComponent(), event.getLocalId());
}
};
designContext.addComponentCreationListener(creationListener);
// create subtree
designContext.readDesign(element, componentRoot);
// make sure that all the member fields are bound
Collection
* 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)
*
* 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.
*
* 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());
}
try {
Document doc = parse(stream);
DesignContext context = designToComponentTree(doc, rootComponent,
annotatedClass);
return context;
} finally {
try {
stream.close();
} catch (IOException e) {
getLogger().log(Level.FINE, "Error closing design stream", e);
}
}
}
private static Logger getLogger() {
return Logger.getLogger(Design.class.getName());
}
/**
* 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(superClass.asSubclass(Component.class),
annotationClass);
}
/**
* Loads a design from the given file name using the given root component.
*
* 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.
*
* The type of the root component must match the root element in the design.
*
* @param filename
* The file name to load. Loaded from the same package as the
* root component
* @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(String filename, Component rootComponent)
throws DesignException {
InputStream stream = rootComponent.getClass().getResourceAsStream(
filename);
if (stream == null) {
throw new DesignException("File " + filename
+ " was not found in the package "
+ rootComponent.getClass().getPackage().getName());
}
try {
return read(stream, rootComponent);
} finally {
try {
stream.close();
} catch (IOException e) {
getLogger().log(Level.FINE, "Error closing design stream", e);
}
}
}
/**
* Loads a design from the given stream using the given root component. If
* rootComponent is null, the type of the root node is read from the design.
*
* 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.
*
* If rootComponent is not null, its type must match the type of the root
* element in the design
*
* @param stream
* The stream to read the design from
* @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(InputStream stream, Component rootComponent) {
if (stream == null) {
throw new DesignException("Stream cannot be null");
}
Document doc = parse(stream);
DesignContext context = designToComponentTree(doc, rootComponent);
return context;
}
/**
* Loads a design from the given input stream
*
* @param design
* The stream to read the design from
* @return The root component of the design
*/
public static Component read(InputStream design) {
DesignContext context = read(design, null);
return context.getRootComponent();
}
/**
* Writes the given component tree in design format to the given output
* stream.
*
* @param component
* the root component of the component tree, null can be used for
* generating an empty design
* @param outputStream
* the output stream to write the design to. The design is always
* written as UTF-8
* @throws IOException
* if writing fails
*/
public static void write(Component component, OutputStream outputStream)
throws IOException {
DesignContext dc = new DesignContext();
dc.setRootComponent(component);
write(dc, outputStream);
}
/**
* Writes the component, given in the design context, in design format to
* the given output stream. The design context is used for writing local ids
* and other information not available in the component tree.
*
* @param designContext
* The DesignContext object specifying the component hierarchy
* and the local id values of the objects. If
* designContext.getRootComponent() is null, an empty design will
* be generated.
* @param outputStream
* the output stream to write the design to. The design is always
* written as UTF-8
* @throws IOException
* if writing fails
*/
public static void write(DesignContext designContext,
OutputStream outputStream) throws IOException {
Document doc = createHtml(designContext);
write(doc, outputStream);
}
/**
* Writes the given jsoup document to the output stream (in UTF-8)
*
* @param doc
* the document to write
* @param outputStream
* the stream to write to
* @throws IOException
* if writing fails
*/
private static void write(Document doc, OutputStream outputStream)
throws IOException {
doc.outputSettings().indentAmount(4);
doc.outputSettings().syntax(Syntax.html);
doc.outputSettings().prettyPrint(true);
outputStream.write(doc.html().getBytes(UTF8));
}
}
Class.forName(className).newInstance()
for finding the
* component class and creating a component instance.
*
* @since 7.4.1
*/
public static class DefaultComponentFactory implements ComponentFactory {
@Override
public Component createComponent(String fullyQualifiedClassName,
DesignContext context) {
Class extends Component> componentClass = resolveComponentClass(
fullyQualifiedClassName, context);
assert Component.class.isAssignableFrom(componentClass) : "resolveComponentClass returned "
+ componentClass + " which is not a Vaadin Component class";
try {
return componentClass.newInstance();
} catch (Exception e) {
throw new DesignException("Could not create component "
+ fullyQualifiedClassName, e);
}
}
/**
* Resolves a component class based on the fully qualified name of the
* class.
*
* @param qualifiedClassName
* the fully qualified name of the resolved class
* @param context
* the design context for which the class is resolved
* @return a component class object representing the provided class name
*/
protected Class extends Component> resolveComponentClass(
String qualifiedClassName, DesignContext context) {
try {
Class> componentClass = Class.forName(qualifiedClassName);
return componentClass.asSubclass(Component.class);
} catch (ClassNotFoundException e) {
throw new DesignException(
"Unable to load component for design", e);
}
}
}
/**
* Default implementation of {@link ComponentMapper},
*
* @since 7.5.0
*/
public static class DefaultComponentMapper implements ComponentMapper {
@Override
public Component tagToComponent(String tagName,
ComponentFactory componentFactory, DesignContext context) {
// Extract the package and class names.
// Otherwise, get the full class name using the prefix to package
// mapping. Example: "vaadin-vertical-layout" ->
// "com.vaadin.ui.VerticalLayout"
String[] parts = tagName.split("-", 2);
if (parts.length < 2) {
throw new DesignException("The tagname '" + tagName
+ "' is invalid: missing prefix.");
}
String prefixName = parts[0];
String packageName = context.getPackage(prefixName);
if (packageName == null) {
throw new DesignException("Unknown tag: " + tagName);
}
String[] classNameParts = parts[1].split("-");
String className = "";
for (String classNamePart : classNameParts) {
// Split will ignore trailing and multiple dashes but that
// should be
// ok
// null
*
* @since 7.4.1
*/
public static void setComponentFactory(ComponentFactory componentFactory) {
if (componentFactory == null) {
throw new IllegalArgumentException(
"Cannot set null component factory");
}
Design.componentFactory = componentFactory;
}
/**
* Gets the currently used component factory.
*
* @see #setComponentFactory(ComponentFactory)
*
* @return the component factory
*
* @since 7.4.1
*/
public static ComponentFactory getComponentFactory() {
return componentFactory;
}
/**
* Sets the component mapper that is used for resolving between tag names
* and component instances.
* null
*
* @since 7.5.0
*/
public static void setComponentMapper(ComponentMapper componentMapper) {
if (componentMapper == null) {
throw new IllegalArgumentException(
"Cannot set null component mapper");
}
Design.componentMapper = componentMapper;
}
/**
* Gets the currently used component mapper.
*
* @see #setComponentMapper(ComponentMapper)
*
* @return the component mapper
*
* @since 7.5.0
*/
public static ComponentMapper getComponentMapper() {
return componentMapper;
}
/**
* Parses the given input stream into a jsoup document
*
* @param html
* the stream containing the design
* @return the parsed jsoup document
* @throws IOException
*/
private static Document parse(InputStream html) {
try {
Document doc = Jsoup.parse(html, UTF8, "", Parser.htmlParser());
return doc;
} catch (IOException e) {
throw new DesignException("The html document cannot be parsed.");
}
}
/**
* Constructs a component hierarchy from the design specified as an html
* tree.
*
*