org.structr.web.advanced.DeploymentTest.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.web.advanced.DeploymentTest.java

Source

/**
 * Copyright (C) 2010-2018 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.web.advanced;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.common.AccessMode;
import org.structr.common.Permission;
import org.structr.common.PropertyView;
import org.structr.common.SecurityContext;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.Localization;
import org.structr.core.entity.MailTemplate;
import org.structr.core.entity.Principal;
import org.structr.core.entity.SchemaMethod;
import org.structr.core.entity.SchemaNode;
import org.structr.core.entity.Security;
import org.structr.core.entity.relationship.PrincipalOwnsNode;
import org.structr.core.graph.NodeAttribute;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.core.property.StartNode;
import org.structr.core.property.StringProperty;
import org.structr.schema.export.StructrSchema;
import org.structr.schema.json.JsonSchema;
import org.structr.schema.json.JsonType;
import org.structr.web.StructrUiTest;
import org.structr.web.common.FileHelper;
import org.structr.web.entity.File;
import org.structr.web.entity.Folder;
import org.structr.web.entity.User;
import org.structr.web.entity.Widget;
import org.structr.web.entity.dom.Content;
import org.structr.web.entity.dom.DOMElement;
import org.structr.web.entity.dom.DOMNode;
import org.structr.web.entity.dom.Page;
import org.structr.web.entity.dom.ShadowDocument;
import org.structr.web.entity.dom.Template;
import org.structr.web.entity.html.Body;
import org.structr.web.entity.html.Div;
import org.structr.web.entity.html.Head;
import org.structr.web.entity.html.Html;
import org.structr.web.entity.html.Li;
import org.structr.web.entity.html.Link;
import org.structr.web.entity.html.Option;
import org.structr.web.entity.html.P;
import org.structr.web.entity.html.Script;
import org.structr.web.entity.html.Select;
import org.structr.web.entity.html.Table;
import org.structr.web.entity.html.Tbody;
import org.structr.web.entity.html.Td;
import org.structr.web.entity.html.Thead;
import org.structr.web.entity.html.Tr;
import org.structr.web.entity.html.Ul;
import org.structr.web.importer.Importer;
import org.structr.web.maintenance.DeployCommand;
import org.structr.web.maintenance.deploy.DeploymentCommentHandler;
import org.structr.websocket.command.CloneComponentCommand;
import org.structr.websocket.command.CreateComponentCommand;
import org.w3c.dom.Node;

public class DeploymentTest extends StructrUiTest {

    private static final Logger logger = LoggerFactory.getLogger(DeploymentTest.class.getName());

    @Test
    public void test01SimplePage() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createSimplePage(securityContext, "test01");

            // test special properties
            page.setProperty(StructrApp.key(Page.class, "showOnErrorCodes"), "404");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test02Visibilities() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test02");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test02");

            final Body body = createElement(page, html, "body");

            // create a div for admin only
            {
                final Div div1 = createElement(page, body, "div");
                createElement(page, div1, "h1", "private - ${find('User')}");

                div1.setProperty(StructrApp.key(DOMNode.class, "showConditions"), "me.isAdmin");
            }

            // create a private div
            {
                final Div div1 = createElement(page, body, "div");
                createElement(page, div1, "h1",
                        "private - test abcdefghjiklmnopqrstuvwyzABCDEFGHIJKLMNOPQRSTUVWXYZ?\"'");

                div1.setProperty(StructrApp.key(DOMNode.class, "showConditions"), "me.isAdmin");
            }

            // create a protected div
            {
                final Div div1 = createElement(page, body, "div");
                createElement(page, div1, "h1", "protected - $%&/()=?{[]}");

                final PropertyMap div1Properties = new PropertyMap();
                div1Properties.put(DOMNode.visibleToPublicUsers, false);
                div1Properties.put(DOMNode.visibleToAuthenticatedUsers, true);
                div1.setProperties(div1.getSecurityContext(), div1Properties);
            }

            // create a public div
            {
                final Div div1 = createElement(page, body, "div");
                createElement(page, div1, "h1", "public");

                final PropertyMap div1Properties = new PropertyMap();
                div1Properties.put(DOMNode.visibleToPublicUsers, true);
                div1Properties.put(DOMNode.visibleToAuthenticatedUsers, true);
                div1.setProperties(div1.getSecurityContext(), div1Properties);
            }

            // create a public only div
            {
                final Div div1 = createElement(page, body, "div");
                createElement(page, div1, "h1", "public only");

                final PropertyMap div1Properties = new PropertyMap();
                div1Properties.put(DOMNode.visibleToPublicUsers, true);
                div1Properties.put(DOMNode.visibleToAuthenticatedUsers, false);
                div1.setProperties(div1.getSecurityContext(), div1Properties);
            }

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test03ContentTypes() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test03");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test03");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Script script = createElement(page, div1, "script");
            final Content content = createContent(page, script,
                    "$(function () {\n\n" + "$('a[data-toggle=\"tab\"]').on('click', function (e) {\n\n"
                            + "var id = $(e.target).attr(\"href\").substr(1) // activated tab\n"
                            + "window.location.hash = id;\n" + "});\n\n" + "});");

            // workaround for strange importer behaviour
            script.setProperty(StructrApp.key(Script.class, "_html_type"), "text/javascript");
            content.setProperty(StructrApp.key(Content.class, "contentType"), "text/javascript");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test04ContentTypes() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test04");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test04");
            createElement(page, head, "link");
            createElement(page, head, "link");
            createComment(page, head, "comment+#");

            final Link link3 = createElement(page, head, "link");

            final PropertyMap link3Properties = new PropertyMap();
            link3Properties.put(StructrApp.key(Link.class, "_html_href"), "/");
            link3Properties.put(StructrApp.key(Link.class, "_html_media"), "screen");
            link3Properties.put(StructrApp.key(Link.class, "_html_type"), "stylesheet");
            link3.setProperties(link3.getSecurityContext(), link3Properties);

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            createElement(page, div1, "h1", "private");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test05SimpleTemplateInPage() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test05");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test05");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Template template = createTemplate(page, div1, "template source - '\"'`");

            final PropertyMap templateProperties = new PropertyMap();
            templateProperties.put(StructrApp.key(Template.class, "functionQuery"), "find('User')");
            templateProperties.put(StructrApp.key(Template.class, "dataKey"), "user");
            template.setProperties(template.getSecurityContext(), templateProperties);

            // append children to template object
            createElement(page, template, "div");
            createElement(page, template, "div");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test06SimpleTemplateInSharedComponents() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test06");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test06");

            final Body body = createElement(page, html, "body");
            createElement(page, body, "div");

            final ShadowDocument shadowDocument = CreateComponentCommand.getOrCreateHiddenDocument();
            createTemplate(shadowDocument, null, "template source - '\"'`");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test07SimpleSharedTemplate() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test07");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test07");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Template template = createTemplate(page, div1, "template source - '\"'`");

            createComponent(template);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test08SharedTemplateInTwoPages() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test08_1");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test08_1");

            final Body body1 = createElement(page1, html1, "body");
            final Div div1 = createElement(page1, body1, "div");

            final Template template1 = createTemplate(page1, div1, "template source - '\"'`");
            final Template component = createComponent(template1);

            // create second page
            final Page page2 = Page.createNewPage(securityContext, "test08_2");
            final Html html2 = createElement(page2, page2, "html");
            final Head head2 = createElement(page2, html2, "head");
            createElement(page2, head2, "title", "test08_2");

            final Body body2 = createElement(page2, html2, "body");
            final Div div2 = createElement(page2, body2, "div");

            // re-use template from above
            cloneComponent(component, div2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test09SharedTemplatesWithChildren() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test09_1");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test09_1");

            final Body body1 = createElement(page1, html1, "body");
            final Div div1 = createElement(page1, body1, "div");

            final Template template1 = createTemplate(page1, div1, "template source - '\"'`");
            createElement(page1, template1, "div", "test1");
            createElement(page1, template1, "div", "test1");

            final Template component = createComponent(template1);

            // create second page
            final Page page2 = Page.createNewPage(securityContext, "test09_2");
            final Html html2 = createElement(page2, page2, "html");
            final Head head2 = createElement(page2, html2, "head");
            createElement(page2, head2, "title", "test09_2");

            final Body body2 = createElement(page2, html2, "body");
            final Div div2 = createElement(page2, body2, "div");

            // re-use template from above
            cloneComponent(component, div2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test10SharedComponent() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test10_1");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test10_1");

            final Body body1 = createElement(page1, html1, "body");
            final Div div1 = createElement(page1, body1, "div");

            createElement(page1, div1, "div", "test1");
            createElement(page1, div1, "div", "test1");

            final Div component = createComponent(div1);

            // create second page
            final Page page2 = Page.createNewPage(securityContext, "test10_2");
            final Html html2 = createElement(page2, page2, "html");
            final Head head2 = createElement(page2, html2, "head");
            createElement(page2, head2, "title", "test10_2");

            final Body body2 = createElement(page2, html2, "body");
            final Div div2 = createElement(page2, body2, "div");

            // re-use template from above
            cloneComponent(component, div2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test11TemplateInTbody() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test11");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test11_1");

            final Body body1 = createElement(page1, html1, "body");
            final Table table = createElement(page1, body1, "table");
            final Tbody tbody = createElement(page1, table, "tbody");

            final Template template1 = createTemplate(page1, tbody, "<tr><td>${user.name}</td></tr>");

            final PropertyMap template1Properties = new PropertyMap();
            template1Properties.put(StructrApp.key(DOMNode.class, "functionQuery"), "find('User')");
            template1Properties.put(StructrApp.key(DOMNode.class, "dataKey"), "user");
            template1.setProperties(template1.getSecurityContext(), template1Properties);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test12EmptyContentElementWithContentType() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page = Page.createNewPage(securityContext, "test12");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test12");

            final Body body = createElement(page, html, "body");
            final Script script1 = createElement(page, body, "script");
            final Script script2 = createElement(page, body, "script");

            script1.setProperty(StructrApp.key(Script.class, "_html_type"), "text/javascript");

            createContent(page, script1, "");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test13EmptyContentElement() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page = Page.createNewPage(securityContext, "test13");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test13");

            final Body body = createElement(page, html, "body");
            final Script script1 = createElement(page, body, "script", "");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), false);
    }

    @Test
    public void test14FileAttributesInFolders() {

        final String folderPath = "/deeply/nested/Folder Structure/with spaces";
        final String fileName = "test14.txt";

        // setup
        try (final Tx tx = app.tx()) {

            final Folder folder = FileHelper.createFolderPath(securityContext, folderPath);
            final File file = FileHelper.createFile(securityContext, "test".getBytes("utf-8"), "text/plain",
                    File.class, fileName);
            final Folder rootFolder = getRootFolder(folder);

            Assert.assertNotNull("Root folder should not be null", rootFolder);

            // root folder needs to have "includeInFrontendExport" set
            rootFolder.setProperty(StructrApp.key(Folder.class, "includeInFrontendExport"), true);

            file.setProperty(StructrApp.key(File.class, "parent"), folder);
            file.setProperty(StructrApp.key(File.class, "visibleToPublicUsers"), true);
            file.setProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers"), true);
            file.setProperty(StructrApp.key(File.class, "enableBasicAuth"), true);
            file.setProperty(StructrApp.key(File.class, "useAsJavascriptLibrary"), true);

            tx.success();

        } catch (IOException | FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true);

        // check
        try (final Tx tx = app.tx()) {

            final Folder folder = app.nodeQuery(Folder.class).andName("with spaces").getFirst();

            Assert.assertNotNull("Invalid deployment result", folder);

            final File file = app.nodeQuery(File.class).and(StructrApp.key(File.class, "parent"), folder)
                    .and(File.name, fileName).getFirst();

            Assert.assertNotNull("Invalid deployment result", file);

            Assert.assertEquals("Deployment import does not restore attributes correctly", folder,
                    file.getParent());
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "visibleToPublicUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "enableBasicAuth")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "useAsJavascriptLibrary")));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

    }

    @Test
    public void test15FileAttributesOnUpdate() {

        final String folderPath = "/deeply/nested/Folder Structure/with spaces";
        final String fileName = "test15.txt";

        // setup
        try (final Tx tx = app.tx()) {

            final Folder folder = FileHelper.createFolderPath(securityContext, folderPath);
            final File file = FileHelper.createFile(securityContext, "test".getBytes("utf-8"), "text/plain",
                    File.class, fileName);
            final Folder rootFolder = getRootFolder(folder);

            Assert.assertNotNull("Root folder should not be null", rootFolder);

            // root folder needs to have "includeInFrontendExport" set
            rootFolder.setProperty(StructrApp.key(Folder.class, "includeInFrontendExport"), true);

            file.setProperty(StructrApp.key(File.class, "parent"), folder);
            file.setProperty(StructrApp.key(File.class, "visibleToPublicUsers"), true);
            file.setProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers"), true);
            file.setProperty(StructrApp.key(File.class, "enableBasicAuth"), true);
            file.setProperty(StructrApp.key(File.class, "useAsJavascriptLibrary"), true);
            file.setProperty(StructrApp.key(File.class, "includeInFrontendExport"), true);

            tx.success();

        } catch (IOException | FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test, don't clean the database but modify the file flags
        doImportExportRoundtrip(true, false, new Function() {

            @Override
            public Object apply(Object t) {

                try (final Tx tx = app.tx()) {

                    final File file = app.nodeQuery(File.class).and(File.name, fileName).getFirst();
                    file.setProperty(StructrApp.key(File.class, "visibleToPublicUsers"), false);
                    file.setProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers"), false);
                    file.setProperty(StructrApp.key(File.class, "enableBasicAuth"), false);
                    file.setProperty(StructrApp.key(File.class, "useAsJavascriptLibrary"), false);
                    file.setProperty(StructrApp.key(File.class, "includeInFrontendExport"), false);

                    tx.success();

                } catch (FrameworkException fex) {
                }

                return null;
            }

        });

        // check
        try (final Tx tx = app.tx()) {

            final Folder folder = app.nodeQuery(Folder.class).andName("with spaces").getFirst();

            Assert.assertNotNull("Invalid deployment result", folder);

            final File file = app.nodeQuery(File.class).and(StructrApp.key(File.class, "parent"), folder)
                    .and(File.name, fileName).getFirst();

            Assert.assertNotNull("Invalid deployment result", file);

            Assert.assertEquals("Deployment import of existing file does not restore attributes correctly", folder,
                    file.getParent());
            Assert.assertTrue("Deployment import of existing file does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "visibleToPublicUsers")));
            Assert.assertTrue("Deployment import of existing file does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers")));
            Assert.assertTrue("Deployment import of existing file does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "enableBasicAuth")));
            Assert.assertTrue("Deployment import of existing file does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "useAsJavascriptLibrary")));
            Assert.assertTrue("Deployment import of existing file does not restore attributes correctly",
                    file.getProperty(StructrApp.key(File.class, "includeInFrontendExport")));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

    }

    @Test
    public void test16SharedTemplateWithChildren() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test16");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test16");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Template template = createTemplate(page, div1, "template source - '\"'`");

            createElement(page, template, "div");
            final DOMNode table = createElement(page, template, "table");
            final DOMNode tr = createElement(page, table, "tr");
            createElement(page, tr, "td");
            createElement(page, tr, "td");

            createComponent(template);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test17NamedNonSharedTemplateWithChildren() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test17");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test17");

            final Body body = createElement(page, html, "body");

            final Template template = createTemplate(page, body, "${render(children)}");
            template.setProperty(AbstractNode.name, "a-template");

            final Template sharedTemplate = createComponent(template);

            // remove original template from page
            app.delete(template);

            createElement(page, sharedTemplate, "div");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test18NonNamedNonSharedTemplateWithChildren() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test18");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test18");

            final Body body = createElement(page, html, "body");

            final Template template = createTemplate(page, body, "${render(children)}");

            final Template sharedTemplate = createComponent(template);

            // remove original template from page
            app.delete(template);

            createElement(page, sharedTemplate, "div");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test19HtmlEntities() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test19");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test19");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Content content = createContent(page, div1, "<b>Test</b>");
            content.setProperty(StructrApp.key(Content.class, "contentType"), "text/html");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test20ExportOwnership() {

        Principal user1 = null;
        Principal user2 = null;

        try (final Tx tx = app.tx()) {

            user1 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
            user2 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

            tx.success();

        } catch (FrameworkException ex) {
            fail("Unexpected exception.");
        }

        Assert.assertNotNull("User was not created, test cannot continue", user1);
        Assert.assertNotNull("User was not created, test cannot continue", user2);

        // setup
        final SecurityContext context1 = SecurityContext.getInstance(user1, AccessMode.Backend);
        final App app1 = StructrApp.getInstance(context1);

        try (final Tx tx = app1.tx()) {

            final Page page = Page.createNewPage(context1, "test20");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test20");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Content content = createContent(page, div1, "<b>Test</b>");
            content.setProperty(StructrApp.key(Content.class, "contentType"), "text/html");

            // set owner to different user
            div1.setProperty(AbstractNode.owner, user2);
            content.setProperty(AbstractNode.owner, user2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test21ExportGrants() {

        Principal user1 = null;
        Principal user2 = null;

        try (final Tx tx = app.tx()) {

            user1 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
            user2 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

            tx.success();

        } catch (FrameworkException ex) {
            fail("Unexpected exception.");
        }

        Assert.assertNotNull("User was not created, test cannot continue", user1);
        Assert.assertNotNull("User was not created, test cannot continue", user2);

        // setup
        final SecurityContext context1 = SecurityContext.getInstance(user1, AccessMode.Backend);
        final App app1 = StructrApp.getInstance(context1);

        try (final Tx tx = app1.tx()) {

            final Page page = Page.createNewPage(context1, "test21");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test21");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            final Content content = createContent(page, div1, "<b>Test</b>");
            content.setProperty(StructrApp.key(Content.class, "contentType"), "text/html");

            // create grants
            page.grant(Permission.read, user2);
            div1.grant(Permission.read, user2);
            content.grant(Permission.read, user2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true, false);
    }

    @Test
    public void test22TemplateOwnershipAndGrants() {

        Principal user1 = null;
        Principal user2 = null;

        try (final Tx tx = app.tx()) {

            user1 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
            user2 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

            tx.success();

        } catch (FrameworkException ex) {
            fail("Unexpected exception.");
        }

        Assert.assertNotNull("User was not created, test cannot continue", user1);
        Assert.assertNotNull("User was not created, test cannot continue", user2);

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test22_1");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test22_1");

            final Body body1 = createElement(page1, html1, "body");
            final Div div1 = createElement(page1, body1, "div");

            createElement(page1, div1, "div", "test1");
            createElement(page1, div1, "div", "test1");

            final Div component = createComponent(div1);

            // create second page
            final Page page2 = Page.createNewPage(securityContext, "test22_2");
            final Html html2 = createElement(page2, page2, "html");
            final Head head2 = createElement(page2, html2, "head");
            createElement(page2, head2, "title", "test22_2");

            final Body body2 = createElement(page2, html2, "body");
            final Div div2 = createElement(page2, body2, "div");

            // re-use template from above
            final Div cloned = cloneComponent(component, div2);

            component.grant(Permission.read, user1);
            cloned.grant(Permission.read, user2);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true, true, new Function() {

            @Override
            public Object apply(Object t) {

                try (final Tx tx = app.tx()) {

                    createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
                    createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

                    tx.success();

                } catch (FrameworkException ex) {
                    fail("Unexpected exception.");
                }

                return null;
            }
        });
    }

    @Test
    public void test23FileOwnershipAndGrants() {

        Principal user1 = null;
        Principal user2 = null;

        try (final Tx tx = app.tx()) {

            user1 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
            user2 = createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

            tx.success();

        } catch (FrameworkException ex) {
            fail("Unexpected exception.");
        }

        Assert.assertNotNull("User was not created, test cannot continue", user1);
        Assert.assertNotNull("User was not created, test cannot continue", user2);

        // setup
        try (final Tx tx = app.tx()) {

            // create some files and folders
            final Folder folder1 = app.create(Folder.class, new NodeAttribute<>(Folder.name, "Folder1"),
                    new NodeAttribute<>(StructrApp.key(Folder.class, "includeInFrontendExport"), true));
            final Folder folder2 = app.create(Folder.class, new NodeAttribute<>(Folder.name, "Folder2"),
                    new NodeAttribute<>(StructrApp.key(Folder.class, "parent"), folder1));

            final File file1 = FileHelper.createFile(securityContext, "test".getBytes(), "text/plain", File.class,
                    "test1.txt");
            final File file2 = FileHelper.createFile(securityContext, "test".getBytes(), "text/plain", File.class,
                    "test2.txt");

            file1.setParent(folder2);
            file2.setParent(folder2);

            folder1.setProperty(Folder.owner, user1);
            folder1.grant(Permission.read, user2);

            folder2.setProperty(Folder.owner, user2);
            folder2.grant(Permission.write, user1);

            file1.setProperty(File.owner, user1);
            file2.setProperty(File.owner, user2);

            file1.setProperty(Folder.owner, user1);
            file1.grant(Permission.read, user2);

            file2.setProperty(Folder.owner, user2);
            file2.grant(Permission.write, user1);

            tx.success();

        } catch (IOException | FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true, true, new Function() {

            @Override
            public Object apply(Object t) {

                try (final Tx tx = app.tx()) {

                    createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user1"));
                    createTestNode(User.class, new NodeAttribute<>(AbstractNode.name, "user2"));

                    tx.success();

                } catch (FrameworkException ex) {
                    fail("Unexpected exception.");
                }

                return null;
            }
        });
    }

    @Test
    public void test24ContentShowConditions() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test24");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test24");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Content content1 = createContent(page, div1, "${current.type}");
            final Content content2 = createContent(page, div1, "${find('User', 'name', '@structr')[0].id}");
            final Content content3 = createContent(page, div1, "${find('User', 'name', '@structr')[0].id}");

            content1.setProperty(StructrApp.key(DOMNode.class, "showConditions"),
                    "eq(current.type, 'MyTestFolder')");
            content2.setProperty(StructrApp.key(DOMNode.class, "showConditions"),
                    "if(equal(extract(first(find('User', 'name' 'structr')), 'name'), '@structr'), true, false)");
            content3.setProperty(StructrApp.key(DOMNode.class, "showConditions"), "(((((([]))))))"); // for testing only

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test25ExtendedBuiltinTypes() {

        cleanDatabaseAndSchema();

        /* This method tests whether files, folders and images that are
         * considered part of application data (derived from built-in
         * types) are ignored in the deployment process.
         */

        // setup
        try (final Tx tx = app.tx()) {

            // create extended folder class
            app.create(SchemaNode.class, new NodeAttribute<>(SchemaNode.name, "ExtendedFolder"),
                    new NodeAttribute<>(SchemaNode.extendsClass, "org.structr.dynamic.Folder"));

            // create extended file class
            app.create(SchemaNode.class, new NodeAttribute<>(SchemaNode.name, "ExtendedFile"),
                    new NodeAttribute<>(SchemaNode.extendsClass, "org.structr.dynamic.File"));

            tx.success();

        } catch (FrameworkException fex) {

            logger.warn("Unexpected exception {}", fex.getMessage());
            fail("Unexpected exception.");
        }

        // setup
        try (final Tx tx = app.tx()) {

            final NodeInterface folder1 = app
                    .create(StructrApp.getConfiguration().getNodeEntityClass("ExtendedFolder"), "folder1");
            final NodeInterface folder2 = app.create(
                    StructrApp.getConfiguration().getNodeEntityClass("ExtendedFolder"),
                    new NodeAttribute<>(Folder.name, "folder2"),
                    new NodeAttribute(StructrApp.key(Folder.class, "parent"), folder1));

            app.create(StructrApp.getConfiguration().getNodeEntityClass("ExtendedFile"),
                    new NodeAttribute<>(File.name, "file1.txt"),
                    new NodeAttribute(StructrApp.key(File.class, "parent"), folder1));

            app.create(StructrApp.getConfiguration().getNodeEntityClass("ExtendedFile"),
                    new NodeAttribute<>(File.name, "file2.txt"),
                    new NodeAttribute(StructrApp.key(File.class, "parent"), folder2));

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("Unexpected exception {}", fex.getMessage());
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), false, true);
    }

    @Test
    public void test26Escaping() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test25");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test25");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Content content1 = createContent(page, div1,
                    "<div><script>var test = '<h3>Title</h3>';</script></div>");

            content1.setProperty(StructrApp.key(Content.class, "contentType"), "text/html");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test27FileAttributes() {

        final String fileName1 = "test27_1.txt";
        final String fileName2 = "test27_2.txt";

        // setup
        try (final Tx tx = app.tx()) {

            final File file1 = FileHelper.createFile(securityContext, "test".getBytes("utf-8"), "text/plain",
                    File.class, fileName1);
            final File file2 = FileHelper.createFile(securityContext, "test".getBytes("utf-8"), "text/plain",
                    File.class, fileName2);

            file1.setProperty(StructrApp.key(File.class, "visibleToPublicUsers"), true);
            file1.setProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers"), true);
            file1.setProperty(StructrApp.key(File.class, "enableBasicAuth"), true);
            file1.setProperty(StructrApp.key(File.class, "useAsJavascriptLibrary"), true);

            file2.setProperty(StructrApp.key(File.class, "visibleToPublicUsers"), true);
            file2.setProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers"), true);
            file2.setProperty(StructrApp.key(File.class, "enableBasicAuth"), true);
            file2.setProperty(StructrApp.key(File.class, "includeInFrontendExport"), true);

            tx.success();

        } catch (IOException | FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true);

        // check
        try (final Tx tx = app.tx()) {

            final File file1 = app.nodeQuery(File.class).and(File.name, fileName1).getFirst();
            final File file2 = app.nodeQuery(File.class).and(File.name, fileName2).getFirst();

            Assert.assertNotNull("Invalid deployment result", file1);
            Assert.assertNotNull("Invalid deployment result", file2);

            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file1.getProperty(StructrApp.key(File.class, "visibleToPublicUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file1.getProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file1.getProperty(StructrApp.key(File.class, "enableBasicAuth")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file1.getProperty(StructrApp.key(File.class, "useAsJavascriptLibrary")));
            Assert.assertFalse("Deployment import does not restore attributes correctly",
                    file1.getProperty(StructrApp.key(File.class, "includeInFrontendExport")));

            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file2.getProperty(StructrApp.key(File.class, "visibleToPublicUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file2.getProperty(StructrApp.key(File.class, "visibleToAuthenticatedUsers")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file2.getProperty(StructrApp.key(File.class, "enableBasicAuth")));
            Assert.assertFalse("Deployment import does not restore attributes correctly",
                    file2.getProperty(StructrApp.key(File.class, "useAsJavascriptLibrary")));
            Assert.assertTrue("Deployment import does not restore attributes correctly",
                    file2.getProperty(StructrApp.key(File.class, "includeInFrontendExport")));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }
    }

    @Test
    public void test28MailTemplates() {

        // setup
        try (final Tx tx = app.tx()) {

            app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "template1"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "text1"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "visibleToPublicUsers"), true));

            app.create(MailTemplate.class,
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "name"), "template2"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "locale"), "en"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "text"), "text2"),
                    new NodeAttribute<>(StructrApp.key(MailTemplate.class, "visibleToAuthenticatedUsers"), true));

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true);

        // check
        try (final Tx tx = app.tx()) {

            final MailTemplate template1 = app.nodeQuery(MailTemplate.class).and(MailTemplate.name, "template1")
                    .getFirst();
            final MailTemplate template2 = app.nodeQuery(MailTemplate.class).and(MailTemplate.name, "template2")
                    .getFirst();

            Assert.assertNotNull("Invalid deployment result", template1);
            Assert.assertNotNull("Invalid deployment result", template2);

            Assert.assertEquals("Invalid MailTemplate deployment result", "template1",
                    template1.getProperty(StructrApp.key(MailTemplate.class, "name")));
            Assert.assertEquals("Invalid MailTemplate deployment result", "de_DE",
                    template1.getProperty(StructrApp.key(MailTemplate.class, "locale")));
            Assert.assertEquals("Invalid MailTemplate deployment result", "text1",
                    template1.getProperty(StructrApp.key(MailTemplate.class, "text")));
            Assert.assertEquals("Invalid MailTemplate deployment result", true,
                    template1.getProperty(StructrApp.key(MailTemplate.class, "visibleToPublicUsers")));
            Assert.assertEquals("Invalid MailTemplate deployment result", false,
                    template1.getProperty(StructrApp.key(MailTemplate.class, "visibleToAuthenticatedUsers")));

            Assert.assertEquals("Invalid MailTemplate deployment result", "template2",
                    template2.getProperty(StructrApp.key(MailTemplate.class, "name")));
            Assert.assertEquals("Invalid MailTemplate deployment result", "en",
                    template2.getProperty(StructrApp.key(MailTemplate.class, "locale")));
            Assert.assertEquals("Invalid MailTemplate deployment result", "text2",
                    template2.getProperty(StructrApp.key(MailTemplate.class, "text")));
            Assert.assertEquals("Invalid MailTemplate deployment result", false,
                    template2.getProperty(StructrApp.key(MailTemplate.class, "visibleToPublicUsers")));
            Assert.assertEquals("Invalid MailTemplate deployment result", true,
                    template2.getProperty(StructrApp.key(MailTemplate.class, "visibleToAuthenticatedUsers")));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }
    }

    @Test
    public void test29Localizations() {

        // setup
        try (final Tx tx = app.tx()) {

            app.create(Localization.class,
                    new NodeAttribute<>(StructrApp.key(Localization.class, "name"), "localization1"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "domain"), "domain1"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "locale"), "de_DE"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "localizedName"), "localizedName1"));

            app.create(Localization.class,
                    new NodeAttribute<>(StructrApp.key(Localization.class, "name"), "localization2"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "domain"), "domain2"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "locale"), "en"),
                    new NodeAttribute<>(StructrApp.key(Localization.class, "localizedName"), "localizedName2"));

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true);

        // check
        try (final Tx tx = app.tx()) {

            final Localization localization1 = app.nodeQuery(Localization.class)
                    .and(Localization.name, "localization1").getFirst();
            final Localization localization2 = app.nodeQuery(Localization.class)
                    .and(Localization.name, "localization2").getFirst();

            Assert.assertNotNull("Invalid deployment result", localization1);
            Assert.assertNotNull("Invalid deployment result", localization2);

            Assert.assertEquals("Invalid Localization deployment result", "localization1",
                    localization1.getProperty(StructrApp.key(Localization.class, "name")));
            Assert.assertEquals("Invalid Localization deployment result", "domain1",
                    localization1.getProperty(StructrApp.key(Localization.class, "domain")));
            Assert.assertEquals("Invalid Localization deployment result", "de_DE",
                    localization1.getProperty(StructrApp.key(Localization.class, "locale")));
            Assert.assertEquals("Invalid Localization deployment result", "localizedName1",
                    localization1.getProperty(StructrApp.key(Localization.class, "localizedName")));
            Assert.assertEquals("Invalid Localization deployment result", true,
                    localization1.getProperty(StructrApp.key(Localization.class, "visibleToPublicUsers")));
            Assert.assertEquals("Invalid Localization deployment result", true,
                    localization1.getProperty(StructrApp.key(Localization.class, "visibleToAuthenticatedUsers")));

            Assert.assertEquals("Invalid Localization deployment result", "localization2",
                    localization2.getProperty(StructrApp.key(Localization.class, "name")));
            Assert.assertEquals("Invalid Localization deployment result", "domain2",
                    localization2.getProperty(StructrApp.key(Localization.class, "domain")));
            Assert.assertEquals("Invalid Localization deployment result", "en",
                    localization2.getProperty(StructrApp.key(Localization.class, "locale")));
            Assert.assertEquals("Invalid Localization deployment result", "localizedName2",
                    localization2.getProperty(StructrApp.key(Localization.class, "localizedName")));
            Assert.assertEquals("Invalid Localization deployment result", true,
                    localization2.getProperty(StructrApp.key(Localization.class, "visibleToPublicUsers")));
            Assert.assertEquals("Invalid Localization deployment result", true,
                    localization2.getProperty(StructrApp.key(Localization.class, "visibleToAuthenticatedUsers")));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }
    }

    @Test
    public void test30IncreasingIndentationCountInRoundtrip() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test30");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test30");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Div div2 = createElement(page, div1, "div", "This is a test.");
            final Table table1 = createElement(page, div2, "table");
            final Thead thead = createElement(page, table1, "thead");
            final Tbody tbody = createElement(page, table1, "tbody");
            final Tr tr1 = createElement(page, thead, "tr");
            final Tr tr2 = createElement(page, tbody, "tr");
            final Td td11 = createElement(page, tr1, "td", "content11", "Content before <select>");
            final Td td12 = createElement(page, tr1, "td", "content12");
            final Td td21 = createElement(page, tr2, "td", "content21");
            final Td td22 = createElement(page, tr2, "td", "content22");
            final Select select = createElement(page, td11, "select");
            final Option option1 = createElement(page, select, "option", "value1");
            final Option option2 = createElement(page, select, "option", "value2");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test31RoundtripWithEmptyContentElements() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test31");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test31");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Div div2 = createElement(page, div1, "div", "");
            final Table table1 = createElement(page, div2, "table");
            final Thead thead = createElement(page, table1, "thead");
            final Tbody tbody = createElement(page, table1, "tbody");
            final Tr tr1 = createElement(page, thead, "tr");
            final Tr tr2 = createElement(page, tbody, "tr");
            final Td td11 = createElement(page, tr1, "td");
            final Content c1 = createContent(page, td11, "");
            final Td td12 = createElement(page, tr1, "td", "content12");
            final P p1 = createElement(page, td12, "p", "");
            final Ul ul = createElement(page, p1, "ul");
            final Li li = createElement(page, ul, "li", "");
            final Td td21 = createElement(page, tr2, "td", "content21");
            final Td td22 = createElement(page, tr2, "td", "content22");
            final Select select = createElement(page, td11, "select");
            final Option option1 = createElement(page, select, "option", "");
            final Option option2 = createElement(page, select, "option", "value2");
            final Content c2 = createContent(page, div2, "");
            final Table table2 = createElement(page, div2, "table");

            // include visibility flags
            page.setProperty(AbstractNode.visibleToAuthenticatedUsers, true);
            c1.setProperty(AbstractNode.visibleToAuthenticatedUsers, true);
            c2.setProperty(AbstractNode.visibleToAuthenticatedUsers, true);

            // modify visibility to produce two consecutive deployment instruction comments
            td12.setProperty(AbstractNode.visibleToPublicUsers, true);
            table2.setProperty(AbstractNode.visibleToPublicUsers, true);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test32RoundtripWithEmptyContentElements() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test32");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test32");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");
            final Div div2 = createElement(page, div1, "div", " ");
            final Ul ul = createElement(page, div1, "ul");
            final Li li = createElement(page, ul, "li", " ");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test33SchemaMethods() {

        // setup
        try (final Tx tx = app.tx()) {

            app.create(SchemaMethod.class, new NodeAttribute<>(SchemaMethod.name, "method1"),
                    new NodeAttribute<>(SchemaMethod.comment, "comment1"),
                    new NodeAttribute<>(SchemaMethod.source, "source1"),
                    new NodeAttribute<>(SchemaMethod.virtualFileName, "virtualFileName1"),
                    new NodeAttribute<>(SchemaMethod.visibleToPublicUsers, true),
                    new NodeAttribute<>(SchemaMethod.visibleToAuthenticatedUsers, false)

            );

            app.create(SchemaMethod.class, new NodeAttribute<>(SchemaMethod.name, "method2"),
                    new NodeAttribute<>(SchemaMethod.comment, "comment2"),
                    new NodeAttribute<>(SchemaMethod.source, "source2"),
                    new NodeAttribute<>(SchemaMethod.virtualFileName, "virtualFileName2"),
                    new NodeAttribute<>(SchemaMethod.visibleToPublicUsers, false),
                    new NodeAttribute<>(SchemaMethod.visibleToAuthenticatedUsers, true));

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        doImportExportRoundtrip(true);

        // check
        try (final Tx tx = app.tx()) {

            final SchemaMethod method1 = app.nodeQuery(SchemaMethod.class).and(SchemaMethod.name, "method1")
                    .getFirst();
            final SchemaMethod method2 = app.nodeQuery(SchemaMethod.class).and(SchemaMethod.name, "method2")
                    .getFirst();

            Assert.assertNotNull("Invalid deployment result", method1);
            Assert.assertNotNull("Invalid deployment result", method2);

            Assert.assertEquals("Invalid SchemaMethod deployment result", "method1",
                    method1.getProperty(SchemaMethod.name));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "comment1",
                    method1.getProperty(SchemaMethod.comment));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "source1",
                    method1.getProperty(SchemaMethod.source));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "virtualFileName1",
                    method1.getProperty(SchemaMethod.virtualFileName));
            Assert.assertEquals("Invalid SchemaMethod deployment result", true,
                    method1.getProperty(SchemaMethod.visibleToPublicUsers));
            Assert.assertEquals("Invalid SchemaMethod deployment result", false,
                    method1.getProperty(SchemaMethod.visibleToAuthenticatedUsers));

            Assert.assertEquals("Invalid SchemaMethod deployment result", "method2",
                    method2.getProperty(SchemaMethod.name));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "comment2",
                    method2.getProperty(SchemaMethod.comment));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "source2",
                    method2.getProperty(SchemaMethod.source));
            Assert.assertEquals("Invalid SchemaMethod deployment result", "virtualFileName2",
                    method2.getProperty(SchemaMethod.virtualFileName));
            Assert.assertEquals("Invalid SchemaMethod deployment result", false,
                    method2.getProperty(SchemaMethod.visibleToPublicUsers));
            Assert.assertEquals("Invalid SchemaMethod deployment result", true,
                    method2.getProperty(SchemaMethod.visibleToAuthenticatedUsers));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }
    }

    @Test
    public void test34WidgetWithTemplate() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page testPage = Page.createNewPage(securityContext, "WidgetTestPage");
            final Html html = createElement(testPage, testPage, "html");
            final Head head = createElement(testPage, html, "head");
            final Body body = createElement(testPage, html, "body");
            final Div div = createElement(testPage, body, "div");
            final Div div2 = createElement(testPage, body, "div");

            div.setProperty(AbstractNode.name, "WidgetTestPage-Div");
            div2.setProperty(AbstractNode.name, "WidgetTestPage-Div2");

            Widget widgetToImport = app.create(Widget.class,
                    new NodeAttribute<>(StructrApp.key(Widget.class, "name"), "TestWidget"),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "source"),
                            "<!-- @structr:content(text/html) --><structr:template>${{Structr.print(\"<div>Test</div>\");}}</structr:template>"),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "configuration"),
                            "{\"processDeploymentInfo\": true}"),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "visibleToPublicUsers"), true),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "visibleToAuthenticatedUsers"), true));

            Importer importer = new Importer(securityContext,
                    widgetToImport.getProperty(new StringProperty("source")), null, null, true, true, false);

            importer.setIsDeployment(true);
            importer.setCommentHandler(new DeploymentCommentHandler());

            importer.parse(true);
            DOMNode template = importer.createComponentChildNodes(div, testPage);
            div.appendChild(template);

            makePublic(testPage, html, head, body, div, div2, template);

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        try (final Tx tx = app.tx()) {

            Div div = (Div) app.nodeQuery().andName("WidgetTestPage-Div").getFirst();

            assertEquals(1, div.treeGetChildCount());

            Object obj = div.treeGetFirstChild();

            assertTrue(Template.class.isAssignableFrom(obj.getClass()));

            Template template = (Template) obj;

            assertEquals("${{Structr.print(\"<div>Test</div>\");}}", template.getTextContent());
            assertEquals("text/html", template.getContentType());

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

    }

    @Test
    public void test35WidgetWithSharedComponentCreation() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page testPage = Page.createNewPage(securityContext, "WidgetTestPage");
            final Html html = createElement(testPage, testPage, "html");
            final Head head = createElement(testPage, html, "head");
            final Body body = createElement(testPage, html, "body");
            final Div div = createElement(testPage, body, "div");
            final Div div2 = createElement(testPage, body, "div");

            div.setProperty(AbstractNode.name, "WidgetTestPage-Div");
            div2.setProperty(AbstractNode.name, "WidgetTestPage-Div2");

            Widget widgetToImport = app.create(Widget.class,
                    new NodeAttribute<>(StructrApp.key(Widget.class, "name"), "TestWidget"),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "source"),
                            "<structr:component src=\"TestComponent\">\n"
                                    + "   <div data-structr-meta-name=\"TestComponent\">\n" + "      Test123\n"
                                    + "   </div>\n" + "</structr:component>"),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "configuration"), ""),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "visibleToPublicUsers"), true),
                    new NodeAttribute<>(StructrApp.key(Widget.class, "visibleToAuthenticatedUsers"), true)

            );

            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("widgetHostBaseUrl", "https://widgets.structr.org/structr/rest/widgets");
            paramMap.put("parentId", widgetToImport.getProperty(new StartNode<>("owner", PrincipalOwnsNode.class)));
            paramMap.put("source", widgetToImport.getProperty(new StringProperty("source")));
            paramMap.put("processDeploymentInfo", false);

            Widget.expandWidget(securityContext, testPage, div, baseUri, paramMap, false);
            Widget.expandWidget(securityContext, testPage, div, baseUri, paramMap, false);

            makePublic(testPage, html, head, body, div, div2);

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        // test
        try (final Tx tx = app.tx()) {

            Div div = app.nodeQuery(Div.class).andName("WidgetTestPage-Div").getFirst();

            assertEquals(2, div.treeGetChildCount());

            Object obj = null;
            for (DOMNode n : div.getAllChildNodes()) {
                obj = n;
                break;
            }

            assertTrue(Div.class.isAssignableFrom(obj.getClass()));

            Div clonedNode = (Div) obj;

            assertEquals(0, clonedNode.getChildNodes().getLength());
            assertEquals(3, app.nodeQuery(Div.class).andName("TestComponent").getResult().size());

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }
    }

    @Test
    public void test36BuiltInTypesWithProperties() {

        // setup schema
        try (final Tx tx = app.tx()) {

            final JsonSchema schema = StructrSchema.createFromDatabase(app);
            Assert.assertNotNull("StructrSchema must return a valid schema object", schema);

            final JsonType pageType = schema.getType("Page");
            final JsonType fileType = schema.getType("File");
            Assert.assertNotNull("Type Page must exist in every schema", pageType);
            Assert.assertNotNull("Type File must exist in every schema", fileType);

            pageType.addIntegerProperty("displayPosition");
            pageType.addStringProperty("icon");

            fileType.addIntegerProperty("test1");
            fileType.addStringProperty("test2");

            // install schema
            StructrSchema.replaceDatabaseSchema(app, schema);

            tx.success();

        } catch (FrameworkException | URISyntaxException fex) {
            fail("Unexpected exception.");
        }

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createSimplePage(securityContext, "page1");

            page.setProperty(StructrApp.key(Page.class, "displayPosition"), 12);
            page.setProperty(StructrApp.key(Page.class, "icon"), "icon");

            final Folder folder = app.create(Folder.class, "files");
            folder.setProperty(StructrApp.key(Folder.class, "includeInFrontendExport"), true);

            // create test file with custom attributes
            app.create(File.class, new NodeAttribute<>(StructrApp.key(File.class, "name"), "test.txt"),
                    new NodeAttribute<>(StructrApp.key(File.class, "parent"), folder),
                    new NodeAttribute<>(StructrApp.key(File.class, "contentType"), "text/plain"),
                    new NodeAttribute<>(StructrApp.key(File.class, "test1"), 123),
                    new NodeAttribute<>(StructrApp.key(File.class, "test2"), "testString"));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test37SharedComponentTemplate() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createSimplePage(securityContext, "page1");
            final Div div = (Div) page.getElementsByTagName("div").item(0);

            try {

                final DOMNode newNode = (DOMNode) page.createTextNode("#template");
                newNode.unlockSystemPropertiesOnce();
                newNode.setProperties(newNode.getSecurityContext(),
                        new PropertyMap(NodeInterface.type, Template.class.getSimpleName()));

                // append template
                div.appendChild(newNode);

                // create component from div
                createComponent(div);

            } catch (FrameworkException fex) {
                fex.printStackTrace();
                fail("Unexpected exception.");
            }

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test38DynamicFileExport() {

        // setup
        try (final Tx tx = app.tx()) {

            app.create(SchemaNode.class, new NodeAttribute<>(SchemaNode.name, "ExtendedFile"),
                    new NodeAttribute<>(SchemaNode.extendsClass, "org.structr.dynamic.File"),
                    new NodeAttribute<>(new StringProperty("_test"), "String"));

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        final Class type = StructrApp.getConfiguration().getNodeEntityClass("ExtendedFile");
        final PropertyKey test = StructrApp.key(type, "test");

        Assert.assertNotNull("Extended file type should exist", type);
        Assert.assertNotNull("Extended file property should exist", test);

        try (final Tx tx = app.tx()) {

            final NodeInterface node = FileHelper.createFile(securityContext, "test".getBytes("utf-8"),
                    "text/plain", type, "test.txt");

            node.setProperty(StructrApp.key(File.class, "includeInFrontendExport"), true);
            node.setProperty(test, "test");

            tx.success();

        } catch (IOException | FrameworkException fex) {
            fex.printStackTrace();
            fail("Unexpected exception.");
        }

        compare(calculateHash(), true);
    }

    @Test
    public void test39EmptyFolderInDeployment() {

        final String folderPath = "/empty/folders/in/filesystem";

        // setup
        try (final Tx tx = app.tx()) {

            final Folder folder = FileHelper.createFolderPath(securityContext, folderPath);
            final Folder rootFolder = getRootFolder(folder);

            Assert.assertNotNull("Root folder should not be null", rootFolder);

            // root folder needs to have "includeInFrontendExport" set
            rootFolder.setProperty(StructrApp.key(Folder.class, "includeInFrontendExport"), true);

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
            fail("Unexpected exception.");
        }

        doImportExportRoundtrip(true, true, null);

        // check
        try (final Tx tx = app.tx()) {

            final Folder folder = app.nodeQuery(Folder.class).andName("filesystem").getFirst();

            Assert.assertNotNull("Invalid deployment result - empty folder from export was not imported!", folder);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

    }

    @Test
    public void test40TwoTemplatesWithSameNameInTwoPages() {

        // setup
        try (final Tx tx = app.tx()) {

            // create first page
            final Page page1 = Page.createNewPage(securityContext, "test40_1");
            final Html html1 = createElement(page1, page1, "html");
            final Head head1 = createElement(page1, html1, "head");
            createElement(page1, head1, "title", "test40_1");
            final Body body1 = createElement(page1, html1, "body");
            final Div div1 = createElement(page1, body1, "div");

            // create first template and give it a name
            final Template template1 = createTemplate(page1, div1, "template source - '\"'`");
            final PropertyMap template1Properties = new PropertyMap();
            template1Properties.put(Template.name, "Test40Template");
            template1.setProperties(template1.getSecurityContext(), template1Properties);

            // create second page
            final Page page2 = Page.createNewPage(securityContext, "test40_2");
            final Html html2 = createElement(page2, page2, "html");
            final Head head2 = createElement(page2, html2, "head");
            createElement(page2, head2, "title", "test40_2");
            final Body body2 = createElement(page2, html2, "body");
            final Div div2 = createElement(page2, body2, "div");

            // create second template and give it the same name as the first one
            final Template template2 = createTemplate(page2, div2, "template source 2 - '\"'`");
            final PropertyMap template2Properties = new PropertyMap();
            template2Properties.put(Template.name, "Test40Template");
            template2.setProperties(template2.getSecurityContext(), template2Properties);

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    @Test
    public void test41CustomAttributes() {

        // setup
        try (final Tx tx = app.tx()) {

            final Page page = Page.createNewPage(securityContext, "test41");
            final Html html = createElement(page, page, "html");
            final Head head = createElement(page, html, "head");
            createElement(page, head, "title", "test41");

            final Body body = createElement(page, html, "body");
            final Div div1 = createElement(page, body, "div");

            div1.setProperty(new StringProperty("_custom_html_aria-expanded"), "true");
            div1.setProperty(new StringProperty("_custom_html_aria-controls"), "#test");

            tx.success();

        } catch (FrameworkException fex) {
            fail("Unexpected exception.");
        }

        // test
        compare(calculateHash(), true);
    }

    // ----- private methods -----
    private void compare(final String sourceHash, final boolean deleteTestDirectory) {
        compare(sourceHash, deleteTestDirectory, true);

    }

    private void compare(final String sourceHash, final boolean deleteTestDirectory, final boolean cleanDatabase) {

        doImportExportRoundtrip(deleteTestDirectory, cleanDatabase, null);

        final String roundtripHash = calculateHash();

        System.out.println("Expected: " + sourceHash);
        System.out.println("Actual:   " + roundtripHash);

        System.out.println(StringUtils.difference(sourceHash, roundtripHash));

        if (!sourceHash.equals(roundtripHash)) {

            System.out.println("Expected: " + sourceHash);
            System.out.println("Actual:   " + roundtripHash);

            System.out.println(StringUtils.difference(sourceHash, roundtripHash));

            fail("Invalid deployment roundtrip result");
        }
    }

    private void doImportExportRoundtrip(final boolean deleteTestDirectory) {
        doImportExportRoundtrip(deleteTestDirectory, true, null);
    }

    private void doImportExportRoundtrip(final boolean deleteTestDirectory, final boolean cleanDatabase,
            final Function callback) {

        final DeployCommand cmd = app.command(DeployCommand.class);
        final Path tmp = Paths.get("/tmp/structr-deployment-test" + System.currentTimeMillis() + System.nanoTime());

        try {
            if (tmp != null) {

                // export to temp directory
                final Map<String, Object> firstExportParams = new HashMap<>();
                firstExportParams.put("mode", "export");
                firstExportParams.put("target", tmp.toString());

                // execute deploy command
                cmd.execute(firstExportParams);

                if (cleanDatabase) {
                    cleanDatabase();
                }

                // apply callback if present
                if (callback != null) {
                    callback.apply(null);
                }

                // import from exported source
                final Map<String, Object> firstImportParams = new HashMap<>();
                firstImportParams.put("source", tmp.toString());

                // execute deploy command
                cmd.execute(firstImportParams);

            } else {

                fail("Unable to create temporary directory.");
            }

        } catch (Throwable t) {

            t.printStackTrace();

        } finally {

            if (deleteTestDirectory) {

                try {
                    // clean directories
                    Files.walkFileTree(tmp, new DeletingFileVisitor());
                    Files.delete(tmp);

                } catch (IOException ioex) {
                }
            }
        }
    }

    private String calculateHash() {

        final StringBuilder buf = new StringBuilder();

        try (final Tx tx = app.tx()) {

            for (final Page page : app.nodeQuery(Page.class).sort(AbstractNode.name).getAsList()) {

                // skip shadow document
                if (page instanceof ShadowDocument) {
                    continue;
                }

                System.out.print("############################# ");
                calculateHash(page, buf, 0);
            }

            for (final Folder folder : app.nodeQuery(Folder.class).sort(AbstractNode.name).getAsList()) {

                if (DeployCommand.okToExport(folder) && folder.includeInFrontendExport()) {

                    System.out.print("############################# ");
                    calculateHash(folder, buf, 0);
                }
            }

            for (final File file : app.nodeQuery(File.class).sort(AbstractNode.name).getAsList()) {

                if (DeployCommand.okToExport(file) && file.includeInFrontendExport()) {

                    System.out.print("############################# ");
                    calculateHash(file, buf, 0);
                }
            }

            tx.success();

        } catch (FrameworkException fex) {

            fail("Unexpected exception");
        }

        return buf.toString();//DigestUtils.md5Hex(buf.toString());
    }

    private void calculateHash(final NodeInterface start, final StringBuilder buf, final int depth) {

        buf.append(start.getType()).append("{");

        hash(start, buf);

        // indent
        for (int i = 0; i < depth; i++) {
            System.out.print("    ");
        }

        System.out.println(start.getType() + ": " + start.getUuid().substring(0, 5));

        if (start instanceof ShadowDocument) {

            for (final DOMNode child : ((ShadowDocument) start).getElements()) {

                // only include toplevel elements of the shadow document
                if (child.getParent() == null) {

                    calculateHash(child, buf, depth + 1);
                }
            }

        } else if (start instanceof DOMNode) {

            for (final DOMNode child : ((DOMNode) start).getChildren()) {

                calculateHash(child, buf, depth + 1);
            }
        }

        buf.append("}");
    }

    private void hash(final NodeInterface node, final StringBuilder buf) {

        // AbstractNode
        buf.append(valueOrEmpty(node, AbstractNode.type));
        buf.append(valueOrEmpty(node, AbstractNode.name));
        buf.append(valueOrEmpty(node, AbstractNode.visibleToPublicUsers));
        buf.append(valueOrEmpty(node, AbstractNode.visibleToAuthenticatedUsers));

        // include owner in content hash generation!
        final Principal owner = node.getOwnerNode();
        if (owner != null) {

            buf.append(valueOrEmpty(owner, AbstractNode.name));
        }

        // include grants in content hash generation!
        for (final Security r : node.getSecurityRelationships()) {

            if (r != null) {

                buf.append(r.getSourceNode().getName());
                buf.append(r.getPermissions());
            }
        }

        // DOMNode
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "showConditions")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "hideConditions")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "showForLocales")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "hideForLocales")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "hideOnIndex")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "hideOnDetail")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "renderDetails")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMNode.class, "sharedComponentConfiguration")));

        if (node instanceof DOMNode) {

            final Page ownerDocument = ((DOMNode) node).getOwnerDocument();
            if (ownerDocument != null) {

                buf.append(valueOrEmpty(ownerDocument, AbstractNode.name));
            }
        }

        // DOMElement
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "dataKey")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "restQuery")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "cypherQuery")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "xpathQuery")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "functionQuery")));

        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-reload")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-confirm")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-action")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-attributes")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-attr")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-name")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-hide")));
        buf.append(valueOrEmpty(node, StructrApp.key(DOMElement.class, "data-structr-raw-value")));

        // Content
        buf.append(valueOrEmpty(node, StructrApp.key(Content.class, "contentType")));
        buf.append(valueOrEmpty(node, StructrApp.key(Content.class, "content")));

        // Page
        buf.append(valueOrEmpty(node, StructrApp.key(Page.class, "cacheForSeconds")));
        buf.append(valueOrEmpty(node, StructrApp.key(Page.class, "dontCache")));
        buf.append(valueOrEmpty(node, StructrApp.key(Page.class, "pageCreatesRawData")));
        buf.append(valueOrEmpty(node, StructrApp.key(Page.class, "position")));
        buf.append(valueOrEmpty(node, StructrApp.key(Page.class, "showOnErrorCodes")));

        // HTML attributes
        if (node instanceof DOMElement) {

            for (final PropertyKey key : ((DOMElement) node).getHtmlAttributes()) {

                buf.append(valueOrEmpty(node, key));
            }
        }

        for (final PropertyKey key : node.getPropertyKeys(PropertyView.All)) {

            if (!key.isPartOfBuiltInSchema()) {

                buf.append(valueOrEmpty(node, key));
            }
        }
    }

    private String valueOrEmpty(final GraphObject obj, final PropertyKey key) {

        final Object value = obj.getProperty(key);
        if (value != null) {

            return key.jsonName() + "=" + value.toString() + ";";
        }

        return "";
    }

    private <T extends Node> T createElement(final Page page, final DOMNode parent, final String tag,
            final String... content) {

        final T child = (T) page.createElement(tag);
        parent.appendChild((DOMNode) child);

        if (content != null && content.length > 0) {

            for (final String text : content) {

                final Node node = page.createTextNode(text);
                child.appendChild(node);
            }
        }

        return child;
    }

    private Template createTemplate(final Page page, final DOMNode parent, final String content)
            throws FrameworkException {

        final Template template = StructrApp.getInstance().create(Template.class,
                new NodeAttribute<>(StructrApp.key(Template.class, "content"), content),
                new NodeAttribute<>(StructrApp.key(Template.class, "ownerDocument"), page));

        if (parent != null) {
            parent.appendChild((DOMNode) template);
        }

        return template;
    }

    private <T> T createContent(final Page page, final DOMNode parent, final String content) {

        final T child = (T) page.createTextNode(content);
        parent.appendChild((DOMNode) child);

        return child;
    }

    private <T> T createComment(final Page page, final DOMNode parent, final String comment) {

        final T child = (T) page.createComment(comment);
        parent.appendChild((DOMNode) child);

        return child;
    }

    private <T> T createComponent(final DOMNode node) throws FrameworkException {
        return (T) new CreateComponentCommand().create(node);
    }

    private <T> T cloneComponent(final DOMNode node, final DOMNode parentNode) throws FrameworkException {
        return (T) new CloneComponentCommand().cloneComponent(node, parentNode);
    }

    private Folder getRootFolder(final Folder folder) {

        Folder parent = folder;
        boolean root = false;

        while (parent != null && !root) {

            if (parent.getParent() != null) {

                parent = parent.getParent();

            } else {

                root = true;
            }
        }

        return parent;
    }

    // ----- nested classes -----
    private static class DeletingFileVisitor implements FileVisitor<Path> {

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

            Files.delete(file);

            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {

            Files.delete(dir);

            return FileVisitResult.CONTINUE;
        }

    }
}