Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

DesignAttributeHandler.java 19KB

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