diff options
Diffstat (limited to 'server/src/com/vaadin/ui/AbstractComponent.java')
-rw-r--r-- | server/src/com/vaadin/ui/AbstractComponent.java | 471 |
1 files changed, 409 insertions, 62 deletions
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()); + } } |