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.

Design.java 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /*
  2. * Copyright 2000-2021 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 static java.nio.charset.StandardCharsets.UTF_8;
  18. import java.beans.IntrospectionException;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.OutputStream;
  22. import java.io.Serializable;
  23. import java.lang.annotation.Annotation;
  24. import java.util.Collection;
  25. import java.util.Locale;
  26. import java.util.logging.Level;
  27. import java.util.logging.Logger;
  28. import org.jsoup.Jsoup;
  29. import org.jsoup.nodes.Document;
  30. import org.jsoup.nodes.Document.OutputSettings.Syntax;
  31. import org.jsoup.nodes.DocumentType;
  32. import org.jsoup.nodes.Element;
  33. import org.jsoup.nodes.Node;
  34. import org.jsoup.parser.Parser;
  35. import org.jsoup.select.Elements;
  36. import com.vaadin.annotations.DesignRoot;
  37. import com.vaadin.server.VaadinServiceClassLoaderUtil;
  38. import com.vaadin.shared.util.SharedUtil;
  39. import com.vaadin.ui.Component;
  40. import com.vaadin.ui.ComponentRootSetter;
  41. import com.vaadin.ui.Composite;
  42. import com.vaadin.ui.CustomComponent;
  43. import com.vaadin.ui.declarative.DesignContext.ComponentCreatedEvent;
  44. import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener;
  45. import com.vaadin.util.ReflectTools;
  46. /**
  47. * Design is used for reading a component hierarchy from an html string or input
  48. * stream and, conversely, for writing an html representation corresponding to a
  49. * given component hierarchy.
  50. *
  51. * <p>
  52. * In html form a valid nonempty component hierarchy contains a single root
  53. * element located under the &lt;body&gt; tag. A hierarchy of components is
  54. * achieved by nesting other elements under the root element. An empty component
  55. * hierarchy is represented as no elements under the &lt;body&gt; tag.
  56. *
  57. * <p>
  58. * For writing a component hierarchy the root element is specified as a
  59. * Component parameter or as a DesignContext object containing the root
  60. * Component. An empty hierarchy can be written by giving a null root Component.
  61. *
  62. * @since 7.4
  63. * @author Vaadin Ltd
  64. */
  65. public class Design implements Serializable {
  66. /**
  67. * Callback for creating instances of a given component class when reading
  68. * designs. The default implementation, {@link DefaultComponentFactory} will
  69. * use <code>Class.forName(className).newInstance()</code>, which might not
  70. * be suitable e.g. in an OSGi environment or if the Component instances
  71. * should be created as managed CDI beans.
  72. * <p>
  73. * Use {@link Design#setComponentFactory(ComponentFactory)} to configure
  74. * Vaadin to use a custom component factory.
  75. *
  76. * @since 7.4.1
  77. */
  78. public interface ComponentFactory extends Serializable {
  79. /**
  80. * Creates a component based on the fully qualified name derived from
  81. * the tag name in the design.
  82. *
  83. * @param fullyQualifiedClassName
  84. * the fully qualified name of the component to create
  85. * @param context
  86. * the design context for which the component is created
  87. *
  88. * @return a newly created component
  89. */
  90. public Component createComponent(String fullyQualifiedClassName,
  91. DesignContext context);
  92. }
  93. /**
  94. * Delegate for handling the mapping between tag names and component
  95. * instances.
  96. * <p>
  97. * Use {@link Design#setComponentMapper(ComponentMapper)} to configure
  98. * Vaadin to use a custom component mapper.
  99. *
  100. * @since 7.5.0
  101. * @author Vaadin Ltd
  102. */
  103. public interface ComponentMapper extends Serializable {
  104. /**
  105. * Resolves and creates a component using the provided component factory
  106. * based on a tag name.
  107. * <p>
  108. * This method should be in sync with
  109. * {@link #componentToTag(Component, DesignContext)} so that the
  110. * resolved tag for a created component is the same as the tag for which
  111. * the component was created.
  112. *
  113. * @param tag
  114. * the tag name to create a component for
  115. * @param componentFactory
  116. * the component factory that actually creates a component
  117. * based on a fully qualified class name
  118. * @param context
  119. * the design context for which the component is created
  120. * @return a newly created component
  121. */
  122. public Component tagToComponent(String tag,
  123. ComponentFactory componentFactory, DesignContext context);
  124. /**
  125. * Resolves a tag name from a component.
  126. *
  127. * @param component
  128. * the component to get a tag name for
  129. * @param context
  130. * the design context for which the tag name is needed
  131. * @return the tag name corresponding to the component
  132. */
  133. public String componentToTag(Component component,
  134. DesignContext context);
  135. }
  136. /**
  137. * Default implementation of {@link ComponentFactory}, using
  138. * <code>Class.forName(className).newInstance()</code> for finding the
  139. * component class and creating a component instance.
  140. *
  141. * @since 7.4.1
  142. */
  143. public static class DefaultComponentFactory implements ComponentFactory {
  144. @Override
  145. public Component createComponent(String fullyQualifiedClassName,
  146. DesignContext context) {
  147. Class<? extends Component> componentClass;
  148. try {
  149. componentClass = resolveComponentClass(fullyQualifiedClassName,
  150. context);
  151. } catch (DesignException e) {
  152. // Try with an inner class.
  153. int lastDot = fullyQualifiedClassName.lastIndexOf('.');
  154. if (lastDot != -1) {
  155. String qualifiedInnerClassName = fullyQualifiedClassName
  156. .substring(0, lastDot) + "$"
  157. + fullyQualifiedClassName.substring(lastDot + 1);
  158. return createComponent(qualifiedInnerClassName, context);
  159. } else {
  160. throw e;
  161. }
  162. }
  163. assert Component.class.isAssignableFrom(
  164. componentClass) : "resolveComponentClass returned "
  165. + componentClass
  166. + " which is not a Vaadin Component class";
  167. try {
  168. return ReflectTools.createInstance(componentClass);
  169. } catch (Exception e) {
  170. throw new DesignException(
  171. "Could not create component " + fullyQualifiedClassName,
  172. e);
  173. }
  174. }
  175. /**
  176. * Resolves a component class based on the fully qualified name of the
  177. * class.
  178. *
  179. * @param qualifiedClassName
  180. * the fully qualified name of the resolved class
  181. * @param context
  182. * the design context for which the class is resolved
  183. * @return a component class object representing the provided class name
  184. */
  185. protected Class<? extends Component> resolveComponentClass(
  186. String qualifiedClassName, DesignContext context) {
  187. try {
  188. Class<?> componentClass = Class.forName(qualifiedClassName,
  189. true,
  190. VaadinServiceClassLoaderUtil.findDefaultClassLoader());
  191. return componentClass.asSubclass(Component.class);
  192. } catch (ClassNotFoundException e) {
  193. throw new DesignException("Unable to load component for design",
  194. e);
  195. }
  196. }
  197. }
  198. /**
  199. * Default implementation of {@link ComponentMapper}.
  200. *
  201. * @since 7.5.0
  202. */
  203. public static class DefaultComponentMapper implements ComponentMapper {
  204. @Override
  205. public Component tagToComponent(String tagName,
  206. ComponentFactory componentFactory, DesignContext context) {
  207. // Extract the package and class names.
  208. // Otherwise, get the full class name using the prefix to package
  209. // mapping. Example: "vaadin-vertical-layout" ->
  210. // "com.vaadin.ui.VerticalLayout"
  211. String[] parts = tagName.split("-", 2);
  212. if (parts.length < 2) {
  213. throw new DesignException("The tagname '" + tagName
  214. + "' is invalid: missing prefix.");
  215. }
  216. String prefixName = parts[0];
  217. String packageName = context.getPackage(prefixName);
  218. if (packageName == null) {
  219. throw new DesignException("Unknown tag: " + tagName);
  220. }
  221. String[] classNameParts = parts[1].split("-");
  222. StringBuilder className = new StringBuilder();
  223. for (String classNamePart : classNameParts) {
  224. // Split will ignore trailing and multiple dashes but that
  225. // should be
  226. // ok
  227. // <vaadin-button--> will be resolved to <vaadin-button>
  228. // <vaadin--button> will be resolved to <vaadin-button>
  229. className.append(SharedUtil.capitalize(classNamePart));
  230. }
  231. String qualifiedClassName = packageName + "." + className;
  232. Component component = componentFactory
  233. .createComponent(qualifiedClassName, context);
  234. if (component == null) {
  235. throw new DesignException("Got unexpected null component from "
  236. + componentFactory.getClass().getName() + " for class "
  237. + qualifiedClassName);
  238. }
  239. return component;
  240. }
  241. @Override
  242. public String componentToTag(Component component,
  243. DesignContext context) {
  244. Class<?> componentClass = component.getClass();
  245. String packageName = getPackageName(componentClass);
  246. String prefix = context.getPackagePrefix(packageName);
  247. if (prefix == null) {
  248. prefix = packageName.replace('.', '_').toLowerCase(Locale.ROOT);
  249. context.addPackagePrefix(prefix, packageName);
  250. }
  251. prefix += "-";
  252. String className = classNameToElementName(
  253. componentClass.getSimpleName());
  254. String tagName = prefix + className;
  255. return tagName;
  256. }
  257. private String getPackageName(Class<?> componentClass) {
  258. if (componentClass.isMemberClass()) {
  259. Class<?> enclosingClass = componentClass.getEnclosingClass();
  260. return getPackageName(enclosingClass) + "."
  261. + enclosingClass.getSimpleName();
  262. } else {
  263. return componentClass.getPackage().getName();
  264. }
  265. }
  266. /**
  267. * Creates the name of the html tag corresponding to the given class
  268. * name. The name is derived by converting each uppercase letter to
  269. * lowercase and inserting a dash before the letter. No dash is inserted
  270. * before the first letter of the class name.
  271. *
  272. * @param className
  273. * the name of the class without a package name
  274. * @return the html tag name corresponding to className
  275. */
  276. private String classNameToElementName(String className) {
  277. StringBuilder result = new StringBuilder();
  278. for (int i = 0; i < className.length(); i++) {
  279. Character c = className.charAt(i);
  280. if (Character.isUpperCase(c)) {
  281. if (i > 0) {
  282. result.append('-');
  283. }
  284. result.append(Character.toLowerCase(c));
  285. } else {
  286. result.append(c);
  287. }
  288. }
  289. return result.toString();
  290. }
  291. }
  292. private static volatile ComponentFactory componentFactory = new DefaultComponentFactory();
  293. private static volatile ComponentMapper componentMapper = new DefaultComponentMapper();
  294. /**
  295. * Sets the component factory that is used for creating component instances
  296. * based on fully qualified class names derived from a design file.
  297. * <p>
  298. * Please note that this setting is global, so care should be taken to avoid
  299. * conflicting changes.
  300. *
  301. * @param componentFactory
  302. * the component factory to set; not <code>null</code>
  303. *
  304. * @since 7.4.1
  305. */
  306. public static void setComponentFactory(ComponentFactory componentFactory) {
  307. if (componentFactory == null) {
  308. throw new IllegalArgumentException(
  309. "Cannot set null component factory");
  310. }
  311. Design.componentFactory = componentFactory;
  312. }
  313. /**
  314. * Gets the currently used component factory.
  315. *
  316. * @see #setComponentFactory(ComponentFactory)
  317. *
  318. * @return the component factory
  319. *
  320. * @since 7.4.1
  321. */
  322. public static ComponentFactory getComponentFactory() {
  323. return componentFactory;
  324. }
  325. /**
  326. * Sets the component mapper that is used for resolving between tag names
  327. * and component instances.
  328. * <p>
  329. * Please note that this setting is global, so care should be taken to avoid
  330. * conflicting changes.
  331. *
  332. * @param componentMapper
  333. * the component mapper to set; not <code>null</code>
  334. *
  335. * @since 7.5.0
  336. */
  337. public static void setComponentMapper(ComponentMapper componentMapper) {
  338. if (componentMapper == null) {
  339. throw new IllegalArgumentException(
  340. "Cannot set null component mapper");
  341. }
  342. Design.componentMapper = componentMapper;
  343. }
  344. /**
  345. * Gets the currently used component mapper.
  346. *
  347. * @see #setComponentMapper(ComponentMapper)
  348. *
  349. * @return the component mapper
  350. *
  351. * @since 7.5.0
  352. */
  353. public static ComponentMapper getComponentMapper() {
  354. return componentMapper;
  355. }
  356. /**
  357. * Parses the given input stream into a jsoup document
  358. *
  359. * @param html
  360. * the stream containing the design
  361. * @return the parsed jsoup document
  362. * @throws IOException
  363. */
  364. private static Document parse(InputStream html) {
  365. try {
  366. Document doc = Jsoup.parse(html, UTF_8.name(), "",
  367. Parser.htmlParser());
  368. return doc;
  369. } catch (IOException e) {
  370. throw new DesignException("The html document cannot be parsed.");
  371. }
  372. }
  373. /**
  374. * Constructs a component hierarchy from the design specified as an html
  375. * tree.
  376. *
  377. * <p>
  378. * If a component root is given, the component instances created during
  379. * reading the design are assigned to its member fields based on their id,
  380. * local id, and caption
  381. *
  382. * @param doc
  383. * the html tree
  384. * @param componentRoot
  385. * optional component root instance. The type must match the type
  386. * of the root element in the design or be a
  387. * {@link CustomComponent} or {@link Composite}, in which case
  388. * the root component will be set as the composition root. Any
  389. * member fields whose type is assignable from {@link Component}
  390. * are bound to fields in the design based on id/local id/caption
  391. */
  392. private static DesignContext designToComponentTree(Document doc,
  393. Component componentRoot) {
  394. if (componentRoot == null) {
  395. return designToComponentTree(doc, null, null);
  396. } else {
  397. return designToComponentTree(doc, componentRoot,
  398. componentRoot.getClass());
  399. }
  400. }
  401. /**
  402. * Constructs a component hierarchy from the design specified as an html
  403. * tree.
  404. *
  405. * <p>
  406. * If a component root is given, the component instances created during
  407. * reading the design are assigned to its member fields based on their id,
  408. * local id, and caption
  409. * <p>
  410. * If the root is a custom component or composite, its composition root will
  411. * be populated with the design contents. Note that even if the root
  412. * component is a custom component/composite, the root element of the design
  413. * should not be to avoid nesting a custom component in a custom component.
  414. *
  415. * @param doc
  416. * the html tree
  417. * @param componentRoot
  418. * optional component root instance. The type must match the type
  419. * of the root element in the design or be a
  420. * {@link CustomComponent} or {@link Composite}, in which case
  421. * the root component will be set as the composition root.
  422. * @param classWithFields
  423. * a class (componentRoot class or a super class) with some
  424. * member fields. The member fields whose type is assignable from
  425. * {@link Component} are bound to fields in the design based on
  426. * id/local id/caption
  427. */
  428. private static DesignContext designToComponentTree(Document doc,
  429. Component componentRoot, Class<?> classWithFields) {
  430. DesignContext designContext = new DesignContext(doc);
  431. designContext.readPackageMappings(doc);
  432. // No special handling for a document without a body element - should be
  433. // taken care of by jsoup.
  434. Element root = doc.body();
  435. Elements children = root.children();
  436. if (children.size() > 1) {
  437. throw new DesignException(
  438. "The first level of a component hierarchy should contain at most one root component, but found "
  439. + children.size() + ".");
  440. }
  441. Element element = children.isEmpty() ? null : children.first();
  442. if (componentRoot != null) {
  443. if (element == null) {
  444. throw new DesignException(
  445. "The root element cannot be null when the specified root Component is"
  446. + " not null.");
  447. }
  448. // user has specified root instance that may have member fields that
  449. // should be bound
  450. final FieldBinder binder;
  451. try {
  452. binder = new FieldBinder(componentRoot, classWithFields);
  453. } catch (IntrospectionException e) {
  454. throw new DesignException(
  455. "Could not bind fields of the root component", e);
  456. }
  457. // create listener for component creations that binds the created
  458. // components to the componentRoot instance fields
  459. ComponentCreationListener creationListener = (
  460. ComponentCreatedEvent event) -> binder.bindField(
  461. event.getComponent(), event.getLocalId());
  462. designContext.addComponentCreationListener(creationListener);
  463. // create subtree
  464. if (ComponentRootSetter.canSetRoot(componentRoot)) {
  465. Component rootComponent = designContext.readDesign(element);
  466. ComponentRootSetter.setRoot(componentRoot, rootComponent);
  467. } else {
  468. designContext.readDesign(element, componentRoot);
  469. }
  470. // make sure that all the member fields are bound
  471. Collection<String> unboundFields = binder.getUnboundFields();
  472. if (!unboundFields.isEmpty()) {
  473. throw new DesignException(
  474. "Found unbound fields from component root "
  475. + unboundFields);
  476. }
  477. // no need to listen anymore
  478. designContext.removeComponentCreationListener(creationListener);
  479. } else {
  480. // createChild creates the entire component hierarchy
  481. componentRoot = element == null ? null
  482. : designContext.readDesign(element);
  483. }
  484. designContext.setRootComponent(componentRoot);
  485. return designContext;
  486. }
  487. /**
  488. * Generates an html tree representation of the component hierarchy having
  489. * the root designContext.getRootComponent(). The hierarchy is stored under
  490. * &lt;body&gt; in the tree. The generated tree represents a valid html
  491. * document.
  492. *
  493. *
  494. * @param designContext
  495. * a DesignContext object specifying the root component
  496. * (designContext.getRootComponent()) of the hierarchy
  497. * @return an html tree representation of the component hierarchy
  498. */
  499. private static Document createHtml(DesignContext designContext) {
  500. // Create the html tree skeleton.
  501. Document doc = new Document("");
  502. DocumentType docType = new DocumentType("html", "", "");
  503. doc.appendChild(docType);
  504. Element html = doc.createElement("html");
  505. doc.appendChild(html);
  506. html.appendChild(doc.createElement("head"));
  507. Element body = doc.createElement("body");
  508. html.appendChild(body);
  509. // Append the design under <body> in the html tree. createNode
  510. // creates the entire component hierarchy rooted at the
  511. // given root node.
  512. Component root = designContext.getRootComponent();
  513. if (root != null) {
  514. Node rootNode = designContext.createElement(root);
  515. body.appendChild(rootNode);
  516. }
  517. designContext.writePackageMappings(doc);
  518. return doc;
  519. }
  520. /**
  521. * Loads a design for the given root component.
  522. * <p>
  523. * This methods assumes that the component class (or a super class) has been
  524. * marked with an {@link DesignRoot} annotation and will either use the
  525. * value from the annotation to locate the design file, or will fall back to
  526. * using a design with the same same as the annotated class file (with an
  527. * .html extension)
  528. * <p>
  529. * Any {@link Component} type fields in the root component which are not
  530. * assigned (i.e. are null) are mapped to corresponding components in the
  531. * design. Matching is done based on field name in the component class and
  532. * id/local id/caption in the design file.
  533. * <p>
  534. * The type of the root component must match the root element in the design
  535. * or the root component must be a {@link CustomComponent} or
  536. * {@link Composite}. If the root is a custom component or composite, its
  537. * composition root will be populated with the design contents. Note that
  538. * even if the root component is a custom component/composite, the root
  539. * element of the design should not be to avoid nesting a custom component
  540. * in a custom component.
  541. *
  542. * @param rootComponent
  543. * The root component of the layout
  544. * @return The design context used in the load operation
  545. * @throws DesignException
  546. * If the design could not be loaded
  547. */
  548. public static DesignContext read(Component rootComponent)
  549. throws DesignException {
  550. // Try to find an @DesignRoot annotation on the class or any parent
  551. // class
  552. Class<? extends Component> annotatedClass = findClassWithAnnotation(
  553. rootComponent.getClass(), DesignRoot.class);
  554. if (annotatedClass == null) {
  555. throw new IllegalArgumentException("The class "
  556. + rootComponent.getClass().getName()
  557. + " or any of its superclasses do not have an @DesignRoot annotation");
  558. }
  559. DesignRoot designAnnotation = annotatedClass
  560. .getAnnotation(DesignRoot.class);
  561. String filename = designAnnotation.value();
  562. if (filename.isEmpty()) {
  563. // No value, assume the html file is named as the class
  564. filename = annotatedClass.getSimpleName() + ".html";
  565. }
  566. InputStream stream = annotatedClass.getResourceAsStream(filename);
  567. if (stream == null) {
  568. throw new DesignException("Unable to find design file " + filename
  569. + " in " + annotatedClass.getPackage().getName());
  570. }
  571. try {
  572. Document doc = parse(stream);
  573. DesignContext context = designToComponentTree(doc, rootComponent,
  574. annotatedClass);
  575. return context;
  576. } finally {
  577. try {
  578. stream.close();
  579. } catch (IOException e) {
  580. getLogger().log(Level.FINE, "Error closing design stream", e);
  581. }
  582. }
  583. }
  584. private static Logger getLogger() {
  585. return Logger.getLogger(Design.class.getName());
  586. }
  587. /**
  588. * Find the first class with the given annotation, starting the search from
  589. * the given class and moving upwards in the class hierarchy.
  590. *
  591. * @param componentClass
  592. * the class to check
  593. * @param annotationClass
  594. * the annotation to look for
  595. * @return the first class with the given annotation or null if no class
  596. * with the annotation was found
  597. */
  598. private static Class<? extends Component> findClassWithAnnotation(
  599. Class<? extends Component> componentClass,
  600. Class<? extends Annotation> annotationClass) {
  601. if (componentClass == null) {
  602. return null;
  603. }
  604. if (componentClass.isAnnotationPresent(annotationClass)) {
  605. return componentClass;
  606. }
  607. Class<?> superClass = componentClass.getSuperclass();
  608. if (!Component.class.isAssignableFrom(superClass)) {
  609. return null;
  610. }
  611. return findClassWithAnnotation(superClass.asSubclass(Component.class),
  612. annotationClass);
  613. }
  614. /**
  615. * Loads a design from the given file name using the given root component.
  616. * <p>
  617. * Any {@link Component} type fields in the root component which are not
  618. * assigned (i.e. are null) are mapped to corresponding components in the
  619. * design. Matching is done based on field name in the component class and
  620. * id/local id/caption in the design file.
  621. * <p>
  622. * The type of the root component must match the root element in the design
  623. * or the root component must be a {@link CustomComponent} or
  624. * {@link Composite}. If the root is a custom component or composite, its
  625. * composition root will be populated with the design contents. Note that
  626. * even if the root component is a custom component/composite, the root
  627. * element of the design should not be to avoid nesting a custom component
  628. * in a custom component.
  629. *
  630. * @param filename
  631. * The file name to load. Loaded from the same package as the
  632. * root component
  633. * @param rootComponent
  634. * The root component of the layout
  635. * @return The design context used in the load operation
  636. * @throws DesignException
  637. * If the design could not be loaded
  638. */
  639. public static DesignContext read(String filename, Component rootComponent)
  640. throws DesignException {
  641. InputStream stream = rootComponent.getClass()
  642. .getResourceAsStream(filename);
  643. if (stream == null) {
  644. throw new DesignException(
  645. "File " + filename + " was not found in the package "
  646. + rootComponent.getClass().getPackage().getName());
  647. }
  648. try {
  649. return read(stream, rootComponent);
  650. } finally {
  651. try {
  652. stream.close();
  653. } catch (IOException e) {
  654. getLogger().log(Level.FINE, "Error closing design stream", e);
  655. }
  656. }
  657. }
  658. /**
  659. * Loads a design from the given stream using the given root component. If
  660. * rootComponent is null, the type of the root node is read from the design.
  661. * <p>
  662. * Any {@link Component} type fields in the root component which are not
  663. * assigned (i.e. are null) are mapped to corresponding components in the
  664. * design. Matching is done based on field name in the component class and
  665. * id/local id/caption in the design file.
  666. * <p>
  667. * The type of the root component must match the root element in the design
  668. * or the root component must be a {@link CustomComponent} or
  669. * {@link Composite}. If the root is a custom component or composite, its
  670. * composition root will be populated with the design contents. Note that
  671. * even if the root component is a custom component/composite, the root
  672. * element of the design should not be to avoid nesting a custom component
  673. * in a custom component.
  674. *
  675. * @param stream
  676. * The stream to read the design from
  677. * @param rootComponent
  678. * The root component of the layout
  679. * @return The design context used in the load operation
  680. * @throws DesignException
  681. * If the design could not be loaded
  682. */
  683. public static DesignContext read(InputStream stream,
  684. Component rootComponent) {
  685. if (stream == null) {
  686. throw new DesignException("Stream cannot be null");
  687. }
  688. Document doc = parse(stream);
  689. DesignContext context = designToComponentTree(doc, rootComponent);
  690. return context;
  691. }
  692. /**
  693. * Loads a design from the given input stream.
  694. *
  695. * @param design
  696. * The stream to read the design from
  697. * @return The root component of the design
  698. */
  699. public static Component read(InputStream design) {
  700. DesignContext context = read(design, null);
  701. return context.getRootComponent();
  702. }
  703. /**
  704. * Writes the given component tree in design format to the given output
  705. * stream.
  706. *
  707. * @param component
  708. * the root component of the component tree, null can be used for
  709. * generating an empty design
  710. * @param outputStream
  711. * the output stream to write the design to. The design is always
  712. * written as UTF-8
  713. * @throws IOException
  714. * if writing fails
  715. */
  716. public static void write(Component component, OutputStream outputStream)
  717. throws IOException {
  718. DesignContext dc = new DesignContext();
  719. dc.setRootComponent(component);
  720. write(dc, outputStream);
  721. }
  722. /**
  723. * Writes the component, given in the design context, in design format to
  724. * the given output stream. The design context is used for writing local ids
  725. * and other information not available in the component tree.
  726. *
  727. * @param designContext
  728. * The DesignContext object specifying the component hierarchy
  729. * and the local id values of the objects. If
  730. * designContext.getRootComponent() is null, an empty design will
  731. * be generated.
  732. * @param outputStream
  733. * the output stream to write the design to. The design is always
  734. * written as UTF-8
  735. * @throws IOException
  736. * if writing fails
  737. */
  738. public static void write(DesignContext designContext,
  739. OutputStream outputStream) throws IOException {
  740. Document doc = createHtml(designContext);
  741. write(doc, outputStream);
  742. }
  743. /**
  744. * Writes the given jsoup document to the output stream (in UTF-8)
  745. *
  746. * @param doc
  747. * the document to write
  748. * @param outputStream
  749. * the stream to write to
  750. * @throws IOException
  751. * if writing fails
  752. */
  753. private static void write(Document doc, OutputStream outputStream)
  754. throws IOException {
  755. doc.outputSettings().indentAmount(4);
  756. doc.outputSettings().syntax(Syntax.html);
  757. doc.outputSettings().prettyPrint(true);
  758. outputStream.write(doc.html().getBytes(UTF_8));
  759. }
  760. private Design() {
  761. }
  762. }