aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/com
diff options
context:
space:
mode:
authorMika Murtojarvi <mika@vaadin.com>2014-11-28 17:58:15 +0200
committerMika Murtojarvi <mika@vaadin.com>2014-12-02 17:49:35 +0200
commitecb037d2d9ead3324b052ad69e73f22ed8672870 (patch)
treec054b1d9432f527bef22cbc78152a72fb4c12948 /server/src/com
parentd3af8be5a52365a51a0dd177617976ad566ead0e (diff)
downloadvaadin-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')
-rw-r--r--server/src/com/vaadin/ui/AbstractComponent.java50
-rw-r--r--server/src/com/vaadin/ui/AbstractOrderedLayout.java1
-rw-r--r--server/src/com/vaadin/ui/DesignSynchronizable.java1
-rw-r--r--server/src/com/vaadin/ui/declarative/ComponentInstantiationException.java18
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignContext.java459
-rw-r--r--server/src/com/vaadin/ui/declarative/LayoutHandler.java167
-rw-r--r--server/src/com/vaadin/ui/declarative/LayoutInflaterException.java18
-rw-r--r--server/src/com/vaadin/ui/declarative/outputFile.txt13
-rw-r--r--server/src/com/vaadin/ui/declarative/testFile.html17
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(&quot;Datum&quot;);
@@ -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