diff options
8 files changed, 347 insertions, 81 deletions
diff --git a/server/src/com/vaadin/ui/declarative/Design.java b/server/src/com/vaadin/ui/declarative/Design.java index 89e992181b..433705c632 100644 --- a/server/src/com/vaadin/ui/declarative/Design.java +++ b/server/src/com/vaadin/ui/declarative/Design.java @@ -19,11 +19,13 @@ import java.beans.IntrospectionException; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.util.Collection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import org.jsoup.nodes.Document.OutputSettings.Syntax; import org.jsoup.nodes.DocumentType; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; @@ -46,28 +48,21 @@ import com.vaadin.ui.declarative.DesignContext.ComponentCreationListener; */ public class Design implements Serializable { /** - * Constructs a component hierarchy from the design specified as an html - * document. The component hierarchy must contain exactly one top-level - * Component. The component should be located under <body>, but also invalid - * html containing the hierarchy without <html>, <head> and <body> tags is - * accepted. - * + * Parses the given input stream into a jsoup document + * * @param html - * the html document describing the component design - * @return the DesignContext created while traversing the tree. The - * top-level component of the created component hierarchy can be - * accessed using result.getRootComponent(), where result is the - * object returned by this method. + * the stream containing the design + * @return the parsed jsoup document * @throws IOException */ - public static DesignContext parse(InputStream html) { - Document doc; + private static Document parse(InputStream html) { try { - doc = Jsoup.parse(html, "UTF-8", "", Parser.htmlParser()); + Document doc = Jsoup.parse(html, "UTF-8", "", Parser.htmlParser()); + return doc; } catch (IOException e) { throw new DesignException("The html document cannot be parsed."); } - return parse(doc, null); + } /** @@ -79,7 +74,7 @@ public class Design implements Serializable { * some uninitialized instance fields. The fields will be automatically * populated when parsing the design based on the component ids, local ids, * and captions of the components in the design. - * + * * @param html * the html document describing the component design * @param rootInstance @@ -91,34 +86,9 @@ public class Design implements Serializable { * object returned by this method. * @throws IOException */ - public static DesignContext parse(InputStream html, Component rootInstance) { - Document doc; - try { - doc = Jsoup.parse(html, "UTF-8", "", Parser.htmlParser()); - } catch (IOException e) { - throw new DesignException("The html document cannot be parsed."); - } - return parse(doc, rootInstance); - } - - /** - * Constructs a component hierarchy from the design specified as an html - * document given as a string. The component hierarchy must contain exactly - * one top-level Component. The component should be located under <body>, - * but also invalid html containing the hierarchy without <html>, <head> and - * <body> tags is accepted. - * - * @param html - * the html document describing the component design - * @return the DesignContext created while traversing the tree. The - * top-level component of the created component hierarchy can be - * accessed using result.getRootComponent(), where result is the - * object returned by this method. - * @throws IOException - */ - public static DesignContext parse(String html) { - Document doc = Jsoup.parse(html); - return parse(doc, null); + private static DesignContext parse(InputStream html, Component rootInstance) { + Document doc = parse(html); + return designToComponentTree(doc, rootInstance); } /** @@ -130,7 +100,7 @@ public class Design implements Serializable { * component with some uninitialized instance fields. The fields will be * automatically populated when parsing the design based on the component * ids, local ids, and captions of the components in the design. - * + * * @param html * the html document describing the component design * @param rootInstance @@ -142,17 +112,19 @@ public class Design implements Serializable { * object returned by this method. * @throws IOException */ - public static DesignContext parse(String html, Component rootInstance) { + private static DesignContext parse(String html, Component rootInstance) { Document doc = Jsoup.parse(html); - return parse(doc, rootInstance); + return designToComponentTree(doc, rootInstance); } /** * Constructs a component hierarchy from the design specified as an html - * tree. If componentRoot is not null, the component instances created - * during synchronizing the design are assigned to its member fields based - * on their id, localId, and caption + * tree. * + * If a component root is given, the component instances created during + * synchronizing the design are assigned to its member fields based on their + * id, local id, and caption + * * @param doc * the html tree * @param componentRoot @@ -160,9 +132,10 @@ public class Design implements Serializable { * type must match the type of the root element in the design. * The member fields whose type is assignable from * {@link Component} are set when parsing the component tree - * + * */ - private static DesignContext parse(Document doc, Component componentRoot) { + private static DesignContext designToComponentTree(Document doc, + Component componentRoot) { DesignContext designContext = new DesignContext(doc); designContext.getPrefixes(doc); // No special handling for a document without a body element - should be @@ -210,7 +183,7 @@ public class Design implements Serializable { // createChild creates the entire component hierarchy componentRoot = designContext.createChild(element); } - designContext.setComponentRoot(componentRoot); + designContext.setRootComponent(componentRoot); return designContext; } @@ -218,13 +191,13 @@ public class Design implements Serializable { * Generates an html tree representation representing the component * hierarchy having the given root. The hierarchy is stored under <body> in * the tree. The generated tree corresponds to a valid html document. - * - * + * + * * @param root * the root of the component hierarchy * @return an html tree representation of the component hierarchy */ - public static Document createHtml(DesignContext designContext) { + private static Document createHtml(DesignContext designContext) { // Create the html tree skeleton. Document doc = new Document(""); DocumentType docType = new DocumentType("html", "", "", ""); @@ -239,7 +212,7 @@ public class Design implements Serializable { // Append the design under <body> in the html tree. createNode // creates the entire component hierarchy rooted at the // given root node. - Component root = designContext.getComponentRoot(); + Component root = designContext.getRootComponent(); Node rootNode = designContext.createNode(root); body.appendChild(rootNode); return doc; @@ -248,14 +221,142 @@ public class Design implements Serializable { /** * Generates an html file corresponding to the component hierarchy with the * given root. - * + * * @param writer * @param root * @throws IOException */ - public static void createHtml(BufferedWriter writer, DesignContext ctx) + private static void createHtml(BufferedWriter writer, DesignContext ctx) throws IOException { String docAsString = createHtml(ctx).toString(); writer.write(docAsString); } + + /** + * Loads a design from the given file name using the given root component. + * <p> + * Any {@link Component} type fields in the root component which are not + * assigned (i.e. are null) are mapped to corresponding components in the + * design. Matching is done based on field name in the component class and + * id/local id/caption in the design file. + * <p> + * The type of the root component must match the root element in the design + * + * @param filename + * The file name to load. Loaded from the same package as the + * root component + * @param rootComponent + * The root component of the layout. + * @return The design context used in the load operation + * @throws DesignException + * If the design could not be loaded + */ + public static DesignContext read(String filename, Component rootComponent) + throws DesignException { + InputStream stream = rootComponent.getClass().getResourceAsStream( + filename); + if (stream == null) { + throw new DesignException("File " + filename + + " was not found in the package " + + rootComponent.getClass().getPackage().getName()); + } + return read(stream, rootComponent); + } + + /** + * Loads a design from the given stream using the given root component. + * <p> + * Any {@link Component} type fields in the root component which are not + * assigned (i.e. are null) are mapped to corresponding components in the + * design. Matching is done based on field name in the component class and + * id/local id/caption in the design file. + * <p> + * The type of the root component must match the root element in the design + * + * @param stream + * The stream to read the design from + * @param rootComponent + * The root component of the layout. + * @return The design context used in the load operation + * @throws DesignException + * If the design could not be loaded + */ + public static DesignContext read(InputStream design, Component rootComponent) { + if (design == null) { + throw new DesignException("Stream cannot be null"); + } + Document doc = parse(design); + DesignContext context = designToComponentTree(doc, rootComponent); + + return context; + } + + /** + * Loads a design from the given input stream + * + * @param design + * The input stream which contains the design + * @return The root component of the design + */ + public static Component read(InputStream design) { + DesignContext context = read(design, null); + return context.getRootComponent(); + + } + + /** + * Writes the given component tree in design format to the given output + * stream + * + * @param component + * the root component of the component tree to write + * @param outputStream + * the output stream to write the design to. The design is always + * written as UTF-8 + * @throws IOException + */ + public static void write(Component component, OutputStream outputStream) + throws IOException { + DesignContext dc = new DesignContext(); + dc.setRootComponent(component); + write(dc, outputStream); + } + + /** + * Writes the component, given in the design context, in design format to + * the given output stream. The design context is used for writing local ids + * and other information not available in the component tree. + * + * @param component + * the root component of the component tree to write + * @param outputStream + * the output stream to write the design to. The design is always + * written as UTF-8 + * @throws IOException + * if writing fails + */ + public static void write(DesignContext designContext, + OutputStream outputStream) throws IOException { + Document doc = createHtml(designContext); + write(doc, outputStream); + } + + /** + * Writes the given jsoup document to the output stream (in UTF-8) + * + * @param doc + * the document to write + * @param outputStream + * the stream to write to + * @throws IOException + * if writing fails + */ + private static void write(Document doc, OutputStream outputStream) + throws IOException { + doc.outputSettings().indentAmount(4); + doc.outputSettings().syntax(Syntax.html); + doc.outputSettings().prettyPrint(true); + outputStream.write(doc.html().getBytes()); + } + } diff --git a/server/src/com/vaadin/ui/declarative/DesignContext.java b/server/src/com/vaadin/ui/declarative/DesignContext.java index 753a2d87ba..a3026fca18 100644 --- a/server/src/com/vaadin/ui/declarative/DesignContext.java +++ b/server/src/com/vaadin/ui/declarative/DesignContext.java @@ -46,7 +46,7 @@ public class DesignContext implements Serializable { .synchronizedMap(new HashMap<Class<?>, Object>()); // The root component of the component hierarchy - private Component componentRoot = null; + private Component rootComponent = null; // Attribute names for global id and caption and the prefix name for a local // id public static final String ID_ATTRIBUTE = "id"; @@ -280,7 +280,7 @@ public class DesignContext implements Serializable { * located under <head> in the html document. * */ - public void getPrefixes(Document doc) { + protected void getPrefixes(Document doc) { Element head = doc.head(); if (head == null) { return; @@ -554,15 +554,15 @@ public class DesignContext implements Serializable { * * @return */ - public Component getComponentRoot() { - return componentRoot; + public Component getRootComponent() { + return rootComponent; } /** * Sets the root component of a created component hierarchy. */ - public void setComponentRoot(Component componentRoot) { - this.componentRoot = componentRoot; + public void setRootComponent(Component rootComponent) { + this.rootComponent = rootComponent; } /** diff --git a/server/tests/src/com/vaadin/tests/design/DesignTest.java b/server/tests/src/com/vaadin/tests/design/DesignTest.java new file mode 100644 index 0000000000..10bc2e0079 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/DesignTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.design; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Iterator; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Panel; +import com.vaadin.ui.TextField; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.declarative.Design; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; + +public class DesignTest { + + @Test + public void readStream() throws FileNotFoundException { + Component root = Design + .read(new FileInputStream( + "server/tests/src/com/vaadin/tests/design/verticallayout-two-children.html")); + VerticalLayout rootLayout = (VerticalLayout) root; + Assert.assertEquals(VerticalLayout.class, root.getClass()); + + Assert.assertEquals(2, rootLayout.getComponentCount()); + Assert.assertEquals(TextField.class, rootLayout.getComponent(0) + .getClass()); + Assert.assertEquals(Button.class, rootLayout.getComponent(1).getClass()); + } + + @Test(expected = DesignException.class) + @Ignore("Feature needs to be fixed") + public void readWithIncorrectRoot() throws FileNotFoundException { + Design.read( + new FileInputStream( + "server/tests/src/com/vaadin/tests/design/verticallayout-one-child.html"), + new Panel()); + } + + public static class MyVerticalLayout extends VerticalLayout { + + } + + @Test + public void readWithSubClassRoot() throws FileNotFoundException { + Design.read( + new FileInputStream( + "server/tests/src/com/vaadin/tests/design/verticallayout-one-child.html"), + new MyVerticalLayout()); + } + + @Test + public void writeComponentToStream() throws IOException { + HorizontalLayout root = new HorizontalLayout(new Button("OK"), + new Button("Cancel")); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Design.write(root, baos); + Component newRoot = Design.read(new ByteArrayInputStream(baos + .toByteArray())); + + assertHierarchyEquals(root, newRoot); + } + + @Test + public void writeDesignContextToStream() throws IOException { + DesignContext dc = Design + .read(new FileInputStream( + "server/tests/src/com/vaadin/tests/design/verticallayout-two-children.html"), + null); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Design.write(dc, baos); + Component newRoot = Design.read(new ByteArrayInputStream(baos + .toByteArray())); + + assertHierarchyEquals(dc.getRootComponent(), newRoot); + } + + private void assertHierarchyEquals(Component expected, Component actual) { + if (expected.getClass() != actual.getClass()) { + throw new AssertionError( + "Component classes do not match. Expected: " + + expected.getClass().getName() + ", was: " + + actual.getClass().getName()); + } + + if (expected instanceof HasComponents) { + HasComponents expectedHC = (HasComponents) expected; + HasComponents actualHC = (HasComponents) actual; + Iterator<Component> eI = expectedHC.iterator(); + Iterator<Component> aI = actualHC.iterator(); + + while (eI.hasNext()) { + assertHierarchyEquals(eI.next(), aI.next()); + } + } + } +} diff --git a/server/tests/src/com/vaadin/tests/design/ParseAllSupportedComponentsTest.java b/server/tests/src/com/vaadin/tests/design/ParseAllSupportedComponentsTest.java index ff13522b2f..4f3f205631 100644 --- a/server/tests/src/com/vaadin/tests/design/ParseAllSupportedComponentsTest.java +++ b/server/tests/src/com/vaadin/tests/design/ParseAllSupportedComponentsTest.java @@ -33,11 +33,12 @@ public class ParseAllSupportedComponentsTest extends TestCase { public void testParsing() { try { DesignContext ctx = Design - .parse(new FileInputStream( - "server/tests/src/com/vaadin/tests/design/all-components.html")); + .read(new FileInputStream( + "server/tests/src/com/vaadin/tests/design/all-components.html"), + null); assertNotNull("The returned design context can not be null", ctx); assertNotNull("the component root can not be null", - ctx.getComponentRoot()); + ctx.getRootComponent()); } catch (FileNotFoundException e) { e.printStackTrace(); fail("Template parsing threw exception"); diff --git a/server/tests/src/com/vaadin/tests/design/ParseLayoutTest.java b/server/tests/src/com/vaadin/tests/design/ParseLayoutTest.java index 07a0b218f7..c517b0e5e2 100644 --- a/server/tests/src/com/vaadin/tests/design/ParseLayoutTest.java +++ b/server/tests/src/com/vaadin/tests/design/ParseLayoutTest.java @@ -15,6 +15,8 @@ */ package com.vaadin.tests.design; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -22,6 +24,7 @@ import java.io.InputStream; import junit.framework.TestCase; +import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; @@ -54,8 +57,10 @@ public class ParseLayoutTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); - ctx = Design.parse(new FileInputStream( - "server/tests/src/com/vaadin/tests/design/testFile.html")); + ctx = Design + .read(new FileInputStream( + "server/tests/src/com/vaadin/tests/design/testFile.html"), + null); } /* @@ -78,15 +83,18 @@ public class ParseLayoutTest extends TestCase { */ @Test public void testThatSerializationPreservesProperties() throws IOException { - Document doc = Design.createHtml(ctx); - DesignContext newContext = Design.parse(doc.toString()); - // Check that the elements can still be found by id and caption + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Design.write(ctx, out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DesignContext newContext = Design.read(in, null); findElements(newContext); checkHierarchy(newContext); // Check the mapping from prefixes to package names using the html tree String[] expectedPrefixes = { "my" }; String[] expectedPackageNames = { "com.addon.mypackage" }; int index = 0; + + Document doc = Jsoup.parse(out.toString("UTF-8")); Element head = doc.head(); for (Node child : head.childNodes()) { if ("meta".equals(child.nodeName())) { @@ -116,7 +124,7 @@ public class ParseLayoutTest extends TestCase { LayoutTemplate template = new LayoutTemplate(); InputStream htmlFile = new FileInputStream( "server/tests/src/com/vaadin/tests/design/testFile.html"); - Design.parse(htmlFile, template); + Design.read(htmlFile, template); assertNotNull(template.getFirstButton()); assertNotNull(template.getSecondButton()); assertNotNull(template.getYetanotherbutton()); @@ -135,7 +143,7 @@ public class ParseLayoutTest extends TestCase { InputStream htmlFile = new FileInputStream( "server/tests/src/com/vaadin/tests/design/testFile.html"); try { - Design.parse(htmlFile, template); + Design.read(htmlFile, template); // we are expecting an exception fail(); } catch (DesignException e) { @@ -148,7 +156,7 @@ public class ParseLayoutTest extends TestCase { * component hierarchy rooted at context.getComponentRoot(). */ private void checkHierarchy(DesignContext context) { - Component root = context.getComponentRoot(); + Component root = context.getRootComponent(); VerticalLayout vlayout = (VerticalLayout) root; int numComponents = vlayout.getComponentCount(); assertEquals("Wrong number of child components", 3, numComponents); diff --git a/server/tests/src/com/vaadin/tests/design/TestLocale.java b/server/tests/src/com/vaadin/tests/design/TestLocale.java index 8b46c9feff..b12e390aed 100644 --- a/server/tests/src/com/vaadin/tests/design/TestLocale.java +++ b/server/tests/src/com/vaadin/tests/design/TestLocale.java @@ -15,6 +15,7 @@ */ package com.vaadin.tests.design; +import java.io.ByteArrayInputStream; import java.util.Locale; import junit.framework.TestCase; @@ -22,13 +23,15 @@ import junit.framework.TestCase; import org.jsoup.nodes.Document; import org.jsoup.nodes.DocumentType; import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; import com.vaadin.ui.Button; +import com.vaadin.ui.Component; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.VerticalLayout; -import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.Design; +import com.vaadin.ui.declarative.DesignContext; /** * Tests the handling of the locale property in parsing and html generation. @@ -71,9 +74,9 @@ public class TestLocale extends TestCase { Label l2 = new Label(); l2.setLocale(Locale.CANADA); hlayout2.addComponent(l2); - ctx.setComponentRoot(vLayout); + ctx.setRootComponent(vLayout); // create the html tree corresponding to the component hierarchy - Document doc = Design.createHtml(ctx); + Document doc = componentToDoc(ctx); // check the created html Element body = doc.body(); Element evLayout = body.child(0); @@ -101,6 +104,28 @@ public class TestLocale extends TestCase { assertEquals("Wrong locale information.", "en_CA", el2.attr("locale")); } + private Document componentToDoc(DesignContext dc) { + // Create the html tree skeleton. + Document doc = new Document(""); + DocumentType docType = new DocumentType("html", "", "", ""); + doc.appendChild(docType); + Element html = doc.createElement("html"); + doc.appendChild(html); + html.appendChild(doc.createElement("head")); + Element body = doc.createElement("body"); + html.appendChild(body); + dc.storePrefixes(doc); + + // Append the design under <body> in the html tree. createNode + // creates the entire component hierarchy rooted at the + // given root node. + Component root = dc.getRootComponent(); + Node rootNode = dc.createNode(root); + body.appendChild(rootNode); + return doc; + + } + /* * Checks that the locale of a component is set when the html element * corresponding to the component specifies a locale. @@ -131,8 +156,9 @@ public class TestLocale extends TestCase { // parse the created document and check the constructed component // hierarchy - VerticalLayout vLayout = (VerticalLayout) Design.parse( - doc.toString()).getComponentRoot(); + String string = doc.html(); + VerticalLayout vLayout = (VerticalLayout) Design + .read(new ByteArrayInputStream(string.getBytes())); assertEquals("Wrong locale.", new Locale("en", "US"), vLayout.getLocale()); HorizontalLayout hLayout = (HorizontalLayout) vLayout.getComponent(0); diff --git a/server/tests/src/com/vaadin/tests/design/verticallayout-one-child.html b/server/tests/src/com/vaadin/tests/design/verticallayout-one-child.html new file mode 100644 index 0000000000..4b0123b9ed --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/verticallayout-one-child.html @@ -0,0 +1,3 @@ +<v-vertical-layout> + <v-button>OK</v-button> +</v-vertical-layout> diff --git a/server/tests/src/com/vaadin/tests/design/verticallayout-two-children.html b/server/tests/src/com/vaadin/tests/design/verticallayout-two-children.html new file mode 100644 index 0000000000..87ae9eee8e --- /dev/null +++ b/server/tests/src/com/vaadin/tests/design/verticallayout-two-children.html @@ -0,0 +1,4 @@ +<v-vertical-layout> + <v-text-field caption="Enter your name" /> + <v-button>Say hello</v-button> +</v-vertical-layout> |