You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DesignAttributeHandler.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui.declarative;
  17. import java.beans.BeanInfo;
  18. import java.beans.IntrospectionException;
  19. import java.beans.Introspector;
  20. import java.beans.PropertyDescriptor;
  21. import java.io.Serializable;
  22. import java.lang.reflect.Method;
  23. import java.util.ArrayList;
  24. import java.util.Collection;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.concurrent.ConcurrentHashMap;
  28. import java.util.logging.Level;
  29. import java.util.logging.Logger;
  30. import java.util.regex.Matcher;
  31. import java.util.regex.Pattern;
  32. import org.jsoup.nodes.Attribute;
  33. import org.jsoup.nodes.Attributes;
  34. import org.jsoup.nodes.Element;
  35. import org.jsoup.nodes.Node;
  36. import com.vaadin.data.util.converter.Converter;
  37. import com.vaadin.shared.util.SharedUtil;
  38. /**
  39. * Default attribute handler implementation used when parsing designs to
  40. * component trees. Handles all the component attributes that do not require
  41. * custom handling.
  42. *
  43. * @since 7.4
  44. * @author Vaadin Ltd
  45. */
  46. public class DesignAttributeHandler implements Serializable {
  47. private static Logger getLogger() {
  48. return Logger.getLogger(DesignAttributeHandler.class.getName());
  49. }
  50. private static Map<Class<?>, AttributeCacheEntry> cache = new ConcurrentHashMap<Class<?>, AttributeCacheEntry>();
  51. // translates string <-> object
  52. private static DesignFormatter FORMATTER = new DesignFormatter();
  53. /**
  54. * Returns the currently used formatter. All primitive types and all types
  55. * needed by Vaadin components are handled by that formatter.
  56. *
  57. * @return An instance of the formatter.
  58. */
  59. public static DesignFormatter getFormatter() {
  60. return FORMATTER;
  61. }
  62. /**
  63. * Clears the children and attributes of the given element
  64. *
  65. * @param design
  66. * the element to be cleared
  67. */
  68. public static void clearElement(Element design) {
  69. Attributes attr = design.attributes();
  70. for (Attribute a : attr.asList()) {
  71. attr.remove(a.getKey());
  72. }
  73. List<Node> children = new ArrayList<Node>();
  74. children.addAll(design.childNodes());
  75. for (Node node : children) {
  76. node.remove();
  77. }
  78. }
  79. /**
  80. * Assigns the specified design attribute to the given component.
  81. *
  82. * @param target
  83. * the target to which the attribute should be set
  84. * @param attribute
  85. * the name of the attribute to be set
  86. * @param value
  87. * the string value of the attribute
  88. * @return true on success
  89. */
  90. public static boolean assignValue(Object target, String attribute,
  91. String value) {
  92. if (target == null || attribute == null || value == null) {
  93. throw new IllegalArgumentException(
  94. "Parameters with null value not allowed");
  95. }
  96. boolean success = false;
  97. try {
  98. Method setter = findSetterForAttribute(target.getClass(), attribute);
  99. if (setter == null) {
  100. // if we don't have the setter, there is no point in continuing
  101. success = false;
  102. } else {
  103. // we have a value from design attributes, let's use that
  104. Object param = getFormatter().parse(value,
  105. setter.getParameterTypes()[0]);
  106. setter.invoke(target, param);
  107. success = true;
  108. }
  109. } catch (Exception e) {
  110. getLogger().log(
  111. Level.WARNING,
  112. "Failed to set value \"" + value + "\" to attribute "
  113. + attribute, e);
  114. }
  115. if (!success) {
  116. getLogger().info(
  117. "property " + attribute
  118. + " ignored by default attribute handler");
  119. }
  120. return success;
  121. }
  122. /**
  123. * Searches for supported setter and getter types from the specified class
  124. * and returns the list of corresponding design attributes
  125. *
  126. * @param clazz
  127. * the class scanned for setters
  128. * @return the list of supported design attributes
  129. */
  130. public static Collection<String> getSupportedAttributes(Class<?> clazz) {
  131. resolveSupportedAttributes(clazz);
  132. return cache.get(clazz).getAttributes();
  133. }
  134. /**
  135. * Resolves the supported attributes and corresponding getters and setters
  136. * for the class using introspection. After resolving, the information is
  137. * cached internally by this class
  138. *
  139. * @param clazz
  140. * the class to resolve the supported attributes for
  141. */
  142. private static void resolveSupportedAttributes(Class<?> clazz) {
  143. if (clazz == null) {
  144. throw new IllegalArgumentException("The clazz can not be null");
  145. }
  146. if (cache.containsKey(clazz)) {
  147. // NO-OP
  148. return;
  149. }
  150. BeanInfo beanInfo;
  151. try {
  152. beanInfo = Introspector.getBeanInfo(clazz);
  153. } catch (IntrospectionException e) {
  154. throw new RuntimeException(
  155. "Could not get supported attributes for class "
  156. + clazz.getName());
  157. }
  158. AttributeCacheEntry entry = new AttributeCacheEntry();
  159. for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
  160. Method getter = descriptor.getReadMethod();
  161. Method setter = descriptor.getWriteMethod();
  162. if (getter != null && setter != null
  163. && getFormatter().canConvert(descriptor.getPropertyType())) {
  164. String attribute = toAttributeName(descriptor.getName());
  165. entry.addAttribute(attribute, getter, setter);
  166. }
  167. }
  168. cache.put(clazz, entry);
  169. }
  170. /**
  171. * Writes the specified attribute to the design if it differs from the
  172. * default value got from the <code> defaultInstance <code>
  173. *
  174. * @param component
  175. * the component used to get the attribute value
  176. * @param attribute
  177. * the key for the attribute
  178. * @param attr
  179. * the attribute list where the attribute will be written
  180. * @param defaultInstance
  181. * the default instance for comparing default values
  182. */
  183. @SuppressWarnings({ "unchecked", "rawtypes" })
  184. public static void writeAttribute(Object component, String attribute,
  185. Attributes attr, Object defaultInstance) {
  186. Method getter = findGetterForAttribute(component.getClass(), attribute);
  187. if (getter == null) {
  188. getLogger().warning(
  189. "Could not find getter for attribute " + attribute);
  190. } else {
  191. try {
  192. // compare the value with default value
  193. Object value = getter.invoke(component);
  194. Object defaultValue = getter.invoke(defaultInstance);
  195. writeAttribute(attribute, attr, value, defaultValue,
  196. (Class) getter.getReturnType());
  197. } catch (Exception e) {
  198. getLogger()
  199. .log(Level.SEVERE,
  200. "Failed to invoke getter for attribute "
  201. + attribute, e);
  202. }
  203. }
  204. }
  205. /**
  206. * Writes the given attribute value to a set of attributes if it differs
  207. * from the default attribute value.
  208. *
  209. * @param attribute
  210. * the attribute key
  211. * @param attributes
  212. * the set of attributes where the new attribute is written
  213. * @param value
  214. * the attribute value
  215. * @param defaultValue
  216. * the default attribute value
  217. * @param inputType
  218. * the type of the input value
  219. */
  220. public static <T> void writeAttribute(String attribute,
  221. Attributes attributes, T value, T defaultValue, Class<T> inputType) {
  222. if (!getFormatter().canConvert(inputType)) {
  223. throw new IllegalArgumentException("input type: "
  224. + inputType.getName() + " not supported");
  225. }
  226. if (!SharedUtil.equals(value, defaultValue)) {
  227. String attributeValue = toAttributeValue(inputType, value);
  228. if ("".equals(attributeValue)
  229. && (inputType == boolean.class || inputType == Boolean.class)) {
  230. attributes.put(attribute, true);
  231. } else {
  232. attributes.put(attribute, attributeValue);
  233. }
  234. }
  235. }
  236. /**
  237. * Reads the given attribute from a set of attributes. If attribute does not
  238. * exist return a given default value.
  239. *
  240. * @param attribute
  241. * the attribute key
  242. * @param attributes
  243. * the set of attributes to read from
  244. * @param defaultValue
  245. * the default value to return if attribute does not exist
  246. * @param outputType
  247. * the output type for the attribute
  248. * @return the attribute value or the default value if the attribute is not
  249. * found
  250. */
  251. public static <T> T readAttribute(String attribute, Attributes attributes,
  252. T defaultValue, Class<T> outputType) {
  253. T value = readAttribute(attribute, attributes, outputType);
  254. if (value != null) {
  255. return value;
  256. }
  257. return defaultValue;
  258. }
  259. /**
  260. * Reads the given attribute from a set of attributes.
  261. *
  262. * @param attribute
  263. * the attribute key
  264. * @param attributes
  265. * the set of attributes to read from
  266. * @param outputType
  267. * the output type for the attribute
  268. * @return the attribute value or null
  269. */
  270. public static <T> T readAttribute(String attribute, Attributes attributes,
  271. Class<T> outputType) {
  272. if (!getFormatter().canConvert(outputType)) {
  273. throw new IllegalArgumentException("output type: "
  274. + outputType.getName() + " not supported");
  275. }
  276. if (!attributes.hasKey(attribute)) {
  277. return null;
  278. } else {
  279. try {
  280. String value = attributes.get(attribute);
  281. return getFormatter().parse(value, outputType);
  282. } catch (Exception e) {
  283. throw new DesignException("Failed to read attribute "
  284. + attribute, e);
  285. }
  286. }
  287. }
  288. /**
  289. * Returns the design attribute name corresponding the given method name.
  290. * For example given a method name <code>setPrimaryStyleName</code> the
  291. * return value would be <code>primary-style-name</code>
  292. *
  293. * @param propertyName
  294. * the property name returned by {@link IntroSpector}
  295. * @return the design attribute name corresponding the given method name
  296. */
  297. private static String toAttributeName(String propertyName) {
  298. propertyName = removeSubsequentUppercase(propertyName);
  299. String[] words = propertyName.split("(?<!^)(?=[A-Z])");
  300. StringBuilder builder = new StringBuilder();
  301. for (int i = 0; i < words.length; i++) {
  302. if (builder.length() > 0) {
  303. builder.append("-");
  304. }
  305. builder.append(words[i].toLowerCase());
  306. }
  307. return builder.toString();
  308. }
  309. /**
  310. * Replaces subsequent UPPERCASE strings of length 2 or more followed either
  311. * by another uppercase letter or an end of string. This is to generalise
  312. * handling of method names like <tt>showISOWeekNumbers</tt>.
  313. *
  314. * @param param
  315. * Input string.
  316. * @return Input string with sequences of UPPERCASE turned into Normalcase.
  317. */
  318. private static String removeSubsequentUppercase(String param) {
  319. StringBuffer result = new StringBuffer();
  320. // match all two-or-more caps letters lead by a non-uppercase letter
  321. // followed by either a capital letter or string end
  322. Pattern pattern = Pattern.compile("(^|[^A-Z])([A-Z]{2,})([A-Z]|$)");
  323. Matcher matcher = pattern.matcher(param);
  324. while (matcher.find()) {
  325. String matched = matcher.group(2);
  326. // if this is a beginning of the string, the whole matched group is
  327. // written in lower case
  328. if (matcher.group(1).isEmpty()) {
  329. matcher.appendReplacement(result, matched.toLowerCase()
  330. + matcher.group(3));
  331. // otherwise the first character of the group stays uppercase,
  332. // while the others are lower case
  333. } else {
  334. matcher.appendReplacement(
  335. result,
  336. matcher.group(1) + matched.substring(0, 1)
  337. + matched.substring(1).toLowerCase()
  338. + matcher.group(3));
  339. }
  340. // in both cases the uppercase letter of the next word (or string's
  341. // end) is added
  342. // this implies there is at least one extra lowercase letter after
  343. // it to be caught by the next call to find()
  344. }
  345. matcher.appendTail(result);
  346. return result.toString();
  347. }
  348. /**
  349. * Serializes the given value to valid design attribute representation
  350. *
  351. * @param sourceType
  352. * the type of the value
  353. * @param value
  354. * the value to be serialized
  355. * @return the given value as design attribute representation
  356. */
  357. private static String toAttributeValue(Class<?> sourceType, Object value) {
  358. if (value == null) {
  359. // TODO: Handle corner case where sourceType is String and default
  360. // value is not null. How to represent null value in attributes?
  361. return "";
  362. }
  363. Converter<String, Object> converter = getFormatter().findConverterFor(
  364. sourceType);
  365. if (converter != null) {
  366. return converter.convertToPresentation(value, String.class, null);
  367. } else {
  368. return value.toString();
  369. }
  370. }
  371. /**
  372. * Returns a setter that can be used for assigning the given design
  373. * attribute to the class
  374. *
  375. * @param clazz
  376. * the class that is scanned for setters
  377. * @param attribute
  378. * the design attribute to find setter for
  379. * @return the setter method or null if not found
  380. */
  381. private static Method findSetterForAttribute(Class<?> clazz,
  382. String attribute) {
  383. resolveSupportedAttributes(clazz);
  384. return cache.get(clazz).getSetter(attribute);
  385. }
  386. /**
  387. * Returns a getter that can be used for reading the given design attribute
  388. * value from the class
  389. *
  390. * @param clazz
  391. * the class that is scanned for getters
  392. * @param attribute
  393. * the design attribute to find getter for
  394. * @return the getter method or null if not found
  395. */
  396. private static Method findGetterForAttribute(Class<?> clazz,
  397. String attribute) {
  398. resolveSupportedAttributes(clazz);
  399. return cache.get(clazz).getGetter(attribute);
  400. }
  401. /**
  402. * Cache object for caching supported attributes and their getters and
  403. * setters
  404. *
  405. * @author Vaadin Ltd
  406. */
  407. private static class AttributeCacheEntry implements Serializable {
  408. private Map<String, Method[]> accessMethods = new ConcurrentHashMap<String, Method[]>();
  409. private void addAttribute(String attribute, Method getter, Method setter) {
  410. Method[] methods = new Method[2];
  411. methods[0] = getter;
  412. methods[1] = setter;
  413. accessMethods.put(attribute, methods);
  414. }
  415. private Collection<String> getAttributes() {
  416. ArrayList<String> attributes = new ArrayList<String>();
  417. attributes.addAll(accessMethods.keySet());
  418. return attributes;
  419. }
  420. private Method getGetter(String attribute) {
  421. Method[] methods = accessMethods.get(attribute);
  422. return (methods != null && methods.length > 0) ? methods[0] : null;
  423. }
  424. private Method getSetter(String attribute) {
  425. Method[] methods = accessMethods.get(attribute);
  426. return (methods != null && methods.length > 1) ? methods[1] : null;
  427. }
  428. }
  429. }