/* * 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.File; import java.lang.reflect.Method; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang3.LocaleUtils; import org.jsoup.nodes.Attribute; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Node; import com.vaadin.server.ExternalResource; import com.vaadin.server.FileResource; import com.vaadin.server.FontAwesome; import com.vaadin.server.Resource; import com.vaadin.server.Sizeable; import com.vaadin.server.ThemeResource; import com.vaadin.ui.Component; import com.vaadin.ui.DesignSynchronizable; /** * 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 { protected static Logger getLogger() { return Logger.getLogger(DesignAttributeHandler.class.getName()); } /** * Clears the children and attributes of the given node * * @since 7.4 * @param design * the node to be cleared */ public static void clearNode(Node 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. If the * attribute is not present, (value is null) the corresponding property is * got from the <code>defaultInstance</code> * * @since 7.4 * @param component * the component to which the attribute should be set * @param attribute * the attribute to be set * @param attributes * the attribute map. If the attributes does not contain the * requested attribute, the value is retrieved from the * <code> defaultInstance</code> * @param defaultInstance * the default instance of the class for fetching the default * values */ public static boolean readAttribute(DesignSynchronizable component, String attribute, Attributes attributes, DesignSynchronizable defaultInstance) { String value = null; if (attributes.hasKey(attribute)) { value = attributes.get(attribute); } // find setter for the property boolean success = false; try { Method setter = findSetterForAttribute(component.getClass(), attribute); if (setter == null) { // if we don't have the setter, there is no point in continuing success = false; } else if (value != null) { // we have a value from design attributes, let's use that Object param = fromAttributeValue( setter.getParameterTypes()[0], value); setter.invoke(component, param); success = true; } else { // otherwise find the getter for the property Method getter = findGetterForSetter(component.getClass(), setter); if (getter != null) { // read the default value from defaults Object defaultValue = getter.invoke(defaultInstance); setter.invoke(component, defaultValue); 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 * * @since 7.4 * @param clazz * the class scanned for setters * @return the list of supported design attributes */ public static List<String> findSupportedAttributes(Class<?> clazz) { List<String> attributes = new ArrayList<String>(); for (Method method : clazz.getMethods()) { // check that the method is setter, has single argument of supported // type and has a corresponding getter if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && isSupported(method.getParameterTypes()[0]) && findGetterForSetter(clazz, method) != null) { attributes.add(toAttributeName(method.getName())); // TODO: we might want to cache the getters and setters? } } return attributes; } /** * Assigns the width for the component based on the design attributes * * @since 7.4 * @param component * the component to assign the width * @param attributes * the attributes to be used for determining the width * @param defaultInstance * the default instance of the class for fetching the default * value */ public static void readWidth(DesignSynchronizable component, Attributes attributes, DesignSynchronizable defaultInstance) { if (attributes.hasKey("width-auto") || attributes.hasKey("size-auto")) { component.setWidth(null); } else if (attributes.hasKey("width-full") || attributes.hasKey("size-full")) { component.setWidth("100%"); } else if (attributes.hasKey("width")) { component.setWidth(attributes.get("width")); } else { component.setWidth(defaultInstance.getWidth(), defaultInstance.getWidthUnits()); } } /** * Assigns the height for the component based on the design attributes * * @since 7.4 * @param component * the component to assign the height * @param attributes * the attributes to be used for determining the height * @param defaultInstance * the default instance of the class for fetching the default * value */ public static void readHeight(DesignSynchronizable component, Attributes attributes, DesignSynchronizable defaultInstance) { if (attributes.hasKey("height-auto") || attributes.hasKey("size-auto")) { component.setHeight(null); } else if (attributes.hasKey("height-full") || attributes.hasKey("size-full")) { component.setHeight("100%"); } else if (attributes.hasKey("height")) { component.setHeight(attributes.get("height")); } else { component.setHeight(defaultInstance.getHeight(), defaultInstance.getHeightUnits()); } } /** * Writes the specified attribute to the design if it differs from the * default value got from the <code> defaultInstance <code> * * @since 7.4 * @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(DesignSynchronizable component, String attribute, Attributes attr, DesignSynchronizable 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 (value == defaultValue || (value != null && value.equals(defaultValue))) { } else { 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); } } } /** * Writes the size related attributes for the component if they differ from * the defaults * * @since 7.4 * @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 */ public static void writeSize(DesignSynchronizable component, Attributes attributes, DesignSynchronizable defaultInstance) { if (areEqualSize(component, defaultInstance)) { // we have default values -> ignore return; } boolean widthFull = component.getWidth() == 100f && component.getWidthUnits().equals(Sizeable.Unit.PERCENTAGE); boolean heightFull = component.getHeight() == 100f && component.getHeightUnits().equals(Sizeable.Unit.PERCENTAGE); boolean widthAuto = component.getWidth() == -1; boolean heightAuto = component.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 { // handle width if (!areEqualWidth(component, defaultInstance)) { if (widthFull) { attributes.put("width-full", "true"); } else if (widthAuto) { attributes.put("width-auto", "true"); } else { attributes.put("width", formatDesignAttribute(component.getWidth()) + component.getWidthUnits().getSymbol()); } } if (!areEqualHeight(component, defaultInstance)) { // handle height if (heightFull) { attributes.put("height-full", "true"); } else if (heightAuto) { attributes.put("height-auto", "true"); } else { attributes.put("height", formatDesignAttribute(component.getHeight()) + component.getHeightUnits().getSymbol()); } } } } /** * Formats the given design attribute value. The method is provided to * ensure consistent number formatting for design attribute values * * @since 7.4 * @param number * the number to be formatted * @return the formatted number */ public static String formatDesignAttribute(float number) { DecimalFormat fmt = new DecimalFormat(); return fmt.format(number); } /** * 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> * * @since 7.4 * @param methodName * the method name * @return the design attribute name corresponding the given method name */ private static String toAttributeName(String methodName) { String[] words = methodName.split("(?<!^)(?=[A-Z])"); StringBuilder builder = new StringBuilder(); // ignore first token ("set") for (int i = 1; i < words.length; i++) { if (builder.length() > 0) { builder.append("-"); } builder.append(words[i].toLowerCase()); } return builder.toString(); } /** * Returns the setter method name corresponding the given design attribute * name. For example given a attribute name <code>primary-style-name</code> * the return value would be <code>setPrimaryStyleName</code>. * * @since 7.4 * @param designAttributeName * the design attribute name * @return the setter method name corresponding the given design attribute * name */ private static String toSetterName(String designAttributeName) { String[] parts = designAttributeName.split("-"); StringBuilder builder = new StringBuilder(); builder.append("set"); for (String part : parts) { builder.append(part.substring(0, 1).toUpperCase()); builder.append(part.substring(1)); } return builder.toString(); } /** * Returns a list of possible getter method names for the corresponding * design attribute name. * * @since 7.4 * @param designAttributeName * the design attribute name * @return the list of getter method names corresponding the given design * attribute name */ private static List<String> toGetterNames(String designAttributeName) { String[] parts = designAttributeName.split("-"); StringBuilder builder = new StringBuilder(); for (String part : parts) { builder.append(part.substring(0, 1).toUpperCase()); builder.append(part.substring(1)); } String propertyName = builder.toString(); List<String> result = new ArrayList<String>(); result.add("get" + propertyName); result.add("is" + propertyName); result.add("has" + propertyName); return result; } /** * Parses the given attribute value to specified target type * * @since 7.4 * @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 value == null || !value.equalsIgnoreCase("false"); } 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 == Locale.class) { return LocaleUtils.toLocale(value); } if (targetType == Resource.class) { return parseResource(value); } return null; } /** * Serializes the given value to valid design attribute representation * (string) * * @since 7.4 * @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 (sourceType == Locale.class) { return value != null ? ((Locale) value).toString() : null; } else 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) { return ((FileResource) value).getSourceFile().getPath(); } else { return null; } } else { return value.toString(); } } 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)); } } /** * Finds a corresponding getter method for the given setter method * * @since 7.4 * @param clazz * the class to search methods from * @param setter * the setter that is used to find the matching getter * @return the matching getter or null if not found */ private static Method findGetterForSetter(Class<?> clazz, Method setter) { String propertyName = setter.getName().substring(3); Class<?> returnType = setter.getParameterTypes()[0]; for (Method method : clazz.getMethods()) { if (isGetterForProperty(method, propertyName) && method.getParameterTypes().length == 0 && method.getReturnType().equals(returnType)) { return method; } } return null; } private static boolean isGetterForProperty(Method method, String property) { String methodName = method.getName(); return methodName.equals("get" + property) || methodName.equals("is" + property) || methodName.equals("has" + property); } /** * Returns a setter that can be used for assigning the given design * attribute to the class * * @since 7.4 * @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) { String methodName = toSetterName(attribute); for (Method method : clazz.getMethods()) { if (method.getName().equals(methodName) && method.getParameterTypes().length == 1 && isSupported(method.getParameterTypes()[0])) { return method; } } getLogger().warning( "Could not find setter with supported type for property " + attribute); return null; } /** * Returns a getter that can be used for reading the value of the given * design attribute from the class * * @since 7.4 * @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) { List<String> methodNames = toGetterNames(attribute); for (Method method : clazz.getMethods()) { if (methodNames.contains(method.getName()) && method.getParameterTypes().length == 0 && isSupported(method.getReturnType())) { return method; } } getLogger().warning( "Could not find getter with supported return type for attribute " + attribute); return null; } 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, Locale.class, Resource.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}. * * @since 7.4 * @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)); } /** * Test if the given components have equal width * * @since 7.4 * @param comp1 * @param comp2 * @return true if the widths of the components are equal */ private static boolean areEqualWidth(Component comp1, Component comp2) { return comp1.getWidth() == comp2.getWidth() && comp1.getWidthUnits().equals(comp2.getWidthUnits()); } /** * Tests if the given components have equal height * * @since 7.4 * @param comp1 * @param comp2 * @return true if the heights of the components are equal */ private static boolean areEqualHeight(Component comp1, Component comp2) { return comp1.getHeight() == comp2.getHeight() && comp1.getHeightUnits().equals(comp2.getHeightUnits()); } /** * Test if the given components have equal size * * @since 7.4 * @param comp1 * @param comp2 * @return true if the widht and height of the components are equal */ private static boolean areEqualSize(Component comp1, Component comp2) { return areEqualWidth(comp1, comp2) && areEqualHeight(comp1, comp2); } }