summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiki <miki@vaadin.com>2015-01-16 14:02:19 +0200
committerVaadin Code Review <review@vaadin.com>2015-02-03 11:15:41 +0000
commita508ed7b4aa062334ad84d7967cb2bdd5d8ecc26 (patch)
tree5215ac87b3063efd9d7c8a67d5b3b008ff931b70
parent293b62ab6ed402b188685027b100025597a37d00 (diff)
downloadvaadin-framework-a508ed7b4aa062334ad84d7967cb2bdd5d8ecc26.tar.gz
vaadin-framework-a508ed7b4aa062334ad84d7967cb2bdd5d8ecc26.zip
Declarative for DateFields (and related) with ISO8601 (#16313)
DesignAttributeHandler supports method names that contains some words in uppercase DesignAttributeHandler and other components now use extensible Formatter with Converters rather than static methods Change-Id: I9f68414bd4821f47ff37a26375091d154cae9a93
-rw-r--r--server/src/com/vaadin/ui/AbsoluteLayout.java4
-rw-r--r--server/src/com/vaadin/ui/AbstractComponent.java18
-rw-r--r--server/src/com/vaadin/ui/AbstractOrderedLayout.java4
-rw-r--r--server/src/com/vaadin/ui/DateField.java41
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java408
-rw-r--r--server/src/com/vaadin/ui/declarative/DesignFormatter.java348
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/DesignDateConverter.java67
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/DesignFormatConverter.java72
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java87
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/DesignShortcutActionConverter.java121
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/DesignToStringConverter.java115
-rw-r--r--server/src/com/vaadin/ui/declarative/converters/ShortcutKeyMapper.java151
-rw-r--r--server/tests/src/com/vaadin/tests/design/DateFieldsTest.java99
-rw-r--r--server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java149
-rw-r--r--server/tests/src/com/vaadin/tests/design/LocaleTest.java12
-rw-r--r--server/tests/src/com/vaadin/tests/event/ShortcutActionTest.java115
16 files changed, 1445 insertions, 366 deletions
diff --git a/server/src/com/vaadin/ui/AbsoluteLayout.java b/server/src/com/vaadin/ui/AbsoluteLayout.java
index 6353a4b25d..63bbe70157 100644
--- a/server/src/com/vaadin/ui/AbsoluteLayout.java
+++ b/server/src/com/vaadin/ui/AbsoluteLayout.java
@@ -759,8 +759,8 @@ public class AbsoluteLayout extends AbstractLayout implements
private void writePositionAttribute(Node node, String key, String symbol,
Float value) {
if (value != null) {
- String valueString = DesignAttributeHandler.formatFloat(value
- .floatValue());
+ String valueString = DesignAttributeHandler.getFormatter().format(
+ value);
node.attr(key, valueString + symbol);
}
}
diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java
index 9ff6dff21e..ebe438b908 100644
--- a/server/src/com/vaadin/ui/AbstractComponent.java
+++ b/server/src/com/vaadin/ui/AbstractComponent.java
@@ -959,8 +959,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
}
// handle immediate
if (attr.hasKey("immediate")) {
- setImmediate(DesignAttributeHandler.parseBoolean(attr
- .get("immediate")));
+ setImmediate(DesignAttributeHandler.getFormatter().parse(
+ attr.get("immediate"), Boolean.class));
}
// handle locale
@@ -984,8 +984,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
// handle responsive
if (attr.hasKey("responsive")) {
- setResponsive(DesignAttributeHandler.parseBoolean(attr
- .get("responsive")));
+ setResponsive(DesignAttributeHandler.getFormatter().parse(
+ attr.get("responsive"), Boolean.class));
}
// check for unsupported attributes
Set<String> supported = new HashSet<String>();
@@ -1138,9 +1138,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
} else if (widthAuto) {
attributes.put("width-auto", "true");
} else {
- String widthString = DesignAttributeHandler
- .formatFloat(getWidth())
- + getWidthUnits().getSymbol();
+ String widthString = DesignAttributeHandler.getFormatter()
+ .format(getWidth()) + getWidthUnits().getSymbol();
attributes.put("width", widthString);
}
@@ -1152,9 +1151,8 @@ public abstract class AbstractComponent extends AbstractClientConnector
} else if (heightAuto) {
attributes.put("height-auto", "true");
} else {
- String heightString = DesignAttributeHandler
- .formatFloat(getHeight())
- + getHeightUnits().getSymbol();
+ String heightString = DesignAttributeHandler.getFormatter()
+ .format(getHeight()) + getHeightUnits().getSymbol();
attributes.put("height", heightString);
}
}
diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
index 67bcfc904c..3aec3b2d7a 100644
--- a/server/src/com/vaadin/ui/AbstractOrderedLayout.java
+++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
@@ -563,8 +563,8 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements
if (expandRatio == 1.0f) {
childElement.attr(":expand", "");
} else if (expandRatio > 0) {
- childElement.attr(":expand",
- DesignAttributeHandler.formatFloat(expandRatio));
+ childElement.attr(":expand", DesignAttributeHandler
+ .getFormatter().format(expandRatio));
}
}
}
diff --git a/server/src/com/vaadin/ui/DateField.java b/server/src/com/vaadin/ui/DateField.java
index 3d683f4902..422b1ffdd8 100644
--- a/server/src/com/vaadin/ui/DateField.java
+++ b/server/src/com/vaadin/ui/DateField.java
@@ -24,6 +24,9 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Element;
import com.vaadin.data.Property;
import com.vaadin.data.Validator;
@@ -40,6 +43,8 @@ import com.vaadin.server.PaintTarget;
import com.vaadin.shared.ui.datefield.DateFieldConstants;
import com.vaadin.shared.ui.datefield.Resolution;
import com.vaadin.shared.ui.datefield.TextualDateFieldState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
/**
* <p>
@@ -1061,4 +1066,40 @@ public class DateField extends AbstractField<Date> implements
}
}
+
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ if (design.hasAttr("value") && !design.attr("value").isEmpty()) {
+ Date date = DesignAttributeHandler.getFormatter().parse(
+ design.attr("value"), Date.class);
+ // formatting will return null if it cannot parse the string
+ if (date == null) {
+ Logger.getLogger(DateField.class.getName()).info(
+ "cannot parse " + design.attr("value") + " as date");
+ }
+ this.setValue(date);
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ if (getValue() != null) {
+ design.attr("value",
+ DesignAttributeHandler.getFormatter().format(getValue()));
+ }
+ }
+
+ /**
+ * Returns current date-out-of-range error message.
+ *
+ * @see #setDateOutOfRangeMessage(String)
+ * @since 7.4
+ * @return Current error message for dates out of range.
+ */
+ public String getDateOutOfRangeMessage() {
+ return dateOutOfRangeMessage;
+ }
+
}
diff --git a/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java b/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java
index be7d023ebf..3e2c01c881 100644
--- a/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java
+++ b/server/src/com/vaadin/ui/declarative/DesignAttributeHandler.java
@@ -19,35 +19,25 @@ 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 java.util.regex.Matcher;
+import java.util.regex.Pattern;
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.data.util.converter.Converter;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.Component;
@@ -65,8 +55,21 @@ public class DesignAttributeHandler implements Serializable {
return Logger.getLogger(DesignAttributeHandler.class.getName());
}
- private static Map<Class, AttributeCacheEntry> cache = Collections
- .synchronizedMap(new HashMap<Class, AttributeCacheEntry>());
+ private static Map<Class<?>, AttributeCacheEntry> cache = Collections
+ .synchronizedMap(new HashMap<Class<?>, AttributeCacheEntry>());
+
+ // translates string <-> object
+ private static DesignFormatter FORMATTER = new DesignFormatter();
+
+ /**
+ * Returns the currently used formatter. All primitive types and all types
+ * needed by Vaadin components are handled by that formatter.
+ *
+ * @return An instance of the formatter.
+ */
+ public static DesignFormatter getFormatter() {
+ return FORMATTER;
+ }
/**
* Clears the children and attributes of the given element
@@ -111,8 +114,8 @@ public class DesignAttributeHandler implements Serializable {
success = false;
} else {
// we have a value from design attributes, let's use that
- Object param = fromAttributeValue(
- setter.getParameterTypes()[0], value);
+ Object param = getFormatter().parse(value,
+ setter.getParameterTypes()[0]);
setter.invoke(target, param);
success = true;
}
@@ -170,7 +173,7 @@ public class DesignAttributeHandler implements Serializable {
Method getter = descriptor.getReadMethod();
Method setter = descriptor.getWriteMethod();
if (getter != null && setter != null
- && isSupported(descriptor.getPropertyType())) {
+ && getFormatter().canConvert(descriptor.getPropertyType())) {
String attribute = toAttributeName(descriptor.getName());
entry.addAttribute(attribute, getter, setter);
}
@@ -229,10 +232,9 @@ public class DesignAttributeHandler implements Serializable {
* @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)) {
+ if (!getFormatter().canConvert(outputType)) {
throw new IllegalArgumentException("output type: "
+ outputType.getName() + " not supported");
}
@@ -241,7 +243,7 @@ public class DesignAttributeHandler implements Serializable {
} else {
try {
String value = attributes.get(attribute);
- return (T) fromAttributeValue(outputType, value);
+ return getFormatter().parse(value, outputType);
} catch (Exception e) {
throw new DesignException("Failed to read attribute "
+ attribute, e);
@@ -266,7 +268,7 @@ public class DesignAttributeHandler implements Serializable {
*/
public static <T> void writeAttribute(String attribute,
Attributes attributes, T value, T defaultValue, Class<T> inputType) {
- if (!isSupported(inputType)) {
+ if (!getFormatter().canConvert(inputType)) {
throw new IllegalArgumentException("input type: "
+ inputType.getName() + " not supported");
}
@@ -277,101 +279,6 @@ public class DesignAttributeHandler implements Serializable {
}
/**
- * 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>
@@ -381,6 +288,7 @@ public class DesignAttributeHandler implements Serializable {
* @return the design attribute name corresponding the given method name
*/
private static String toAttributeName(String propertyName) {
+ propertyName = removeSubsequentUppercase(propertyName);
String[] words = propertyName.split("(?<!^)(?=[A-Z])");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < words.length; i++) {
@@ -393,56 +301,43 @@ public class DesignAttributeHandler implements Serializable {
}
/**
- * Parses the given attribute value to specified target type
+ * Replaces subsequent UPPERCASE strings of length 2 or more followed either
+ * by another uppercase letter or an end of string. This is to generalise
+ * handling of method names like <tt>showISOWeekNumbers</tt>.
*
- * @param targetType
- * the target type for the value
- * @param value
- * the parsed value
- * @return the object of specified target type
+ * @param param
+ * Input string.
+ * @return Input string with sequences of UPPERCASE turned into Normalcase.
*/
- 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);
+ private static String removeSubsequentUppercase(String param) {
+ StringBuffer result = new StringBuffer();
+ // match all two-or-more caps letters lead by a non-uppercase letter
+ // followed by either a capital letter or string end
+ Pattern pattern = Pattern.compile("(^|[^A-Z])([A-Z]{2,})([A-Z]|$)");
+ Matcher matcher = pattern.matcher(param);
+ while (matcher.find()) {
+ String matched = matcher.group(2);
+ // if this is a beginning of the string, the whole matched group is
+ // written in lower case
+ if (matcher.group(1).isEmpty()) {
+ matcher.appendReplacement(result, matched.toLowerCase()
+ + matcher.group(3));
+ // otherwise the first character of the group stays uppercase,
+ // while the others are lower case
+ } else {
+ matcher.appendReplacement(
+ result,
+ matcher.group(1) + matched.substring(0, 1)
+ + matched.substring(1).toLowerCase()
+ + matcher.group(3));
+ }
+ // in both cases the uppercase letter of the next word (or string's
+ // end) is added
+ // this implies there is at least one extra lowercase letter after
+ // it to be caught by the next call to find()
}
- return null;
+ matcher.appendTail(result);
+ return result.toString();
}
/**
@@ -460,57 +355,16 @@ public class DesignAttributeHandler implements Serializable {
// 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);
+ Converter<String, Object> converter = getFormatter().findConverterFor(
+ sourceType);
+ if (converter != null) {
+ return converter.convertToPresentation(value, String.class, null);
} 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
*
@@ -542,29 +396,6 @@ public class DesignAttributeHandler implements Serializable {
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
@@ -599,117 +430,4 @@ public class DesignAttributeHandler implements Serializable {
}
}
- /**
- * 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/DesignFormatter.java b/server/src/com/vaadin/ui/declarative/DesignFormatter.java
new file mode 100644
index 0000000000..fdce563104
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/DesignFormatter.java
@@ -0,0 +1,348 @@
+/*
+ * 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.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.server.Resource;
+import com.vaadin.ui.declarative.converters.DesignDateConverter;
+import com.vaadin.ui.declarative.converters.DesignFormatConverter;
+import com.vaadin.ui.declarative.converters.DesignResourceConverter;
+import com.vaadin.ui.declarative.converters.DesignShortcutActionConverter;
+import com.vaadin.ui.declarative.converters.ShortcutKeyMapper;
+import com.vaadin.ui.declarative.converters.DesignToStringConverter;
+
+/**
+ * Class focused on flexible and consistent formatting and parsing of different
+ * values throughout reading and writing {@link Design}. An instance of this
+ * class is used by {@link DesignAttributeHandler}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignFormatter implements Serializable {
+
+ private final Map<Class<?>, Converter<String, ?>> converterMap = Collections
+ .synchronizedMap(new HashMap<Class<?>, Converter<String, ?>>());
+
+ /**
+ * Creates the formatter with default types already mapped.
+ */
+ public DesignFormatter() {
+ mapDefaultTypes();
+ }
+
+ /**
+ * Maps default types to their converters.
+ *
+ */
+ protected void mapDefaultTypes() {
+ // numbers use standard toString/valueOf approach
+ for (Class<?> c : new Class<?>[] { Integer.class, Byte.class,
+ Short.class, Long.class, BigDecimal.class }) {
+ DesignToStringConverter<?> conv = new DesignToStringConverter(c);
+ converterMap.put(c, conv);
+ try {
+ converterMap.put((Class<?>) c.getField("TYPE").get(null), conv);
+ } catch (Exception e) {
+ ; // this will never happen
+ }
+ }
+ // booleans use a bit different converter than the standard one
+ // "false" is boolean false, everything else is boolean true
+ Converter<String, Boolean> booleanConverter = new Converter<String, Boolean>() {
+
+ @Override
+ public Boolean convertToModel(String value,
+ Class<? extends Boolean> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return !value.equalsIgnoreCase("false");
+ }
+
+ @Override
+ public String convertToPresentation(Boolean value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return String.valueOf(value.booleanValue());
+ }
+
+ @Override
+ public Class<Boolean> getModelType() {
+ return Boolean.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+ };
+ converterMap.put(Boolean.class, booleanConverter);
+ converterMap.put(boolean.class, booleanConverter);
+
+ // floats and doubles use formatters
+ DecimalFormatSymbols symbols = new DecimalFormatSymbols(new Locale(
+ "en_US"));
+ DecimalFormat fmt = new DecimalFormat("0.###", symbols);
+ fmt.setGroupingUsed(false);
+ converterMap.put(Float.class, new DesignFormatConverter<Float>(
+ Float.class, fmt));
+ converterMap.put(Float.TYPE, new DesignFormatConverter<Float>(
+ Float.class, fmt));
+ converterMap.put(Double.class, new DesignFormatConverter<Double>(
+ Double.class, fmt));
+ converterMap.put(Double.TYPE, new DesignFormatConverter<Double>(
+ Double.class, fmt));
+
+ // strings do nothing
+ converterMap.put(String.class, new Converter<String, String>() {
+
+ @Override
+ public String convertToModel(String value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return value;
+ }
+
+ @Override
+ public String convertToPresentation(String value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return value;
+ }
+
+ @Override
+ public Class<String> getModelType() {
+ return String.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+ });
+
+ // char takes the first character from the string
+ Converter<String, Character> charConverter = new DesignToStringConverter<Character>(
+ Character.class) {
+
+ @Override
+ public Character convertToModel(String value,
+ Class<? extends Character> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return value.charAt(0);
+ }
+
+ };
+ converterMap.put(Character.class, charConverter);
+ converterMap.put(Character.TYPE, charConverter);
+
+ // date conversion has its own class
+ converterMap.put(Date.class, new DesignDateConverter());
+
+ // as shortcut action and resource
+ converterMap.put(ShortcutAction.class,
+ new DesignShortcutActionConverter(ShortcutKeyMapper.DEFAULT));
+
+ converterMap.put(Resource.class, new DesignResourceConverter());
+
+ // timezones use different static method and do not use toString()
+ converterMap.put(TimeZone.class, new DesignToStringConverter<TimeZone>(
+ TimeZone.class, "getTimeZone") {
+ @Override
+ public String convertToPresentation(TimeZone value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return value.getID();
+ }
+ });
+ }
+
+ /**
+ * Adds a converter for a new type.
+ *
+ * @param converter
+ * Converter to add.
+ */
+ protected <T> void addConverter(Converter<String, T> converter) {
+ converterMap.put(converter.getModelType(), converter);
+ }
+
+ /**
+ * Adds a converter for a given type.
+ *
+ * @param type
+ * Type to convert to/from.
+ * @param converter
+ * Converter.
+ */
+ protected <T> void addConverter(Class<?> type,
+ Converter<String, ?> converter) {
+ converterMap.put(type, converter);
+ }
+
+ /**
+ * Removes the converter for given type, if it was present.
+ *
+ * @param type
+ * Type to remove converter for.
+ */
+ protected void removeConverter(Class<?> type) {
+ converterMap.remove(type);
+ }
+
+ /**
+ * Returns a set of classes that have a converter registered. This is <b>not
+ * the same</b> as the list of supported classes - subclasses of classes in
+ * this set are also supported.
+ *
+ * @return An unmodifiable set of classes that have a converter registered.
+ */
+ protected Set<Class<?>> getRegisteredClasses() {
+ return Collections.unmodifiableSet(converterMap.keySet());
+ }
+
+ /**
+ * Parses a given string as a value of given type
+ *
+ * @param value
+ * String value to convert.
+ * @param type
+ * Expected result type.
+ * @return String converted to the expected result type using a registered
+ * converter for that type.
+ */
+ public <T> T parse(String value, Class<? extends T> type) {
+ Converter<String, T> converter = findConverterFor(type);
+ if (converter != null) {
+ return converter.convertToModel(value, type, null);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Finds a formatter for a given object and attempts to format it.
+ *
+ * @param object
+ * Object to format.
+ * @return String representation of the object, as returned by the
+ * registered converter.
+ */
+ public String format(Object object) {
+ return format(object, object == null ? Object.class : object.getClass());
+ }
+
+ /**
+ * Formats an object according to a converter suitable for a given type.
+ *
+ * @param object
+ * Object to format.
+ * @param type
+ * Type of the object.
+ * @return String representation of the object, as returned by the
+ * registered converter.
+ */
+ public <T> String format(T object, Class<? extends T> type) {
+ if (object == null) {
+ return null;
+ } else {
+ return findConverterFor(object.getClass()).convertToPresentation(
+ object, String.class, null);
+ }
+ }
+
+ /**
+ * Checks whether or not a value of a given type can be converted. If a
+ * converter for a superclass is found, this will return true.
+ *
+ * @param type
+ * Type to check.
+ * @return <b>true</b> when either a given type or its supertype has a
+ * converter, <b>false</b> otherwise.
+ */
+ public boolean canConvert(Class<?> type) {
+ return findConverterFor(type) != null;
+ }
+
+ /**
+ * Finds a converter for a given type. May return a converter for a
+ * superclass instead, if one is found and {@code strict} is false.
+ *
+ * @param sourceType
+ * Type to find a converter for.
+ * @param strict
+ * Whether or not search should be strict. When this is
+ * <b>false</b>, a converter for a superclass of given type may
+ * be returned.
+ * @return A valid converter for a given type or its supertype, <b>null</b>
+ * if it was not found.
+ */
+ @SuppressWarnings("unchecked")
+ protected <T> Converter<String, T> findConverterFor(
+ Class<? extends T> sourceType, boolean strict) {
+ if (sourceType.isEnum()) {
+ // enums can be read in lowercase
+ return new DesignToStringConverter<T>(sourceType) {
+
+ @Override
+ public T convertToModel(String value,
+ Class<? extends T> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return super.convertToModel(value.toUpperCase(),
+ targetType, locale);
+ }
+ };
+ } else if (converterMap.containsKey(sourceType)) {
+ return ((Converter<String, T>) converterMap.get(sourceType));
+ } else if (!strict) {
+ for (Class<?> supported : converterMap.keySet()) {
+ if (supported.isAssignableFrom(sourceType)) {
+ return ((Converter<String, T>) converterMap.get(supported));
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a converter for a given type. May return a converter for a
+ * superclass instead, if one is found.
+ *
+ * @param sourceType
+ * Type to find a converter for.
+ * @return A valid converter for a given type or its subtype, <b>null</b> if
+ * it was not found.
+ */
+ protected <T> Converter<String, T> findConverterFor(
+ Class<? extends T> sourceType) {
+ return findConverterFor(sourceType, false);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignDateConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignDateConverter.java
new file mode 100644
index 0000000000..d2d63ad16e
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/DesignDateConverter.java
@@ -0,0 +1,67 @@
+/*
+ * 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.converters;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+
+/**
+ * A date converter to be used by {@link DesignAttributeHandler}. Provides
+ * ISO-compliant way of storing date and time.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignDateConverter implements Converter<String, Date> {
+
+ @Override
+ public Date convertToModel(String value, Class<? extends Date> targetType,
+ Locale locale) throws Converter.ConversionException {
+ for (String pattern : new String[] { "yyyy-MM-dd HH:mm:ssZ",
+ "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH",
+ "yyyy-MM-dd", "yyyy-MM", "yyyy" }) {
+ try {
+ return new SimpleDateFormat(pattern).parse(value);
+ } catch (ParseException e) {
+ // not parseable, ignore and try another format
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String convertToPresentation(Date value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ").format(value);
+ }
+
+ @Override
+ public Class<Date> getModelType() {
+ return Date.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignFormatConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignFormatConverter.java
new file mode 100644
index 0000000000..e9b26fce0b
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/DesignFormatConverter.java
@@ -0,0 +1,72 @@
+/*
+ * 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.converters;
+
+import java.text.Format;
+import java.text.ParseException;
+import java.util.Locale;
+
+import com.vaadin.data.util.converter.Converter;
+
+/**
+ * Converter based on Java Formats rather than static methods.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <TYPE>
+ * Type of the object to format.
+ */
+public class DesignFormatConverter<TYPE> implements Converter<String, TYPE> {
+
+ private final Format format;
+ private final Class<? extends TYPE> type;
+
+ /**
+ * Constructs an instance of the converter.
+ */
+ public DesignFormatConverter(Class<? extends TYPE> type, Format format) {
+ this.type = type;
+ this.format = format;
+ }
+
+ @Override
+ public TYPE convertToModel(String value, Class<? extends TYPE> targetType,
+ Locale locale) throws Converter.ConversionException {
+ try {
+ return targetType.cast(this.format.parseObject(value));
+ } catch (ParseException e) {
+ throw new Converter.ConversionException(e);
+ }
+ }
+
+ @Override
+ public String convertToPresentation(TYPE value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ return this.format.format(value);
+ }
+
+ @Override
+ public Class<TYPE> getModelType() {
+ return (Class<TYPE>) this.type;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java
new file mode 100644
index 0000000000..70e46b8e7f
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/DesignResourceConverter.java
@@ -0,0 +1,87 @@
+/*
+ * 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.converters;
+
+import java.io.File;
+import java.util.Locale;
+
+import com.vaadin.data.util.converter.Converter;
+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.ui.declarative.DesignAttributeHandler;
+
+/**
+ * A converter for {@link Resource} implementations supported by
+ * {@link DesignAttributeHandler}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignResourceConverter implements Converter<String, Resource> {
+
+ @Override
+ public Resource convertToModel(String value,
+ Class<? extends Resource> targetType, Locale locale)
+ throws Converter.ConversionException {
+ 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));
+ }
+ }
+
+ @Override
+ public String convertToPresentation(Resource value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ 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 {
+ throw new Converter.ConversionException("unknown Resource type - "
+ + value.getClass().getName());
+ }
+ }
+
+ @Override
+ public Class<Resource> getModelType() {
+ return Resource.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignShortcutActionConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignShortcutActionConverter.java
new file mode 100644
index 0000000000..d9d84a1263
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/DesignShortcutActionConverter.java
@@ -0,0 +1,121 @@
+/*
+ * 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.converters;
+
+import java.util.Locale;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.event.ShortcutAction;
+
+/**
+ * Converter for {@link ShortcutActions}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignShortcutActionConverter implements
+ Converter<String, ShortcutAction> {
+
+ /**
+ * Default instance of the shortcut key mapper.
+ */
+ private final ShortcutKeyMapper keyMapper;
+
+ /**
+ * Constructs the converter with given key mapper.
+ *
+ * @param mapper
+ * Key mapper to use.
+ */
+ public DesignShortcutActionConverter(ShortcutKeyMapper mapper) {
+ keyMapper = mapper;
+ }
+
+ @Override
+ public ShortcutAction convertToModel(String value,
+ Class<? extends ShortcutAction> targetType, Locale locale)
+ throws Converter.ConversionException {
+ if (value.length() == 0) {
+ return null;
+ }
+ String[] data = value.split(" ", 2);
+
+ String[] parts = data[0].split("-");
+ // handle keycode
+ String keyCodePart = parts[parts.length - 1];
+ int keyCode = getKeyMapper().getKeycodeForString(keyCodePart);
+ if (keyCode < 0) {
+ throw new IllegalArgumentException("Invalid shortcut definition "
+ + value);
+ }
+ // 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 = getKeyMapper().getKeycodeForString(parts[i]);
+ if (modifier > 0) {
+ modifiers[i] = modifier;
+ } else {
+ throw new IllegalArgumentException(
+ "Invalid shortcut definition " + value);
+ }
+ }
+ return new ShortcutAction(data.length == 2 ? data[1] : null, keyCode,
+ modifiers);
+ }
+
+ @Override
+ public String convertToPresentation(ShortcutAction value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ StringBuilder sb = new StringBuilder();
+ // handle modifiers
+ if (value.getModifiers() != null) {
+ for (int modifier : value.getModifiers()) {
+ sb.append(getKeyMapper().getStringForKeycode(modifier)).append(
+ "-");
+ }
+ }
+ // handle keycode
+ sb.append(getKeyMapper().getStringForKeycode(value.getKeyCode()));
+ if (value.getCaption() != null) {
+ sb.append(" ").append(value.getCaption());
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public Class<ShortcutAction> getModelType() {
+ return ShortcutAction.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+ /**
+ * Returns the currently used key mapper.
+ *
+ * @return Key mapper.
+ */
+ public ShortcutKeyMapper getKeyMapper() {
+ return keyMapper;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/DesignToStringConverter.java b/server/src/com/vaadin/ui/declarative/converters/DesignToStringConverter.java
new file mode 100644
index 0000000000..d80119bea1
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/DesignToStringConverter.java
@@ -0,0 +1,115 @@
+/*
+ * 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.converters;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Locale;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+
+/**
+ * Utility class for {@link DesignAttributeHandler} that deals with converting
+ * various types to string.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <TYPE>
+ * Type of the data being converted.
+ */
+public class DesignToStringConverter<TYPE> implements Converter<String, TYPE> {
+
+ private final Class<? extends TYPE> type;
+
+ private final String staticMethodName;
+
+ /**
+ * A string that corresponds to how a null value is stored.
+ */
+ public static final String NULL_VALUE_REPRESENTATION = "";
+
+ /**
+ * Constructs the converter for a given type. Implicitly requires that a
+ * static method {@code valueOf(String)} is present in the type to do the
+ * conversion.
+ *
+ * @param type
+ * Type of values to convert.
+ */
+ public DesignToStringConverter(Class<? extends TYPE> type) {
+ this(type, "valueOf");
+ }
+
+ /**
+ * Constructs the converter for a given type, giving the name of the public
+ * static method that does the conversion from String.
+ *
+ * @param type
+ * Type to convert.
+ * @param staticMethodName
+ * Method to call when converting from String to this type. This
+ * must be public and static method that returns an object of
+ * passed type.
+ */
+ public DesignToStringConverter(Class<? extends TYPE> type, String staticMethodName) {
+ this.type = type;
+ this.staticMethodName = staticMethodName;
+ }
+
+ @Override
+ public TYPE convertToModel(String value, Class<? extends TYPE> targetType,
+ Locale locale) throws Converter.ConversionException {
+ try {
+ return type.cast(type
+ .getMethod(this.staticMethodName, String.class).invoke(
+ null, value));
+ } catch (IllegalAccessException e) {
+ throw new Converter.ConversionException(e);
+ } catch (IllegalArgumentException e) {
+ throw new Converter.ConversionException(e);
+ } catch (InvocationTargetException e) {
+ throw new Converter.ConversionException(e);
+ } catch (NoSuchMethodException e) {
+ throw new Converter.ConversionException(e);
+ } catch (SecurityException e) {
+ throw new Converter.ConversionException(e);
+ } catch (RuntimeException e) {
+ throw new Converter.ConversionException(e);
+ }
+ }
+
+ @Override
+ public String convertToPresentation(TYPE value,
+ Class<? extends String> targetType, Locale locale)
+ throws Converter.ConversionException {
+ if (value == null) {
+ return NULL_VALUE_REPRESENTATION;
+ } else {
+ return value.toString();
+ }
+ }
+
+ @Override
+ public Class<TYPE> getModelType() {
+ return (Class<TYPE>) this.type;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/declarative/converters/ShortcutKeyMapper.java b/server/src/com/vaadin/ui/declarative/converters/ShortcutKeyMapper.java
new file mode 100644
index 0000000000..46c38ce0e0
--- /dev/null
+++ b/server/src/com/vaadin/ui/declarative/converters/ShortcutKeyMapper.java
@@ -0,0 +1,151 @@
+/*
+ * 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.converters;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.event.ShortcutAction.KeyCode;
+import com.vaadin.event.ShortcutAction.ModifierKey;
+
+/**
+ * Provides mappings between shortcut keycodes and their representation in
+ * design attributes. Contains a default framework implementation as a field.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface ShortcutKeyMapper extends Serializable {
+
+ /**
+ * Gets the key code for a given string.
+ *
+ * @param attributePresentation
+ * String
+ * @return Key code.
+ */
+ public int getKeycodeForString(String attributePresentation);
+
+ /**
+ * Returns a string for a given key code.
+ *
+ * @param keyCode
+ * Key code.
+ * @return String.
+ */
+ public String getStringForKeycode(int keyCode);
+
+ /**
+ * An instance of a default keymapper.
+ */
+ public static final ShortcutKeyMapper DEFAULT = new ShortcutKeyMapper() {
+
+ private final Map<Integer, String> keyCodeMap = Collections
+ .synchronizedMap(new HashMap<Integer, String>());
+ private final Map<String, Integer> presentationMap = Collections
+ .synchronizedMap(new HashMap<String, Integer>());
+
+ {
+ // 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 void mapKey(int keyCode, String presentation) {
+ keyCodeMap.put(keyCode, presentation);
+ presentationMap.put(presentation, keyCode);
+ }
+
+ @Override
+ public int getKeycodeForString(String attributePresentation) {
+ Integer code = presentationMap.get(attributePresentation);
+ return code != null ? code.intValue() : -1;
+ }
+
+ @Override
+ public String getStringForKeycode(int keyCode) {
+ return keyCodeMap.get(keyCode);
+ }
+
+ };
+} \ No newline at end of file
diff --git a/server/tests/src/com/vaadin/tests/design/DateFieldsTest.java b/server/tests/src/com/vaadin/tests/design/DateFieldsTest.java
new file mode 100644
index 0000000000..b8aa6ad3c9
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/design/DateFieldsTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.tests.design;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+import org.junit.Test;
+
+import com.vaadin.shared.ui.datefield.Resolution;
+import com.vaadin.ui.DateField;
+import com.vaadin.ui.InlineDateField;
+import com.vaadin.ui.PopupDateField;
+import com.vaadin.ui.declarative.Design;
+
+/**
+ * Tests the declarative support for implementations of {@link DateField}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateFieldsTest {
+
+ @Test
+ public void testInlineDateFieldToFromDesign() throws Exception {
+ InlineDateField field = new InlineDateField("Day is",
+ new SimpleDateFormat("yyyy-MM-dd").parse("2003-02-27"));
+ field.setResolution(Resolution.DAY);
+ field.setShowISOWeekNumbers(true);
+ field.setRangeStart(new SimpleDateFormat("yyyy-MM-dd")
+ .parse("2001-02-27"));
+ field.setRangeEnd(new SimpleDateFormat("yyyy-MM-dd")
+ .parse("2011-02-27"));
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ Design.write(field, bos);
+
+ InlineDateField result = (InlineDateField) Design
+ .read(new ByteArrayInputStream(bos.toByteArray()));
+ assertEquals(field.getResolution(), result.getResolution());
+ assertEquals(field.getCaption(), result.getCaption());
+ assertEquals(field.getValue(), result.getValue());
+ assertEquals(field.getRangeStart(), result.getRangeStart());
+ assertEquals(field.getRangeEnd(), result.getRangeEnd());
+ }
+
+ @Test
+ public void testPopupDateFieldFromDesign() throws Exception {
+ ByteArrayInputStream bis = new ByteArrayInputStream(
+ "<!DOCTYPE html><html><head></head><body><v-popup-date-field show-iso-week-numbers caption=\"Day is\" resolution=\"MINUTE\" range-end=\"2019-01-15\" input-prompt=\"Pick a day\" value=\"2003-02-27 07:15\"></v-popup-date-field></body></html>"
+ .getBytes());
+ PopupDateField result = (PopupDateField) Design.read(bis);
+ assertEquals(Resolution.MINUTE, result.getResolution());
+ assertEquals("Day is", result.getCaption());
+ assertTrue(result.isShowISOWeekNumbers());
+ assertEquals("Pick a day", result.getInputPrompt());
+ assertEquals(
+ new SimpleDateFormat("yyyy-MM-dd HH:mm")
+ .parse("2003-02-27 07:15"),
+ result.getValue());
+ assertEquals(new SimpleDateFormat("yyyy-MM-dd").parse("2019-01-15"),
+ result.getRangeEnd());
+
+ }
+
+ @Test
+ public void testPopupDateFieldFromDesignInTicket() throws Exception {
+ ByteArrayInputStream bis = new ByteArrayInputStream(
+ "<!DOCTYPE html><html><head></head><body><v-date-field range-start=\"2014-05-05\" range-end=\"2014-06-05\" date-out-of-range-message=\"Please select a sensible date\" resolution=\"day\" date-format=\"yyyy-MM-dd\" lenient show-iso-week-numbers parse-error-message=\"You are doing it wrong\" time-zone=\"GMT+5\" value=\"2014-05-15\"></v-date-field></body></html>"
+ .getBytes());
+ DateField result = (DateField) Design.read(bis);
+ assertEquals(Resolution.DAY, result.getResolution());
+ assertTrue(result.isShowISOWeekNumbers());
+ assertEquals(new SimpleDateFormat("yyyy-MM-dd").parse("2014-05-15"),
+ result.getValue());
+ assertEquals(new SimpleDateFormat("yyyy-MM-dd").parse("2014-06-05"),
+ result.getRangeEnd());
+ assertEquals(TimeZone.getTimeZone("GMT+5"), result.getTimeZone());
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java
new file mode 100644
index 0000000000..c7909751a1
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.tests.design;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.TimeZone;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.FileResource;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.declarative.DesignFormatter;
+
+/**
+ * Various tests related to formatter.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DesignFormatterTest {
+
+ private DesignFormatter formatter;
+
+ @Before
+ public void setUp() {
+ // initialise with default classes
+ formatter = new DesignFormatter();
+ }
+
+ @Test
+ public void testSupportedClasses() {
+
+ for (Class<?> type : new Class<?>[] { String.class, Boolean.class,
+ Integer.class, Float.class, Byte.class, Short.class,
+ Double.class, ShortcutAction.class, Date.class,
+ FileResource.class, ExternalResource.class,
+ ThemeResource.class, Resource.class, TimeZone.class }) {
+ assertTrue("not supported " + type.getSimpleName(),
+ formatter.canConvert(type));
+ }
+ }
+
+ @Test
+ public void testDate() throws Exception {
+ Date date = new SimpleDateFormat("yyyy-MM-dd").parse("2012-02-17");
+ String formatted = formatter.format(date);
+ Date result = formatter.parse(formatted, Date.class);
+
+ // writing will always give full date string
+ assertEquals("2012-02-17 00:00:00+0200", formatted);
+ assertEquals(date, result);
+
+ // try short date as well
+ result = formatter.parse("2012-02-17", Date.class);
+ assertEquals(date, result);
+ }
+
+ @Test
+ public void testShortcutActions() {
+ ShortcutAction action = new ShortcutAction("&^d");
+ String formatted = formatter.format(action);
+ // note the space here - it separates key combination from caption
+ assertEquals("alt-ctrl-d d", formatted);
+
+ ShortcutAction result = formatter
+ .parse(formatted, ShortcutAction.class);
+ assertTrue(equals(action, result));
+ }
+
+ @Test
+ public void testShortcutActionNoCaption() {
+ ShortcutAction action = new ShortcutAction(null,
+ ShortcutAction.KeyCode.D, new int[] {
+ ShortcutAction.ModifierKey.ALT,
+ ShortcutAction.ModifierKey.CTRL });
+ String formatted = formatter.format(action);
+ assertEquals("alt-ctrl-d", formatted);
+
+ ShortcutAction result = formatter
+ .parse(formatted, ShortcutAction.class);
+ assertTrue(equals(action, result));
+ }
+
+ @Test
+ public void testTimeZone() {
+ TimeZone zone = TimeZone.getTimeZone("GMT+2");
+ String formatted = formatter.format(zone);
+ assertEquals("GMT+02:00", formatted);
+ TimeZone result = formatter.parse(formatted, TimeZone.class);
+ assertEquals(zone, result);
+ // try shorthand notation as well
+ result = formatter.parse("GMT+2", TimeZone.class);
+ assertEquals(zone, result);
+ }
+
+ /**
+ * A static method to allow comparison two different actions.
+ *
+ * @param act
+ * One action to compare.
+ * @param other
+ * Second action to compare.
+ * @return <b>true</b> when both actions are the same (caption, icon, and
+ * key combination).
+ */
+ public static final boolean equals(ShortcutAction act, ShortcutAction other) {
+ if (SharedUtil.equals(other.getCaption(), act.getCaption())
+ && SharedUtil.equals(other.getIcon(), act.getIcon())
+ && act.getKeyCode() == other.getKeyCode()
+ && act.getModifiers().length == other.getModifiers().length) {
+ HashSet<Integer> thisSet = new HashSet<Integer>(
+ act.getModifiers().length);
+ // this is a bit tricky comparison, but there is no nice way of
+ // making int[] into a Set
+ for (int mod : act.getModifiers()) {
+ thisSet.add(mod);
+ }
+ for (int mod : other.getModifiers()) {
+ thisSet.remove(mod);
+ }
+ return thisSet.isEmpty();
+ }
+ return false;
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/tests/design/LocaleTest.java b/server/tests/src/com/vaadin/tests/design/LocaleTest.java
index 939080fbbc..8f0ef4d13e 100644
--- a/server/tests/src/com/vaadin/tests/design/LocaleTest.java
+++ b/server/tests/src/com/vaadin/tests/design/LocaleTest.java
@@ -15,15 +15,18 @@
*/
package com.vaadin.tests.design;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import java.io.ByteArrayInputStream;
import java.util.Locale;
-import junit.framework.TestCase;
-
import org.jsoup.nodes.Document;
import org.jsoup.nodes.DocumentType;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
+import org.junit.Before;
+import org.junit.Test;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
@@ -32,11 +35,6 @@ import com.vaadin.ui.Label;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.declarative.Design;
import com.vaadin.ui.declarative.DesignContext;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
/**
* Tests the handling of the locale property in parsing and html generation.
diff --git a/server/tests/src/com/vaadin/tests/event/ShortcutActionTest.java b/server/tests/src/com/vaadin/tests/event/ShortcutActionTest.java
new file mode 100644
index 0000000000..9af23b86b1
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/event/ShortcutActionTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tests.event;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+
+import org.junit.Test;
+
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.tests.design.DesignFormatterTest;
+
+/**
+ * Tests various things about shortcut actions.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ShortcutActionTest {
+
+ private static final String[] KEYS = "a b c d e f g h i j k l m n o p q r s t u v w x y z"
+ .split("\\s+");
+
+ @Test
+ public void testHashCodeUniqueness() {
+ HashSet<ShortcutAction> set = new HashSet<ShortcutAction>();
+ for (String modifier : new String[] { "^", "&", "_", "&^", "&_", "_^",
+ "&^_" }) {
+ for (String key : KEYS) {
+ ShortcutAction action = new ShortcutAction(modifier + key);
+ for (ShortcutAction other : set) {
+ assertFalse(equals(action, other));
+ }
+ set.add(action);
+ }
+ }
+ }
+
+ @Test
+ public void testModifierOrderIrrelevant() {
+ for (String key : KEYS) {
+ // two modifiers
+ for (String modifier : new String[] { "&^", "&_", "_^" }) {
+ ShortcutAction action1 = new ShortcutAction(modifier + key);
+ ShortcutAction action2 = new ShortcutAction(
+ modifier.substring(1) + modifier.substring(0, 1) + key);
+ assertTrue(modifier + key, equals(action1, action2));
+ }
+ // three modifiers
+ ShortcutAction action1 = new ShortcutAction("&^_" + key);
+ for (String modifier : new String[] { "&_^", "^&_", "^_&", "_^&",
+ "_&^" }) {
+ ShortcutAction action2 = new ShortcutAction(modifier + key);
+ assertTrue(modifier + key, equals(action1, action2));
+
+ }
+ }
+ }
+
+ @Test
+ public void testSameKeycodeDifferentCaptions() {
+ ShortcutAction act1 = new ShortcutAction("E&xit");
+ ShortcutAction act2 = new ShortcutAction("Lu&xtorpeda - Autystyczny");
+ assertFalse(equals(act1, act2));
+ }
+
+ /**
+ * A static method to allow comparison two different actions.
+ *
+ * @see DesignFormatterTest
+ *
+ * @param act
+ * One action to compare.
+ * @param other
+ * Second action to compare.
+ * @return <b>true</b> when both actions are the same (caption, icon, and
+ * key combination).
+ */
+ public static final boolean equals(ShortcutAction act, ShortcutAction other) {
+ if (SharedUtil.equals(other.getCaption(), act.getCaption())
+ && SharedUtil.equals(other.getIcon(), act.getIcon())
+ && act.getKeyCode() == other.getKeyCode()
+ && act.getModifiers().length == other.getModifiers().length) {
+ HashSet<Integer> thisSet = new HashSet<Integer>(
+ act.getModifiers().length);
+ // this is a bit tricky comparison, but there is no nice way of
+ // making int[] into a Set
+ for (int mod : act.getModifiers()) {
+ thisSet.add(mod);
+ }
+ for (int mod : other.getModifiers()) {
+ thisSet.remove(mod);
+ }
+ return thisSet.isEmpty();
+ }
+ return false;
+ }
+
+}