summaryrefslogtreecommitdiffstats
path: root/server/src/com/vaadin/ui
diff options
context:
space:
mode:
authorLeif Åstrand <leif@vaadin.com>2014-12-18 18:30:09 +0200
committerLeif Åstrand <leif@vaadin.com>2014-12-18 18:30:09 +0200
commite3d0fbbab68f9ef8642a975741d9f576993b1f38 (patch)
tree5711a31c5af1d3773a86b4d25c9f8064aa435ecc /server/src/com/vaadin/ui
parent434fb5bf5cf62490686367e9193b7898077bbd44 (diff)
parent68eec666b55c42b4a55235c00bc78f6212eb2062 (diff)
downloadvaadin-framework-e3d0fbbab68f9ef8642a975741d9f576993b1f38.tar.gz
vaadin-framework-e3d0fbbab68f9ef8642a975741d9f576993b1f38.zip
Merge remote-tracking branch 'origin/master' into grid
Change-Id: Ic7629f9cedb3c948edf4f63c678ca499e3d52805
Diffstat (limited to 'server/src/com/vaadin/ui')
-rw-r--r--server/src/com/vaadin/ui/AbsoluteLayout.java107
-rw-r--r--server/src/com/vaadin/ui/AbstractColorPicker.java8
-rw-r--r--server/src/com/vaadin/ui/AbstractComponent.java471
-rw-r--r--server/src/com/vaadin/ui/AbstractField.java57
-rw-r--r--server/src/com/vaadin/ui/AbstractOrderedLayout.java125
-rw-r--r--server/src/com/vaadin/ui/AbstractSelect.java2
-rw-r--r--server/src/com/vaadin/ui/AbstractSingleComponentContainer.java52
-rw-r--r--server/src/com/vaadin/ui/AbstractSplitPanel.java130
-rw-r--r--server/src/com/vaadin/ui/AbstractTextField.java53
-rw-r--r--server/src/com/vaadin/ui/Button.java90
-rw-r--r--server/src/com/vaadin/ui/Calendar.java2
-rw-r--r--server/src/com/vaadin/ui/CheckBox.java61
-rw-r--r--server/src/com/vaadin/ui/Component.java41
-rw-r--r--server/src/com/vaadin/ui/CssLayout.java42
-rw-r--r--server/src/com/vaadin/ui/Field.java4
-rw-r--r--server/src/com/vaadin/ui/Label.java57
-rw-r--r--server/src/com/vaadin/ui/Panel.java22
-rw-r--r--server/src/com/vaadin/ui/PasswordField.java37
-rw-r--r--server/src/com/vaadin/ui/TabSheet.java177
-rw-r--r--server/src/com/vaadin/ui/TextArea.java26
-rw-r--r--server/src/com/vaadin/ui/TextField.java37
-rw-r--r--server/src/com/vaadin/ui/declarative/Design.java481
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java715
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignContext.java719
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignException.java39
-rw-r--r--server/src/com/vaadin/ui/declarative/FieldBinder.java265
-rw-r--r--server/src/com/vaadin/ui/declarative/FieldBindingException.java34
-rw-r--r--server/src/com/vaadin/ui/themes/ValoTheme.java25
28 files changed, 3806 insertions, 73 deletions
diff --git a/server/src/com/vaadin/ui/AbsoluteLayout.java b/server/src/com/vaadin/ui/AbsoluteLayout.java
index af47981db6..12aa8ea9a6 100644
--- a/server/src/com/vaadin/ui/AbsoluteLayout.java
+++ b/server/src/com/vaadin/ui/AbsoluteLayout.java
@@ -21,6 +21,10 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
import com.vaadin.event.LayoutEvents.LayoutClickEvent;
import com.vaadin.event.LayoutEvents.LayoutClickListener;
import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
@@ -30,6 +34,8 @@ import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc;
import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
/**
* AbsoluteLayout is a layout implementation that mimics html absolute
@@ -40,6 +46,13 @@ import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState;
public class AbsoluteLayout extends AbstractLayout implements
LayoutClickNotifier {
+ // constants for design attributes
+ private static final String ATTR_TOP = ":top";
+ private static final String ATTR_RIGHT = ":right";
+ private static final String ATTR_BOTTOM = ":bottom";
+ private static final String ATTR_LEFT = ":left";
+ private static final String ATTR_Z_INDEX = ":z-index";
+
private AbsoluteLayoutServerRpc rpc = new AbsoluteLayoutServerRpc() {
@Override
@@ -658,4 +671,98 @@ public class AbsoluteLayout extends AbstractLayout implements
public void removeListener(LayoutClickListener listener) {
removeLayoutClickListener(listener);
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Node,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // process default attributes
+ super.readDesign(design, designContext);
+ // handle children
+ for (Element childComponent : design.children()) {
+ Attributes attr = childComponent.attributes();
+ Component newChild = designContext.readDesign(childComponent);
+ StringBuilder css = new StringBuilder();
+ if (attr.hasKey(ATTR_TOP)) {
+ css.append("top:").append(attr.get(ATTR_TOP)).append(";");
+ }
+ if (attr.hasKey(ATTR_RIGHT)) {
+ css.append("right:").append(attr.get(ATTR_RIGHT)).append(";");
+ }
+ if (attr.hasKey(ATTR_BOTTOM)) {
+ css.append("bottom:").append(attr.get(ATTR_BOTTOM)).append(";");
+ }
+ if (attr.hasKey(ATTR_LEFT)) {
+ css.append("left:").append(attr.get(ATTR_LEFT)).append(";");
+ }
+ if (attr.hasKey(ATTR_Z_INDEX)) {
+ css.append("z-index:").append(attr.get(ATTR_Z_INDEX))
+ .append(";");
+ }
+ addComponent(newChild, css.toString());
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Node,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ AbsoluteLayout def = designContext.getDefaultInstance(this);
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+ // handle children
+ for (Component child : this) {
+ Element childElement = designContext.createElement(child);
+ design.appendChild(childElement);
+ child.writeDesign(childElement, designContext);
+ // handle position
+ ComponentPosition position = getPosition(child);
+ writePositionAttribute(childElement, ATTR_TOP, position
+ .getTopUnits().getSymbol(), position.getTopValue());
+ writePositionAttribute(childElement, ATTR_RIGHT, position
+ .getRightUnits().getSymbol(), position.getRightValue());
+ writePositionAttribute(childElement, ATTR_BOTTOM, position
+ .getBottomUnits().getSymbol(), position.getBottomValue());
+ writePositionAttribute(childElement, ATTR_LEFT, position
+ .getLeftUnits().getSymbol(), position.getLeftValue());
+ // handle z-index
+ if (position.getZIndex() >= 0) {
+ childElement
+ .attr(ATTR_Z_INDEX, String.valueOf(position.zIndex));
+ }
+ }
+ }
+
+ /**
+ * Private method for writing position attributes
+ *
+ * @since
+ * @param node
+ * target node
+ * @param key
+ * attribute key
+ * @param symbol
+ * value symbol
+ * @param value
+ * the value
+ */
+ private void writePositionAttribute(Node node, String key, String symbol,
+ Float value) {
+ if (value != null) {
+ String valueString = DesignAttributeHandler.formatFloat(value
+ .floatValue());
+ node.attr(key, valueString + symbol);
+ }
+ }
+
}
diff --git a/server/src/com/vaadin/ui/AbstractColorPicker.java b/server/src/com/vaadin/ui/AbstractColorPicker.java
index acf3b2c042..608a42d33b 100644
--- a/server/src/com/vaadin/ui/AbstractColorPicker.java
+++ b/server/src/com/vaadin/ui/AbstractColorPicker.java
@@ -455,9 +455,11 @@ public abstract class AbstractColorPicker extends AbstractComponent implements
* @param htmlContentAllowed
* <code>true</code> if caption is rendered as HTML,
* <code>false</code> otherwise
+ * @deprecated as of , use {@link #setCaptionAsHtml(boolean)} instead
*/
+ @Deprecated
public void setHtmlContentAllowed(boolean htmlContentAllowed) {
- getState().htmlContentAllowed = htmlContentAllowed;
+ setCaptionAsHtml(htmlContentAllowed);
}
/**
@@ -465,8 +467,10 @@ public abstract class AbstractColorPicker extends AbstractComponent implements
*
* @return <code>true</code> if the caption text is to be rendered as HTML,
* <code>false</code> otherwise
+ * @deprecated as of , use {@link #isCaptionAsHtml()} instead
*/
+ @Deprecated
public boolean isHtmlContentAllowed() {
- return getState(false).htmlContentAllowed;
+ return isCaptionAsHtml();
}
}
diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java
index 5c4fba739d..9ff6dff21e 100644
--- a/server/src/com/vaadin/ui/AbstractComponent.java
+++ b/server/src/com/vaadin/ui/AbstractComponent.java
@@ -16,29 +16,44 @@
package com.vaadin.ui;
-import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.StringTokenizer;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
import com.vaadin.event.ActionManager;
import com.vaadin.event.ConnectorActionManager;
import com.vaadin.event.ShortcutListener;
import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractErrorMessage.ContentMode;
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.ErrorMessage.ErrorLevel;
+import com.vaadin.server.Extension;
import com.vaadin.server.Resource;
+import com.vaadin.server.Responsive;
+import com.vaadin.server.SizeWithUnit;
+import com.vaadin.server.Sizeable;
+import com.vaadin.server.UserError;
import com.vaadin.server.VaadinSession;
import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.ComponentConstants;
import com.vaadin.shared.ui.ComponentStateUtil;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.Field.ValueChangeEvent;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.util.ReflectTools;
/**
@@ -46,7 +61,7 @@ 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
*/
@@ -83,8 +98,6 @@ public abstract class AbstractComponent extends AbstractClientConnector
private float height = SIZE_UNDEFINED;
private Unit widthUnit = Unit.PIXELS;
private Unit heightUnit = Unit.PIXELS;
- private static final Pattern sizePattern = Pattern
- .compile(SharedUtil.SIZE_PATTERN);
/**
* Keeps track of the Actions added to this component; the actual
@@ -98,6 +111,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
private Boolean explicitImmediateValue;
+ protected static final String DESIGN_ATTR_PLAIN_TEXT = "plain-text";
+
/* Constructor */
/**
@@ -241,7 +256,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.
*/
@@ -250,6 +265,35 @@ public abstract class AbstractComponent extends AbstractClientConnector
getState().caption = caption;
}
+ /**
+ * Sets whether the caption is rendered as HTML.
+ * <p>
+ * If set to true, the captions are rendered in the browser as HTML and the
+ * developer is responsible for ensuring no harmful HTML is used. If set to
+ * false, the caption is rendered in the browser as plain text.
+ * <p>
+ * The default is false, i.e. to render that caption as plain text.
+ *
+ * @param captionAsHtml
+ * true if the captions are rendered as HTML, false if rendered
+ * as plain text
+ */
+ public void setCaptionAsHtml(boolean captionAsHtml) {
+ getState().captionAsHtml = captionAsHtml;
+ }
+
+ /**
+ * Checks whether captions are rendered as HTML
+ * <p>
+ * The default is false, i.e. to render that caption as plain text.
+ *
+ * @return true if the captions are rendered as HTML, false if rendered as
+ * plain text
+ */
+ public boolean isCaptionAsHtml() {
+ return getState(false).captionAsHtml;
+ }
+
/*
* Don't add a JavaDoc comment here, we use the default documentation from
* implemented interface.
@@ -272,7 +316,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;);
@@ -284,8 +328,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.
*/
@@ -311,7 +355,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.
*/
@@ -377,7 +421,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.
@@ -438,11 +482,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.
*/
@@ -491,7 +535,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
@@ -512,7 +556,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
@@ -525,9 +569,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() {
@@ -537,9 +581,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.
*/
@@ -616,7 +660,7 @@ public abstract class AbstractComponent extends AbstractClientConnector
/**
* Build CSS compatible string representation of height.
- *
+ *
* @return CSS height
*/
private String getCSSHeight() {
@@ -625,7 +669,7 @@ public abstract class AbstractComponent extends AbstractClientConnector
/**
* Build CSS compatible string representation of width.
- *
+ *
* @return CSS width
*/
private String getCSSWidth() {
@@ -635,12 +679,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
@@ -731,7 +775,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
@@ -742,7 +786,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
*/
@@ -873,7 +917,7 @@ public abstract class AbstractComponent extends AbstractClientConnector
*/
@Override
public void setWidth(String width) {
- Size size = parseStringSize(width);
+ SizeWithUnit size = SizeWithUnit.parseStringSize(width);
if (size != null) {
setWidth(size.getSize(), size.getUnit());
} else {
@@ -888,7 +932,7 @@ public abstract class AbstractComponent extends AbstractClientConnector
*/
@Override
public void setHeight(String height) {
- Size size = parseStringSize(height);
+ SizeWithUnit size = SizeWithUnit.parseStringSize(height);
if (size != null) {
setHeight(size.getSize(), size.getUnit());
} else {
@@ -897,51 +941,350 @@ public abstract class AbstractComponent extends AbstractClientConnector
}
/*
- * Returns array with size in index 0 unit in index 1. Null or empty string
- * will produce {-1,Unit#PIXELS}
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#readDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
*/
- private static Size parseStringSize(String s) {
- if (s == null) {
- return null;
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ Attributes attr = design.attributes();
+ // handle default attributes
+ for (String attribute : getDefaultAttributes()) {
+ if (design.hasAttr(attribute)) {
+ DesignAttributeHandler.assignValue(this, attribute,
+ design.attr(attribute));
+ }
+
+ }
+ // handle immediate
+ if (attr.hasKey("immediate")) {
+ setImmediate(DesignAttributeHandler.parseBoolean(attr
+ .get("immediate")));
+ }
+
+ // handle locale
+ if (attr.hasKey("locale")) {
+ setLocale(getLocaleFromString(attr.get("locale")));
+ }
+ // handle width and height
+ readSize(attr);
+ // handle component error
+ if (attr.hasKey("error")) {
+ UserError error = new UserError(attr.get("error"),
+ ContentMode.HTML, ErrorLevel.ERROR);
+ setComponentError(error);
+ }
+ // Tab index when applicable
+ if (design.hasAttr("tabindex") && this instanceof Focusable) {
+ ((Focusable) this).setTabIndex(DesignAttributeHandler
+ .readAttribute("tabindex", design.attributes(),
+ Integer.class));
+ }
+
+ // handle responsive
+ if (attr.hasKey("responsive")) {
+ setResponsive(DesignAttributeHandler.parseBoolean(attr
+ .get("responsive")));
+ }
+ // check for unsupported attributes
+ Set<String> supported = new HashSet<String>();
+ supported.addAll(getDefaultAttributes());
+ supported.addAll(getCustomAttributes());
+ for (Attribute a : attr) {
+ if (!a.getKey().startsWith(":") && !supported.contains(a.getKey())) {
+ getLogger().info(
+ "Unsupported attribute found when reading from design : "
+ + a.getKey());
+ }
}
- s = s.trim();
- if ("".equals(s)) {
+ }
+
+ /**
+ * Constructs a Locale corresponding to the given string. The string should
+ * consist of one, two or three parts with '_' between the different parts
+ * if there is more than one part. The first part specifies the language,
+ * the second part the country and the third part the variant of the locale.
+ *
+ * @param localeString
+ * the locale specified as a string
+ * @return the Locale object corresponding to localeString
+ */
+ private Locale getLocaleFromString(String localeString) {
+ if (localeString == null) {
return null;
}
- float size = 0;
- Unit unit = null;
- Matcher matcher = sizePattern.matcher(s);
- if (matcher.find()) {
- size = Float.parseFloat(matcher.group(1));
- if (size < 0) {
- size = -1;
- unit = Unit.PIXELS;
- } else {
- String symbol = matcher.group(2);
- unit = Unit.getUnitFromSymbol(symbol);
+ String[] parts = localeString.split("_");
+ if (parts.length > 3) {
+ throw new RuntimeException("Cannot parse the locale string: "
+ + localeString);
+ }
+ switch (parts.length) {
+ case 1:
+ return new Locale(parts[0]);
+ case 2:
+ return new Locale(parts[0], parts[1]);
+ default:
+ return new Locale(parts[0], parts[1], parts[2]);
+ }
+ }
+
+ /**
+ * Toggles responsiveness of this component.
+ *
+ * @since 7.4
+ * @param responsive
+ * boolean enables responsiveness, false disables
+ */
+ private void setResponsive(boolean responsive) {
+ if (responsive) {
+ // make responsive if necessary
+ if (!isResponsive()) {
+ Responsive.makeResponsive(this);
+ }
+ } else {
+ // remove responsive extensions
+ List<Extension> extensions = new ArrayList<Extension>(
+ getExtensions());
+ for (Extension e : extensions) {
+ if (e instanceof Responsive) {
+ removeExtension(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if the component is responsive
+ *
+ * @since 7.4
+ * @return true if the component is responsive
+ */
+ private boolean isResponsive() {
+ for (Extension e : getExtensions()) {
+ if (e instanceof Responsive) {
+ return true;
}
+ }
+ return false;
+ }
+
+ /**
+ * Reads the size of this component from the given design attributes. If the
+ * attributes do not contain relevant size information, defaults is
+ * consulted.
+ *
+ * @param attributes
+ * the design attributes
+ * @param defaultInstance
+ * instance of the class that has default sizing.
+ */
+ private void readSize(Attributes attributes) {
+ // read width
+ if (attributes.hasKey("width-auto") || attributes.hasKey("size-auto")) {
+ this.setWidth(null);
+ } else if (attributes.hasKey("width-full")
+ || attributes.hasKey("size-full")) {
+ this.setWidth("100%");
+ } else if (attributes.hasKey("width")) {
+ this.setWidth(attributes.get("width"));
+ }
+
+ // read height
+ if (attributes.hasKey("height-auto") || attributes.hasKey("size-auto")) {
+ this.setHeight(null);
+ } else if (attributes.hasKey("height-full")
+ || attributes.hasKey("size-full")) {
+ this.setHeight("100%");
+ } else if (attributes.hasKey("height")) {
+ this.setHeight(attributes.get("height"));
+ }
+ }
+
+ /**
+ * Writes the size related attributes for the component if they differ from
+ * the defaults
+ *
+ * @param component
+ * the component
+ * @param attributes
+ * the attribute map where the attribute are written
+ * @param defaultInstance
+ * the default instance of the class for fetching the default
+ * values
+ */
+ private void writeSize(Attributes attributes, Component defaultInstance) {
+ if (hasEqualSize(defaultInstance)) {
+ // we have default values -> ignore
+ return;
+ }
+ boolean widthFull = getWidth() == 100f
+ && getWidthUnits().equals(Sizeable.Unit.PERCENTAGE);
+ boolean heightFull = getHeight() == 100f
+ && getHeightUnits().equals(Sizeable.Unit.PERCENTAGE);
+ boolean widthAuto = getWidth() == -1;
+ boolean heightAuto = getHeight() == -1;
+
+ // first try the full shorthands
+ if (widthFull && heightFull) {
+ attributes.put("size-full", "true");
+ } else if (widthAuto && heightAuto) {
+ attributes.put("size-auto", "true");
} else {
- throw new IllegalArgumentException("Invalid size argument: \"" + s
- + "\" (should match " + sizePattern.pattern() + ")");
+ // handle width
+ if (!hasEqualWidth(defaultInstance)) {
+ if (widthFull) {
+ attributes.put("width-full", "true");
+ } else if (widthAuto) {
+ attributes.put("width-auto", "true");
+ } else {
+ String widthString = DesignAttributeHandler
+ .formatFloat(getWidth())
+ + getWidthUnits().getSymbol();
+ attributes.put("width", widthString);
+
+ }
+ }
+ if (!hasEqualHeight(defaultInstance)) {
+ // handle height
+ if (heightFull) {
+ attributes.put("height-full", "true");
+ } else if (heightAuto) {
+ attributes.put("height-auto", "true");
+ } else {
+ String heightString = DesignAttributeHandler
+ .formatFloat(getHeight())
+ + getHeightUnits().getSymbol();
+ attributes.put("height", heightString);
+ }
+ }
}
- return new Size(size, unit);
}
- private static class Size implements Serializable {
- float size;
- Unit unit;
+ /**
+ * Test if the given component has equal width with this instance
+ *
+ * @param component
+ * the component for the width comparison
+ * @return true if the widths are equal
+ */
+ private boolean hasEqualWidth(Component component) {
+ return getWidth() == component.getWidth()
+ && getWidthUnits().equals(component.getWidthUnits());
+ }
- public Size(float size, Unit unit) {
- this.size = size;
- this.unit = unit;
+ /**
+ * Test if the given component has equal height with this instance
+ *
+ * @param component
+ * the component for the height comparison
+ * @return true if the heights are equal
+ */
+ private boolean hasEqualHeight(Component component) {
+ return getHeight() == component.getHeight()
+ && getHeightUnits().equals(component.getHeightUnits());
+ }
+
+ /**
+ * Test if the given components has equal size with this instance
+ *
+ * @param component
+ * the component for the size comparison
+ * @return true if the sizes are equal
+ */
+ private boolean hasEqualSize(Component component) {
+ return hasEqualWidth(component) && hasEqualHeight(component);
+ }
+
+ /**
+ * Returns a collection of attributes that do not require custom handling
+ * when reading or writing design. These are typically attributes of some
+ * primitive type. The default implementation searches setters with
+ * primitive values
+ *
+ * @return a collection of attributes that can be read and written using the
+ * default approach.
+ */
+ private Collection<String> getDefaultAttributes() {
+ Collection<String> attributes = DesignAttributeHandler
+ .getSupportedAttributes(this.getClass());
+ attributes.removeAll(getCustomAttributes());
+ return attributes;
+ }
+
+ /**
+ * Returns a collection of attributes that should not be handled by the
+ * basic implementation of the {@link readDesign} and {@link writeDesign}
+ * methods. Typically these are handled in a custom way in the overridden
+ * versions of the above methods
+ *
+ * @since 7.4
+ *
+ * @return the collection of attributes that are not handled by the basic
+ * implementation
+ */
+ protected Collection<String> getCustomAttributes() {
+ ArrayList<String> l = new ArrayList<String>(
+ Arrays.asList(customAttributes));
+ if (this instanceof Focusable) {
+ l.add("tab-index");
+ l.add("tabindex");
}
+ return l;
+ }
+
+ private static final String[] customAttributes = new String[] { "width",
+ "height", "debug-id", "error", "width-auto", "height-auto",
+ "width-full", "height-full", "size-auto", "size-full",
+ "responsive", "immediate", "locale", "read-only", "_id" };
- public float getSize() {
- return size;
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#writeDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // clear element contents
+ DesignAttributeHandler.clearElement(design);
+ AbstractComponent def = designContext.getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ // handle default attributes
+ for (String attribute : getDefaultAttributes()) {
+ DesignAttributeHandler.writeAttribute(this, attribute, attr, def);
+ }
+ // handle immediate
+ if (explicitImmediateValue != null) {
+ DesignAttributeHandler.writeAttribute("immediate", attr,
+ explicitImmediateValue, def.isImmediate(), Boolean.class);
+ }
+ // handle locale
+ if (getLocale() != null
+ && (getParent() == null || !getLocale().equals(
+ getParent().getLocale()))) {
+ design.attr("locale", getLocale().toString());
+ }
+ // handle size
+ writeSize(attr, def);
+ // handle component error
+ String errorMsg = getComponentError() != null ? getComponentError()
+ .getFormattedHtmlMessage() : null;
+ String defErrorMsg = def.getComponentError() != null ? def
+ .getComponentError().getFormattedHtmlMessage() : null;
+ if (!SharedUtil.equals(errorMsg, defErrorMsg)) {
+ attr.put("error", errorMsg);
+ }
+ // handle tab index
+ if (this instanceof Focusable) {
+ DesignAttributeHandler.writeAttribute("tabindex", attr,
+ ((Focusable) this).getTabIndex(),
+ ((Focusable) def).getTabIndex(), Integer.class);
}
- public Unit getUnit() {
- return unit;
+ // handle responsive
+ if (isResponsive()) {
+ attr.put("responsive", "");
}
}
@@ -952,7 +1295,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() {
@@ -996,7 +1339,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
@@ -1012,4 +1355,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
}
return false;
}
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(AbstractComponent.class.getName());
+ }
}
diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java
index df7bbb68a2..9b9c7efd86 100644
--- a/server/src/com/vaadin/ui/AbstractField.java
+++ b/server/src/com/vaadin/ui/AbstractField.java
@@ -25,6 +25,10 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
import com.vaadin.data.Buffered;
import com.vaadin.data.Property;
@@ -43,6 +47,8 @@ import com.vaadin.server.CompositeErrorMessage;
import com.vaadin.server.ErrorMessage;
import com.vaadin.shared.AbstractFieldState;
import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
/**
* <p>
@@ -1738,4 +1744,55 @@ public abstract class AbstractField<T> extends AbstractComponent implements
isListeningToPropertyEvents = false;
}
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ Attributes attr = design.attributes();
+ if (design.hasAttr("readonly")) {
+ setReadOnly(DesignAttributeHandler.readAttribute("readonly", attr,
+ Boolean.class));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> attributes = super.getCustomAttributes();
+ attributes.add("readonly");
+ // must be handled by subclasses
+ attributes.add("value");
+ return attributes;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ AbstractField def = (AbstractField) designContext
+ .getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ // handle readonly
+ DesignAttributeHandler.writeAttribute("readonly", attr,
+ super.isReadOnly(), def.isReadOnly(), Boolean.class);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(AbstractField.class.getName());
+ }
}
diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
index 638f6bc3f9..67bcfc904c 100644
--- a/server/src/com/vaadin/ui/AbstractOrderedLayout.java
+++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
@@ -16,8 +16,13 @@
package com.vaadin.ui;
+import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
import com.vaadin.event.LayoutEvents.LayoutClickEvent;
import com.vaadin.event.LayoutEvents.LayoutClickListener;
@@ -26,10 +31,13 @@ import com.vaadin.server.Sizeable;
import com.vaadin.shared.Connector;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.AlignmentInfo;
import com.vaadin.shared.ui.MarginInfo;
import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc;
import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState;
import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState.ChildComponentData;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
@SuppressWarnings("serial")
public abstract class AbstractOrderedLayout extends AbstractLayout implements
@@ -459,4 +467,121 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements
setExpandRatio(target, expandRatio);
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // process default attributes
+ super.readDesign(design, designContext);
+ // handle margin
+ if (design.hasAttr("margin")) {
+ setMargin(DesignAttributeHandler.readAttribute("margin",
+ design.attributes(), Boolean.class));
+ }
+ // handle children
+ for (Element childComponent : design.children()) {
+ Attributes attr = childComponent.attributes();
+ Component newChild = designContext.readDesign(childComponent);
+ addComponent(newChild);
+ // handle alignment
+ int bitMask = 0;
+ if (attr.hasKey(":middle")) {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_VERTICAL_CENTER;
+ } else if (attr.hasKey(":bottom")) {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_BOTTOM;
+ } else {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_TOP;
+ }
+ if (attr.hasKey(":center")) {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_HORIZONTAL_CENTER;
+ } else if (attr.hasKey(":right")) {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_RIGHT;
+ } else {
+ bitMask += AlignmentInfo.Bits.ALIGNMENT_LEFT;
+ }
+ setComponentAlignment(newChild, new Alignment(bitMask));
+ // handle expand ratio
+ if (attr.hasKey(":expand")) {
+ String value = attr.get(":expand");
+ if (value.length() > 0) {
+ try {
+ float ratio = Float.valueOf(value);
+ setExpandRatio(newChild, ratio);
+ } catch (NumberFormatException nfe) {
+ getLogger().info(
+ "Failed to parse expand ratio " + value);
+ }
+ } else {
+ setExpandRatio(newChild, 1.0f);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // write default attributes
+ super.writeDesign(design, designContext);
+ // handle margin
+ AbstractOrderedLayout def = (AbstractOrderedLayout) designContext
+ .getDefaultInstance(this);
+ if (getMargin().getBitMask() != def.getMargin().getBitMask()) {
+ design.attr("margin", "");
+ }
+ // handle children
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+
+ for (Component child : this) {
+ Element childElement = designContext.createElement(child);
+ design.appendChild(childElement);
+ // handle alignment
+ Alignment alignment = getComponentAlignment(child);
+ if (alignment.isMiddle()) {
+ childElement.attr(":middle", "");
+ } else if (alignment.isBottom()) {
+ childElement.attr(":bottom", "");
+ }
+ if (alignment.isCenter()) {
+ childElement.attr(":center", "");
+ } else if (alignment.isRight()) {
+ childElement.attr(":right", "");
+ }
+ // handle expand ratio
+ float expandRatio = getExpandRatio(child);
+ if (expandRatio == 1.0f) {
+ childElement.attr(":expand", "");
+ } else if (expandRatio > 0) {
+ childElement.attr(":expand",
+ DesignAttributeHandler.formatFloat(expandRatio));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> customAttributes = super.getCustomAttributes();
+ customAttributes.add("margin");
+ return customAttributes;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractOrderedLayout.class.getName());
+ }
}
diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java
index 1a3eeb88a3..d5e47b2286 100644
--- a/server/src/com/vaadin/ui/AbstractSelect.java
+++ b/server/src/com/vaadin/ui/AbstractSelect.java
@@ -1729,6 +1729,8 @@ public abstract class AbstractSelect extends AbstractField<Object> implements
/**
* Removes orphaned ids from selection.
+ *
+ * @since 7.4
*/
protected void adjustSelection() {
Object value = getValue();
diff --git a/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java b/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java
index e7b5205f2d..244feb3bb9 100644
--- a/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java
+++ b/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java
@@ -18,9 +18,13 @@ package com.vaadin.ui;
import java.util.Collections;
import java.util.Iterator;
+import org.jsoup.nodes.Element;
+
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinSession;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
/**
* Abstract base class for component containers that have only one child
@@ -274,4 +278,50 @@ public abstract class AbstractSingleComponentContainer extends
repaintChangedChildTree(dirtyChild, childrenMayBecomeUndefined, true);
}
-}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // process default attributes
+ super.readDesign(design, designContext);
+ // handle child element, checking that the design specifies at most one
+ // child
+ int childCount = design.children().size();
+ if (childCount > 1) {
+ throw new DesignException("The container of type "
+ + getClass().toString()
+ + " can have only one child component.");
+ } else if (childCount == 1) {
+ Element childElement = design.children().get(0);
+ Component newChild = designContext.readDesign(childElement);
+ setContent(newChild);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // write default attributes (also clears children and attributes)
+ super.writeDesign(design, designContext);
+ AbstractSingleComponentContainer def = designContext
+ .getDefaultInstance(this);
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+ // handle child component
+ Component child = getContent();
+ if (child != null) {
+ Element childNode = designContext.createElement(child);
+ design.appendChild(childNode);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/AbstractSplitPanel.java b/server/src/com/vaadin/ui/AbstractSplitPanel.java
index a78f192fa2..414681f5dd 100644
--- a/server/src/com/vaadin/ui/AbstractSplitPanel.java
+++ b/server/src/com/vaadin/ui/AbstractSplitPanel.java
@@ -18,16 +18,23 @@ package com.vaadin.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
+import java.util.Collection;
import java.util.Iterator;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.server.SizeWithUnit;
import com.vaadin.server.Sizeable;
import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState;
import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
import com.vaadin.util.ReflectTools;
/**
@@ -351,7 +358,7 @@ public abstract class AbstractSplitPanel extends AbstractComponentContainer {
* by the first region, but if split position is reversed the measuring is
* done by the second region instead.
*
- * @since
+ * @since 7.4
* @return {@code true} if reversed, {@code false} otherwise.
* @see #setSplitPosition(float, boolean)
*/
@@ -558,4 +565,125 @@ public abstract class AbstractSplitPanel extends AbstractComponentContainer {
private SplitterState getSplitterState(boolean markAsDirty) {
return ((AbstractSplitPanelState) super.getState(markAsDirty)).splitterState;
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // handle default attributes
+ super.readDesign(design, designContext);
+ // handle custom attributes, use default values if no explicit value
+ // set
+ // There is no setter for reversed, so it will be handled using
+ // setSplitPosition.
+ boolean reversed = false;
+ if (design.hasAttr("reversed")) {
+ reversed = DesignAttributeHandler.readAttribute("reversed",
+ design.attributes(), Boolean.class);
+ setSplitPosition(getSplitPosition(), reversed);
+ }
+ if (design.hasAttr("split-position")) {
+ SizeWithUnit splitPosition = SizeWithUnit.parseStringSize(
+ design.attr("split-position"), Unit.PERCENTAGE);
+ setSplitPosition(splitPosition.getSize(), splitPosition.getUnit(),
+ reversed);
+ }
+ if (design.hasAttr("min-split-position")) {
+ SizeWithUnit minSplitPosition = SizeWithUnit.parseStringSize(
+ design.attr("min-split-position"), Unit.PERCENTAGE);
+ setMinSplitPosition(minSplitPosition.getSize(),
+ minSplitPosition.getUnit());
+ }
+ if (design.hasAttr("max-split-position")) {
+ SizeWithUnit maxSplitPosition = SizeWithUnit.parseStringSize(
+ design.attr("max-split-position"), Unit.PERCENTAGE);
+ setMaxSplitPosition(maxSplitPosition.getSize(),
+ maxSplitPosition.getUnit());
+ }
+ // handle children
+ if (design.children().size() > 2) {
+ throw new DesignException(
+ "A split panel can contain at most two components.");
+ }
+ for (Element childElement : design.children()) {
+ Component childComponent = designContext.readDesign(childElement);
+ if (childElement.hasAttr(":second")) {
+ setSecondComponent(childComponent);
+ } else {
+ addComponent(childComponent);
+ }
+ }
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> attributes = super.getCustomAttributes();
+ // the setters of the properties do not accept strings such as "20px"
+ attributes.add("split-position");
+ attributes.add("min-split-position");
+ attributes.add("max-split-position");
+ // no explicit setter for reversed
+ attributes.add("reversed");
+ return attributes;
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // handle default attributes (also clears children and attributes)
+ super.writeDesign(design, designContext);
+ // handle custom attributes (write only if a value is not the
+ // default value)
+ AbstractSplitPanel def = (AbstractSplitPanel) designContext
+ .getDefaultInstance(this);
+ if (getSplitPosition() != def.getSplitPosition()
+ || !def.getSplitPositionUnit().equals(getSplitPositionUnit())) {
+ String splitPositionString = asString(getSplitPosition())
+ + getSplitPositionUnit();
+ design.attr("split-position", splitPositionString);
+ }
+ if (getMinSplitPosition() != def.getMinSplitPosition()
+ || !def.getMinSplitPositionUnit().equals(
+ getMinSplitPositionUnit())) {
+ design.attr("min-split-position", asString(getMinSplitPosition())
+ + getMinSplitPositionUnit());
+ }
+ if (getMaxSplitPosition() != def.getMaxSplitPosition()
+ || !def.getMaxSplitPositionUnit().equals(
+ getMaxSplitPositionUnit())) {
+ design.attr("max-split-position", asString(getMaxSplitPosition())
+ + getMaxSplitPositionUnit());
+ }
+ if (getSplitterState().positionReversed) {
+ design.attr("reversed", "");
+ }
+ // handle child components
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+ Component firstComponent = getFirstComponent();
+ Component secondComponent = getSecondComponent();
+ if (firstComponent != null) {
+ Element childElement = designContext.createElement(firstComponent);
+ design.appendChild(childElement);
+ }
+ if (secondComponent != null) {
+ Element childElement = designContext.createElement(secondComponent);
+ if (firstComponent == null) {
+ childElement.attr(":second", "");
+ }
+ design.appendChild(childElement);
+ }
+ }
+
+ private String asString(float number) {
+ int truncated = (int) number;
+ if (truncated == number) {
+ return "" + truncated;
+ }
+ return "" + number;
+ }
}
diff --git a/server/src/com/vaadin/ui/AbstractTextField.java b/server/src/com/vaadin/ui/AbstractTextField.java
index ea0372bc8c..93025ac0fd 100644
--- a/server/src/com/vaadin/ui/AbstractTextField.java
+++ b/server/src/com/vaadin/ui/AbstractTextField.java
@@ -16,8 +16,12 @@
package com.vaadin.ui;
+import java.util.Collection;
import java.util.Map;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.BlurNotifier;
@@ -31,6 +35,8 @@ import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.shared.ui.textfield.AbstractTextFieldState;
import com.vaadin.shared.ui.textfield.TextFieldConstants;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
public abstract class AbstractTextField extends AbstractField<String> implements
BlurNotifier, FocusNotifier, TextChangeNotifier, LegacyComponent {
@@ -757,4 +763,51 @@ public abstract class AbstractTextField extends AbstractField<String> implements
removeBlurListener(listener);
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#readDesign(org.jsoup.nodes.Element ,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ Attributes attr = design.attributes();
+ if (attr.hasKey("maxlength")) {
+ setMaxLength(DesignAttributeHandler.readAttribute("maxlength",
+ attr, Integer.class));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> customAttributes = super.getCustomAttributes();
+ customAttributes.add("maxlength");
+ customAttributes.add("max-length"); // to prevent this appearing in
+ // output
+ customAttributes.add("cursor-position");
+ return customAttributes;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#writeDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ AbstractTextField def = (AbstractTextField) designContext
+ .getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ DesignAttributeHandler.writeAttribute("maxlength", attr,
+ getMaxLength(), def.getMaxLength(), Integer.class);
+ }
+
}
diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java
index e58ad7bee5..677f29ba13 100644
--- a/server/src/com/vaadin/ui/Button.java
+++ b/server/src/com/vaadin/ui/Button.java
@@ -18,6 +18,10 @@ package com.vaadin.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
+import java.util.Collection;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
import com.vaadin.event.Action;
import com.vaadin.event.FieldEvents;
@@ -35,6 +39,8 @@ import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.button.ButtonServerRpc;
import com.vaadin.shared.ui.button.ButtonState;
import com.vaadin.ui.Component.Focusable;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.util.ReflectTools;
/**
@@ -636,7 +642,7 @@ public class Button extends AbstractComponent implements
/**
* Set whether the caption text is rendered as HTML or not. You might need
- * to retheme button to allow higher content than the original text style.
+ * to re-theme button to allow higher content than the original text style.
*
* If set to true, the captions are passed to the browser as html and the
* developer is responsible for ensuring no harmful html is used. If set to
@@ -647,7 +653,7 @@ public class Button extends AbstractComponent implements
* <code>false</code> otherwise
*/
public void setHtmlContentAllowed(boolean htmlContentAllowed) {
- getState().htmlContentAllowed = htmlContentAllowed;
+ getState().captionAsHtml = htmlContentAllowed;
}
/**
@@ -657,7 +663,85 @@ public class Button extends AbstractComponent implements
* <code>false</code> otherwise
*/
public boolean isHtmlContentAllowed() {
- return getState(false).htmlContentAllowed;
+ return getState(false).captionAsHtml;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ Attributes attr = design.attributes();
+ String content = design.html();
+ setCaption(content);
+ // plain-text (default is html)
+ Boolean plain = DesignAttributeHandler.readAttribute(
+ DESIGN_ATTR_PLAIN_TEXT, attr, Boolean.class);
+ if (plain == null || !plain) {
+ setHtmlContentAllowed(true);
+ }
+ if (attr.hasKey("icon-alt")) {
+ setIconAlternateText(DesignAttributeHandler.readAttribute(
+ "icon-alt", attr, String.class));
+ }
+ // click-shortcut
+ removeClickShortcut();
+ ShortcutAction action = DesignAttributeHandler.readAttribute(
+ "click-shortcut", attr, ShortcutAction.class);
+ if (action != null) {
+ setClickShortcut(action.getKeyCode(), action.getModifiers());
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add(DESIGN_ATTR_PLAIN_TEXT);
+ result.add("caption");
+ result.add("icon-alt");
+ result.add("icon-alternate-text");
+ result.add("click-shortcut");
+ result.add("html-content-allowed");
+ result.add("caption-as-html");
+ return result;
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ Attributes attr = design.attributes();
+ Button def = (Button) designContext.getDefaultInstance(this);
+ String content = getCaption();
+ if (content != null) {
+ design.html(content);
+ }
+ // plain-text (default is html)
+ if (!isHtmlContentAllowed()) {
+ design.attr(DESIGN_ATTR_PLAIN_TEXT, "");
+ }
+ // icon-alt
+ DesignAttributeHandler.writeAttribute("icon-alt", attr,
+ getIconAlternateText(), def.getIconAlternateText(),
+ String.class);
+ // click-shortcut
+ if (clickShortcut != null) {
+ DesignAttributeHandler.writeAttribute("click-shortcut", attr,
+ clickShortcut, null, ShortcutAction.class);
+ }
+ }
}
diff --git a/server/src/com/vaadin/ui/Calendar.java b/server/src/com/vaadin/ui/Calendar.java
index 72ff6eb0e0..5b5c390fa1 100644
--- a/server/src/com/vaadin/ui/Calendar.java
+++ b/server/src/com/vaadin/ui/Calendar.java
@@ -918,7 +918,7 @@ public class Calendar extends AbstractComponent implements
*
* @return true if the client is allowed to click events
* @see #isClientChangeAllowed()
- * @deprecated Override {@link #fireEventClick(Integer)} instead.
+ * @deprecated As of 7.4, override {@link #fireEventClick(Integer)} instead.
*/
@Deprecated
protected boolean isEventClickAllowed() {
diff --git a/server/src/com/vaadin/ui/CheckBox.java b/server/src/com/vaadin/ui/CheckBox.java
index 7d9da30f29..e98a2b61b9 100644
--- a/server/src/com/vaadin/ui/CheckBox.java
+++ b/server/src/com/vaadin/ui/CheckBox.java
@@ -16,6 +16,11 @@
package com.vaadin.ui;
+import java.util.Collection;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
import com.vaadin.data.Property;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
@@ -25,6 +30,8 @@ import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc;
import com.vaadin.shared.ui.checkbox.CheckBoxState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
public class CheckBox extends AbstractField<Boolean> {
@@ -203,4 +210,58 @@ public class CheckBox extends AbstractField<Boolean> {
Boolean value = getValue();
return (null == value) ? false : value.booleanValue();
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#readDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ if (design.hasAttr("checked")) {
+ this.setValue(DesignAttributeHandler.readAttribute("checked",
+ design.attributes(), Boolean.class));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> attributes = super.getCustomAttributes();
+ attributes.add("checked");
+ return attributes;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#writeDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ CheckBox def = (CheckBox) designContext.getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ DesignAttributeHandler.writeAttribute("checked", attr, getValue(),
+ def.getValue(), Boolean.class);
+ }
+
+ @Override
+ public void clear() {
+ setValue(Boolean.FALSE);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return getValue() == null || getValue().equals(Boolean.FALSE);
+
+ }
+
}
diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java
index e10b5e1cd9..9e0816a398 100644
--- a/server/src/com/vaadin/ui/Component.java
+++ b/server/src/com/vaadin/ui/Component.java
@@ -19,6 +19,8 @@ package com.vaadin.ui;
import java.io.Serializable;
import java.util.Locale;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.ConnectorEvent;
import com.vaadin.event.ConnectorEventListener;
import com.vaadin.event.FieldEvents;
@@ -27,6 +29,7 @@ import com.vaadin.server.ErrorMessage;
import com.vaadin.server.Resource;
import com.vaadin.server.Sizeable;
import com.vaadin.server.VariableOwner;
+import com.vaadin.ui.declarative.DesignContext;
/**
* {@code Component} is the top-level interface that is and must be implemented
@@ -726,6 +729,44 @@ public interface Component extends ClientConnector, Sizeable, Serializable {
*/
public String getDescription();
+ /* Declarative support */
+
+ /**
+ * Reads the component state from the given design.
+ * <p>
+ * The component is responsible not only for updating its own state but also
+ * for ensuring that its children update their state based on the design.
+ * <p>
+ * It is assumed that the component is in its default state when this method
+ * is called. Reading should only take into consideration attributes
+ * specified in the design and not reset any unspecified attributes to their
+ * defaults.
+ * <p>
+ * This method must not modify the design.
+ *
+ * @since 7.4
+ * @param design
+ * The design as HTML to obtain the state from
+ * @param designContext
+ * The DesignContext instance used for parsing the design
+ */
+ public void readDesign(Element design, DesignContext designContext);
+
+ /**
+ * Writes the component state to the given design.
+ * <p>
+ * The component is responsible not only for writing its own state but also
+ * for ensuring that its children write their state to the design.
+ * <p>
+ * This method must not modify the component state.
+ *
+ * @since 7.4
+ * @param design
+ * The design as HTML to update with the current state
+ * @param designContext
+ */
+ public void writeDesign(Element design, DesignContext designContext);
+
/* Component event framework */
/**
diff --git a/server/src/com/vaadin/ui/CssLayout.java b/server/src/com/vaadin/ui/CssLayout.java
index 350423576f..dbedfa53ff 100644
--- a/server/src/com/vaadin/ui/CssLayout.java
+++ b/server/src/com/vaadin/ui/CssLayout.java
@@ -18,6 +18,8 @@ package com.vaadin.ui;
import java.util.Iterator;
import java.util.LinkedList;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.LayoutEvents.LayoutClickEvent;
import com.vaadin.event.LayoutEvents.LayoutClickListener;
import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
@@ -26,6 +28,7 @@ import com.vaadin.shared.EventId;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc;
import com.vaadin.shared.ui.csslayout.CssLayoutState;
+import com.vaadin.ui.declarative.DesignContext;
/**
* CssLayout is a layout component that can be used in browser environment only.
@@ -354,4 +357,43 @@ public class CssLayout extends AbstractLayout implements LayoutClickNotifier {
return components.get(index);
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ // process default attributes
+ super.readDesign(design, designContext);
+ // handle children
+ for (Element childComponent : design.children()) {
+ Component newChild = designContext.readDesign(childComponent);
+ addComponent(newChild);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ // write default attributes
+ super.writeDesign(design, designContext);
+ CssLayout def = designContext.getDefaultInstance(this);
+ // handle children
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+ Element designElement = design;
+ for (Component child : this) {
+ Element childNode = designContext.createElement(child);
+ designElement.appendChild(childNode);
+ }
+ }
+
}
diff --git a/server/src/com/vaadin/ui/Field.java b/server/src/com/vaadin/ui/Field.java
index 6dee4de6cb..8a9acd570f 100644
--- a/server/src/com/vaadin/ui/Field.java
+++ b/server/src/com/vaadin/ui/Field.java
@@ -120,7 +120,7 @@ public interface Field<T> extends Component, BufferedValidatable, Property<T>,
* In general, "empty" state is same as null. As an exception, TextField
* also treats empty string as "empty".
*
- * @since
+ * @since 7.4
* @return true if the field is empty, false otherwise
*/
public boolean isEmpty();
@@ -131,7 +131,7 @@ public interface Field<T> extends Component, BufferedValidatable, Property<T>,
* The field value is typically reset to the initial value of the field.
* Calling {@link #isEmpty()} on a cleared field must always returns true.
*
- * @since
+ * @since 7.4
*/
public void clear();
diff --git a/server/src/com/vaadin/ui/Label.java b/server/src/com/vaadin/ui/Label.java
index c73840e6e9..a6ee11bdd5 100644
--- a/server/src/com/vaadin/ui/Label.java
+++ b/server/src/com/vaadin/ui/Label.java
@@ -17,8 +17,11 @@
package com.vaadin.ui;
import java.lang.reflect.Method;
+import java.util.Collection;
import java.util.Locale;
+import org.jsoup.nodes.Element;
+
import com.vaadin.data.Property;
import com.vaadin.data.util.AbstractProperty;
import com.vaadin.data.util.LegacyPropertyHelper;
@@ -27,6 +30,7 @@ import com.vaadin.data.util.converter.ConverterUtil;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.shared.ui.label.LabelState;
import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.declarative.DesignContext;
/**
* Label component for showing non-editable short texts.
@@ -570,4 +574,57 @@ public class Label extends AbstractComponent implements Property<String>,
return LegacyPropertyHelper.legacyPropertyToString(this);
}
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ String innerHtml = design.html();
+ if (innerHtml != null && !"".equals(innerHtml)) {
+ setValue(innerHtml);
+ }
+ if (design.hasAttr(DESIGN_ATTR_PLAIN_TEXT)) {
+ setContentMode(ContentMode.TEXT);
+ } else {
+ setContentMode(ContentMode.HTML);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("value");
+ result.add("content-mode");
+ result.add("plain-text");
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ String content = getValue();
+ if (content != null) {
+ design.html(getValue());
+ }
+ // plain-text (default is html)
+ if (getContentMode() == ContentMode.TEXT) {
+ design.attr(DESIGN_ATTR_PLAIN_TEXT, "");
+ }
+ }
}
diff --git a/server/src/com/vaadin/ui/Panel.java b/server/src/com/vaadin/ui/Panel.java
index 9b1d8fd5fa..6458d5f57d 100644
--- a/server/src/com/vaadin/ui/Panel.java
+++ b/server/src/com/vaadin/ui/Panel.java
@@ -16,8 +16,11 @@
package com.vaadin.ui;
+import java.util.Collection;
import java.util.Map;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.Action;
import com.vaadin.event.Action.Handler;
import com.vaadin.event.ActionManager;
@@ -31,6 +34,7 @@ import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.panel.PanelServerRpc;
import com.vaadin.shared.ui.panel.PanelState;
import com.vaadin.ui.Component.Focusable;
+import com.vaadin.ui.declarative.DesignContext;
/**
* Panel - a simple single component container.
@@ -339,4 +343,22 @@ public class Panel extends AbstractSingleComponentContainer implements
return (PanelState) super.getState(markAsDirty);
}
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> attributes = super.getCustomAttributes();
+ return attributes;
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ // handle tabindex
+ Panel def = (Panel) designContext.getDefaultInstance(this);
+ }
+
}
diff --git a/server/src/com/vaadin/ui/PasswordField.java b/server/src/com/vaadin/ui/PasswordField.java
index 107e40c149..1894804775 100644
--- a/server/src/com/vaadin/ui/PasswordField.java
+++ b/server/src/com/vaadin/ui/PasswordField.java
@@ -15,7 +15,12 @@
*/
package com.vaadin.ui;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
import com.vaadin.data.Property;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
/**
* A field that is used to enter secret text information like passwords. The
@@ -76,4 +81,36 @@ public class PasswordField extends AbstractTextField {
this();
setCaption(caption);
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#readDesign(org.jsoup.nodes.Element ,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ Attributes attr = design.attributes();
+ if (attr.hasKey("value")) {
+ setValue(DesignAttributeHandler.readAttribute("value", attr,
+ String.class));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractTextField#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ AbstractTextField def = (AbstractTextField) designContext
+ .getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ DesignAttributeHandler.writeAttribute("value", attr, getValue(),
+ def.getValue(), String.class);
+ }
}
diff --git a/server/src/com/vaadin/ui/TabSheet.java b/server/src/com/vaadin/ui/TabSheet.java
index 88002104b1..266c93e81f 100644
--- a/server/src/com/vaadin/ui/TabSheet.java
+++ b/server/src/com/vaadin/ui/TabSheet.java
@@ -19,11 +19,15 @@ package com.vaadin.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.BlurNotifier;
@@ -40,6 +44,9 @@ import com.vaadin.shared.ui.tabsheet.TabsheetClientRpc;
import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc;
import com.vaadin.shared.ui.tabsheet.TabsheetState;
import com.vaadin.ui.Component.Focusable;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
import com.vaadin.ui.themes.Reindeer;
import com.vaadin.ui.themes.Runo;
@@ -243,7 +250,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable,
* the removed tab if it's not the last one, otherwise will choose the
* closer enabled tab to the left.
*
- * @since
+ * @since 7.4
* @param removedTabIndex
* the index of the selected tab which was just remove.
* @return the index of the tab to be selected or -1 if there are no more
@@ -1446,4 +1453,172 @@ public class TabSheet extends AbstractComponentContainer implements Focusable,
protected TabsheetState getState() {
return (TabsheetState) super.getState();
}
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#readDesign(org.jsoup.nodes .Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ // create new tabs
+ for (Element tab : design.children()) {
+ if (!tab.tagName().equals("tab")) {
+ throw new DesignException("Invalid tag name for tabsheet tab "
+ + tab.tagName());
+ }
+ readTabFromDesign(tab, designContext);
+ }
+ }
+
+ /**
+ * Reads the given tab element from design
+ *
+ * @since 7.4
+ *
+ * @param tabElement
+ * the element to be read
+ * @param designContext
+ * the design context
+ */
+ private void readTabFromDesign(Element tabElement,
+ DesignContext designContext) {
+ Attributes attr = tabElement.attributes();
+ if (tabElement.children().size() != 1) {
+ throw new DesignException(
+ "A tab must have exactly one child element");
+ }
+ // create the component that is in tab content
+ Element content = tabElement.child(0);
+ Component child = designContext.readDesign(content);
+ Tab tab = this.addTab(child);
+ if (attr.hasKey("visible")) {
+ tab.setVisible(DesignAttributeHandler.readAttribute("visible",
+ attr, Boolean.class));
+ }
+ if (attr.hasKey("closable")) {
+ tab.setClosable(DesignAttributeHandler.readAttribute("closable",
+ attr, Boolean.class));
+ }
+ if (attr.hasKey("caption")) {
+ tab.setCaption(DesignAttributeHandler.readAttribute("caption",
+ attr, String.class));
+ }
+ if (attr.hasKey("enabled")) {
+ tab.setEnabled(DesignAttributeHandler.readAttribute("enabled",
+ attr, Boolean.class));
+ }
+ if (attr.hasKey("icon")) {
+ tab.setIcon(DesignAttributeHandler.readAttribute("icon", attr,
+ Resource.class));
+ }
+ if (attr.hasKey("icon-alt")) {
+ tab.setIconAlternateText(DesignAttributeHandler.readAttribute(
+ "icon-alt", attr, String.class));
+ }
+ if (attr.hasKey("description")) {
+ tab.setDescription(DesignAttributeHandler.readAttribute(
+ "description", attr, String.class));
+ }
+ if (attr.hasKey("style-name")) {
+ tab.setStyleName(DesignAttributeHandler.readAttribute("style-name",
+ attr, String.class));
+ }
+ if (attr.hasKey("id")) {
+ tab.setId(DesignAttributeHandler.readAttribute("id", attr,
+ String.class));
+ }
+ if (attr.hasKey("selected")) {
+ boolean selected = DesignAttributeHandler.readAttribute("selected",
+ attr, Boolean.class);
+ if (selected) {
+ this.setSelectedTab(tab.getComponent());
+ }
+ }
+ }
+
+ /**
+ * Writes the given tab to design
+ *
+ * @since 7.4
+ * @param design
+ * the design node for tabsheet
+ * @param designContext
+ * the design context
+ * @param tab
+ * the tab to be written
+ */
+ private void writeTabToDesign(Element design, DesignContext designContext,
+ Tab tab) {
+ // get default tab instance
+ Tab def = new TabSheetTabImpl(null, null, null);
+ // create element for tab
+ Element tabElement = design.appendElement("tab");
+ // add tab content
+ tabElement.appendChild(designContext.createElement(tab.getComponent()));
+ Attributes attr = tabElement.attributes();
+ // write attributes
+ DesignAttributeHandler.writeAttribute("visible", attr, tab.isVisible(),
+ def.isVisible(), Boolean.class);
+ DesignAttributeHandler.writeAttribute("closable", attr,
+ tab.isClosable(), def.isClosable(), Boolean.class);
+ DesignAttributeHandler.writeAttribute("caption", attr,
+ tab.getCaption(), def.getCaption(), String.class);
+ DesignAttributeHandler.writeAttribute("enabled", attr, tab.isEnabled(),
+ def.isEnabled(), Boolean.class);
+ DesignAttributeHandler.writeAttribute("icon", attr, tab.getIcon(),
+ def.getIcon(), Resource.class);
+ DesignAttributeHandler.writeAttribute("icon-alt", attr,
+ tab.getIconAlternateText(), def.getIconAlternateText(),
+ String.class);
+ DesignAttributeHandler.writeAttribute("description", attr,
+ tab.getDescription(), def.getDescription(), String.class);
+ DesignAttributeHandler.writeAttribute("style-name", attr,
+ tab.getStyleName(), def.getStyleName(), String.class);
+ DesignAttributeHandler.writeAttribute("id", attr, tab.getId(),
+ def.getId(), String.class);
+ if (getSelectedTab() != null
+ && getSelectedTab().equals(tab.getComponent())) {
+ // use write attribute to get consistent handling for boolean
+ DesignAttributeHandler.writeAttribute("selected", attr, true,
+ false, boolean.class);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#getCustomAttributes()
+ */
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> attributes = super.getCustomAttributes();
+ // no need to list tab attributes since they are considered internal
+ return attributes;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractComponent#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ TabSheet def = (TabSheet) designContext.getDefaultInstance(this);
+ Attributes attr = design.attributes();
+
+ // write tabs
+ if (!designContext.shouldWriteChildren(this, def)) {
+ return;
+ }
+ for (Component component : this) {
+ Tab tab = this.getTab(component);
+ writeTabToDesign(design, designContext, tab);
+ }
+ }
+
}
diff --git a/server/src/com/vaadin/ui/TextArea.java b/server/src/com/vaadin/ui/TextArea.java
index e38be8ad3c..c8103f9c5b 100644
--- a/server/src/com/vaadin/ui/TextArea.java
+++ b/server/src/com/vaadin/ui/TextArea.java
@@ -16,8 +16,11 @@
package com.vaadin.ui;
+import org.jsoup.nodes.Element;
+
import com.vaadin.data.Property;
import com.vaadin.shared.ui.textarea.TextAreaState;
+import com.vaadin.ui.declarative.DesignContext;
/**
* A text field that supports multi line editing.
@@ -133,4 +136,27 @@ public class TextArea extends AbstractTextField {
return getState(false).wordwrap;
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#readDesign(org.jsoup.nodes.Element ,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ setValue(design.html());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractTextField#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ design.html(getValue());
+ }
}
diff --git a/server/src/com/vaadin/ui/TextField.java b/server/src/com/vaadin/ui/TextField.java
index 1fc10c6ced..2a61e93211 100644
--- a/server/src/com/vaadin/ui/TextField.java
+++ b/server/src/com/vaadin/ui/TextField.java
@@ -16,7 +16,12 @@
package com.vaadin.ui;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
import com.vaadin.data.Property;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
/**
* <p>
@@ -102,6 +107,38 @@ public class TextField extends AbstractTextField {
/*
* (non-Javadoc)
*
+ * @see com.vaadin.ui.AbstractTextField#readDesign(org.jsoup.nodes.Element,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ Attributes attr = design.attributes();
+ if (attr.hasKey("value")) {
+ setValue(DesignAttributeHandler.readAttribute("value", attr,
+ String.class));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractTextField#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ AbstractTextField def = (AbstractTextField) designContext
+ .getDefaultInstance(this);
+ Attributes attr = design.attributes();
+ DesignAttributeHandler.writeAttribute("value", attr, getValue(),
+ def.getValue(), String.class);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
* @see com.vaadin.ui.AbstractField#clear()
*/
@Override
diff --git a/server/src/com/vaadin/ui/declarative/Design.java b/server/src/com/vaadin/ui/declarative/Design.java
new file mode 100644
index 0000000000..59393a7815
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/Design.java
@@ -0,0 +1,481 @@
+/*
+ * 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 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.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.
+ *
+ * <p>
+ * In html form a valid nonempty component hierarchy contains a single root
+ * element located under the &lt;body&gt; 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 &lt;body&gt; tag.
+ *
+ * <p>
+ * 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 {
+ /**
+ * 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, "UTF-8", "", 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
+ * document. The hierarchy must contain at most one top-level component,
+ * which should be located under &lt;body&gt;. Also invalid html containing
+ * the hierarchy without &lt;html&gt;, &lt;head&gt; and &lt;body&gt; tags is
+ * accepted. You can optionally pass an instance for the root component with
+ * some uninitialized instance fields. The fields will be automatically
+ * populated when parsing the design based on the component ids, local ids,
+ * and captions of the components in the design.
+ *
+ * @param html
+ * the html document describing the component design
+ * @param rootInstance
+ * the root instance with fields to be mapped to components in
+ * the 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
+ */
+ private static DesignContext parse(InputStream html, Component rootInstance) {
+ Document doc = parse(html);
+ return designToComponentTree(doc, rootInstance);
+ }
+
+ /**
+ * Constructs a component hierarchy from the design specified as an html
+ * document given as a string. The hierarchy must contain at most one
+ * top-level component, which should be located under &lt;body&gt;. Also
+ * invalid html containing the hierarchy without &lt;html&gt;, &lt;head&gt;
+ * and &lt;body&gt; tags is accepted. You can optionally pass an instance
+ * for the root component with some uninitialized instance fields. The
+ * fields will be automatically populated when parsing the design based on
+ * the component ids, local ids, and captions of the components in the
+ * design.
+ *
+ * @param html
+ * the html document describing the component design
+ * @param rootInstance
+ * the root instance with fields to be mapped to components in
+ * the 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
+ */
+ private static DesignContext parse(String html, Component rootInstance) {
+ Document doc = Jsoup.parse(html);
+ return designToComponentTree(doc, rootInstance);
+ }
+
+ /**
+ * Constructs a component hierarchy from the design specified as an html
+ * tree.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * 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 exactly one root component, but found "
+ + children.size());
+ }
+ Element element = children.first();
+ if (componentRoot != 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<String> unboundFields = binder.getUnboundFields();
+ if (!unboundFields.isEmpty()) {
+ throw new DesignException(
+ "Found unbound fields from component root "
+ + unboundFields);
+ }
+ // no need to listen anymore
+ designContext.removeComponentCreationListener(creationListener);
+ } else {
+ // createChild creates the entire component hierarchy
+ componentRoot = designContext.readDesign(element);
+ }
+ designContext.setRootComponent(componentRoot);
+ return designContext;
+ }
+
+ /**
+ * Generates an html tree representation of the component hierarchy having
+ * the root designContext.getRootComponent(). The hierarchy is stored under
+ * &lt;body&gt; in the tree. The generated tree represents a valid html
+ * document.
+ *
+ *
+ * @param designContext
+ * a DesignContext object specifying the root component
+ * (designContext.getRootComponent()) of the hierarchy
+ * @return an html tree representation of the component hierarchy
+ */
+ private 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);
+
+ // Append the design under <body> in the html tree. createNode
+ // creates the entire component hierarchy rooted at the
+ // given root node.
+ Component root = designContext.getRootComponent();
+ Node rootNode = designContext.createElement(root);
+ body.appendChild(rootNode);
+ designContext.writePackageMappings(doc);
+ return doc;
+ }
+
+ /**
+ * Loads a design for the given root component.
+ * <p>
+ * This methods assumes that the component class (or a super class) has been
+ * marked with an {@link DesignRoot} annotation and will either use the
+ * value from the annotation to locate the design file, or will fall back to
+ * using a design with the same same as the annotated class file (with an
+ * .html extension)
+ * <p>
+ * Any {@link Component} type fields in the root component which are not
+ * assigned (i.e. are null) are mapped to corresponding components in the
+ * design. Matching is done based on field name in the component class and
+ * id/local id/caption in the design file.
+ * <p>
+ * The type of the root component must match the root element in the design
+ *
+ * @param rootComponent
+ * The root component of the layout
+ * @return The design context used in the load operation
+ * @throws DesignException
+ * If the design could not be loaded
+ */
+ public static DesignContext read(Component rootComponent)
+ throws DesignException {
+ // Try to find an @DesignRoot annotation on the class or any parent
+ // class
+ Class<? extends Component> annotatedClass = findClassWithAnnotation(
+ rootComponent.getClass(), DesignRoot.class);
+ if (annotatedClass == null) {
+ throw new IllegalArgumentException(
+ "The class "
+ + rootComponent.getClass().getName()
+ + " or any of its superclasses do not have an @DesignRoot annotation");
+ }
+
+ DesignRoot designAnnotation = annotatedClass
+ .getAnnotation(DesignRoot.class);
+ String filename = designAnnotation.value();
+ if (filename.equals("")) {
+ // No value, assume the html file is named as the class
+ filename = annotatedClass.getSimpleName() + ".html";
+ }
+
+ InputStream stream = annotatedClass.getResourceAsStream(filename);
+ if (stream == null) {
+ throw new DesignException("Unable to find design file " + filename
+ + " in " + annotatedClass.getPackage().getName());
+ }
+
+ Document doc = parse(stream);
+ DesignContext context = designToComponentTree(doc, rootComponent,
+ annotatedClass);
+
+ return context;
+
+ }
+
+ /**
+ * Find the first class with the given annotation, starting the search from
+ * the given class and moving upwards in the class hierarchy.
+ *
+ * @param componentClass
+ * the class to check
+ * @param annotationClass
+ * the annotation to look for
+ * @return the first class with the given annotation or null if no class
+ * with the annotation was found
+ */
+ private static Class<? extends Component> findClassWithAnnotation(
+ Class<? extends Component> componentClass,
+ Class<? extends Annotation> annotationClass) {
+ if (componentClass == null) {
+ return null;
+ }
+
+ if (componentClass.isAnnotationPresent(annotationClass)) {
+ return componentClass;
+ }
+
+ Class<?> superClass = componentClass.getSuperclass();
+ if (!Component.class.isAssignableFrom(superClass)) {
+ return null;
+ }
+
+ return findClassWithAnnotation((Class<? extends Component>) superClass,
+ annotationClass);
+ }
+
+ /**
+ * Loads a design from the given file name using the given root component.
+ * <p>
+ * Any {@link Component} type fields in the root component which are not
+ * assigned (i.e. are null) are mapped to corresponding components in the
+ * design. Matching is done based on field name in the component class and
+ * id/local id/caption in the design file.
+ * <p>
+ * The type of the root component must match the root element in the design.
+ *
+ * @param 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());
+ }
+ return read(stream, rootComponent);
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Any {@link Component} type fields in the root component which are not
+ * assigned (i.e. are null) are mapped to corresponding components in the
+ * design. Matching is done based on field name in the component class and
+ * id/local id/caption in the design file.
+ * <p>
+ * 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 design, Component rootComponent) {
+ if (design == null) {
+ throw new DesignException("Stream cannot be null");
+ }
+ Document doc = parse(design);
+ 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
+ * @param outputStream
+ * the output stream to write the design to. The design is always
+ * written as UTF-8
+ * @throws IOException
+ */
+ 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
+ * @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());
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java b/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java
new file mode 100644
index 0000000000..be7d023ebf
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java
@@ -0,0 +1,715 @@
+/*
+ * 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.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.File;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.event.ShortcutAction.KeyCode;
+import com.vaadin.event.ShortcutAction.ModifierKey;
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.FileResource;
+import com.vaadin.server.FontAwesome;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Component;
+
+/**
+ * Default attribute handler implementation used when parsing designs to
+ * component trees. Handles all the component attributes that do not require
+ * custom handling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignAttributeHandler implements Serializable {
+
+ private static Logger getLogger() {
+ return Logger.getLogger(DesignAttributeHandler.class.getName());
+ }
+
+ private static Map<Class, AttributeCacheEntry> cache = Collections
+ .synchronizedMap(new HashMap<Class, AttributeCacheEntry>());
+
+ /**
+ * Clears the children and attributes of the given element
+ *
+ * @param design
+ * the element to be cleared
+ */
+ public static void clearElement(Element design) {
+ Attributes attr = design.attributes();
+ for (Attribute a : attr.asList()) {
+ attr.remove(a.getKey());
+ }
+ List<Node> children = new ArrayList<Node>();
+ children.addAll(design.childNodes());
+ for (Node node : children) {
+ node.remove();
+ }
+ }
+
+ /**
+ * Assigns the specified design attribute to the given component.
+ *
+ * @param target
+ * the target to which the attribute should be set
+ * @param attribute
+ * the name of the attribute to be set
+ * @param value
+ * the string value of the attribute
+ * @return true on success
+ */
+ public static boolean assignValue(Object target, String attribute,
+ String value) {
+ if (target == null || attribute == null || value == null) {
+ throw new IllegalArgumentException(
+ "Parameters with null value not allowed");
+ }
+ boolean success = false;
+ try {
+ Method setter = findSetterForAttribute(target.getClass(), attribute);
+ if (setter == null) {
+ // if we don't have the setter, there is no point in continuing
+ success = false;
+ } else {
+ // we have a value from design attributes, let's use that
+ Object param = fromAttributeValue(
+ setter.getParameterTypes()[0], value);
+ setter.invoke(target, param);
+ success = true;
+ }
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING,
+ "Failed to set attribute " + attribute, e);
+ }
+ if (!success) {
+ getLogger().info(
+ "property " + attribute
+ + " ignored by default attribute handler");
+ }
+ return success;
+ }
+
+ /**
+ * Searches for supported setter and getter types from the specified class
+ * and returns the list of corresponding design attributes
+ *
+ * @param clazz
+ * the class scanned for setters
+ * @return the list of supported design attributes
+ */
+ public static Collection<String> getSupportedAttributes(Class<?> clazz) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getAttributes();
+ }
+
+ /**
+ * Resolves the supported attributes and corresponding getters and setters
+ * for the class using introspection. After resolving, the information is
+ * cached internally by this class
+ *
+ * @param clazz
+ * the class to resolve the supported attributes for
+ */
+ private static void resolveSupportedAttributes(Class<?> clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("The clazz can not be null");
+ }
+ if (cache.containsKey(clazz.getCanonicalName())) {
+ // NO-OP
+ return;
+ }
+ BeanInfo beanInfo;
+ try {
+ beanInfo = Introspector.getBeanInfo(clazz);
+ } catch (IntrospectionException e) {
+ throw new RuntimeException(
+ "Could not get supported attributes for class "
+ + clazz.getName());
+ }
+ AttributeCacheEntry entry = new AttributeCacheEntry();
+ for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
+ Method getter = descriptor.getReadMethod();
+ Method setter = descriptor.getWriteMethod();
+ if (getter != null && setter != null
+ && isSupported(descriptor.getPropertyType())) {
+ String attribute = toAttributeName(descriptor.getName());
+ entry.addAttribute(attribute, getter, setter);
+ }
+ }
+ cache.put(clazz, entry);
+ }
+
+ /**
+ * Writes the specified attribute to the design if it differs from the
+ * default value got from the <code> defaultInstance <code>
+ *
+ * @param component
+ * the component used to get the attribute value
+ * @param attribute
+ * the key for the attribute
+ * @param attr
+ * the attribute list where the attribute will be written
+ * @param defaultInstance
+ * the default instance for comparing default values
+ */
+ public static void writeAttribute(Component component, String attribute,
+ Attributes attr, Component defaultInstance) {
+ Method getter = findGetterForAttribute(component.getClass(), attribute);
+ if (getter == null) {
+ getLogger().warning(
+ "Could not find getter for attribute " + attribute);
+ } else {
+ try {
+ // compare the value with default value
+ Object value = getter.invoke(component);
+ Object defaultValue = getter.invoke(defaultInstance);
+ // if the values are not equal, write the data
+ if (!SharedUtil.equals(value, defaultValue)) {
+ String attributeValue = toAttributeValue(
+ getter.getReturnType(), value);
+ attr.put(attribute, attributeValue);
+ }
+ } catch (Exception e) {
+ getLogger()
+ .log(Level.SEVERE,
+ "Failed to invoke getter for attribute "
+ + attribute, e);
+ }
+ }
+ }
+
+ /**
+ * Reads the given attribute from a set of attributes.
+ *
+ * @param attribute
+ * the attribute key
+ * @param attributes
+ * the set of attributes to read from
+ * @param outputType
+ * the output type for the attribute
+ * @return the attribute value or the default value if the attribute is not
+ * found
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T readAttribute(String attribute, Attributes attributes,
+ Class<T> outputType) {
+ if (!isSupported(outputType)) {
+ throw new IllegalArgumentException("output type: "
+ + outputType.getName() + " not supported");
+ }
+ if (!attributes.hasKey(attribute)) {
+ return null;
+ } else {
+ try {
+ String value = attributes.get(attribute);
+ return (T) fromAttributeValue(outputType, value);
+ } catch (Exception e) {
+ throw new DesignException("Failed to read attribute "
+ + attribute, e);
+ }
+ }
+ }
+
+ /**
+ * Writes the given attribute value to a set of attributes if it differs
+ * from the default attribute value.
+ *
+ * @param attribute
+ * the attribute key
+ * @param attributes
+ * the set of attributes where the new attribute is written
+ * @param value
+ * the attribute value
+ * @param defaultValue
+ * the default attribute value
+ * @param inputType
+ * the type of the input value
+ */
+ public static <T> void writeAttribute(String attribute,
+ Attributes attributes, T value, T defaultValue, Class<T> inputType) {
+ if (!isSupported(inputType)) {
+ throw new IllegalArgumentException("input type: "
+ + inputType.getName() + " not supported");
+ }
+ if (!SharedUtil.equals(value, defaultValue)) {
+ String attributeValue = toAttributeValue(inputType, value);
+ attributes.put(attribute, attributeValue);
+ }
+ }
+
+ /**
+ * Formats the given design attribute value. The method is provided to
+ * ensure consistent number formatting for design attribute values
+ *
+ * @param number
+ * the number to be formatted
+ * @return the formatted number
+ */
+ public static String formatFloat(float number) {
+ return getDecimalFormat().format(number);
+ }
+
+ /**
+ * Formats the given design attribute value. The method is provided to
+ * ensure consistent number formatting for design attribute values
+ *
+ * @param number
+ * the number to be formatted
+ * @return the formatted number
+ */
+ public static String formatDouble(double number) {
+ return getDecimalFormat().format(number);
+ }
+
+ /**
+ * Convert ShortcutAction to attribute string presentation
+ *
+ * @param shortcut
+ * the shortcut action
+ * @return the action as attribute string presentation
+ */
+ private static String formatShortcutAction(ShortcutAction shortcut) {
+ StringBuilder sb = new StringBuilder();
+ // handle modifiers
+ if (shortcut.getModifiers() != null) {
+ for (int modifier : shortcut.getModifiers()) {
+ sb.append(ShortcutKeyMapper.getStringForKeycode(modifier))
+ .append("-");
+ }
+ }
+ // handle keycode
+ sb.append(ShortcutKeyMapper.getStringForKeycode(shortcut.getKeyCode()));
+ return sb.toString();
+ }
+
+ /**
+ * Reads shortcut action from attribute presentation
+ *
+ * @param attributeValue
+ * attribute presentation of shortcut action
+ * @return shortcut action with keycode and modifier keys from attribute
+ * value
+ */
+ private static ShortcutAction readShortcutAction(String attributeValue) {
+ if (attributeValue.length() == 0) {
+ return null;
+ }
+ String[] parts = attributeValue.split("-");
+ // handle keycode
+ String keyCodePart = parts[parts.length - 1];
+ int keyCode = ShortcutKeyMapper.getKeycodeForString(keyCodePart);
+ if (keyCode < 0) {
+ throw new IllegalArgumentException("Invalid shortcut definition "
+ + attributeValue);
+ }
+ // handle modifiers
+ int[] modifiers = null;
+ if (parts.length > 1) {
+ modifiers = new int[parts.length - 1];
+ }
+ for (int i = 0; i < parts.length - 1; i++) {
+ int modifier = ShortcutKeyMapper.getKeycodeForString(parts[i]);
+ if (modifier > 0) {
+ modifiers[i] = modifier;
+ } else {
+ throw new IllegalArgumentException(
+ "Invalid shortcut definition " + attributeValue);
+ }
+ }
+ return new ShortcutAction(null, keyCode, modifiers);
+ }
+
+ /**
+ * Creates the decimal format used when writing attributes to the design.
+ *
+ * @return the decimal format
+ */
+ private static DecimalFormat getDecimalFormat() {
+ DecimalFormatSymbols symbols = new DecimalFormatSymbols(new Locale(
+ "en_US"));
+ DecimalFormat fmt = new DecimalFormat("0.###", symbols);
+ fmt.setGroupingUsed(false);
+ return fmt;
+ }
+
+ /**
+ * Returns the design attribute name corresponding the given method name.
+ * For example given a method name <code>setPrimaryStyleName</code> the
+ * return value would be <code>primary-style-name</code>
+ *
+ * @param propertyName
+ * the property name returned by {@link IntroSpector}
+ * @return the design attribute name corresponding the given method name
+ */
+ private static String toAttributeName(String propertyName) {
+ String[] words = propertyName.split("(?<!^)(?=[A-Z])");
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < words.length; i++) {
+ if (builder.length() > 0) {
+ builder.append("-");
+ }
+ builder.append(words[i].toLowerCase());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Parses the given attribute value to specified target type
+ *
+ * @param targetType
+ * the target type for the value
+ * @param value
+ * the parsed value
+ * @return the object of specified target type
+ */
+ private static Object fromAttributeValue(Class<?> targetType, String value) {
+ if (targetType == String.class) {
+ return value;
+ }
+ // special handling for boolean type. The attribute evaluates to true if
+ // it is present and the value is not "false" or "FALSE". Thus empty
+ // value evaluates to true.
+ if (targetType == Boolean.TYPE || targetType == Boolean.class) {
+ return parseBoolean(value);
+ }
+ if (targetType == Integer.TYPE || targetType == Integer.class) {
+ return Integer.valueOf(value);
+ }
+ if (targetType == Byte.TYPE || targetType == Byte.class) {
+ return Byte.valueOf(value);
+ }
+ if (targetType == Short.TYPE || targetType == Short.class) {
+ return Short.valueOf(value);
+ }
+ if (targetType == Long.TYPE || targetType == Long.class) {
+ return Long.valueOf(value);
+ }
+ if (targetType == Character.TYPE || targetType == Character.class) {
+ return value.charAt(0);
+ }
+ if (targetType == Float.TYPE || targetType == Float.class) {
+ return Float.valueOf(value);
+ }
+ if (targetType == Double.TYPE || targetType == Double.class) {
+ return Double.valueOf(value);
+ }
+ if (targetType == Resource.class) {
+ return parseResource(value);
+ }
+ if (Enum.class.isAssignableFrom(targetType)) {
+ return Enum.valueOf((Class<? extends Enum>) targetType,
+ value.toUpperCase());
+ }
+ if (targetType == ShortcutAction.class) {
+ return readShortcutAction(value);
+ }
+ return null;
+ }
+
+ /**
+ * Serializes the given value to valid design attribute representation
+ *
+ * @param sourceType
+ * the type of the value
+ * @param value
+ * the value to be serialized
+ * @return the given value as design attribute representation
+ */
+ private static String toAttributeValue(Class<?> sourceType, Object value) {
+ if (value == null) {
+ // TODO: Handle corner case where sourceType is String and default
+ // value is not null. How to represent null value in attributes?
+ return "";
+ }
+ if (sourceType == Resource.class) {
+ if (value instanceof ExternalResource) {
+ return ((ExternalResource) value).getURL();
+ } else if (value instanceof ThemeResource) {
+ return "theme://" + ((ThemeResource) value).getResourceId();
+ } else if (value instanceof FontAwesome) {
+ return "font://" + ((FontAwesome) value).name();
+ } else if (value instanceof FileResource) {
+ String path = ((FileResource) value).getSourceFile().getPath();
+ if (File.separatorChar != '/') {
+ // make sure we use '/' as file separator in templates
+ return path.replace(File.separatorChar, '/');
+ } else {
+ return path;
+ }
+ } else {
+ getLogger().warning(
+ "Unknown resource type " + value.getClass().getName());
+ return null;
+ }
+ } else if (sourceType == Float.class || sourceType == Float.TYPE) {
+ return formatFloat(((Float) value).floatValue());
+ } else if (sourceType == Double.class || sourceType == Double.TYPE) {
+ return formatDouble(((Double) value).doubleValue());
+ } else if (sourceType == ShortcutAction.class) {
+ return formatShortcutAction((ShortcutAction) value);
+ } else {
+ return value.toString();
+ }
+ }
+
+ /**
+ * Parses the given attribute value as resource
+ *
+ * @param value
+ * the attribute value to be parsed
+ * @return resource instance based on the attribute value
+ */
+ private static Resource parseResource(String value) {
+ if (value.startsWith("http://")) {
+ return new ExternalResource(value);
+ } else if (value.startsWith("theme://")) {
+ return new ThemeResource(value.substring(8));
+ } else if (value.startsWith("font://")) {
+ return FontAwesome.valueOf(value.substring(7));
+ } else {
+ return new FileResource(new File(value));
+ }
+ }
+
+ /**
+ * Returns a setter that can be used for assigning the given design
+ * attribute to the class
+ *
+ * @param clazz
+ * the class that is scanned for setters
+ * @param attribute
+ * the design attribute to find setter for
+ * @return the setter method or null if not found
+ */
+ private static Method findSetterForAttribute(Class<?> clazz,
+ String attribute) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getSetter(attribute);
+ }
+
+ /**
+ * Returns a getter that can be used for reading the given design attribute
+ * value from the class
+ *
+ * @param clazz
+ * the class that is scanned for getters
+ * @param attribute
+ * the design attribute to find getter for
+ * @return the getter method or null if not found
+ */
+ private static Method findGetterForAttribute(Class<?> clazz,
+ String attribute) {
+ resolveSupportedAttributes(clazz);
+ return cache.get(clazz).getGetter(attribute);
+ }
+
+ // supported property types
+ private static final List<Class<?>> supportedClasses = Arrays
+ .asList(new Class<?>[] { String.class, Boolean.class,
+ Integer.class, Byte.class, Short.class, Long.class,
+ Character.class, Float.class, Double.class, Resource.class,
+ ShortcutAction.class });
+
+ /**
+ * Returns true if the specified value type is supported by this class.
+ * Currently the handler supports primitives, {@link Locale.class} and
+ * {@link Resource.class}.
+ *
+ * @param valueType
+ * the value type to be tested
+ * @return true if the value type is supported, otherwise false
+ */
+ private static boolean isSupported(Class<?> valueType) {
+ return valueType != null
+ && (valueType.isPrimitive()
+ || supportedClasses.contains(valueType) || Enum.class
+ .isAssignableFrom(valueType));
+ }
+
+ /**
+ * Cache object for caching supported attributes and their getters and
+ * setters
+ *
+ * @author Vaadin Ltd
+ */
+ private static class AttributeCacheEntry implements Serializable {
+ private Map<String, Method[]> accessMethods = Collections
+ .synchronizedMap(new HashMap<String, Method[]>());
+
+ private void addAttribute(String attribute, Method getter, Method setter) {
+ Method[] methods = new Method[2];
+ methods[0] = getter;
+ methods[1] = setter;
+ accessMethods.put(attribute, methods);
+ }
+
+ private Collection<String> getAttributes() {
+ ArrayList<String> attributes = new ArrayList<String>();
+ attributes.addAll(accessMethods.keySet());
+ return attributes;
+ }
+
+ private Method getGetter(String attribute) {
+ Method[] methods = accessMethods.get(attribute);
+ return (methods != null && methods.length > 0) ? methods[0] : null;
+ }
+
+ private Method getSetter(String attribute) {
+ Method[] methods = accessMethods.get(attribute);
+ return (methods != null && methods.length > 1) ? methods[1] : null;
+ }
+ }
+
+ /**
+ * Provides mappings between shortcut keycodes and their representation in
+ * design attributes
+ *
+ * @author Vaadin Ltd
+ */
+ private static class ShortcutKeyMapper implements Serializable {
+
+ private static Map<Integer, String> keyCodeMap = Collections
+ .synchronizedMap(new HashMap<Integer, String>());
+ private static Map<String, Integer> presentationMap = Collections
+ .synchronizedMap(new HashMap<String, Integer>());
+
+ static {
+ // map modifiers
+ mapKey(ModifierKey.ALT, "alt");
+ mapKey(ModifierKey.CTRL, "ctrl");
+ mapKey(ModifierKey.META, "meta");
+ mapKey(ModifierKey.SHIFT, "shift");
+ // map keys
+ mapKey(KeyCode.ENTER, "enter");
+ mapKey(KeyCode.ESCAPE, "escape");
+ mapKey(KeyCode.PAGE_UP, "pageup");
+ mapKey(KeyCode.PAGE_DOWN, "pagedown");
+ mapKey(KeyCode.TAB, "tab");
+ mapKey(KeyCode.ARROW_LEFT, "left");
+ mapKey(KeyCode.ARROW_UP, "up");
+ mapKey(KeyCode.ARROW_RIGHT, "right");
+ mapKey(KeyCode.ARROW_DOWN, "down");
+ mapKey(KeyCode.BACKSPACE, "backspace");
+ mapKey(KeyCode.DELETE, "delete");
+ mapKey(KeyCode.INSERT, "insert");
+ mapKey(KeyCode.END, "end");
+ mapKey(KeyCode.HOME, "home");
+ mapKey(KeyCode.F1, "f1");
+ mapKey(KeyCode.F2, "f2");
+ mapKey(KeyCode.F3, "f3");
+ mapKey(KeyCode.F4, "f4");
+ mapKey(KeyCode.F5, "f5");
+ mapKey(KeyCode.F6, "f6");
+ mapKey(KeyCode.F7, "f7");
+ mapKey(KeyCode.F8, "f8");
+ mapKey(KeyCode.F9, "f9");
+ mapKey(KeyCode.F10, "f10");
+ mapKey(KeyCode.F11, "f11");
+ mapKey(KeyCode.F12, "f12");
+ mapKey(KeyCode.NUM0, "0");
+ mapKey(KeyCode.NUM1, "1");
+ mapKey(KeyCode.NUM2, "2");
+ mapKey(KeyCode.NUM3, "3");
+ mapKey(KeyCode.NUM4, "4");
+ mapKey(KeyCode.NUM5, "5");
+ mapKey(KeyCode.NUM6, "6");
+ mapKey(KeyCode.NUM7, "7");
+ mapKey(KeyCode.NUM8, "8");
+ mapKey(KeyCode.NUM9, "9");
+ mapKey(KeyCode.SPACEBAR, "spacebar");
+ mapKey(KeyCode.A, "a");
+ mapKey(KeyCode.B, "b");
+ mapKey(KeyCode.C, "c");
+ mapKey(KeyCode.D, "d");
+ mapKey(KeyCode.E, "e");
+ mapKey(KeyCode.F, "f");
+ mapKey(KeyCode.G, "g");
+ mapKey(KeyCode.H, "h");
+ mapKey(KeyCode.I, "i");
+ mapKey(KeyCode.J, "j");
+ mapKey(KeyCode.K, "k");
+ mapKey(KeyCode.L, "l");
+ mapKey(KeyCode.M, "m");
+ mapKey(KeyCode.N, "n");
+ mapKey(KeyCode.O, "o");
+ mapKey(KeyCode.P, "p");
+ mapKey(KeyCode.Q, "q");
+ mapKey(KeyCode.R, "r");
+ mapKey(KeyCode.S, "s");
+ mapKey(KeyCode.T, "t");
+ mapKey(KeyCode.U, "u");
+ mapKey(KeyCode.V, "v");
+ mapKey(KeyCode.X, "x");
+ mapKey(KeyCode.Y, "y");
+ mapKey(KeyCode.Z, "z");
+ }
+
+ private static void mapKey(int keyCode, String presentation) {
+ keyCodeMap.put(keyCode, presentation);
+ presentationMap.put(presentation, keyCode);
+ }
+
+ private static int getKeycodeForString(String attributePresentation) {
+ Integer code = presentationMap.get(attributePresentation);
+ return code != null ? code.intValue() : -1;
+ }
+
+ private static String getStringForKeycode(int keyCode) {
+ return keyCodeMap.get(keyCode);
+ }
+ }
+
+ /**
+ * Converts the given string attribute value to its corresponding boolean.
+ *
+ * An empty string and "true" are considered to represent a true value and
+ * "false" to represent a false value.
+ *
+ * @param booleanValue
+ * the boolean value from an attribute
+ * @return the parsed boolean
+ */
+ public static boolean parseBoolean(String booleanValue) {
+ return !booleanValue.equalsIgnoreCase("false");
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java
new file mode 100644
index 0000000000..ade2494638
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/DesignContext.java
@@ -0,0 +1,719 @@
+/*
+ * 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.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+import com.vaadin.annotations.DesignRoot;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HasComponents;
+
+/**
+ * This class contains contextual information that is collected when a component
+ * tree is constructed based on HTML design template. This information includes
+ * mappings from local ids, global ids and captions to components , as well as a
+ * mapping between prefixes and package names (such as "v" -> "com.vaadin.ui").
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignContext implements Serializable {
+
+ // cache for object instances
+ private static Map<Class<?>, Component> instanceCache = Collections
+ .synchronizedMap(new HashMap<Class<?>, Component>());
+
+ // The root component of the component hierarchy
+ private Component rootComponent = 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_ATTRIBUTE = "_id";
+ // Mappings from ids to components. Modified when reading from design.
+ private Map<String, Component> idToComponent = new HashMap<String, Component>();
+ private Map<String, Component> localIdToComponent = new HashMap<String, Component>();
+ private Map<String, Component> captionToComponent = new HashMap<String, Component>();
+ // Mapping from components to local ids. Accessed when writing to
+ // design. Modified when reading from design.
+ private Map<Component, String> componentToLocalId = new HashMap<Component, 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 (this includes at least "v" which is always taken to refer
+ // to "com.vaadin.ui".
+ private Map<String, String> defaultPrefixes = new HashMap<String, String>();
+
+ // component creation listeners
+ private List<ComponentCreationListener> listeners = new ArrayList<ComponentCreationListener>();
+
+ 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);
+ mapPrefixToPackage(prefix, packageName);
+ }
+ }
+
+ public DesignContext() {
+ this(new Document(""));
+ }
+
+ /**
+ * Returns a component having the specified local id. If no component is
+ * found, returns null.
+ *
+ * @param localId
+ * The local id of the component
+ * @return a component whose local id equals localId
+ */
+ public Component getComponentByLocalId(String localId) {
+ return localIdToComponent.get(localId);
+ }
+
+ /**
+ * Returns a component having the specified global id. If no component is
+ * found, returns null.
+ *
+ * @param globalId
+ * The global id of the component
+ * @return a component whose global id equals globalId
+ */
+ public Component getComponentById(String globalId) {
+ return idToComponent.get(globalId);
+ }
+
+ /**
+ * Returns a component having the specified caption. If no component is
+ * found, returns null.
+ *
+ * @param caption
+ * The caption of the component
+ * @return a component whose caption equals the caption given as a parameter
+ */
+ public Component getComponentByCaption(String caption) {
+ return captionToComponent.get(caption);
+ }
+
+ /**
+ * Creates a mapping between the given global id and the component. Returns
+ * true if globalId was already mapped to some component. Otherwise returns
+ * false. Also sets the id of the component to globalId.
+ *
+ * If there is a mapping from the component to a global id (gid) different
+ * from globalId, the mapping from gid to component is removed.
+ *
+ * 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.
+ *
+ * @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.
+ */
+ private boolean mapId(String globalId, Component component) {
+ Component oldComponent = idToComponent.get(globalId);
+ if (oldComponent != null && !oldComponent.equals(component)) {
+ oldComponent.setId(null);
+ }
+ String oldGID = component.getId();
+ if (oldGID != null && !oldGID.equals(globalId)) {
+ idToComponent.remove(oldGID);
+ }
+ component.setId(globalId);
+ idToComponent.put(globalId, component);
+ return oldComponent != null && !oldComponent.equals(component);
+ }
+
+ /**
+ * 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.
+ *
+ * @param localId
+ * 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.
+ */
+ private boolean mapLocalId(String localId, Component 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.
+ *
+ * @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.
+ */
+ private boolean mapCaption(String caption, Component 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.
+ *
+ * @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;
+ }
+
+ /**
+ * 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.
+ *
+ * @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.
+ */
+ private 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.
+ *
+ * @param abstractComponent
+ * @return the default instance for the given class. The return value must
+ * not be modified by the caller
+ */
+ public <T> T getDefaultInstance(Component component) {
+ // If the root is a @DesignRoot component, it can't use itself as a
+ // reference or the written design will be empty
+
+ // If the root component in some other way initializes itself in the
+ // constructor
+ if (getRootComponent() == component
+ && component.getClass().isAnnotationPresent(DesignRoot.class)) {
+ return (T) getDefaultInstance((Class<? extends Component>) component
+ .getClass().getSuperclass());
+ }
+ return (T) getDefaultInstance(component.getClass());
+ }
+
+ private Component getDefaultInstance(
+ Class<? extends Component> componentClass) {
+ Component instance = instanceCache.get(componentClass);
+ if (instance == null) {
+ try {
+ instance = componentClass.newInstance();
+ instanceCache.put(componentClass, instance);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Could not instantiate "
+ + componentClass.getName());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Could not instantiate "
+ + componentClass.getName());
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Reads and stores the mappings from prefixes to package names from meta
+ * tags located under <head> in the html document.
+ */
+ protected void readPackageMappings(Document doc) {
+ Element head = doc.head();
+ if (head == null) {
+ return;
+ }
+ for (Node child : head.childNodes()) {
+ if (child instanceof Element) {
+ Element childElement = (Element) child;
+ if ("meta".equals(childElement.tagName())) {
+ Attributes attributes = childElement.attributes();
+ if (attributes.hasKey("name")
+ && attributes.hasKey("content")
+ && "package-mapping".equals(attributes.get("name"))) {
+ String contentString = attributes.get("content");
+ String[] parts = contentString.split(":");
+ if (parts.length != 2) {
+ throw new DesignException("The meta tag '"
+ + child.toString() + "' cannot be parsed.");
+ }
+ String prefixName = parts[0];
+ String packageName = parts[1];
+ twoWayMap(prefixName, packageName, prefixToPackage,
+ packageToPrefix);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes the package mappings (prefix -> package name) of this object to
+ * the specified document.
+ * <p>
+ * The prefixes are stored as <meta> tags under <head> in the document.
+ *
+ * @param doc
+ * the Jsoup document tree where the package mappings are written
+ */
+ public void writePackageMappings(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 writeDesign. As a result of the
+ * writeDesign() call, this method creates the entire subtree rooted at the
+ * returned Node.
+ *
+ * @param childComponent
+ * The component with state that is written in to the node
+ * @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 Element createElement(Component childComponent) {
+ Class<?> componentClass = childComponent.getClass();
+ String packageName = componentClass.getPackage().getName();
+ String prefix = packageToPrefix.get(packageName);
+ if (prefix == null) {
+ prefix = packageName.replace('.', '_');
+ twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix);
+ }
+ prefix = prefix + "-";
+ String className = classNameToElementName(componentClass
+ .getSimpleName());
+ Element newElement = doc.createElement(prefix + className);
+ childComponent.writeDesign(newElement, this);
+ // Handle the local id. Global id and caption should have been taken
+ // care of by writeDesign.
+ String localId = componentToLocalId.get(childComponent);
+ if (localId != null) {
+ newElement.attr(LOCAL_ID_ATTRIBUTE, localId);
+ }
+ return newElement;
+ }
+
+ /**
+ * Creates the name of the html tag corresponding to the given class name.
+ * The name is derived by converting each uppercase letter to lowercase and
+ * inserting a dash before the letter. No dash is inserted before the first
+ * letter of the class name.
+ *
+ * @param className
+ * the name of the class without a package name
+ * @return the html tag name corresponding to className
+ */
+ private String classNameToElementName(String className) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < className.length(); i++) {
+ Character c = className.charAt(i);
+ if (Character.isUpperCase(c)) {
+ if (i > 0) {
+ result.append("-");
+ }
+ result.append(Character.toLowerCase(c));
+ } else {
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Reads the given design node and creates the corresponding component tree
+ *
+ * @param componentDesign
+ * The design element containing the description of the component
+ * to be created
+ * @return the root component of component tree
+ */
+ public Component readDesign(Element componentDesign) {
+ // Create the component.
+ Component component = instantiateComponent(componentDesign);
+ readDesign(componentDesign, component);
+ fireComponentCreatedEvent(componentToLocalId.get(component), component);
+ return component;
+ }
+
+ /**
+ *
+ * Reads the given design node and populates the given component with the
+ * corresponding component tree
+ * <p>
+ * Additionally registers the component id, local id and caption of the
+ * given component and all its children in the context
+ *
+ * @param componentDesign
+ * The design element containing the description of the component
+ * to be created
+ * @param component
+ * The component which corresponds to the design element
+ */
+ public void readDesign(Element componentDesign, Component component) {
+ component.readDesign(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: only update the mapping, the id has already been set for
+ // the component
+ String id = component.getId();
+ if (id != null && id.length() > 0) {
+ boolean mappingExists = mapId(id, component);
+ if (mappingExists) {
+ throw new DesignException(
+ "The following global id is not unique: " + id);
+ }
+ }
+ // local id: this is not a property of a component, so need to fetch it
+ // from the attributes of componentDesign
+ if (attributes.hasKey(LOCAL_ID_ATTRIBUTE)) {
+ String localId = attributes.get(LOCAL_ID_ATTRIBUTE);
+ boolean mappingExists = mapLocalId(localId, component);
+ if (mappingExists) {
+ throw new DesignException(
+ "the following local id is not unique: " + localId);
+ }
+ }
+ // caption: a property of a component, possibly not unique
+ String caption = component.getCaption();
+ if (caption != null) {
+ mapCaption(caption, component);
+ }
+ }
+
+ /**
+ * Creates a Component corresponding to the given node. Does not set the
+ * attributes for the created object.
+ *
+ * @param node
+ * a node of an html tree
+ * @return a Component corresponding to node, with no attributes set.
+ */
+ private Component instantiateComponent(Node node) {
+ // Extract the package and class names.
+ String qualifiedClassName = tagNameToClassName(node);
+ try {
+ Class<? extends Component> componentClass = resolveComponentClass(qualifiedClassName);
+ Component newComponent = componentClass.newInstance();
+ return newComponent;
+ } catch (Exception e) {
+ throw new DesignException("No component class could be found for "
+ + node.nodeName() + ".", e);
+ }
+ }
+
+ /**
+ * Returns the qualified class name corresponding to the given html tree
+ * node. The class name is extracted from the tag name of node.
+ *
+ * @param node
+ * an html tree node
+ * @return The qualified class name corresponding to the given node.
+ */
+ private String tagNameToClassName(Node node) {
+ String tagName = node.nodeName();
+ if (tagName.equals("v-addon")) {
+ return node.attr("class");
+ }
+ // 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 DesignException("The tagname '" + tagName
+ + "' is invalid: missing prefix.");
+ }
+ String prefixName = parts[0];
+ String packageName = prefixToPackage.get(prefixName);
+ if (packageName == null) {
+ throw new DesignException("Unknown tag: " + tagName);
+ }
+ int firstCharacterIndex = prefixName.length() + 1; // +1 is for '-'
+ tagName = tagName.substring(firstCharacterIndex,
+ firstCharacterIndex + 1).toUpperCase(Locale.ENGLISH)
+ + tagName.substring(firstCharacterIndex + 1);
+ int i;
+ while ((i = tagName.indexOf("-")) != -1) {
+ int length = tagName.length();
+ if (i != length - 1) {
+ tagName = tagName.substring(0, i)
+ + tagName.substring(i + 1, i + 2).toUpperCase(
+ Locale.ENGLISH) + tagName.substring(i + 2);
+
+ } else {
+ // Ends with "-"
+ System.out.println("A tag name should not end with '-'.");
+ }
+ }
+ return packageName + "." + tagName;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Class<? extends Component> resolveComponentClass(
+ String qualifiedClassName) throws ClassNotFoundException {
+ Class<?> componentClass = null;
+ componentClass = Class.forName(qualifiedClassName);
+
+ // Check that we're dealing with a Component.
+ if (isComponent(componentClass)) {
+ return (Class<? extends Component>) componentClass;
+ } else {
+ throw new IllegalArgumentException(String.format(
+ "Resolved class %s is not a %s.", componentClass.getName(),
+ Component.class.getName()));
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link Class} implements the
+ * {@link Component} interface of Vaadin Framework otherwise {@code false}.
+ *
+ * @param componentClass
+ * {@link Class} to check against {@link Component} interface.
+ * @return {@code true} if the given {@link Class} is a {@link Component},
+ * {@code false} otherwise.
+ */
+ private static boolean isComponent(Class<?> componentClass) {
+ if (componentClass != null) {
+ return Component.class.isAssignableFrom(componentClass);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the root component of a created component hierarchy.
+ *
+ * @return the root component of the hierarchy
+ */
+ public Component getRootComponent() {
+ return rootComponent;
+ }
+
+ /**
+ * Sets the root component of a created component hierarchy.
+ *
+ * @param rootComponent
+ * the root component of the hierarchy
+ */
+ public void setRootComponent(Component rootComponent) {
+ this.rootComponent = rootComponent;
+ }
+
+ /**
+ * Adds a component creation listener. The listener will be notified when
+ * components are created while parsing a design template
+ *
+ * @param listener
+ * the component creation listener to be added
+ */
+ public void addComponentCreationListener(ComponentCreationListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes a component creation listener.
+ *
+ * @param listener
+ * the component creation listener to be removed
+ */
+ public void removeComponentCreationListener(
+ ComponentCreationListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Fires component creation event
+ *
+ * @param localId
+ * localId of the component
+ * @param component
+ * the component that was created
+ */
+ private void fireComponentCreatedEvent(String localId, Component component) {
+ ComponentCreatedEvent event = new ComponentCreatedEvent(localId,
+ component);
+ for (ComponentCreationListener listener : listeners) {
+ listener.componentCreated(event);
+ }
+ }
+
+ /**
+ * Interface to be implemented by component creation listeners
+ *
+ * @author Vaadin Ltd
+ */
+ public interface ComponentCreationListener extends Serializable {
+
+ /**
+ * Called when component has been created in the design context
+ *
+ * @param event
+ * the component creation event containing information on the
+ * created component
+ */
+ public void componentCreated(ComponentCreatedEvent event);
+ }
+
+ /**
+ * Component creation event that is fired when a component is created in the
+ * context
+ *
+ * @author Vaadin Ltd
+ */
+ public class ComponentCreatedEvent implements Serializable {
+ private String localId;
+ private Component component;
+ private DesignContext context;
+
+ /**
+ * Creates a new instance of ComponentCreatedEvent
+ *
+ * @param localId
+ * the local id of the created component
+ * @param component
+ * the created component
+ */
+ private ComponentCreatedEvent(String localId, Component component) {
+ this.localId = localId;
+ this.component = component;
+ context = DesignContext.this;
+ }
+
+ /**
+ * Returns the local id of the created component or null if not exist
+ *
+ * @return the localId
+ */
+ public String getLocalId() {
+ return localId;
+ }
+
+ /**
+ * Returns the created component
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ return component;
+ }
+ }
+
+ /**
+ * Helper method for component write implementors to determine whether their
+ * children should be written out or not
+ *
+ * @param c
+ * The component being written
+ * @param defaultC
+ * The default instance for the component
+ * @return whether the children of c should be written
+ */
+ public boolean shouldWriteChildren(Component c, Component defaultC) {
+ if (c == getRootComponent()) {
+ // The root component should always write its children - otherwise
+ // the result is empty
+ return true;
+ }
+
+ if (defaultC instanceof HasComponents
+ && ((HasComponents) defaultC).iterator().hasNext()) {
+ // Easy version which assumes that this is a custom component if the
+ // constructor adds children
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/server/src/com/vaadin/ui/declarative/DesignException.java b/server/src/com/vaadin/ui/declarative/DesignException.java
new file mode 100644
index 0000000000..01482f84b0
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/DesignException.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+@SuppressWarnings("serial")
+/**
+ * An exception that is used when reading or writing a design fails.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignException extends RuntimeException {
+
+ public DesignException() {
+ super();
+ }
+
+ public DesignException(String message) {
+ super(message);
+ }
+
+ public DesignException(String message, Throwable e) {
+ super(message, e);
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/declarative/FieldBinder.java b/server/src/com/vaadin/ui/declarative/FieldBinder.java
new file mode 100644
index 0000000000..bd906682fa
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/FieldBinder.java
@@ -0,0 +1,265 @@
+/*
+ * 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.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.vaadin.ui.Component;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * Binder utility that binds member fields of a design class instance to given
+ * component instances. Only fields of type {@link Component} are bound
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FieldBinder implements Serializable {
+
+ // the instance containing the bound fields
+ private Object bindTarget;
+ // mapping between field names and Fields
+ private Map<String, Field> fieldMap = new HashMap<String, Field>();
+
+ /**
+ * Creates a new instance of LayoutFieldBinder.
+ *
+ * @param design
+ * the design class instance containing the fields to bind
+ * @throws IntrospectionException
+ * if the given design class can not be introspected
+ */
+ public FieldBinder(Object design) throws IntrospectionException {
+ this(design, design.getClass());
+ }
+
+ /**
+ * Creates a new instance of LayoutFieldBinder.
+ *
+ * @param design
+ * the instance containing the fields
+ * @param classWithFields
+ * the class which defines the fields to bind
+ * @throws IntrospectionException
+ * if the given design class can not be introspected
+ */
+ public FieldBinder(Object design, Class<?> classWithFields)
+ throws IntrospectionException {
+ if (design == null) {
+ throw new IllegalArgumentException("The design must not be null");
+ }
+ bindTarget = design;
+ resolveFields(classWithFields);
+ }
+
+ /**
+ * Returns a collection of field names that are not bound.
+ *
+ * @return a collection of fields assignable to Component that are not bound
+ */
+ public Collection<String> getUnboundFields() throws FieldBindingException {
+ List<String> unboundFields = new ArrayList<String>();
+ for (Field f : fieldMap.values()) {
+ try {
+ Object value = ReflectTools.getJavaFieldValue(bindTarget, f);
+ if (value == null) {
+ unboundFields.add(f.getName());
+ }
+ } catch (IllegalArgumentException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ } catch (IllegalAccessException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldBindingException("Could not get field value", e);
+ }
+ }
+ if (unboundFields.size() > 0) {
+ getLogger().severe(
+ "Found unbound fields in component root :" + unboundFields);
+ }
+ return unboundFields;
+ }
+
+ /**
+ * Resolves the fields of the design class instance.
+ */
+ private void resolveFields(Class<?> classWithFields) {
+ for (Field memberField : getFields(classWithFields)) {
+ if (Component.class.isAssignableFrom(memberField.getType())) {
+ fieldMap.put(memberField.getName().toLowerCase(Locale.ENGLISH),
+ memberField);
+ }
+ }
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The name of the bound field is constructed based on the
+ * id or caption of the instance, depending on which one is defined. If a
+ * field is already bound (not null), {@link FieldBindingException} is
+ * thrown.
+ *
+ * @param instance
+ * the instance to be bound to a field
+ * @return true on success, otherwise false
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ public boolean bindField(Component instance) {
+ return bindField(instance, null);
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The fields are matched based on localId, id and caption.
+ *
+ * @param instance
+ * the instance to be bound to a field
+ * @param localId
+ * the localId used for mapping the field to an instance field
+ * @return true on success
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ public boolean bindField(Component instance, String localId) {
+ // check that the field exists, is correct type and is null
+ boolean success = bindFieldByIdentifier(localId, instance);
+ if (!success) {
+ success = bindFieldByIdentifier(instance.getId(), instance);
+ }
+ if (!success) {
+ success = bindFieldByIdentifier(instance.getCaption(), instance);
+ }
+ if (!success) {
+ String idInfo = "localId: " + localId + " id: " + instance.getId()
+ + " caption: " + instance.getCaption();
+ getLogger().finest(
+ "Could not bind component to a field "
+ + instance.getClass().getName() + " " + idInfo);
+ }
+ return success;
+ }
+
+ /**
+ * Tries to bind the given {@link Component} instance to a member field of
+ * the bind target. The field is matched based on the given identifier. If a
+ * field is already bound (not null), {@link FieldBindingException} is
+ * thrown.
+ *
+ * @param identifier
+ * the identifier for the field.
+ * @param instance
+ * the instance to be bound to a field
+ * @return true on success
+ * @throws FieldBindingException
+ * if error occurs when trying to bind the instance to a field
+ */
+ private boolean bindFieldByIdentifier(String identifier, Component instance) {
+ try {
+ // create and validate field name
+ String fieldName = asFieldName(identifier);
+ if (fieldName.length() == 0) {
+ return false;
+ }
+ // validate that the field can be found
+ Field field = fieldMap.get(fieldName.toLowerCase(Locale.ENGLISH));
+ if (field == null) {
+ getLogger().fine(
+ "No field was found by identifier " + identifier);
+ return false;
+ }
+ // validate that the field is not set
+ Object fieldValue = ReflectTools.getJavaFieldValue(bindTarget,
+ field);
+ if (fieldValue != null) {
+ getLogger().fine(
+ "The field \"" + fieldName
+ + "\" was already mapped. Ignoring.");
+ } else {
+ // set the field value
+ ReflectTools.setJavaFieldValue(bindTarget, field, instance);
+ }
+ return true;
+ } catch (IllegalAccessException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ } catch (IllegalArgumentException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldBindingException("Field binding failed", e);
+ }
+ }
+
+ /**
+ * Converts the given identifier to a valid field name by stripping away
+ * illegal character and setting the first letter of the name to lower case.
+ *
+ * @param identifier
+ * the identifier to be converted to field name
+ * @return the field name corresponding the identifier
+ */
+ private static String asFieldName(String identifier) {
+ if (identifier == null) {
+ return "";
+ }
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < identifier.length(); i++) {
+ char character = identifier.charAt(i);
+ if (Character.isJavaIdentifierPart(character)) {
+ result.append(character);
+ }
+ }
+ // lowercase first letter
+ if (result.length() > 0 && Character.isLetter(result.charAt(0))) {
+ result.setCharAt(0, Character.toLowerCase(result.charAt(0)));
+ }
+ return result.toString();
+ }
+
+ /**
+ * Returns a list containing Field objects reflecting all the fields of the
+ * class or interface represented by this Class object. The fields in
+ * superclasses are excluded.
+ *
+ * @param searchClass
+ * the class to be scanned for fields
+ * @return the list of fields in this class
+ */
+ protected static List<java.lang.reflect.Field> getFields(
+ Class<?> searchClass) {
+ ArrayList<java.lang.reflect.Field> memberFields = new ArrayList<java.lang.reflect.Field>();
+
+ for (java.lang.reflect.Field memberField : searchClass
+ .getDeclaredFields()) {
+ memberFields.add(memberField);
+ }
+ return memberFields;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(FieldBinder.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/FieldBindingException.java b/server/src/com/vaadin/ui/declarative/FieldBindingException.java
new file mode 100644
index 0000000000..d8b587a14c
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/FieldBindingException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Exception that is thrown when an error occurs during field binding when
+ * reading a design template
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FieldBindingException extends RuntimeException {
+
+ public FieldBindingException(String message) {
+ super(message);
+ }
+
+ public FieldBindingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/server/src/com/vaadin/ui/themes/ValoTheme.java b/server/src/com/vaadin/ui/themes/ValoTheme.java
index da80375114..1285bf7f67 100644
--- a/server/src/com/vaadin/ui/themes/ValoTheme.java
+++ b/server/src/com/vaadin/ui/themes/ValoTheme.java
@@ -882,6 +882,31 @@ public class ValoTheme {
/**
* <p>
+ * When you use the Valo menu and wish to enable responsive features of the
+ * menu, you need to add this style name to the UI containing the menu.
+ * </p>
+ *
+ * <p>
+ * You only need to add this style name to the UI containing a Valo menu, if
+ * you're using the Responsive extension with the UI.
+ * </p>
+ *
+ * <h4>Example</h4>
+ *
+ * <p>
+ * To enable responsivity in the Valo menu, the following example code
+ * should be executed in your UI containing the menu.
+ * </p>
+ *
+ * <pre>
+ * Responsive.makeResponsive(this);
+ * addStyleName(ValoTheme.UI_WITH_MENU);
+ * </pre>
+ */
+ public static final String UI_WITH_MENU = "valo-menu-responsive";
+
+ /**
+ * <p>
* Set the <em><b>primary</b></em> style name of a CssLayout to this, and
* add any number of layouts with the {@link #MENU_PART} style inside it.
* </p>