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.

DesignContext.java 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  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.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.concurrent.ConcurrentHashMap;
  23. import org.jsoup.nodes.Attributes;
  24. import org.jsoup.nodes.Document;
  25. import org.jsoup.nodes.Element;
  26. import org.jsoup.nodes.Node;
  27. import com.vaadin.annotations.DesignRoot;
  28. import com.vaadin.shared.util.SharedUtil;
  29. import com.vaadin.ui.Component;
  30. import com.vaadin.ui.HasComponents;
  31. import com.vaadin.ui.declarative.Design.ComponentFactory;
  32. /**
  33. * This class contains contextual information that is collected when a component
  34. * tree is constructed based on HTML design template. This information includes
  35. * mappings from local ids, global ids and captions to components , as well as a
  36. * mapping between prefixes and package names (such as "v" -> "com.vaadin.ui").
  37. *
  38. * @since 7.4
  39. * @author Vaadin Ltd
  40. */
  41. public class DesignContext implements Serializable {
  42. // cache for object instances
  43. private static Map<Class<?>, Component> instanceCache = new ConcurrentHashMap<Class<?>, Component>();
  44. // The root component of the component hierarchy
  45. private Component rootComponent = null;
  46. // Attribute names for global id and caption and the prefix name for a local
  47. // id
  48. public static final String ID_ATTRIBUTE = "id";
  49. public static final String CAPTION_ATTRIBUTE = "caption";
  50. public static final String LOCAL_ID_ATTRIBUTE = "_id";
  51. // Mappings from ids to components. Modified when reading from design.
  52. private Map<String, Component> idToComponent = new HashMap<String, Component>();
  53. private Map<String, Component> localIdToComponent = new HashMap<String, Component>();
  54. private Map<String, Component> captionToComponent = new HashMap<String, Component>();
  55. // Mapping from components to local ids. Accessed when writing to
  56. // design. Modified when reading from design.
  57. private Map<Component, String> componentToLocalId = new HashMap<Component, String>();
  58. private Document doc; // required for calling createElement(String)
  59. // namespace mappings
  60. private Map<String, String> packageToPrefix = new HashMap<String, String>();
  61. private Map<String, String> prefixToPackage = new HashMap<String, String>();
  62. // prefix names for which no package-mapping element will be created in the
  63. // html tree (this includes at least "v" which is always taken to refer
  64. // to "com.vaadin.ui".
  65. private Map<String, String> defaultPrefixes = new HashMap<String, String>();
  66. // component creation listeners
  67. private List<ComponentCreationListener> listeners = new ArrayList<ComponentCreationListener>();
  68. public DesignContext(Document doc) {
  69. this.doc = doc;
  70. // Initialize the mapping between prefixes and package names.
  71. defaultPrefixes.put("v", "com.vaadin.ui");
  72. for (String prefix : defaultPrefixes.keySet()) {
  73. String packageName = defaultPrefixes.get(prefix);
  74. mapPrefixToPackage(prefix, packageName);
  75. }
  76. }
  77. public DesignContext() {
  78. this(new Document(""));
  79. }
  80. /**
  81. * Returns a component having the specified local id. If no component is
  82. * found, returns null.
  83. *
  84. * @param localId
  85. * The local id of the component
  86. * @return a component whose local id equals localId
  87. */
  88. public Component getComponentByLocalId(String localId) {
  89. return localIdToComponent.get(localId);
  90. }
  91. /**
  92. * Returns a component having the specified global id. If no component is
  93. * found, returns null.
  94. *
  95. * @param globalId
  96. * The global id of the component
  97. * @return a component whose global id equals globalId
  98. */
  99. public Component getComponentById(String globalId) {
  100. return idToComponent.get(globalId);
  101. }
  102. /**
  103. * Returns a component having the specified caption. If no component is
  104. * found, returns null.
  105. *
  106. * @param caption
  107. * The caption of the component
  108. * @return a component whose caption equals the caption given as a parameter
  109. */
  110. public Component getComponentByCaption(String caption) {
  111. return captionToComponent.get(caption);
  112. }
  113. /**
  114. * Creates a mapping between the given global id and the component. Returns
  115. * true if globalId was already mapped to some component. Otherwise returns
  116. * false. Also sets the id of the component to globalId.
  117. *
  118. * If there is a mapping from the component to a global id (gid) different
  119. * from globalId, the mapping from gid to component is removed.
  120. *
  121. * If the string was mapped to a component c different from the given
  122. * component, the mapping from c to the string is removed. Similarly, if
  123. * component was mapped to some string s different from globalId, the
  124. * mapping from s to component is removed.
  125. *
  126. * @param globalId
  127. * The new global id of the component.
  128. * @param component
  129. * The component whose global id is to be set.
  130. * @return true, if there already was a global id mapping from the string to
  131. * some component.
  132. */
  133. private boolean mapId(String globalId, Component component) {
  134. Component oldComponent = idToComponent.get(globalId);
  135. if (oldComponent != null && !oldComponent.equals(component)) {
  136. oldComponent.setId(null);
  137. }
  138. String oldGID = component.getId();
  139. if (oldGID != null && !oldGID.equals(globalId)) {
  140. idToComponent.remove(oldGID);
  141. }
  142. component.setId(globalId);
  143. idToComponent.put(globalId, component);
  144. return oldComponent != null && !oldComponent.equals(component);
  145. }
  146. /**
  147. * Creates a mapping between the given local id and the component. Returns
  148. * true if localId was already mapped to some component or if component was
  149. * mapped to some string. Otherwise returns false.
  150. *
  151. * If the string was mapped to a component c different from the given
  152. * component, the mapping from c to the string is removed. Similarly, if
  153. * component was mapped to some string s different from localId, the mapping
  154. * from s to component is removed.
  155. *
  156. * @param localId
  157. * The new local id of the component.
  158. * @param component
  159. * The component whose local id is to be set.
  160. * @return true, if there already was a local id mapping from the string to
  161. * some component or from the component to some string. Otherwise
  162. * returns false.
  163. */
  164. private boolean mapLocalId(String localId, Component component) {
  165. return twoWayMap(localId, component, localIdToComponent,
  166. componentToLocalId);
  167. }
  168. /**
  169. * Creates a mapping between the given caption and the component. Returns
  170. * true if caption was already mapped to some component.
  171. *
  172. * Note that unlike mapGlobalId, if some component already has the given
  173. * caption, the caption is not cleared from the component. This allows
  174. * non-unique captions. However, only one of the components corresponding to
  175. * a given caption can be found using the map captionToComponent. Hence, any
  176. * captions that are used to identify an object should be unique.
  177. *
  178. * @param caption
  179. * The new caption of the component.
  180. * @param component
  181. * The component whose caption is to be set.
  182. * @return true, if there already was a caption mapping from the string to
  183. * some component.
  184. */
  185. private boolean mapCaption(String caption, Component component) {
  186. return captionToComponent.put(caption, component) != null;
  187. }
  188. /**
  189. * Creates a two-way mapping between key and value, i.e. adds key -> value
  190. * to keyToValue and value -> key to valueToKey. If key was mapped to a
  191. * value v different from the given value, the mapping from v to key is
  192. * removed. Similarly, if value was mapped to some key k different from key,
  193. * the mapping from k to value is removed.
  194. *
  195. * Returns true if there already was a mapping from key to some value v or
  196. * if there was a mapping from value to some key k. Otherwise returns false.
  197. *
  198. * @param key
  199. * The new key in keyToValue.
  200. * @param value
  201. * The new value in keyToValue.
  202. * @param keyToValue
  203. * A map from keys to values.
  204. * @param valueToKey
  205. * A map from values to keys.
  206. * @return whether there already was some mapping from key to a value or
  207. * from value to a key.
  208. */
  209. private <S, T> boolean twoWayMap(S key, T value, Map<S, T> keyToValue,
  210. Map<T, S> valueToKey) {
  211. T oldValue = keyToValue.put(key, value);
  212. if (oldValue != null && !oldValue.equals(value)) {
  213. valueToKey.remove(oldValue);
  214. }
  215. S oldKey = valueToKey.put(value, key);
  216. if (oldKey != null && !oldKey.equals(key)) {
  217. keyToValue.remove(oldKey);
  218. }
  219. return oldValue != null || oldKey != null;
  220. }
  221. /**
  222. * Creates a two-way mapping between a prefix and a package name. Return
  223. * true if prefix was already mapped to some package name or packageName to
  224. * some prefix.
  225. *
  226. * @param prefix
  227. * the prefix name without an ending dash (for instance, "v" is
  228. * always used for "com.vaadin.ui")
  229. * @param packageName
  230. * the name of the package corresponding to prefix
  231. * @return whether there was a mapping from prefix to some package name or
  232. * from packageName to some prefix.
  233. */
  234. private boolean mapPrefixToPackage(String prefix, String packageName) {
  235. return twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix);
  236. }
  237. /**
  238. * Returns the default instance for the given class. The instance must not
  239. * be modified by the caller.
  240. *
  241. * @param abstractComponent
  242. * @return the default instance for the given class. The return value must
  243. * not be modified by the caller
  244. */
  245. public <T> T getDefaultInstance(Component component) {
  246. // If the root is a @DesignRoot component, it can't use itself as a
  247. // reference or the written design will be empty
  248. // If the root component in some other way initializes itself in the
  249. // constructor
  250. if (getRootComponent() == component
  251. && component.getClass().isAnnotationPresent(DesignRoot.class)) {
  252. return (T) getDefaultInstance((Class<? extends Component>) component
  253. .getClass().getSuperclass());
  254. }
  255. return (T) getDefaultInstance(component.getClass());
  256. }
  257. private Component getDefaultInstance(
  258. Class<? extends Component> componentClass) {
  259. Component instance = instanceCache.get(componentClass);
  260. if (instance == null) {
  261. instance = instantiateClass(componentClass.getName());
  262. instanceCache.put(componentClass, instance);
  263. }
  264. return instance;
  265. }
  266. /**
  267. * Reads and stores the mappings from prefixes to package names from meta
  268. * tags located under <head> in the html document.
  269. */
  270. protected void readPackageMappings(Document doc) {
  271. Element head = doc.head();
  272. if (head == null) {
  273. return;
  274. }
  275. for (Node child : head.childNodes()) {
  276. if (child instanceof Element) {
  277. Element childElement = (Element) child;
  278. if ("meta".equals(childElement.tagName())) {
  279. Attributes attributes = childElement.attributes();
  280. if (attributes.hasKey("name")
  281. && attributes.hasKey("content")
  282. && "package-mapping".equals(attributes.get("name"))) {
  283. String contentString = attributes.get("content");
  284. String[] parts = contentString.split(":");
  285. if (parts.length != 2) {
  286. throw new DesignException("The meta tag '"
  287. + child.toString() + "' cannot be parsed.");
  288. }
  289. String prefixName = parts[0];
  290. String packageName = parts[1];
  291. twoWayMap(prefixName, packageName, prefixToPackage,
  292. packageToPrefix);
  293. }
  294. }
  295. }
  296. }
  297. }
  298. /**
  299. * Writes the package mappings (prefix -> package name) of this object to
  300. * the specified document.
  301. * <p>
  302. * The prefixes are stored as <meta> tags under <head> in the document.
  303. *
  304. * @param doc
  305. * the Jsoup document tree where the package mappings are written
  306. */
  307. public void writePackageMappings(Document doc) {
  308. Element head = doc.head();
  309. for (String prefix : prefixToPackage.keySet()) {
  310. // Only store the prefix-name mapping if it is not a default mapping
  311. // (such as "v" -> "com.vaadin.ui")
  312. if (defaultPrefixes.get(prefix) == null) {
  313. Node newNode = doc.createElement("meta");
  314. newNode.attr("name", "package-mapping");
  315. String prefixToPackageName = prefix + ":"
  316. + prefixToPackage.get(prefix);
  317. newNode.attr("content", prefixToPackageName);
  318. head.appendChild(newNode);
  319. }
  320. }
  321. }
  322. /**
  323. * Creates an html tree node corresponding to the given element. Also
  324. * initializes its attributes by calling writeDesign. As a result of the
  325. * writeDesign() call, this method creates the entire subtree rooted at the
  326. * returned Node.
  327. *
  328. * @param childComponent
  329. * The component with state that is written in to the node
  330. * @return An html tree node corresponding to the given component. The tag
  331. * name of the created node is derived from the class name of
  332. * childComponent.
  333. */
  334. public Element createElement(Component childComponent) {
  335. Class<?> componentClass = childComponent.getClass();
  336. String packageName = componentClass.getPackage().getName();
  337. String prefix = packageToPrefix.get(packageName);
  338. if (prefix == null) {
  339. prefix = packageName.replace('.', '_');
  340. twoWayMap(prefix, packageName, prefixToPackage, packageToPrefix);
  341. }
  342. prefix = prefix + "-";
  343. String className = classNameToElementName(componentClass
  344. .getSimpleName());
  345. Element newElement = doc.createElement(prefix + className);
  346. childComponent.writeDesign(newElement, this);
  347. // Handle the local id. Global id and caption should have been taken
  348. // care of by writeDesign.
  349. String localId = componentToLocalId.get(childComponent);
  350. if (localId != null) {
  351. newElement.attr(LOCAL_ID_ATTRIBUTE, localId);
  352. }
  353. return newElement;
  354. }
  355. /**
  356. * Creates the name of the html tag corresponding to the given class name.
  357. * The name is derived by converting each uppercase letter to lowercase and
  358. * inserting a dash before the letter. No dash is inserted before the first
  359. * letter of the class name.
  360. *
  361. * @param className
  362. * the name of the class without a package name
  363. * @return the html tag name corresponding to className
  364. */
  365. private String classNameToElementName(String className) {
  366. StringBuilder result = new StringBuilder();
  367. for (int i = 0; i < className.length(); i++) {
  368. Character c = className.charAt(i);
  369. if (Character.isUpperCase(c)) {
  370. if (i > 0) {
  371. result.append("-");
  372. }
  373. result.append(Character.toLowerCase(c));
  374. } else {
  375. result.append(c);
  376. }
  377. }
  378. return result.toString();
  379. }
  380. /**
  381. * Reads the given design node and creates the corresponding component tree
  382. *
  383. * @param componentDesign
  384. * The design element containing the description of the component
  385. * to be created.
  386. * @return the root component of component tree
  387. */
  388. public Component readDesign(Element componentDesign) {
  389. // Create the component.
  390. Component component = instantiateComponent(componentDesign);
  391. readDesign(componentDesign, component);
  392. fireComponentCreatedEvent(componentToLocalId.get(component), component);
  393. return component;
  394. }
  395. /**
  396. *
  397. * Reads the given design node and populates the given component with the
  398. * corresponding component tree
  399. * <p>
  400. * Additionally registers the component id, local id and caption of the
  401. * given component and all its children in the context
  402. *
  403. * @param componentDesign
  404. * The design element containing the description of the component
  405. * to be created
  406. * @param component
  407. * The component which corresponds to the design element
  408. */
  409. public void readDesign(Element componentDesign, Component component) {
  410. component.readDesign(componentDesign, this);
  411. // Get the ids and the caption of the component and store them in the
  412. // maps of this design context.
  413. org.jsoup.nodes.Attributes attributes = componentDesign.attributes();
  414. // global id: only update the mapping, the id has already been set for
  415. // the component
  416. String id = component.getId();
  417. if (id != null && id.length() > 0) {
  418. boolean mappingExists = mapId(id, component);
  419. if (mappingExists) {
  420. throw new DesignException(
  421. "The following global id is not unique: " + id);
  422. }
  423. }
  424. // local id: this is not a property of a component, so need to fetch it
  425. // from the attributes of componentDesign
  426. if (attributes.hasKey(LOCAL_ID_ATTRIBUTE)) {
  427. String localId = attributes.get(LOCAL_ID_ATTRIBUTE);
  428. boolean mappingExists = mapLocalId(localId, component);
  429. if (mappingExists) {
  430. throw new DesignException(
  431. "the following local id is not unique: " + localId);
  432. }
  433. }
  434. // caption: a property of a component, possibly not unique
  435. String caption = component.getCaption();
  436. if (caption != null) {
  437. mapCaption(caption, component);
  438. }
  439. }
  440. /**
  441. * Creates a Component corresponding to the given node. Does not set the
  442. * attributes for the created object.
  443. *
  444. * @param node
  445. * a node of an html tree
  446. * @return a Component corresponding to node, with no attributes set.
  447. */
  448. private Component instantiateComponent(Node node) {
  449. // Extract the package and class names.
  450. String qualifiedClassName = tagNameToClassName(node);
  451. return instantiateClass(qualifiedClassName);
  452. }
  453. /**
  454. * Instantiates given class via ComponentFactory.
  455. * @param qualifiedClassName class name to instantiate
  456. * @return instance of a given class
  457. */
  458. private Component instantiateClass(String qualifiedClassName) {
  459. ComponentFactory factory = Design.getComponentFactory();
  460. Component component = factory.createComponent(qualifiedClassName, this);
  461. if (component == null) {
  462. throw new DesignException("Got unexpected null component from "
  463. + factory.getClass().getName() + " for class "
  464. + qualifiedClassName);
  465. }
  466. return component;
  467. }
  468. /**
  469. * Returns the qualified class name corresponding to the given html tree
  470. * node. The class name is extracted from the tag name of node.
  471. *
  472. * @param node
  473. * an html tree node
  474. * @return The qualified class name corresponding to the given node.
  475. */
  476. private String tagNameToClassName(Node node) {
  477. String tagName = node.nodeName();
  478. if (tagName.equals("v-addon")) {
  479. return node.attr("class");
  480. }
  481. // Otherwise, get the full class name using the prefix to package
  482. // mapping. Example: "v-vertical-layout" ->
  483. // "com.vaadin.ui.VerticalLayout"
  484. String[] parts = tagName.split("-", 2);
  485. if (parts.length < 2) {
  486. throw new DesignException("The tagname '" + tagName
  487. + "' is invalid: missing prefix.");
  488. }
  489. String prefixName = parts[0];
  490. String packageName = prefixToPackage.get(prefixName);
  491. if (packageName == null) {
  492. throw new DesignException("Unknown tag: " + tagName);
  493. }
  494. String[] classNameParts = parts[1].split("-");
  495. String className = "";
  496. for (String classNamePart : classNameParts) {
  497. // Split will ignore trailing and multiple dashes but that should be
  498. // ok
  499. // <v-button--> will be resolved to <v-button>
  500. // <v--button> will be resolved to <v-button>
  501. className += SharedUtil.capitalize(classNamePart);
  502. }
  503. return packageName + "." + className;
  504. }
  505. /**
  506. * Returns the root component of a created component hierarchy.
  507. *
  508. * @return the root component of the hierarchy
  509. */
  510. public Component getRootComponent() {
  511. return rootComponent;
  512. }
  513. /**
  514. * Sets the root component of a created component hierarchy.
  515. *
  516. * @param rootComponent
  517. * the root component of the hierarchy
  518. */
  519. public void setRootComponent(Component rootComponent) {
  520. this.rootComponent = rootComponent;
  521. }
  522. /**
  523. * Adds a component creation listener. The listener will be notified when
  524. * components are created while parsing a design template
  525. *
  526. * @param listener
  527. * the component creation listener to be added
  528. */
  529. public void addComponentCreationListener(ComponentCreationListener listener) {
  530. listeners.add(listener);
  531. }
  532. /**
  533. * Removes a component creation listener.
  534. *
  535. * @param listener
  536. * the component creation listener to be removed
  537. */
  538. public void removeComponentCreationListener(
  539. ComponentCreationListener listener) {
  540. listeners.remove(listener);
  541. }
  542. /**
  543. * Fires component creation event
  544. *
  545. * @param localId
  546. * localId of the component
  547. * @param component
  548. * the component that was created
  549. */
  550. private void fireComponentCreatedEvent(String localId, Component component) {
  551. ComponentCreatedEvent event = new ComponentCreatedEvent(localId,
  552. component);
  553. for (ComponentCreationListener listener : listeners) {
  554. listener.componentCreated(event);
  555. }
  556. }
  557. /**
  558. * Interface to be implemented by component creation listeners
  559. *
  560. * @author Vaadin Ltd
  561. */
  562. public interface ComponentCreationListener extends Serializable {
  563. /**
  564. * Called when component has been created in the design context
  565. *
  566. * @param event
  567. * the component creation event containing information on the
  568. * created component
  569. */
  570. public void componentCreated(ComponentCreatedEvent event);
  571. }
  572. /**
  573. * Component creation event that is fired when a component is created in the
  574. * context
  575. *
  576. * @author Vaadin Ltd
  577. */
  578. public class ComponentCreatedEvent implements Serializable {
  579. private String localId;
  580. private Component component;
  581. private DesignContext context;
  582. /**
  583. * Creates a new instance of ComponentCreatedEvent
  584. *
  585. * @param localId
  586. * the local id of the created component
  587. * @param component
  588. * the created component
  589. */
  590. private ComponentCreatedEvent(String localId, Component component) {
  591. this.localId = localId;
  592. this.component = component;
  593. context = DesignContext.this;
  594. }
  595. /**
  596. * Returns the local id of the created component or null if not exist
  597. *
  598. * @return the localId
  599. */
  600. public String getLocalId() {
  601. return localId;
  602. }
  603. /**
  604. * Returns the created component
  605. *
  606. * @return the component
  607. */
  608. public Component getComponent() {
  609. return component;
  610. }
  611. }
  612. /**
  613. * Helper method for component write implementors to determine whether their
  614. * children should be written out or not
  615. *
  616. * @param c
  617. * The component being written
  618. * @param defaultC
  619. * The default instance for the component
  620. * @return whether the children of c should be written
  621. */
  622. public boolean shouldWriteChildren(Component c, Component defaultC) {
  623. if (c == getRootComponent()) {
  624. // The root component should always write its children - otherwise
  625. // the result is empty
  626. return true;
  627. }
  628. if (defaultC instanceof HasComponents
  629. && ((HasComponents) defaultC).iterator().hasNext()) {
  630. // Easy version which assumes that this is a custom component if the
  631. // constructor adds children
  632. return false;
  633. }
  634. return true;
  635. }
  636. }