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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /*
  2. * Copyright 2000-2016 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.ui.AlignmentInfo;
  38. import com.vaadin.shared.util.SharedUtil;
  39. import com.vaadin.ui.Alignment;
  40. /**
  41. * Default attribute handler implementation used when parsing designs to
  42. * component trees. Handles all the component attributes that do not require
  43. * custom handling.
  44. *
  45. * @since 7.4
  46. * @author Vaadin Ltd
  47. */
  48. public class DesignAttributeHandler implements Serializable {
  49. private static Logger getLogger() {
  50. return Logger.getLogger(DesignAttributeHandler.class.getName());
  51. }
  52. private static Map<Class<?>, AttributeCacheEntry> cache = new ConcurrentHashMap<Class<?>, AttributeCacheEntry>();
  53. // translates string <-> object
  54. private static DesignFormatter FORMATTER = new DesignFormatter();
  55. /**
  56. * Returns the currently used formatter. All primitive types and all types
  57. * needed by Vaadin components are handled by that formatter.
  58. *
  59. * @return An instance of the formatter.
  60. */
  61. public static DesignFormatter getFormatter() {
  62. return FORMATTER;
  63. }
  64. /**
  65. * Clears the children and attributes of the given element
  66. *
  67. * @param design
  68. * the element to be cleared
  69. */
  70. public static void clearElement(Element design) {
  71. Attributes attr = design.attributes();
  72. for (Attribute a : attr.asList()) {
  73. attr.remove(a.getKey());
  74. }
  75. List<Node> children = new ArrayList<Node>();
  76. children.addAll(design.childNodes());
  77. for (Node node : children) {
  78. node.remove();
  79. }
  80. }
  81. /**
  82. * Assigns the specified design attribute to the given component.
  83. *
  84. * @param target
  85. * the target to which the attribute should be set
  86. * @param attribute
  87. * the name of the attribute to be set
  88. * @param value
  89. * the string value of the attribute
  90. * @return true on success
  91. */
  92. public static boolean assignValue(Object target, String attribute,
  93. String value) {
  94. if (target == null || attribute == null || value == null) {
  95. throw new IllegalArgumentException(
  96. "Parameters with null value not allowed");
  97. }
  98. boolean success = false;
  99. try {
  100. Method setter = findSetterForAttribute(target.getClass(),
  101. attribute);
  102. if (setter == null) {
  103. // if we don't have the setter, there is no point in continuing
  104. success = false;
  105. } else {
  106. // we have a value from design attributes, let's use that
  107. Object param = getFormatter().parse(value,
  108. setter.getParameterTypes()[0]);
  109. setter.invoke(target, param);
  110. success = true;
  111. }
  112. } catch (Exception e) {
  113. getLogger().log(Level.WARNING, "Failed to set value \"" + value
  114. + "\" to attribute " + attribute, e);
  115. }
  116. if (!success) {
  117. getLogger().info("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
  160. .getPropertyDescriptors()) {
  161. Method getter = descriptor.getReadMethod();
  162. Method setter = descriptor.getWriteMethod();
  163. if (getter != null && setter != null && getFormatter()
  164. .canConvert(descriptor.getPropertyType())) {
  165. String attribute = toAttributeName(descriptor.getName());
  166. entry.addAttribute(attribute, getter, setter);
  167. }
  168. }
  169. cache.put(clazz, entry);
  170. }
  171. /**
  172. * Writes the specified attribute to the design if it differs from the
  173. * default value got from the <code> defaultInstance <code>
  174. *
  175. * @param component
  176. * the component used to get the attribute value
  177. * @param attribute
  178. * the key for the attribute
  179. * @param attr
  180. * the attribute list where the attribute will be written
  181. * @param defaultInstance
  182. * the default instance for comparing default values
  183. */
  184. @SuppressWarnings({ "unchecked", "rawtypes" })
  185. public static void writeAttribute(Object component, String attribute,
  186. Attributes attr, Object defaultInstance) {
  187. Method getter = findGetterForAttribute(component.getClass(), attribute);
  188. if (getter == null) {
  189. getLogger().warning(
  190. "Could not find getter for attribute " + attribute);
  191. } else {
  192. try {
  193. // compare the value with default value
  194. Object value = getter.invoke(component);
  195. Object defaultValue = getter.invoke(defaultInstance);
  196. writeAttribute(attribute, attr, value, defaultValue,
  197. (Class) getter.getReturnType());
  198. } catch (Exception e) {
  199. getLogger().log(Level.SEVERE,
  200. "Failed to invoke getter for attribute " + attribute,
  201. 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,
  222. Class<T> inputType) {
  223. if (!getFormatter().canConvert(inputType)) {
  224. throw new IllegalArgumentException(
  225. "input type: " + inputType.getName() + " not supported");
  226. }
  227. if (!SharedUtil.equals(value, defaultValue)) {
  228. String attributeValue = toAttributeValue(inputType, value);
  229. if ("".equals(attributeValue) && (inputType == boolean.class
  230. || inputType == Boolean.class)) {
  231. attributes.put(attribute, true);
  232. } else {
  233. attributes.put(attribute, attributeValue);
  234. }
  235. }
  236. }
  237. /**
  238. * Reads the given attribute from a set of attributes. If attribute does not
  239. * exist return a given default value.
  240. *
  241. * @param attribute
  242. * the attribute key
  243. * @param attributes
  244. * the set of attributes to read from
  245. * @param defaultValue
  246. * the default value to return if attribute does not exist
  247. * @param outputType
  248. * the output type for the attribute
  249. * @return the attribute value or the default value if the attribute is not
  250. * found
  251. */
  252. public static <T> T readAttribute(String attribute, Attributes attributes,
  253. T defaultValue, Class<T> outputType) {
  254. T value = readAttribute(attribute, attributes, outputType);
  255. if (value != null) {
  256. return value;
  257. }
  258. return defaultValue;
  259. }
  260. /**
  261. * Reads the given attribute from a set of attributes.
  262. *
  263. * @param attribute
  264. * the attribute key
  265. * @param attributes
  266. * the set of attributes to read from
  267. * @param outputType
  268. * the output type for the attribute
  269. * @return the attribute value or null
  270. */
  271. public static <T> T readAttribute(String attribute, Attributes attributes,
  272. Class<T> outputType) {
  273. if (!getFormatter().canConvert(outputType)) {
  274. throw new IllegalArgumentException(
  275. "output type: " + outputType.getName() + " not supported");
  276. }
  277. if (!attributes.hasKey(attribute)) {
  278. return null;
  279. } else {
  280. try {
  281. String value = attributes.get(attribute);
  282. return getFormatter().parse(value, outputType);
  283. } catch (Exception e) {
  284. throw new DesignException(
  285. "Failed to read attribute " + attribute, e);
  286. }
  287. }
  288. }
  289. /**
  290. * Returns the design attribute name corresponding the given method name.
  291. * For example given a method name <code>setPrimaryStyleName</code> the
  292. * return value would be <code>primary-style-name</code>
  293. *
  294. * @param propertyName
  295. * the property name returned by {@link IntroSpector}
  296. * @return the design attribute name corresponding the given method name
  297. */
  298. private static String toAttributeName(String propertyName) {
  299. propertyName = removeSubsequentUppercase(propertyName);
  300. String[] words = propertyName.split("(?<!^)(?=[A-Z])");
  301. StringBuilder builder = new StringBuilder();
  302. for (int i = 0; i < words.length; i++) {
  303. if (builder.length() > 0) {
  304. builder.append("-");
  305. }
  306. builder.append(words[i].toLowerCase());
  307. }
  308. return builder.toString();
  309. }
  310. /**
  311. * Replaces subsequent UPPERCASE strings of length 2 or more followed either
  312. * by another uppercase letter or an end of string. This is to generalise
  313. * handling of method names like <tt>showISOWeekNumbers</tt>.
  314. *
  315. * @param param
  316. * Input string.
  317. * @return Input string with sequences of UPPERCASE turned into Normalcase.
  318. */
  319. private static String removeSubsequentUppercase(String param) {
  320. StringBuffer result = new StringBuffer();
  321. // match all two-or-more caps letters lead by a non-uppercase letter
  322. // followed by either a capital letter or string end
  323. Pattern pattern = Pattern.compile("(^|[^A-Z])([A-Z]{2,})([A-Z]|$)");
  324. Matcher matcher = pattern.matcher(param);
  325. while (matcher.find()) {
  326. String matched = matcher.group(2);
  327. // if this is a beginning of the string, the whole matched group is
  328. // written in lower case
  329. if (matcher.group(1).isEmpty()) {
  330. matcher.appendReplacement(result,
  331. matched.toLowerCase() + matcher.group(3));
  332. // otherwise the first character of the group stays uppercase,
  333. // while the others are lower case
  334. } else {
  335. matcher.appendReplacement(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. @SuppressWarnings("unchecked")
  364. Converter<String, Object> converter = (Converter<String, Object>) getFormatter()
  365. .findConverterFor(sourceType);
  366. if (converter != null) {
  367. return converter.convertToPresentation(value, null);
  368. } else {
  369. return value.toString();
  370. }
  371. }
  372. /**
  373. * Returns a setter that can be used for assigning the given design
  374. * attribute to the class
  375. *
  376. * @param clazz
  377. * the class that is scanned for setters
  378. * @param attribute
  379. * the design attribute to find setter for
  380. * @return the setter method or null if not found
  381. */
  382. private static Method findSetterForAttribute(Class<?> clazz,
  383. String attribute) {
  384. resolveSupportedAttributes(clazz);
  385. return cache.get(clazz).getSetter(attribute);
  386. }
  387. /**
  388. * Returns a getter that can be used for reading the given design attribute
  389. * value from the class
  390. *
  391. * @param clazz
  392. * the class that is scanned for getters
  393. * @param attribute
  394. * the design attribute to find getter for
  395. * @return the getter method or null if not found
  396. */
  397. private static Method findGetterForAttribute(Class<?> clazz,
  398. String attribute) {
  399. resolveSupportedAttributes(clazz);
  400. return cache.get(clazz).getGetter(attribute);
  401. }
  402. /**
  403. * Cache object for caching supported attributes and their getters and
  404. * setters
  405. *
  406. * @author Vaadin Ltd
  407. */
  408. private static class AttributeCacheEntry implements Serializable {
  409. private Map<String, Method[]> accessMethods = new ConcurrentHashMap<String, Method[]>();
  410. private void addAttribute(String attribute, Method getter,
  411. Method setter) {
  412. Method[] methods = new Method[2];
  413. methods[0] = getter;
  414. methods[1] = setter;
  415. accessMethods.put(attribute, methods);
  416. }
  417. private Collection<String> getAttributes() {
  418. ArrayList<String> attributes = new ArrayList<String>();
  419. attributes.addAll(accessMethods.keySet());
  420. return attributes;
  421. }
  422. private Method getGetter(String attribute) {
  423. Method[] methods = accessMethods.get(attribute);
  424. return (methods != null && methods.length > 0) ? methods[0] : null;
  425. }
  426. private Method getSetter(String attribute) {
  427. Method[] methods = accessMethods.get(attribute);
  428. return (methods != null && methods.length > 1) ? methods[1] : null;
  429. }
  430. }
  431. /**
  432. * Read the alignment from the given child component attributes.
  433. *
  434. * @since 7.6.4
  435. * @param attr
  436. * the child component attributes
  437. * @return the component alignment
  438. */
  439. public static Alignment readAlignment(Attributes attr) {
  440. int bitMask = 0;
  441. if (attr.hasKey(":middle")) {
  442. bitMask += AlignmentInfo.Bits.ALIGNMENT_VERTICAL_CENTER;
  443. } else if (attr.hasKey(":bottom")) {
  444. bitMask += AlignmentInfo.Bits.ALIGNMENT_BOTTOM;
  445. } else {
  446. bitMask += AlignmentInfo.Bits.ALIGNMENT_TOP;
  447. }
  448. if (attr.hasKey(":center")) {
  449. bitMask += AlignmentInfo.Bits.ALIGNMENT_HORIZONTAL_CENTER;
  450. } else if (attr.hasKey(":right")) {
  451. bitMask += AlignmentInfo.Bits.ALIGNMENT_RIGHT;
  452. } else {
  453. bitMask += AlignmentInfo.Bits.ALIGNMENT_LEFT;
  454. }
  455. return new Alignment(bitMask);
  456. }
  457. /**
  458. * Writes the alignment to the given child element attributes.
  459. *
  460. * @since 7.6.4
  461. * @param childElement
  462. * the child element
  463. * @param alignment
  464. * the component alignment
  465. */
  466. public static void writeAlignment(Element childElement,
  467. Alignment alignment) {
  468. if (alignment.isMiddle()) {
  469. childElement.attr(":middle", true);
  470. } else if (alignment.isBottom()) {
  471. childElement.attr(":bottom", true);
  472. }
  473. if (alignment.isCenter()) {
  474. childElement.attr(":center", true);
  475. } else if (alignment.isRight()) {
  476. childElement.attr(":right", true);
  477. }
  478. }
  479. }