org.structr.web.entity.dom.DOMNode.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.web.entity.dom.DOMNode.java

Source

/**
 * Copyright (C) 2010-2016 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.entity.dom;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.httpclient.Header;
import org.apache.commons.lang3.StringUtils;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.Relationship;
import org.structr.common.Permission;
import org.structr.common.SecurityContext;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import static org.structr.core.GraphObject.id;
import org.structr.core.GraphObjectMap;
import org.structr.core.Predicate;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.LinkedTreeNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.notion.PropertyNotion;
import org.structr.core.parser.Functions;
import org.structr.core.property.BooleanProperty;
import org.structr.core.property.CollectionIdProperty;
import org.structr.core.property.EndNode;
import org.structr.core.property.EndNodes;
import org.structr.core.property.EntityIdProperty;
import org.structr.core.property.GenericProperty;
import org.structr.core.property.Property;
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.core.script.Scripting;
import org.structr.function.AddHeaderFunction;
import org.structr.function.CreateJarFileFunction;
import org.structr.function.FromCsvFunction;
import org.structr.function.FromJsonFunction;
import org.structr.function.FromXmlFunction;
import org.structr.function.GetFunction;
import org.structr.function.GetRequestHeaderFunction;
import org.structr.function.HeadFunction;
import org.structr.function.IncludeFunction;
import org.structr.function.IsLocaleFunction;
import org.structr.function.JarEntryFunction;
import org.structr.function.LogEventFunction;
import org.structr.function.ParseFunction;
import org.structr.function.PostFunction;
import org.structr.function.RenderFunction;
import org.structr.function.SetDetailsObjectFunction;
import org.structr.function.SetResponseHeaderFunction;
import org.structr.function.StripHtmlFunction;
import org.structr.function.ToJsonFunction;
import org.structr.web.common.GraphDataSource;
import org.structr.web.common.RenderContext;
import org.structr.web.common.RenderContext.EditMode;
import org.structr.web.datasource.CypherGraphDataSource;
import org.structr.web.datasource.FunctionDataSource;
import org.structr.web.datasource.IdRequestParameterGraphDataSource;
import org.structr.web.datasource.NodeGraphDataSource;
import org.structr.web.datasource.RestDataSource;
import org.structr.web.datasource.XPathGraphDataSource;
import org.structr.web.entity.LinkSource;
import org.structr.web.entity.Renderable;
import static org.structr.web.entity.dom.DOMNode.dataKey;
import static org.structr.web.entity.dom.DOMNode.ownerDocument;
import org.structr.web.entity.dom.relationship.DOMChildren;
import org.structr.web.entity.dom.relationship.DOMSiblings;
import org.structr.web.entity.relation.PageLink;
import org.structr.web.entity.relation.RenderNode;
import org.structr.web.entity.relation.Sync;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;

/**
 * Combines AbstractNode and org.w3c.dom.Node.
 *
 *
 */
public abstract class DOMNode extends LinkedTreeNode<DOMChildren, DOMSiblings, DOMNode>
        implements Node, Renderable, DOMAdoptable, DOMImportable {

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

    // ----- error messages for DOMExceptions -----
    protected static final String NO_MODIFICATION_ALLOWED_MESSAGE = "Permission denied.";
    protected static final String INVALID_ACCESS_ERR_MESSAGE = "Permission denied.";
    protected static final String INDEX_SIZE_ERR_MESSAGE = "Index out of range.";
    protected static final String CANNOT_SPLIT_TEXT_WITHOUT_PARENT = "Cannot split text element without parent and/or owner document.";
    protected static final String WRONG_DOCUMENT_ERR_MESSAGE = "Node does not belong to this document.";
    protected static final String HIERARCHY_REQUEST_ERR_MESSAGE_SAME_NODE = "A node cannot accept itself as a child.";
    protected static final String HIERARCHY_REQUEST_ERR_MESSAGE_ANCESTOR = "A node cannot accept its own ancestor as child.";
    protected static final String HIERARCHY_REQUEST_ERR_MESSAGE_DOCUMENT = "A document may only have one html element.";
    protected static final String HIERARCHY_REQUEST_ERR_MESSAGE_ELEMENT = "A document may only accept an html element as its document element.";
    protected static final String NOT_SUPPORTED_ERR_MESSAGE = "Node type not supported.";
    protected static final String NOT_FOUND_ERR_MESSAGE = "Node is not a child.";
    protected static final String NOT_SUPPORTED_ERR_MESSAGE_IMPORT_DOC = "Document nodes cannot be imported into another document.";
    protected static final String NOT_SUPPORTED_ERR_MESSAGE_ADOPT_DOC = "Document nodes cannot be adopted by another document.";
    protected static final String NOT_SUPPORTED_ERR_MESSAGE_RENAME = "Renaming of nodes is not supported by this implementation.";

    private static final List<GraphDataSource<List<GraphObject>>> listSources = new LinkedList<>();
    private Page cachedOwnerDocument;

    static {

        // register data sources
        listSources.add(new IdRequestParameterGraphDataSource("nodeId"));
        listSources.add(new RestDataSource());
        listSources.add(new NodeGraphDataSource());
        listSources.add(new FunctionDataSource());
        listSources.add(new CypherGraphDataSource());
        listSources.add(new XPathGraphDataSource());
    }
    public static final Property<String> dataKey = new StringProperty("dataKey").indexed();
    public static final Property<String> cypherQuery = new StringProperty("cypherQuery");
    public static final Property<String> xpathQuery = new StringProperty("xpathQuery");
    public static final Property<String> restQuery = new StringProperty("restQuery");
    public static final Property<String> functionQuery = new StringProperty("functionQuery");
    public static final Property<Boolean> renderDetails = new BooleanProperty("renderDetails");

    public static final Property<List<DOMNode>> syncedNodes = new EndNodes("syncedNodes", Sync.class,
            new PropertyNotion(id));
    public static final Property<DOMNode> sharedComponent = new StartNode("sharedComponent", Sync.class,
            new PropertyNotion(id));

    public static final Property<Boolean> hideOnIndex = new BooleanProperty("hideOnIndex").indexed();
    public static final Property<Boolean> hideOnDetail = new BooleanProperty("hideOnDetail").indexed();
    public static final Property<String> showForLocales = new StringProperty("showForLocales").indexed();
    public static final Property<String> hideForLocales = new StringProperty("hideForLocales").indexed();
    public static final Property<String> showConditions = new StringProperty("showConditions").indexed();
    public static final Property<String> hideConditions = new StringProperty("hideConditions").indexed();

    public static final Property<DOMNode> parent = new StartNode<>("parent", DOMChildren.class);
    public static final Property<String> parentId = new EntityIdProperty("parentId", parent);
    public static final Property<List<DOMNode>> children = new EndNodes<>("children", DOMChildren.class);
    public static final Property<List<String>> childrenIds = new CollectionIdProperty("childrenIds", children);
    public static final Property<DOMNode> previousSibling = new StartNode<>("previousSibling", DOMSiblings.class);
    public static final Property<DOMNode> nextSibling = new EndNode<>("nextSibling", DOMSiblings.class);
    public static final Property<String> nextSiblingId = new EntityIdProperty("nextSiblingId", nextSibling);

    public static final Property<Page> ownerDocument = new EndNode<>("ownerDocument", PageLink.class);
    public static final Property<String> pageId = new EntityIdProperty("pageId", ownerDocument);
    public static final Property<Boolean> isDOMNode = new BooleanProperty("isDOMNode").defaultValue(true)
            .readOnly();

    public static final Property<String> dataStructrIdProperty = new StringProperty("data-structr-id");
    public static final Property<String> dataHashProperty = new StringProperty("data-structr-hash");

    static {

        // extend set of builtin functions
        Functions.functions.put("render", new RenderFunction());
        Functions.functions.put("include", new IncludeFunction());
        Functions.functions.put("strip_html", new StripHtmlFunction());
        Functions.functions.put("POST", new PostFunction());
        Functions.functions.put("GET", new GetFunction());
        Functions.functions.put("HEAD", new HeadFunction());
        Functions.functions.put("parse", new ParseFunction());
        Functions.functions.put("to_json", new ToJsonFunction());
        Functions.functions.put("from_json", new FromJsonFunction());
        Functions.functions.put("from_csv", new FromCsvFunction());
        Functions.functions.put("from_xml", new FromXmlFunction());
        Functions.functions.put("add_header", new AddHeaderFunction());
        Functions.functions.put("set_response_header", new SetResponseHeaderFunction());
        Functions.functions.put("get_request_header", new GetRequestHeaderFunction());
        Functions.functions.put("log_event", new LogEventFunction());
        Functions.functions.put("is_locale", new IsLocaleFunction());
        Functions.functions.put("create_jar_file", new CreateJarFileFunction());
        Functions.functions.put("jar_entry", new JarEntryFunction());
        Functions.functions.put("set_details_object", new SetDetailsObjectFunction());
    }

    public abstract boolean isSynced();

    public abstract boolean contentEquals(final DOMNode otherNode);

    public abstract void updateFromNode(final DOMNode otherNode) throws FrameworkException;

    public String getIdHash() {

        return getUuid();

    }

    public String getIdHashOrProperty() {

        String idHash = getProperty(DOMNode.dataHashProperty);
        if (idHash == null) {

            idHash = getIdHash();
        }

        return idHash;
    }

    /**
     * This method will be called by the DOM logic when this node gets a new child. Override this method if you need to set properties on the child depending on its type etc.
     *
     * @param newChild
     */
    protected void handleNewChild(Node newChild) {

        final Page page = (Page) getOwnerDocument();

        for (final DOMNode child : getAllChildNodes()) {

            try {

                child.setProperty(ownerDocument, page);

            } catch (FrameworkException ex) {
                ex.printStackTrace();
            }

        }

    }

    @Override
    public Class<DOMChildren> getChildLinkType() {
        return DOMChildren.class;
    }

    @Override
    public Class<DOMSiblings> getSiblingLinkType() {
        return DOMSiblings.class;
    }

    // ----- public methods -----
    public List<DOMChildren> getChildRelationships() {
        return treeGetChildRelationships();
    }

    public String getPositionPath() {

        String path = "";

        DOMNode currentNode = this;
        while (currentNode.getParentNode() != null) {

            DOMNode parentNode = (DOMNode) currentNode.getParentNode();

            path = "/" + parentNode.treeGetChildPosition(currentNode) + path;

            currentNode = parentNode;

        }

        return path;

    }

    @Override
    public boolean onModification(SecurityContext securityContext, ErrorBuffer errorBuffer)
            throws FrameworkException {

        try {

            increasePageVersion();

        } catch (FrameworkException ex) {

            logger.log(Level.WARNING, "Updating page version failed", ex);

        }

        return isValid(errorBuffer);

    }

    /**
     * Render the node including data binding (outer rendering).
     *
     * @param renderContext
     * @param depth
     * @throws FrameworkException
     */
    @Override
    public void render(final RenderContext renderContext, final int depth) throws FrameworkException {

        if (!securityContext.isVisible(this)) {
            return;
        }

        final GraphObject details = renderContext.getDetailsDataObject();
        final boolean detailMode = details != null;

        if (detailMode && getProperty(hideOnDetail)) {
            return;
        }

        if (!detailMode && getProperty(hideOnIndex)) {
            return;
        }

        final EditMode editMode = renderContext.getEditMode(securityContext.getUser(false));

        if (EditMode.RAW.equals(editMode) || EditMode.WIDGET.equals(editMode)) {

            renderContent(renderContext, depth);

        } else {

            final String subKey = getProperty(dataKey);

            if (StringUtils.isNotBlank(subKey)) {

                setDataRoot(renderContext, this, subKey);

                final GraphObject currentDataNode = renderContext.getDataObject();

                // fetch (optional) list of external data elements
                final List<GraphObject> listData = checkListSources(securityContext, renderContext);

                final PropertyKey propertyKey;

                if (getProperty(renderDetails) && detailMode) {

                    renderContext.setDataObject(details);
                    renderContext.putDataObject(subKey, details);
                    renderContent(renderContext, depth);

                } else {

                    if (listData.isEmpty() && currentDataNode != null) {

                        // There are two alternative ways of retrieving sub elements:
                        // First try to get generic properties,
                        // if that fails, try to create a propertyKey for the subKey
                        final Object elements = currentDataNode.getProperty(new GenericProperty(subKey));
                        renderContext.setRelatedProperty(new GenericProperty(subKey));
                        renderContext.setSourceDataObject(currentDataNode);

                        if (elements != null) {

                            if (elements instanceof Iterable) {

                                for (Object o : (Iterable) elements) {

                                    if (o instanceof GraphObject) {

                                        GraphObject graphObject = (GraphObject) o;
                                        renderContext.putDataObject(subKey, graphObject);
                                        renderContent(renderContext, depth);

                                    }
                                }

                            }

                        } else {

                            propertyKey = StructrApp.getConfiguration()
                                    .getPropertyKeyForJSONName(currentDataNode.getClass(), subKey, false);
                            renderContext.setRelatedProperty(propertyKey);

                            if (propertyKey != null) {

                                final Object value = currentDataNode.getProperty(propertyKey);
                                if (value != null) {

                                    if (value instanceof Iterable) {

                                        for (final Object o : ((Iterable) value)) {

                                            if (o instanceof GraphObject) {

                                                renderContext.putDataObject(subKey, (GraphObject) o);
                                                renderContent(renderContext, depth);

                                            }
                                        }
                                    }
                                }
                            }

                        }

                        // reset data node in render context
                        renderContext.setDataObject(currentDataNode);
                        renderContext.setRelatedProperty(null);

                    } else {

                        renderContext.setListSource(listData);
                        renderNodeList(securityContext, renderContext, depth, subKey);

                    }

                }

            } else {

                renderContent(renderContext, depth);
            }
        }

    }

    public Template getClosestTemplate(final Page page) {

        DOMNode node = this;

        while (node != null) {

            if (node instanceof Template) {

                final Template template = (Template) node;

                Document doc = template.getOwnerDocument();

                if (doc == null) {

                    doc = node.getClosestPage();
                }

                if (doc != null && (page == null || doc.equals(page))) {

                    return template;

                }

                final List<DOMNode> _syncedNodes = template.getProperty(DOMNode.syncedNodes);

                for (final DOMNode syncedNode : _syncedNodes) {

                    doc = syncedNode.getOwnerDocument();

                    if (doc != null && (page == null || doc.equals(page))) {

                        return (Template) syncedNode;

                    }

                }

            }

            node = (DOMNode) node.getParentNode();

        }

        return null;

    }

    public Page getClosestPage() {

        DOMNode node = this;

        while (node != null) {

            if (node instanceof Page) {

                return (Page) node;
            }

            node = (DOMNode) node.getParentNode();

        }

        return null;
    }

    // ----- private methods -----
    /**
     * Get all ancestors of this node
     *
     * @return list of ancestors
     */
    private List<Node> getAncestors() {

        List<Node> ancestors = new ArrayList();

        Node _parent = getParentNode();
        while (_parent != null) {

            ancestors.add(_parent);
            _parent = _parent.getParentNode();
        }

        return ancestors;

    }

    // ----- protected methods -----
    protected void setDataRoot(final RenderContext renderContext, final AbstractNode node, final String dataKey) {
        // an outgoing RENDER_NODE relationship points to the data node where rendering starts
        for (RenderNode rel : node.getOutgoingRelationships(RenderNode.class)) {

            NodeInterface dataRoot = rel.getTargetNode();

            // set start node of this rendering to the data root node
            renderContext.putDataObject(dataKey, dataRoot);

            // allow only one data tree to be rendered for now
            break;
        }
    }

    protected void renderNodeList(SecurityContext securityContext, RenderContext renderContext, int depth,
            String dataKey) throws FrameworkException {

        final Iterable<GraphObject> listSource = renderContext.getListSource();
        if (listSource != null) {
            for (GraphObject dataObject : listSource) {

                // make current data object available in renderContext
                renderContext.putDataObject(dataKey, dataObject);

                renderContent(renderContext, depth + 1);

            }
            renderContext.clearDataObject(dataKey);
        }
    }

    protected void migrateSyncRels() {
        try {

            org.neo4j.graphdb.Node n = getNode();

            Iterable<Relationship> incomingSyncRels = n.getRelationships(DynamicRelationshipType.withName("SYNC"),
                    Direction.INCOMING);
            Iterable<Relationship> outgoingSyncRels = n.getRelationships(DynamicRelationshipType.withName("SYNC"),
                    Direction.OUTGOING);

            if (getOwnerDocument() instanceof ShadowDocument) {

                // We are a shared component and must not have any incoming SYNC rels
                for (Relationship r : incomingSyncRels) {
                    r.delete();
                }

            } else {

                for (Relationship r : outgoingSyncRels) {
                    r.delete();
                }

                for (Relationship r : incomingSyncRels) {

                    DOMElement possibleSharedComp = StructrApp.getInstance().get(DOMElement.class,
                            (String) r.getStartNode().getProperty("id"));

                    if (!(possibleSharedComp.getOwnerDocument() instanceof ShadowDocument)) {

                        r.delete();

                    }

                }
            }

        } catch (FrameworkException ex) {
            Logger.getLogger(DOMElement.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    protected List<GraphObject> checkListSources(final SecurityContext securityContext,
            final RenderContext renderContext) {

        // try registered data sources first
        for (GraphDataSource<List<GraphObject>> source : listSources) {

            try {

                List<GraphObject> graphData = source.getData(renderContext, this);
                if (graphData != null && !graphData.isEmpty()) {
                    return graphData;
                }

            } catch (FrameworkException fex) {

                fex.printStackTrace();

                logger.log(Level.WARNING, "Could not retrieve data from graph data source {0}: {1}",
                        new Object[] { source, fex });
            }
        }

        return Collections.EMPTY_LIST;
    }

    /**
     * Increase version of the page.
     *
     * A {@link Page} is a {@link DOMNode} as well, so we have to check 'this' as well.
     *
     * @throws FrameworkException
     */
    protected void increasePageVersion() throws FrameworkException {

        Page page = null;

        if (this instanceof Page) {

            page = (Page) this;

        } else {

            // ignore page-less nodes
            if (getProperty(DOMNode.parent) == null) {
                return;
            }
        }

        if (page == null) {

            final List<Node> ancestors = getAncestors();
            if (!ancestors.isEmpty()) {

                final DOMNode rootNode = (DOMNode) ancestors.get(ancestors.size() - 1);
                if (rootNode instanceof Page) {
                    page = (Page) rootNode;
                } else {
                    rootNode.increasePageVersion();
                }

            } else {

                final List<DOMNode> _syncedNodes = getProperty(DOMNode.syncedNodes);
                for (final DOMNode syncedNode : _syncedNodes) {

                    syncedNode.increasePageVersion();
                }
            }

        }

        if (page != null) {

            page.unlockReadOnlyPropertiesOnce();
            page.increaseVersion();

        }

    }

    protected boolean avoidWhitespace() {

        return false;

    }

    protected void checkIsChild(Node otherNode) throws DOMException {

        if (otherNode instanceof DOMNode) {

            Node _parent = otherNode.getParentNode();

            if (!isSameNode(_parent)) {

                throw new DOMException(DOMException.NOT_FOUND_ERR, NOT_FOUND_ERR_MESSAGE);
            }

            // validation successful
            return;
        }

        throw new DOMException(DOMException.NOT_SUPPORTED_ERR, NOT_SUPPORTED_ERR_MESSAGE);
    }

    protected void checkHierarchy(Node otherNode) throws DOMException {

        // we can only check DOMNodes
        if (otherNode instanceof DOMNode) {

            // verify that the other node is not this node
            if (isSameNode(otherNode)) {
                throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, HIERARCHY_REQUEST_ERR_MESSAGE_SAME_NODE);
            }

            // verify that otherNode is not one of the
            // the ancestors of this node
            // (prevent circular relationships)
            Node _parent = getParentNode();
            while (_parent != null) {

                if (_parent.isSameNode(otherNode)) {
                    throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
                            HIERARCHY_REQUEST_ERR_MESSAGE_ANCESTOR);
                }

                _parent = _parent.getParentNode();
            }

            // TODO: check hierarchy constraints imposed by the schema
            // validation sucessful
            return;
        }

        throw new DOMException(DOMException.NOT_SUPPORTED_ERR, NOT_SUPPORTED_ERR_MESSAGE);
    }

    protected void checkSameDocument(Node otherNode) throws DOMException {

        Document doc = getOwnerDocument();

        if (doc != null) {

            Document otherDoc = otherNode.getOwnerDocument();

            // Shadow doc is neutral
            if (otherDoc != null && !doc.equals(otherDoc) && !(doc instanceof ShadowDocument)) {

                throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, WRONG_DOCUMENT_ERR_MESSAGE);
            }

            if (otherDoc == null) {

                ((DOMNode) otherNode).doAdopt((Page) doc);

            }
        }
    }

    protected void checkWriteAccess() throws DOMException {

        if (!isGranted(Permission.write, securityContext)) {

            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, NO_MODIFICATION_ALLOWED_MESSAGE);
        }
    }

    protected void checkReadAccess() throws DOMException {

        if (securityContext.isVisible(this) || isGranted(Permission.read, securityContext)) {
            return;
        }

        throw new DOMException(DOMException.INVALID_ACCESS_ERR, INVALID_ACCESS_ERR_MESSAGE);
    }

    protected String indent(final int depth) {

        StringBuilder indent = new StringBuilder("\n");

        for (int d = 0; d < depth; d++) {

            indent.append("  ");

        }

        return indent.toString();
    }

    /**
     * Decide whether this node should be displayed for the given conditions string.
     *
     * @param renderContext
     * @return true if node should be displayed
     */
    protected boolean displayForConditions(final RenderContext renderContext) {

        // In raw or widget mode, render everything
        EditMode editMode = renderContext.getEditMode(securityContext.getUser(false));
        if (EditMode.RAW.equals(editMode) || EditMode.WIDGET.equals(editMode)) {
            return true;
        }

        String _showConditions = getProperty(DOMNode.showConditions);
        String _hideConditions = getProperty(DOMNode.hideConditions);

        // If both fields are empty, render node
        if (StringUtils.isBlank(_hideConditions) && StringUtils.isBlank(_showConditions)) {
            return true;
        }
        try {
            // If hide conditions evaluate to "true", don't render
            if (StringUtils.isNotBlank(_hideConditions) && Boolean.TRUE
                    .equals(Scripting.evaluate(renderContext, this, "${".concat(_hideConditions).concat("}")))) {
                return false;
            }

        } catch (FrameworkException ex) {
            logger.log(Level.SEVERE, "Hide conditions " + _hideConditions + " could not be evaluated.", ex);
        }
        try {
            // If show conditions evaluate to "false", don't render
            if (StringUtils.isNotBlank(_showConditions) && Boolean.FALSE
                    .equals(Scripting.evaluate(renderContext, this, "${".concat(_showConditions).concat("}")))) {
                return false;
            }

        } catch (FrameworkException ex) {
            logger.log(Level.SEVERE, "Show conditions " + _showConditions + " could not be evaluated.", ex);
        }

        return true;

    }

    /**
     * Decide whether this node should be displayed for the given locale settings.
     *
     * @param renderContext
     * @return true if node should be displayed
     */
    protected boolean displayForLocale(final RenderContext renderContext) {

        // In raw or widget mode, render everything
        EditMode editMode = renderContext.getEditMode(securityContext.getUser(false));
        if (EditMode.RAW.equals(editMode) || EditMode.WIDGET.equals(editMode)) {
            return true;
        }

        String localeString = renderContext.getLocale().toString();

        String show = getProperty(DOMNode.showForLocales);
        String hide = getProperty(DOMNode.hideForLocales);

        // If both fields are empty, render node
        if (StringUtils.isBlank(hide) && StringUtils.isBlank(show)) {
            return true;
        }

        // If locale string is found in hide, don't render
        if (StringUtils.contains(hide, localeString)) {
            return false;
        }

        // If locale string is found in hide, don't render
        if (StringUtils.isNotBlank(show) && !StringUtils.contains(show, localeString)) {
            return false;
        }

        return true;

    }

    protected String escapeForHtml(final String raw) {

        return StringUtils.replaceEach(raw, new String[] { "&", "<", ">" },
                new String[] { "&amp;", "&lt;", "&gt;" });

    }

    protected String escapeForHtmlAttributes(final String raw) {

        return StringUtils.replaceEach(raw, new String[] { "&", "<", ">", "\"", "'" },
                new String[] { "&amp;", "&lt;", "&gt;", "&quot;", "&#39;" });

    }

    protected void collectNodesByPredicate(Node startNode, DOMNodeList results, Predicate<Node> predicate,
            int depth, boolean stopOnFirstHit) {

        if (predicate.evaluate(securityContext, startNode)) {

            results.add(startNode);

            if (stopOnFirstHit) {

                return;
            }
        }

        NodeList _children = startNode.getChildNodes();
        if (_children != null) {

            int len = _children.getLength();
            for (int i = 0; i < len; i++) {

                Node child = _children.item(i);

                collectNodesByPredicate(child, results, predicate, depth + 1, stopOnFirstHit);
            }
        }
    }

    // ----- interface org.w3c.dom.Node -----
    @Override
    public String getTextContent() throws DOMException {

        final DOMNodeList results = new DOMNodeList();
        final TextCollector textCollector = new TextCollector();

        collectNodesByPredicate(this, results, textCollector, 0, false);

        return textCollector.getText();
    }

    @Override
    public void setTextContent(String textContent) throws DOMException {
        // TODO: implement?
    }

    @Override
    public Node getParentNode() {
        // FIXME: type cast correct here?
        return (Node) getProperty(parent);
    }

    @Override
    public NodeList getChildNodes() {
        checkReadAccess();
        return new DOMNodeList(treeGetChildren());
    }

    @Override
    public Node getFirstChild() {
        checkReadAccess();
        return treeGetFirstChild();
    }

    @Override
    public Node getLastChild() {
        return treeGetLastChild();
    }

    @Override
    public Node getPreviousSibling() {
        return listGetPrevious(this);
    }

    @Override
    public Node getNextSibling() {
        return listGetNext(this);
    }

    @Override
    public Document getOwnerDocument() {
        return getProperty(ownerDocument);
    }

    @Override
    public Node insertBefore(final Node newChild, final Node refChild) throws DOMException {

        // according to DOM spec, insertBefore with null refChild equals appendChild
        if (refChild == null) {

            return appendChild(newChild);
        }

        checkWriteAccess();

        checkSameDocument(newChild);
        checkSameDocument(refChild);

        checkHierarchy(newChild);
        checkHierarchy(refChild);

        if (newChild instanceof DocumentFragment) {

            // When inserting document fragments, we must take
            // care of the special case that the nodes already
            // have a NEXT_LIST_ENTRY relationship coming from
            // the document fragment, so we must first remove
            // the node from the document fragment and then
            // add it to the new parent.
            final DocumentFragment fragment = (DocumentFragment) newChild;
            Node currentChild = fragment.getFirstChild();

            while (currentChild != null) {

                // save next child in fragment list for later use
                Node savedNextChild = currentChild.getNextSibling();

                // remove child from document fragment
                fragment.removeChild(currentChild);

                // insert child into new parent
                insertBefore(currentChild, refChild);

                // next
                currentChild = savedNextChild;
            }

        } else {

            final Node _parent = newChild.getParentNode();
            if (_parent != null) {

                _parent.removeChild(newChild);
            }

            try {

                // do actual tree insertion here
                treeInsertBefore((DOMNode) newChild, (DOMNode) refChild);

            } catch (FrameworkException frex) {

                if (frex.getStatus() == 404) {

                    throw new DOMException(DOMException.NOT_FOUND_ERR, frex.getMessage());

                } else {

                    throw new DOMException(DOMException.INVALID_STATE_ERR, frex.getMessage());
                }
            }

            // allow parent to set properties in new child
            handleNewChild(newChild);
        }

        return refChild;
    }

    @Override
    public Node replaceChild(final Node newChild, final Node oldChild) throws DOMException {

        checkWriteAccess();

        checkSameDocument(newChild);
        checkSameDocument(oldChild);

        checkHierarchy(newChild);
        checkHierarchy(oldChild);

        if (newChild instanceof DocumentFragment) {

            // When inserting document fragments, we must take
            // care of the special case that the nodes already
            // have a NEXT_LIST_ENTRY relationship coming from
            // the document fragment, so we must first remove
            // the node from the document fragment and then
            // add it to the new parent.
            // replace indirectly using insertBefore and remove
            final DocumentFragment fragment = (DocumentFragment) newChild;
            Node currentChild = fragment.getFirstChild();

            while (currentChild != null) {

                // save next child in fragment list for later use
                final Node savedNextChild = currentChild.getNextSibling();

                // remove child from document fragment
                fragment.removeChild(currentChild);

                // add child to new parent
                insertBefore(currentChild, oldChild);

                // next
                currentChild = savedNextChild;
            }

            // finally, remove reference element
            removeChild(oldChild);

        } else {

            Node _parent = newChild.getParentNode();
            if (_parent != null && _parent instanceof DOMNode) {

                _parent.removeChild(newChild);
            }

            try {
                // replace directly
                treeReplaceChild((DOMNode) newChild, (DOMNode) oldChild);

            } catch (FrameworkException frex) {

                if (frex.getStatus() == 404) {

                    throw new DOMException(DOMException.NOT_FOUND_ERR, frex.getMessage());

                } else {

                    throw new DOMException(DOMException.INVALID_STATE_ERR, frex.getMessage());
                }
            }

            // allow parent to set properties in new child
            handleNewChild(newChild);
        }

        return oldChild;
    }

    @Override
    public Node removeChild(final Node node) throws DOMException {

        checkWriteAccess();
        checkSameDocument(node);
        checkIsChild(node);

        try {

            treeRemoveChild((DOMNode) node);

        } catch (FrameworkException fex) {

            throw new DOMException(DOMException.INVALID_STATE_ERR, fex.toString());
        }

        return node;
    }

    @Override
    public Node appendChild(final Node newChild) throws DOMException {

        checkWriteAccess();
        checkSameDocument(newChild);
        checkHierarchy(newChild);

        try {

            if (newChild instanceof DocumentFragment) {

                // When inserting document fragments, we must take
                // care of the special case that the nodes already
                // have a NEXT_LIST_ENTRY relationship coming from
                // the document fragment, so we must first remove
                // the node from the document fragment and then
                // add it to the new parent.
                // replace indirectly using insertBefore and remove
                final DocumentFragment fragment = (DocumentFragment) newChild;
                Node currentChild = fragment.getFirstChild();

                while (currentChild != null) {

                    // save next child in fragment list for later use
                    final Node savedNextChild = currentChild.getNextSibling();

                    // remove child from document fragment
                    fragment.removeChild(currentChild);

                    // append child to new parent
                    appendChild(currentChild);

                    // next
                    currentChild = savedNextChild;
                }

            } else {

                final Node _parent = newChild.getParentNode();

                if (_parent != null && _parent instanceof DOMNode) {
                    _parent.removeChild(newChild);
                }

                treeAppendChild((DOMNode) newChild);

                // allow parent to set properties in new child
                handleNewChild(newChild);
            }

        } catch (FrameworkException fex) {

            throw new DOMException(DOMException.INVALID_STATE_ERR, fex.toString());
        }

        return newChild;
    }

    @Override
    public boolean hasChildNodes() {
        return !getProperty(children).isEmpty();
    }

    @Override
    public Node cloneNode(boolean deep) {

        if (deep) {

            return cloneAndAppendChildren(securityContext, this);

        } else {

            final PropertyMap properties = new PropertyMap();

            for (Iterator<PropertyKey> it = getPropertyKeys(uiView.name()).iterator(); it.hasNext();) {

                final PropertyKey key = it.next();

                // omit system properties (except type), parent/children and page relationships
                if (key.equals(GraphObject.type) || (!key.isUnvalidated() && !key.equals(GraphObject.id)
                        && !key.equals(DOMNode.ownerDocument) && !key.equals(DOMNode.pageId)
                        && !key.equals(DOMNode.parent) && !key.equals(DOMNode.parentId)
                        && !key.equals(DOMElement.syncedNodes) && !key.equals(DOMNode.children)
                        && !key.equals(DOMNode.childrenIds))) {

                    properties.put(key, getProperty(key));
                }
            }

            // htmlView is necessary for the cloning of DOM nodes - otherwise some properties won't be cloned
            for (Iterator<PropertyKey> it = getPropertyKeys(DOMElement.htmlView.name()).iterator(); it.hasNext();) {

                final PropertyKey key = it.next();

                // omit system properties (except type), parent/children and page relationships
                if (key.equals(GraphObject.type) || (!key.isUnvalidated() && !key.equals(GraphObject.id)
                        && !key.equals(DOMNode.ownerDocument) && !key.equals(DOMNode.pageId)
                        && !key.equals(DOMNode.parent) && !key.equals(DOMNode.parentId)
                        && !key.equals(DOMElement.syncedNodes) && !key.equals(DOMNode.children)
                        && !key.equals(DOMNode.childrenIds))) {

                    properties.put(key, getProperty(key));
                }
            }

            if (this instanceof LinkSource) {

                final LinkSource linkSourceElement = (LinkSource) this;

                properties.put(LinkSource.linkable, linkSourceElement.getProperty(LinkSource.linkable));

            }

            final App app = StructrApp.getInstance(securityContext);

            try {
                final DOMNode node = app.create(getClass(), properties);

                return node;

            } catch (FrameworkException ex) {

                throw new DOMException(DOMException.INVALID_STATE_ERR, ex.toString());

            }

        }
    }

    @Override
    public boolean isSupported(String string, String string1) {
        return false;
    }

    @Override
    public String getNamespaceURI() {
        return null; //return "http://www.w3.org/1999/xhtml";
    }

    @Override
    public String getPrefix() {
        return null;
    }

    @Override
    public void setPrefix(String prefix) throws DOMException {
    }

    @Override
    public String getBaseURI() {
        return null;
    }

    @Override
    public short compareDocumentPosition(Node node) throws DOMException {
        return 0;
    }

    @Override
    public boolean isSameNode(Node node) {

        if (node != null && node instanceof DOMNode) {

            String otherId = ((DOMNode) node).getProperty(GraphObject.id);
            String ourId = getProperty(GraphObject.id);

            if (ourId != null && otherId != null && ourId.equals(otherId)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public String lookupPrefix(String string) {
        return null;
    }

    @Override
    public boolean isDefaultNamespace(String string) {
        return true;
    }

    @Override
    public String lookupNamespaceURI(String string) {
        return null;
    }

    @Override
    public boolean isEqualNode(Node node) {
        return equals(node);
    }

    @Override
    public Object getFeature(String string, String string1) {
        return null;
    }

    @Override
    public Object setUserData(String string, Object o, UserDataHandler udh) {
        return null;
    }

    @Override
    public Object getUserData(String string) {
        return null;
    }

    @Override
    public final void normalize() {

        Document document = getOwnerDocument();
        if (document != null) {

            // merge adjacent text nodes until there is only one left
            Node child = getFirstChild();
            while (child != null) {

                if (child instanceof Text) {

                    Node next = child.getNextSibling();
                    if (next != null && next instanceof Text) {

                        String text1 = child.getNodeValue();
                        String text2 = next.getNodeValue();

                        // create new text node
                        Text newText = document.createTextNode(text1.concat(text2));

                        removeChild(child);
                        insertBefore(newText, next);
                        removeChild(next);

                        child = newText;

                    } else {

                        // advance to next node
                        child = next;
                    }

                } else {

                    // advance to next node
                    child = child.getNextSibling();

                }
            }

            // recursively normalize child nodes
            if (hasChildNodes()) {

                Node currentChild = getFirstChild();
                while (currentChild != null) {

                    currentChild.normalize();
                    currentChild = currentChild.getNextSibling();
                }
            }
        }

    }

    // ----- interface DOMAdoptable -----
    @Override
    public Node doAdopt(final Page _page) throws DOMException {

        if (_page != null) {

            try {
                setProperty(ownerDocument, _page);

            } catch (FrameworkException fex) {

                throw new DOMException(DOMException.INVALID_STATE_ERR, fex.getMessage());

            }
        }

        return this;
    }

    public static GraphObjectMap extractHeaders(final Header[] headers) {

        final GraphObjectMap map = new GraphObjectMap();

        for (final Header header : headers) {

            map.put(new StringProperty(header.getName()), header.getValue());
        }

        return map;
    }

    // ----- static methods -----

    public static Set<DOMNode> getAllChildNodes(final DOMNode node) {

        Set<DOMNode> allChildNodes = new HashSet();

        getAllChildNodes(node, allChildNodes);

        return allChildNodes;
    }

    private static void getAllChildNodes(final DOMNode node, final Set<DOMNode> allChildNodes) {

        Node n = node.getFirstChild();

        while (n != null) {

            if (n instanceof DOMNode) {

                DOMNode domNode = (DOMNode) n;

                if (!allChildNodes.contains(domNode)) {

                    allChildNodes.add(domNode);
                    allChildNodes.addAll(getAllChildNodes(domNode));

                } else {

                    // break loop!
                    break;
                }
            }

            n = n.getNextSibling();
        }
    }

    /**
     * Recursively clone given node, all its direct children and connect the cloned child nodes to the clone parent node.
     *
     * @param securityContext
     * @param nodeToClone
     * @return
     */
    public static DOMNode cloneAndAppendChildren(final SecurityContext securityContext, final DOMNode nodeToClone) {

        final DOMNode newNode = (DOMNode) nodeToClone.cloneNode(false);

        final List<DOMNode> childrenToClone = (List<DOMNode>) nodeToClone.getChildNodes();

        for (final DOMNode childNodeToClone : childrenToClone) {

            final DOMNode newChildNode = (DOMNode) cloneAndAppendChildren(securityContext, childNodeToClone);
            newNode.appendChild(newChildNode);

        }

        return newNode;
    }

    // ----- interface Syncable -----
    @Override
    public List<GraphObject> getSyncData() throws FrameworkException {

        final List<GraphObject> data = super.getSyncData();

        // nodes
        data.addAll(getProperty(DOMNode.children));

        final DOMNode sibling = getProperty(DOMNode.nextSibling);
        if (sibling != null) {

            data.add(sibling);
        }

        // relationships
        for (final DOMChildren child : getOutgoingRelationships(DOMChildren.class)) {
            data.add(child);
        }

        final DOMSiblings siblingRel = getOutgoingRelationship(DOMSiblings.class);
        if (siblingRel != null) {

            data.add(siblingRel);
        }

        // for template nodes
        data.add(getProperty(DOMNode.sharedComponent));
        data.add(getIncomingRelationship(Sync.class));

        // add parent page
        data.add(getProperty(ownerDocument));
        data.add(getOutgoingRelationship(PageLink.class));

        // add parent element
        data.add(getProperty(DOMNode.parent));
        data.add(getIncomingRelationship(DOMChildren.class));

        return data;
    }

    // ----- nested classes -----
    protected static class TextCollector implements Predicate<Node> {

        private StringBuilder textBuffer = new StringBuilder(200);

        @Override
        public boolean evaluate(SecurityContext securityContext, Node... obj) {

            if (obj[0] instanceof Text) {
                textBuffer.append(((Text) obj[0]).getTextContent());
            }

            return false;
        }

        public String getText() {
            return textBuffer.toString();
        }
    }

    protected static class TagPredicate implements Predicate<Node> {

        private String tagName = null;

        public TagPredicate(String tagName) {
            this.tagName = tagName;
        }

        @Override
        public boolean evaluate(SecurityContext securityContext, Node... obj) {

            if (obj[0] instanceof DOMElement) {

                DOMElement elem = (DOMElement) obj[0];

                if (tagName.equals(elem.getProperty(DOMElement.tag))) {
                    return true;
                }
            }

            return false;
        }
    }

    // ----- private methods -----
    public static String objectToString(final Object source) {

        if (source != null) {
            return source.toString();
        }

        return null;
    }

    /**
     * Returns the owner document of this DOMNode, following an OUTGOING "PAGE" relationship.
     *
     * @return the owner node of this node
     */
    public Document getOwnerDocumentAsSuperUser() {

        if (cachedOwnerDocument == null) {

            final PageLink ownership = getOutgoingRelationshipAsSuperUser(PageLink.class);
            if (ownership != null) {

                Page page = ownership.getTargetNode();
                cachedOwnerDocument = page;
            }
        }

        return cachedOwnerDocument;
    }
}