diff options
author | Mika Murtojarvi <mika@vaadin.com> | 2014-11-28 17:58:15 +0200 |
---|---|---|
committer | Mika Murtojarvi <mika@vaadin.com> | 2014-12-02 17:49:35 +0200 |
commit | ecb037d2d9ead3324b052ad69e73f22ed8672870 (patch) | |
tree | c054b1d9432f527bef22cbc78152a72fb4c12948 /server/src/com | |
parent | d3af8be5a52365a51a0dd177617976ad566ead0e (diff) | |
download | vaadin-framework-ecb037d2d9ead3324b052ad69e73f22ed8672870.tar.gz vaadin-framework-ecb037d2d9ead3324b052ad69e73f22ed8672870.zip |
Vaadin declarative: parser and html generator.
The parser creates a Component hierarchy from a given html file. The
html generator does the converse, outputs html given a component
hierarchy with a single root.
Current TODOs: 1) add automatic tests - some of the functionality may
not have been tested in any way. 2) Remove files that are not relevant
for this change set but were accidentally included in a commit.
Change-Id: I222e01291aab75c2249d4aa4904f16fb153d4397
Diffstat (limited to 'server/src/com')
9 files changed, 551 insertions, 193 deletions
diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java index 27d9017374..3b0402b2a9 100644 --- a/server/src/com/vaadin/ui/AbstractComponent.java +++ b/server/src/com/vaadin/ui/AbstractComponent.java @@ -54,13 +54,13 @@ import com.vaadin.util.ReflectTools; * {@link Component} interface. Basic UI components that are not derived from an * external component can inherit this class to easily qualify as Vaadin * components. Most components in Vaadin do just that. - * + * * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("serial") public abstract class AbstractComponent extends AbstractClientConnector - implements Component, DesignSynchronizable { + implements DesignSynchronizable { /* Private members */ @@ -249,7 +249,7 @@ public abstract class AbstractComponent extends AbstractClientConnector * Sets the component's caption <code>String</code>. Caption is the visible * name of the component. This method will trigger a * {@link RepaintRequestEvent}. - * + * * @param caption * the new caption <code>String</code> for the component. */ @@ -280,7 +280,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Sets the locale of this component. - * + * * <pre> * // Component for which the locale is meaningful * InlineDateField date = new InlineDateField("Datum"); @@ -292,8 +292,8 @@ public abstract class AbstractComponent extends AbstractClientConnector * date.setResolution(DateField.RESOLUTION_DAY); * layout.addComponent(date); * </pre> - * - * + * + * * @param locale * the locale to become this component's locale. */ @@ -319,7 +319,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Sets the component's icon. This method will trigger a * {@link RepaintRequestEvent}. - * + * * @param icon * the icon to be shown with the component's caption. */ @@ -385,7 +385,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Sets the component's immediate mode to the specified status. - * + * * @param immediate * the boolean value specifying if the component should be in the * immediate mode after the call. @@ -446,11 +446,11 @@ public abstract class AbstractComponent extends AbstractClientConnector * Sets the component's description. See {@link #getDescription()} for more * information on what the description is. This method will trigger a * {@link RepaintRequestEvent}. - * + * * The description is displayed as HTML in tooltips or directly in certain * components so care should be taken to avoid creating the possibility for * HTML injection and possibly XSS vulnerabilities. - * + * * @param description * the new description string for the component. */ @@ -499,7 +499,7 @@ public abstract class AbstractComponent extends AbstractClientConnector * To find the Window that contains the component, use {@code Window w = * getParent(Window.class);} * </p> - * + * * @param <T> * The type of the ancestor * @param parentType @@ -520,7 +520,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Gets the error message for this component. - * + * * @return ErrorMessage containing the description of the error state of the * component or null, if the component contains no errors. Extending * classes should override this method if they support other error @@ -533,9 +533,9 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Gets the component's error message. - * + * * @link Terminal.ErrorMessage#ErrorMessage(String, int) - * + * * @return the component's error message. */ public ErrorMessage getComponentError() { @@ -545,9 +545,9 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Sets the component's error message. The message may contain certain XML * tags, for more information see - * + * * @link Component.ErrorMessage#ErrorMessage(String, int) - * + * * @param componentError * the new <code>ErrorMessage</code> of the component. */ @@ -624,7 +624,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Build CSS compatible string representation of height. - * + * * @return CSS height */ private String getCSSHeight() { @@ -633,7 +633,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Build CSS compatible string representation of width. - * + * * @return CSS width */ private String getCSSWidth() { @@ -643,12 +643,12 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Returns the shared state bean with information to be sent from the server * to the client. - * + * * Subclasses should override this method and set any relevant fields of the * state returned by super.getState(). - * + * * @since 7.0 - * + * * @return updated component shared state */ @Override @@ -739,7 +739,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Sets the data object, that can be used for any application specific data. * The component does not use or modify this data. - * + * * @param data * the Application specific data. * @since 3.1 @@ -750,7 +750,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Gets the application specific data. See {@link #setData(Object)}. - * + * * @return the Application specific data set with setData function. * @since 3.1 */ @@ -1035,7 +1035,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Gets the {@link ActionManager} used to manage the * {@link ShortcutListener}s added to this {@link Field}. - * + * * @return the ActionManager in use */ protected ActionManager getActionManager() { @@ -1079,7 +1079,7 @@ public abstract class AbstractComponent extends AbstractClientConnector /** * Determine whether a <code>content</code> component is equal to, or the * ancestor of this component. - * + * * @param content * the potential ancestor element * @return <code>true</code> if the relationship holds diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java index e45a1362b0..13c11ddf17 100644 --- a/server/src/com/vaadin/ui/AbstractOrderedLayout.java +++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -486,7 +486,6 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements Attributes attr = childComponent.attributes(); DesignSynchronizable newChild = designContext .createChild(childComponent); - newChild.synchronizeFromDesign(childComponent, designContext); addComponent(newChild); // handle alignment int bitMask = 0; diff --git a/server/src/com/vaadin/ui/DesignSynchronizable.java b/server/src/com/vaadin/ui/DesignSynchronizable.java index a27ba0f539..dc421bc47d 100644 --- a/server/src/com/vaadin/ui/DesignSynchronizable.java +++ b/server/src/com/vaadin/ui/DesignSynchronizable.java @@ -59,5 +59,4 @@ public interface DesignSynchronizable extends Component { * @param designContext */ public void synchronizeToDesign(Node design, DesignContext designContext); - } diff --git a/server/src/com/vaadin/ui/declarative/ComponentInstantiationException.java b/server/src/com/vaadin/ui/declarative/ComponentInstantiationException.java new file mode 100644 index 0000000000..5a8ec05d87 --- /dev/null +++ b/server/src/com/vaadin/ui/declarative/ComponentInstantiationException.java @@ -0,0 +1,18 @@ +package com.vaadin.ui.declarative; + +@SuppressWarnings("serial") +public class ComponentInstantiationException extends RuntimeException { + + public ComponentInstantiationException() { + super(); + } + + public ComponentInstantiationException(String message) { + super(message); + } + + public ComponentInstantiationException(String message, Throwable e) { + super(message, e); + } + +} diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java index 17439d0f8a..54d74f417c 100644 --- a/server/src/com/vaadin/ui/declarative/DesignContext.java +++ b/server/src/com/vaadin/ui/declarative/DesignContext.java @@ -25,19 +25,15 @@ import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; -import org.jsoup.nodes.TextNode; import com.vaadin.ui.Component; import com.vaadin.ui.DesignSynchronizable; /** - * DesignContext can create a component corresponding to a given html tree node - * or an html tree node corresponding to a given component. DesignContext also - * keeps track of id values found in the current html tree and can detect - * non-uniqueness of these values. Non-id attributes are handled by the - * component classes instead of DesignContext. + * This class contains contextual information that is collected when a component + * tree is constructed based on HTML design template. * - * @since + * @since 7.4 * @author Vaadin Ltd */ public class DesignContext { @@ -46,43 +42,205 @@ public class DesignContext { private static Map<Class<?>, Object> instanceCache = Collections .synchronizedMap(new HashMap<Class<?>, Object>()); + // The root component of the component hierarchy + private DesignSynchronizable componentRoot = null; + // Attribute names for global id and caption and the prefix name for a local + // id public static final String ID_ATTRIBUTE = "id"; public static final String CAPTION_ATTRIBUTE = "caption"; public static final String LOCAL_ID_PREFIX = "_"; - private Map<String, DesignSynchronizable> globalIds = new HashMap<String, DesignSynchronizable>(); - private Map<String, DesignSynchronizable> localIds = new HashMap<String, DesignSynchronizable>(); - private Map<String, DesignSynchronizable> captions = new HashMap<String, DesignSynchronizable>(); - private Document doc; // used for accessing - // Document.createElement(String) + // Mappings from IDs to components. Modified when synchronizing from design. + private Map<String, DesignSynchronizable> globalIdToComponent = new HashMap<String, DesignSynchronizable>(); + private Map<String, DesignSynchronizable> localIdToComponent = new HashMap<String, DesignSynchronizable>(); + private Map<String, DesignSynchronizable> captionToComponent = new HashMap<String, DesignSynchronizable>(); + // Mapping from components to local IDs. Accessed when synchronizing to + // design. Modified when synchronizing from design. + private Map<DesignSynchronizable, String> componentToLocalId = new HashMap<DesignSynchronizable, String>(); + private Document doc; // required for calling createElement(String) // namespace mappings private Map<String, String> packageToPrefix = new HashMap<String, String>(); private Map<String, String> prefixToPackage = new HashMap<String, String>(); // prefix names for which no package-mapping element will be created in the - // html tree + // html tree (this includes at least "v" which is always taken to refer + // to "com.vaadin.ui". private Map<String, String> defaultPrefixes = new HashMap<String, String>(); - public DesignContext() { - doc = new Document(""); - // Initialize the mapping between prefixes and package names. First add - // any default mappings (v -> com.vaadin.ui). The default mappings are - // the prefixes for which - // no meta tags will be created when writing a design to html. + public DesignContext(Document doc) { + this.doc = doc; + // Initialize the mapping between prefixes and package names. defaultPrefixes.put("v", "com.vaadin.ui"); for (String prefix : defaultPrefixes.keySet()) { String packageName = defaultPrefixes.get(prefix); - prefixToPackage.put(prefix, packageName); - packageToPrefix.put(packageName, prefix); + mapPrefixToPackage(prefix, packageName); + } + } + + public DesignContext() { + this(new Document("")); + } + + /** + * Creates a mapping between the given global id and the component. Returns + * true if globalId was already mapped to some component or if component was + * mapped to some string. Otherwise returns false. Also sets the id of the + * component to globalId. + * + * If the string was mapped to a component c different from the given + * component, the mapping from c to the string is removed. Similarly, if + * component was mapped to some string s different from globalId, the + * mapping from s to component is removed. + * + * @since + * @param globalId + * The new global id of the component. + * @param component + * The component whose global id is to be set. + * @return true, if there already was a global id mapping from the string to + * some component or from the component to some string. Otherwise + * returns false. + */ + public boolean mapGlobalId(String globalId, DesignSynchronizable component) { + DesignSynchronizable oldComponent = globalIdToComponent.get(globalId); + if (oldComponent != null && !oldComponent.equals(component)) { + oldComponent.setId(null); + } + String oldGID = component.getId(); + if (oldGID != null && !oldGID.equals(globalId)) { + globalIdToComponent.remove(oldGID); + } + component.setId(globalId); + return oldComponent != null || oldGID != null; + } + + /** + * Creates a mapping between the given local id and the component. Returns + * true if localId was already mapped to some component or if component was + * mapped to some string. Otherwise returns false. + * + * If the string was mapped to a component c different from the given + * component, the mapping from c to the string is removed. Similarly, if + * component was mapped to some string s different from localId, the mapping + * from s to component is removed. + * + * @since + * @param globalId + * The new local id of the component. + * @param component + * The component whose local id is to be set. + * @return true, if there already was a local id mapping from the string to + * some component or from the component to some string. Otherwise + * returns false. + */ + public boolean mapLocalId(String localId, DesignSynchronizable component) { + return twoWayMap(localId, component, localIdToComponent, + componentToLocalId); + } + + /** + * Creates a mapping between the given caption and the component. Returns + * true if caption was already mapped to some component. + * + * Note that unlike mapGlobalId, if some component already has the given + * caption, the caption is not cleared from the component. This allows + * non-unique captions. However, only one of the components corresponding to + * a given caption can be found using the map captionToComponent. Hence, any + * captions that are used to identify an object should be unique. + * + * @since + * @param caption + * The new caption of the component. + * @param component + * The component whose caption is to be set. + * @return true, if there already was a caption mapping from the string to + * some component. + */ + public boolean mapCaption(String caption, DesignSynchronizable component) { + return captionToComponent.put(caption, component) != null; + } + + /** + * Creates a two-way mapping between key and value, i.e. adds key -> value + * to keyToValue and value -> key to valueToKey. If key was mapped to a + * value v different from the given value, the mapping from v to key is + * removed. Similarly, if value was mapped to some key k different from key, + * the mapping from k to value is removed. + * + * Returns true if there already was a mapping from key to some value v or + * if there was a mapping from value to some key k. Otherwise returns false. + * + * @since + * @param key + * The new key in keyToValue. + * @param value + * The new value in keyToValue. + * @param keyToValue + * A map from keys to values. + * @param valueToKey + * A map from values to keys. + * @return whether there already was some mapping from key to a value or + * from value to a key. + */ + private <S, T> boolean twoWayMap(S key, T value, Map<S, T> keyToValue, + Map<T, S> valueToKey) { + T oldValue = keyToValue.put(key, value); + if (oldValue != null && !oldValue.equals(value)) { + valueToKey.remove(oldValue); } + S oldKey = valueToKey.put(value, key); + if (oldKey != null && !oldKey.equals(key)) { + keyToValue.remove(oldKey); + } + return oldValue != null || oldKey != null; } /** - * Get the mappings from prefixes to package names from meta tags located - * under <head> in the html document. + * Creates a two-way mapping between a prefix and a package name. Return + * true if prefix was already mapped to some package name or packageName to + * some prefix. + * + * @since + * @param prefix + * the prefix name without an ending dash (for instance, "v" is + * always used for "com.vaadin.ui") + * @param packageName + * the name of the package corresponding to prefix + * @return whether there was a mapping from prefix to some package name or + * from packageName to some prefix. + */ + public boolean mapPrefixToPackage(String prefix, String packageName) { + return twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix); + } + + /** + * Returns the default instance for the given class. The instance must not + * be modified by the caller. + * + * @since + * @param instanceClass + * @return + */ + public <T> T getDefaultInstance(Class<T> instanceClass) { + T instance = (T) instanceCache.get(instanceClass); + if (instance == null) { + try { + instance = instanceClass.newInstance(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + instanceCache.put(instanceClass, instance); + } + return instance; + } + + /** + * Get and store the mappings from prefixes to package names from meta tags + * located under <head> in the html document. * * @since */ public void getPrefixes(Document doc) { - // TODO this method has not been tested in any way. Element head = doc.head(); if (head == null) { return; @@ -98,13 +256,13 @@ public class DesignContext { String contentString = attributes.get("content"); String[] parts = contentString.split(":"); if (parts.length != 2) { - throw new RuntimeException("The meta tag '" + throw new LayoutInflaterException("The meta tag '" + child.toString() + "' cannot be parsed."); } String prefixName = parts[0]; String packageName = parts[1]; - prefixToPackage.put(prefixName, packageName); - packageToPrefix.put(packageName, prefixName); + twoWayMap(prefixName, packageName, prefixToPackage, + packageToPrefix); } } } @@ -112,33 +270,58 @@ public class DesignContext { } /** - * Creates an html tree node corresponding to the given element. Note that - * this method does not set the attribute values. That can be done by - * calling childComponent.synchronizeToDesign(result, designContext), where - * result is the node returned by this method and designContext is this - * context. + * + */ + public void storePrefixes(Document doc) { + Element head = doc.head(); + for (String prefix : prefixToPackage.keySet()) { + // Only store the prefix-name mapping if it is not a default mapping + // (such as "v" -> "com.vaadin.ui") + if (defaultPrefixes.get(prefix) == null) { + Node newNode = doc.createElement("meta"); + newNode.attr("name", "package-mapping"); + String prefixToPackageName = prefix + ":" + + prefixToPackage.get(prefix); + newNode.attr("content", prefixToPackageName); + head.appendChild(newNode); + + } + } + } + + /** + * Creates an html tree node corresponding to the given element. Also + * initializes its attributes by calling synchronizeToDesign. As a result of + * the synchronizeToDesign() call, this method creates the entire subtree + * rooted at the returned Node. * * @since * @param childComponent * A component implementing the DesignSynchronizable interface. - * @return An html tree node corresponding to the given component, with no - * attributes set. The tag name of the created node is derived from - * the class name of childComponent. + * @return An html tree node corresponding to the given component. The tag + * name of the created node is derived from the class name of + * childComponent. */ public Node createNode(DesignSynchronizable childComponent) { - // TODO handle namespaces and id's. Class<?> componentClass = childComponent.getClass(); String packageName = componentClass.getPackage().getName(); String prefix = packageToPrefix.get(packageName); if (prefix == null) { prefix = packageName.replace('.', '_'); - prefixToPackage.put(prefix, packageName); - packageToPrefix.put(packageName, prefix); + twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix); } prefix = prefix + "-"; String className = classNameToElementName(componentClass .getSimpleName()); Element newElement = doc.createElement(prefix + className); + childComponent.synchronizeToDesign(newElement, this); + // Handle the local id. Global id and caption should have been taken + // care of by synchronizeToDesign. + String localId = componentToLocalId.get(childComponent); + if (localId != null) { + localId = LOCAL_ID_PREFIX + localId; + newElement.attr(localId, ""); + } return newElement; } @@ -171,11 +354,8 @@ public class DesignContext { /** * Creates a DesignSynchronizable object corresponding to the given html - * node. Note that the attributes of the node are not taken into account by - * this method, except IDs. To get the attributes, call - * result.synchronizeFromDesign(componentDesign, designContext), where - * result is the node returned by this method and designContext is this - * context. + * node. Also calls synchronizeFromDesign() for the created node, in effect + * creating the entire component hierarchy rooted at the returned component. * * @since * @param componentDesign @@ -187,92 +367,78 @@ public class DesignContext { public DesignSynchronizable createChild(Node componentDesign) { // Create the component. DesignSynchronizable component = instantiateComponent(componentDesign); + component.synchronizeFromDesign(componentDesign, this); // Get the IDs and the caption of the component and store them in the // maps of this design context. org.jsoup.nodes.Attributes attributes = componentDesign.attributes(); - - // Global id - String id = attributes.get(ID_ATTRIBUTE); + // global id: only update the mapping, the id has already been set for + // the component + String id = component.getCaption(); if (id != null && id.length() > 0) { - Component oldComponent = globalIds.put(camelCase(id), component); - if (oldComponent != null) { - throw new RuntimeException("Duplicate ids: " + id); + boolean mappingExists = mapGlobalId(id, component); + if (mappingExists) { + throw new LayoutInflaterException( + "The following global id is not unique: " + id); } } - - // Local id + // local id: this is not a property of a component, so need to fetch it + // from the attributes of componentDesign String localId = null; for (Attribute attribute : attributes.asList()) { if (attribute.getKey().startsWith(LOCAL_ID_PREFIX)) { if (localId != null) { - throw new RuntimeException( - "Duplicate local ids specified: " + localId - + " and " + attribute.getValue()); + throw new LayoutInflaterException( + "Duplicate local ids specified: " + + localId + + " and " + + attribute.getKey().substring( + LOCAL_ID_PREFIX.length())); } localId = attribute.getKey() .substring(LOCAL_ID_PREFIX.length()); - localIds.put(camelCase(localId), component); - } - } - - // Caption - String caption = null; - if (componentDesign.nodeName().equals("v-button")) { - String buttonCaption = textContent(componentDesign); - if (buttonCaption != null && !(buttonCaption.equals(""))) { - caption = buttonCaption; - } - } - if (caption == null) { - String componentCaption = attributes.get(CAPTION_ATTRIBUTE); - if (componentCaption != null && !("".equals(componentCaption))) { - caption = componentCaption; + mapLocalId(localId, component); // two-way map } } + // caption: a property of a component, possibly not unique + String caption = component.getCaption(); if (caption != null) { - Component oldComponent = captions - .put(camelCase(caption), component); - if (oldComponent != null) { - throw new RuntimeException("Duplicate captions: " + caption); - } + mapCaption(caption, component); } return component; } /** - * Returns the text content of an html tree node. Used for getting the - * caption of a button. + * Creates a DesignSynchronizable component corresponding to the given node. + * Does not set the attributes for the created object. * * @since * @param node - * A node of an html tree - * @return the text content of node, obtained by concatenating the text - * contents of its children + * a node of an html tree + * @return a DesignSynchronizable object corresponding to node, with no + * attributes set. */ - private String textContent(Node node) { - String text = ""; - for (Node child : node.childNodes()) { - if (child instanceof TextNode) { - text += ((TextNode) child).text(); - } + private DesignSynchronizable instantiateComponent(Node node) { + // Extract the package and class names. + String qualifiedClassName = tagNameToClassName(node); + try { + Class<? extends DesignSynchronizable> componentClass = resolveComponentClass(qualifiedClassName); + DesignSynchronizable newComponent = componentClass.newInstance(); + return newComponent; + } catch (Exception e) { + throw createException(e, qualifiedClassName); } - return text; } /** - * Creates a DesignSynchronizable component corresponding to the given node. + * Returns the qualified class name corresponding to the given html tree + * node. If the node is not a span or a div, the class name is extracted + * from the tag name of node. * * @since * @param node - * a node of an html tree - * @return a DesignSynchronizable object corresponding to node + * an html tree node + * @return The qualified class name corresponding to the given node. */ - private DesignSynchronizable instantiateComponent(Node node) { - // Extract the package and class names. - String qualifiedClassName = tagNameToClassName(node); - return createComponent(qualifiedClassName); - } - private String tagNameToClassName(Node node) { String tagName = node.nodeName(); if (tagName.equals("v-addon")) { @@ -281,18 +447,19 @@ public class DesignContext { || tagName.toLowerCase(Locale.ENGLISH).equals("div")) { return "com.vaadin.ui.Label"; } - // Otherwise, get the package name from the prefixToPackage mapping. + // Otherwise, get the full class name using the prefix to package + // mapping. Example: "v-vertical-layout" -> + // "com.vaadin.ui.VerticalLayout" String[] parts = tagName.split("-"); if (parts.length < 2) { - throw new RuntimeException("The tagname '" + tagName + throw new LayoutInflaterException("The tagname '" + tagName + "' is invalid: missing prefix."); } String prefixName = parts[0]; String packageName = prefixToPackage.get(prefixName); if (packageName == null) { - throw new RuntimeException("Unknown tag: " + tagName); + throw new LayoutInflaterException("Unknown tag: " + tagName); } - // v-vertical-layout -> com.vaadin.ui.VerticalLayout int firstCharacterIndex = prefixName.length() + 1; // +1 is for '-' tagName = tagName.substring(firstCharacterIndex, firstCharacterIndex + 1).toUpperCase(Locale.ENGLISH) @@ -307,32 +474,12 @@ public class DesignContext { } else { // Ends with "-", WTF? - System.out.println("ends with '-', really?"); + System.out.println("A tag name should not end with '-'."); } } return packageName + "." + tagName; } - /** - * Returns a new component instance of given class name. If the component - * cannot be instantiated a ComponentInstantiationException is thrown. - * - * @param qualifiedClassName - * The full class name of the object to be created. - * @return a new DesignSynchronizable instance. - * @throws ComponentInstantiationException - */ - public DesignSynchronizable createComponent(String qualifiedClassName) { - try { - Class<? extends DesignSynchronizable> componentClass = resolveComponentClass(qualifiedClassName); - DesignSynchronizable newComponent = componentClass.newInstance(); - return newComponent; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - @SuppressWarnings("unchecked") private Class<? extends DesignSynchronizable> resolveComponentClass( String qualifiedClassName) throws ClassNotFoundException { @@ -349,6 +496,20 @@ public class DesignContext { } } + /* + * Create a new ComponentInstantiationException. + */ + private ComponentInstantiationException createException(Exception e, + String qualifiedClassName) { + String message = String.format( + "Couldn't instantiate a component for %s.", qualifiedClassName); + if (e != null) { + return new ComponentInstantiationException(message, e); + } else { + return new ComponentInstantiationException(message); + } + } + /** * Returns {@code true} if the given {@link Class} implements the * {@link Component} interface of Vaadin Framework otherwise {@code false}. @@ -358,7 +519,7 @@ public class DesignContext { * @return {@code true} if the given {@link Class} is a {@link Component}, * {@code false} otherwise. */ - public static boolean isDesignSynchronizable(Class<?> componentClass) { + private static boolean isDesignSynchronizable(Class<?> componentClass) { if (componentClass != null) { return DesignSynchronizable.class.isAssignableFrom(componentClass); } else { @@ -366,54 +527,20 @@ public class DesignContext { } } - private String camelCase(String localId) { - // TODO does this method do what it should (it was taken from another - // project without any modifications) - - // Remove all but a-Z, 0-9 (used for names) and _- and space (used - // for separators) - // localId = localId.replaceAll("[^a-zA-Z0-9_- ]", ""); - return localId.replaceAll("[^a-zA-Z0-9]", "").toLowerCase( - Locale.ENGLISH); - // String[] parts = localId.split("[ -_]+"); - // String thisPart = parts[0]; - // String camelCase = - // thisPart.substring(0,1).toLowerCase(Locale.ENGLISH); - // if (parts[0].length() > 1) { - // camelCase += thisPart.substring(1); - // } - // - // for (int i=1; i < parts.length; i++) { - // thisPart = parts[i]; - // camelCase += thisPart.substring(0,1).toUpperCase(Locale.ENGLISH); - // if (thisPart.length() > 1) { - // camelCase += thisPart.substring(1); - // } - // } - // return camelCase; - } - /** - * Returns the default instance for the given class. The instance must not - * be modified by the caller. + * Returns the root component of a created component hierarchy. * * @since - * @param instanceClass * @return */ - public <T> T getDefaultInstance(Class<T> instanceClass) { - T instance = (T) instanceCache.get(instanceClass); - if (instance == null) { - try { - instance = instanceClass.newInstance(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - instanceCache.put(instanceClass, instance); - } - return instance; + public DesignSynchronizable getComponentRoot() { + return componentRoot; } -} + /** + * Sets the root component of a created component hierarchy. + */ + public void setComponentRoot(DesignSynchronizable componentRoot) { + this.componentRoot = componentRoot; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/declarative/LayoutHandler.java b/server/src/com/vaadin/ui/declarative/LayoutHandler.java new file mode 100644 index 0000000000..132bf5f5b5 --- /dev/null +++ b/server/src/com/vaadin/ui/declarative/LayoutHandler.java @@ -0,0 +1,167 @@ +/* + * 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.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.DocumentType; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.parser.Parser; + +import com.vaadin.ui.DesignSynchronizable; + +/** + * LayoutHandler is used for parsing a component hierarchy from an html file + * and, conversely, for generating an html tree representation corresponding to + * a given component hierarchy. For both parsing and tree generation the + * component hierarchy must contain a single root. + * + * + * @since + * @author Vaadin Ltd + */ +public class LayoutHandler { + /** + * Constructs a component hierarchy from the design specified as an html + * document. The 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. + * + * @since + * @param html + * the html document describing the component 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) { + Document doc; + try { + doc = Jsoup.parse(html, "UTF-8", "", Parser.htmlParser()); + } catch (IOException e) { + throw new LayoutInflaterException( + "The html document cannot be parsed."); + } + 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. + Node root = doc.body(); + DesignSynchronizable componentRoot = null; + for (Node element : root.childNodes()) { + if (element instanceof Element) { + if (componentRoot != null) { + throw new LayoutInflaterException( + "The first level of a component hierarchy should contain a single root component, but found " + + "two: " + + componentRoot + + " and " + + element + "."); + } + // createChild creates the entire component hierarchy + componentRoot = designContext.createChild(element); + designContext.setComponentRoot(componentRoot); + } + } + return designContext; + } + + /** + * Generates an html tree representation representing the component + * hierarchy having the given root. The hierarchy is stored under <body> in + * the tree. The generated tree corresponds to a valid html document. + * + * + * @since + * @param root + * the root of the component hierarchy + * @return an html tree representation of the component hierarchy + */ + public static Document createHtml(DesignContext designContext) { + // Create the html tree skeleton. + Document doc = new Document(""); + DocumentType docType = new DocumentType("html", "", "", ""); + doc.appendChild(docType); + Element html = doc.createElement("html"); + doc.appendChild(html); + html.appendChild(doc.createElement("head")); + Element body = doc.createElement("body"); + html.appendChild(body); + designContext.storePrefixes(doc); + + // Append the design under <body> in the html tree. createNode + // creates the entire component hierarchy rooted at the + // given root node. + DesignSynchronizable root = designContext.getComponentRoot(); + Node rootNode = designContext.createNode(root); + body.appendChild(rootNode); + return doc; + } + + /** + * Generates an html file corresponding to the component hierarchy with the + * given root. + * + * @since + * @param writer + * @param root + * @throws IOException + */ + public static void createHtml(BufferedWriter writer, DesignContext ctx) + throws IOException { + String docAsString = createHtml(ctx).toString(); + writer.write(docAsString); + } + + /** + * Used for testing only. + * + * This method reads and constructs a component hierarchy from a file whose + * name is given as the first parameter. The constructed hierarchy is then + * transformed back to html form, the resulting html being written to a file + * whose name is given as the second parameter. + * + * This is useful for checking that: 1) a design can be successfully parsed + * 2) a component hierarchy can be transformed to html form and 3) No + * relevant information is lost in these conversions. + * + */ + public static void main(String[] args) throws IOException { + String inputFileName = args[0]; + String outputFileName = args[1]; + // Read and parse the contents of the output file. + FileInputStream fis = new FileInputStream(inputFileName); + DesignContext ctx = parse(fis); + DesignSynchronizable root = ctx.getComponentRoot(); + // Write + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(outputFileName))); + createHtml(writer, ctx); + fis.close(); + writer.close(); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/declarative/LayoutInflaterException.java b/server/src/com/vaadin/ui/declarative/LayoutInflaterException.java new file mode 100644 index 0000000000..3482856645 --- /dev/null +++ b/server/src/com/vaadin/ui/declarative/LayoutInflaterException.java @@ -0,0 +1,18 @@ +package com.vaadin.ui.declarative; + +@SuppressWarnings("serial") +public class LayoutInflaterException extends RuntimeException { + + public LayoutInflaterException(String message) { + super(message); + } + + public LayoutInflaterException(String message, Throwable e) { + super(message, e); + } + + public LayoutInflaterException(Throwable e) { + super(e); + } + +} diff --git a/server/src/com/vaadin/ui/declarative/outputFile.txt b/server/src/com/vaadin/ui/declarative/outputFile.txt new file mode 100644 index 0000000000..d0e76034b0 --- /dev/null +++ b/server/src/com/vaadin/ui/declarative/outputFile.txt @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head></head> + <body> + <v-vertical-layout> + <v-horizontal-layout> + <v-label></v-label> + <v-native-button></v-native-button> + <v-button></v-button> + </v-horizontal-layout> + </v-vertical-layout> + </body> +</html>
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/declarative/testFile.html b/server/src/com/vaadin/ui/declarative/testFile.html new file mode 100644 index 0000000000..5d7714473d --- /dev/null +++ b/server/src/com/vaadin/ui/declarative/testFile.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <meta name="package-mapping" content="my:com.addon.mypackage"/> + </head> + <body> + <v-vertical-layout width="500px"> + <v-horizontal-layout> + <v-label plain-text caption="FooBar"></v-label> + <v-native-button caption="Native click me"></v-native-button> + <v-button plain-text caption="Click me" width = "150px"></v-button> + </v-horizontal-layout> + <v-text-field caption = "Text input"/> + <v-text-area caption = "Text area"/ height="200px" width="300px"> + </v-vertical-layout> + </body> +</html>
\ No newline at end of file |