diff options
author | Leif Åstrand <leif@vaadin.com> | 2014-12-18 18:30:09 +0200 |
---|---|---|
committer | Leif Åstrand <leif@vaadin.com> | 2014-12-18 18:30:09 +0200 |
commit | e3d0fbbab68f9ef8642a975741d9f576993b1f38 (patch) | |
tree | 5711a31c5af1d3773a86b4d25c9f8064aa435ecc /server/src/com/vaadin/ui | |
parent | 434fb5bf5cf62490686367e9193b7898077bbd44 (diff) | |
parent | 68eec666b55c42b4a55235c00bc78f6212eb2062 (diff) | |
download | vaadin-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')
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("Datum"); @@ -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 <body> tag. A hierarchy of components is + * achieved by nesting other elements under the root element. An empty component + * hierarchy is represented as no elements under the <body> tag. + * + * <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 <body>. Also invalid html containing + * the hierarchy without <html>, <head> and <body> 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 <body>. Also + * invalid html containing the hierarchy without <html>, <head> + * and <body> 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 + * <body> 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> |