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