org.structr.common.BasicTest.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.common.BasicTest.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 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.structr.common;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
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.api.NotFoundException;
import org.structr.api.NotInTransactionException;
import org.structr.api.graph.RelationshipType;
import org.structr.api.util.FixedSizeCache;
import org.structr.api.util.Iterables;
import org.structr.bolt.wrapper.NodeWrapper;
import org.structr.bolt.wrapper.RelationshipWrapper;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.Result;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AOneToOne;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.AbstractRelationship;
import org.structr.core.entity.BOneToOne;
import org.structr.core.entity.DynamicResourceAccess;
import org.structr.core.entity.GenericNode;
import org.structr.core.entity.GenericRelationship;
import org.structr.core.entity.Group;
import org.structr.core.entity.Localization;
import org.structr.core.entity.Location;
import org.structr.core.entity.OneThreeOneToOne;
import org.structr.core.entity.OneTwoOneToOne;
import org.structr.core.entity.Principal;
import org.structr.core.entity.Relation;
import org.structr.core.entity.ResourceAccess;
import org.structr.core.entity.SchemaNode;
import org.structr.core.entity.SchemaRelationshipNode;
import org.structr.core.entity.Security;
import org.structr.core.entity.SixOneManyToMany;
import org.structr.core.entity.SixOneOneToOne;
import org.structr.core.entity.SixThreeOneToMany;
import org.structr.core.entity.TestFour;
import org.structr.core.entity.TestNine;
import org.structr.core.entity.TestOne;
import org.structr.core.entity.TestSeven;
import org.structr.core.entity.TestSix;
import org.structr.core.entity.TestTen;
import org.structr.core.entity.TestThirteen;
import org.structr.core.entity.TestThree;
import org.structr.core.entity.TestTwo;
import org.structr.core.entity.relationship.NodeHasLocation;
import org.structr.core.entity.relationship.PrincipalOwnsNode;
import org.structr.core.graph.NodeAttribute;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.NodeServiceCommand;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.core.property.StringProperty;

/**
 *
 */
public class BasicTest extends StructrTest {

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

    @Test
    public void test00SimpleCreateOperation() {

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

            final PropertyMap properties = new PropertyMap();

            properties.put(TestSix.name, "name");

            // test null value for a 1:1 related property
            properties.put(TestSix.oneToOneTestThree, null);

            app.create(TestSix.class, properties);

            tx.success();

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

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

            final TestSix test = app.nodeQuery(TestSix.class).getFirst();

            assertNotNull("Invalid simple object creation result", test);
            assertEquals("Invalid simple object creation result", "name", test.getProperty(AbstractNode.name));
            assertEquals("Invalid simple object creation result", null,
                    test.getProperty(TestSix.oneToOneTestThree));

            tx.success();

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

    /**
     * Test successful deletion of a node.
     *
     * The node shouldn't be found afterwards.
     * Creation and deletion are executed in two different transactions.
     *
     */
    @Test
    public void test01DeleteNode() {

        try {

            final PropertyMap props = new PropertyMap();
            final String type = "GenericNode";
            final String name = "GenericNode-name";
            NodeInterface node = null;
            String uuid = null;

            props.put(AbstractNode.type, type);
            props.put(AbstractNode.name, name);

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

                node = app.create(GenericNode.class, props);
                tx.success();
            }

            assertTrue(node != null);

            try (final Tx tx = app.tx()) {
                uuid = node.getUuid();
            }

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

                app.delete(node);
                tx.success();
            }

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

                Result result = app.nodeQuery().uuid(uuid).getResult();

                assertEquals("Node should have been deleted", 0, result.size());

            } catch (FrameworkException fe) {
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    @Test
    public void test01DeleteRelationship() {

        try {

            final TestOne testOne = createTestNode(TestOne.class);
            final TestSix testSix = createTestNode(TestSix.class);
            SixOneOneToOne rel = null;

            assertNotNull(testOne);
            assertNotNull(testSix);

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

                rel = app.create(testSix, testOne, SixOneOneToOne.class);
                tx.success();
            }

            assertNotNull(rel);

            try {
                // try to delete relationship
                rel.getRelationship().delete(true);

                fail("Should have raised an org.neo4j.graphdb.NotInTransactionException");
            } catch (NotInTransactionException e) {
            }

            // Relationship still there
            assertNotNull(rel);

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

                app.delete(rel);
                tx.success();
            }

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

                String uuid = rel.getUuid();
                fail("Deleted entity should have thrown an exception on access.");

            } catch (NotFoundException iex) {
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    /**
     * DELETE_NONE should not trigger any delete cascade
     */
    @Test
    public void test03CascadeDeleteNone() {

        /* this test is flawed in that it expects the cascading
         * not to take place but expects to run without an
         * exception, but deleting one node of the two will leave
         * the other (TestTwo) in an invalid state according
         * to its isValid() method!
        try {
            
           // Create a relationship with DELETE_NONE
           AbstractRelationship rel = cascadeRel(TestOne.class, TestTwo.class, Relation.DELETE_NONE);
           AbstractNode startNode   = rel.getStartNode();
           AbstractNode endNode     = rel.getEndNode();
           final String startNodeId = startNode.getUuid();
           final String endNodeId   = endNode.getUuid();
           boolean exception        = false;
            
           deleteCascade(startNode);
           assertNodeNotFound(startNodeId);
           assertNodeExists(endNodeId);
            
           // Create another relationship with DELETE_NONE
           rel = cascadeRel(TestOne.class, TestTwo.class, Relation.DELETE_NONE);
            
           try {
        deleteCascade(rel.getEndNode());
            
           } catch (FrameworkException fex) {
            
        assertEquals(422, fex.getStatus());
        exception = true;
           }
            
           assertTrue("Exception should be raised", exception);
            
        } catch (FrameworkException ex) {
            
           logger.warn("", ex);
            
           logger.error(ex.toString());
           fail("Unexpected exception");
            
        }
         */
    }

    /**
     * DELETE_INCOMING should not trigger delete cascade from start to end node,
     * but from end to start node
     */
    @Test
    public void test04CascadeDeleteIncoming() {

        /* this test is flawed in that it expects the cascading
         * not to take place but expectes to run without an
         * exception, but deleting one node of the two will leave
         * the other (TestTwo) in an invalid state according
         * to its isValid() method!
        try {
            
           // Create a relationship with DELETE_INCOMING
           AbstractRelationship rel = cascadeRel(TestOne.class, TestTwo.class, Relation.DELETE_INCOMING);
           final String startNodeId = rel.getStartNode().getUuid();
           final String endNodeId   = rel.getEndNode().getUuid();
           boolean exception        = false;
            
           deleteCascade(rel.getStartNode());
            
           // Start node should not be found after deletion
           assertNodeNotFound(startNodeId);
            
           // End node should be found after deletion of start node
           assertNodeExists(endNodeId);
            
           // Create another relationship with DELETE_INCOMING
           rel = cascadeRel(TestOne.class, TestTwo.class, Relation.DELETE_INCOMING);
            
           try {
        deleteCascade(rel.getEndNode());
            
           } catch (FrameworkException fex) {
            
        assertEquals(422, fex.getStatus());
        exception = true;
           }
            
           assertTrue("Exception should be raised", exception);
            
        } catch (FrameworkException ex) {
            
           logger.warn("", ex);
            
           logger.error(ex.toString());
           fail("Unexpected exception");
            
        }
        */
    }

    /**
     * DELETE_OUTGOING should trigger delete cascade from start to end node,
     * but not from end to start node.
     */
    @Test
    public void test05CascadeDeleteOutgoing() {

        try {

            // Create a relationship with DELETE_OUTGOING
            AbstractRelationship rel = cascadeRel(TestOne.class, TestTwo.class, Relation.SOURCE_TO_TARGET);
            NodeInterface sourceNode;
            NodeInterface targetNode;
            String startNodeId;
            String endNodeId;

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                sourceNode = rel.getSourceNode();
            }

            deleteCascade(sourceNode);

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

                // Start node should not be found after deletion
                assertNodeNotFound(startNodeId);

                // End node should not be found after deletion
                assertNodeNotFound(endNodeId);
            }

            // Create another relationship with DELETE_OUTGOING
            rel = cascadeRel(TestOne.class, TestTwo.class, Relation.SOURCE_TO_TARGET);

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                targetNode = rel.getTargetNode();
            }

            deleteCascade(targetNode);

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

                // End node should not be found after deletion
                assertNodeNotFound(endNodeId);

                // Start node should still exist deletion of end node
                assertNodeExists(startNodeId);
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    /**
     * DELETE_INCOMING + DELETE_OUTGOING should trigger delete cascade from start to end node
     * and from end node to start node
     */
    @Test
    public void test06CascadeDeleteBidirectional() {

        try {

            // Create a relationship with DELETE_INCOMING
            AbstractRelationship rel = cascadeRel(TestOne.class, TestTwo.class,
                    Relation.TARGET_TO_SOURCE | Relation.SOURCE_TO_TARGET);
            NodeInterface sourceNode;
            NodeInterface targetNode;
            String startNodeId;
            String endNodeId;

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                sourceNode = rel.getSourceNode();
            }

            deleteCascade(sourceNode);

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

                // Start node should not be found after deletion
                assertNodeNotFound(startNodeId);

                // End node should not be found after deletion of start node
                assertNodeNotFound(endNodeId);
            }

            // Create a relationship with DELETE_INCOMING
            rel = cascadeRel(TestOne.class, TestTwo.class, Relation.TARGET_TO_SOURCE | Relation.SOURCE_TO_TARGET);

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                targetNode = rel.getTargetNode();
            }

            deleteCascade(targetNode);

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

                // End node should not be found after deletion
                assertNodeNotFound(endNodeId);

                // Start node should not be found after deletion of end node
                assertNodeNotFound(startNodeId);
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    /**
     * DELETE_IF_CONSTRAINT_WOULD_BE_VIOLATED should
     * trigger delete cascade from start to end node only
     * if the remote node would not be valid afterwards
     */
    @Test
    public void test07CascadeDeleteConditional() {

        try {

            AbstractRelationship rel = cascadeRel(TestOne.class, TestTwo.class, Relation.CONSTRAINT_BASED);
            NodeInterface sourceNode;
            String startNodeId;
            String endNodeId;

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                sourceNode = rel.getSourceNode();
            }

            deleteCascade(sourceNode);

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

                // Start node should be deleted
                assertNodeNotFound(startNodeId);

                // End node should be deleted
                assertNodeNotFound(endNodeId);
            }

            rel = cascadeRel(TestOne.class, TestThree.class, Relation.CONSTRAINT_BASED);

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                sourceNode = rel.getSourceNode();
            }

            deleteCascade(sourceNode);

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

                // Start node should be deleted
                assertNodeNotFound(startNodeId);

                // End node should still be there
                assertNodeExists(endNodeId);
            }

            rel = cascadeRel(TestOne.class, TestFour.class, Relation.CONSTRAINT_BASED);

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

                startNodeId = rel.getSourceNode().getUuid();
                endNodeId = rel.getTargetNode().getUuid();
                sourceNode = rel.getSourceNode();
            }

            deleteCascade(sourceNode);

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

                // Start node should be deleted
                assertNodeNotFound(startNodeId);

                // End node should still be there
                assertNodeExists(endNodeId);
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    @Test
    public void test08OverlappingDeleteCascades() {

        /*
         * This test creates a ternary tree of depth 2 (39 nodes)
         * linked with relationship type "TEN", with two additional
         * links throughout the tree. It creates a situation where
         * two delete cascades overlap when a node is deleted and
         * tests the correct handling of such a situation.
         *
         *           1-------+       1->2, 1->3, 1->4, 1->D
         *         / | \     |
         *        /  |  \    |
         *       2   3   4  /
         *      /|\ /|\ /|\/
         *      567 89A BCD
         */

        try {

            final List<TestTen> rootNodes = new LinkedList<>();
            final List<TestTen> allChildren = new LinkedList<>();
            final List<TestTen> allGrandChildren = new LinkedList<>();

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

                // create some nodes..
                rootNodes.addAll(createTestNodes(TestTen.class, 3));

                for (final TestTen node : rootNodes) {

                    final List<TestTen> children = createTestNodes(TestTen.class, 3);
                    node.setProperty(TestTen.tenTenChildren, children);

                    for (final TestTen child : children) {

                        final List<TestTen> grandChildren = createTestNodes(TestTen.class, 3);
                        child.setProperty(TestTen.tenTenChildren, grandChildren);

                        allGrandChildren.addAll(grandChildren);
                    }

                    allChildren.addAll(children);
                }

                // create some additional links off a different type but with cascading delete
                rootNodes.get(0).setProperty(TestTen.testChild, allGrandChildren.get(0));
                allChildren.get(0).setProperty(TestTen.testChild, allGrandChildren.get(1));

                tx.success();
            }

            // check preconditions: exactly 39 nodes should exist
            try (final Tx tx = app.tx()) {

                assertEquals("Wrong number of nodes", 39, app.nodeQuery(TestTen.class).getAsList().size());
                tx.success();
            }

            // delete one root node
            try (final Tx tx = app.tx()) {

                app.delete(rootNodes.get(0));
                tx.success();
            }

            // check conditions after deletion, 26 nodes shoud exist
            try (final Tx tx = app.tx()) {

                assertEquals("Wrong number of nodes", 26, app.nodeQuery(TestTen.class).getAsList().size());
                tx.success();
            }

        } catch (FrameworkException ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    @Test
    public void test01CreateNode() {

        try {

            try {

                // Create node out of transaction => should give a NotInTransactionException
                app.create(TestOne.class);
                fail("Should have raised a NotInTransactionException");
            } catch (NotInTransactionException e) {
            }

            try {

                // Try to create node without parameters => should fail
                app.create(TestOne.class);
                fail("Should have raised a NotInTransactionException");
            } catch (NotInTransactionException e) {
            }

            AbstractNode node = null;

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

                node = app.create(TestOne.class);
                tx.success();
            }

            assertTrue(node != null);
            assertTrue(node instanceof TestOne);

        } catch (FrameworkException ex) {

            logger.error("", ex);
            fail("Unexpected exception");

        }

    }

    @Test
    public void test02CreateNodeWithExistingUuid() {

        try {

            final PropertyMap props = new PropertyMap();
            TestOne node = null;

            final String uuid = StringUtils.replace(UUID.randomUUID().toString(), "-", "");

            props.put(GraphObject.id, uuid);

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

                node = app.create(TestOne.class, props);
                tx.success();
            }

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

                assertTrue(node != null);
                assertTrue(node instanceof TestOne);
                assertEquals(node.getUuid(), uuid);
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }
    }

    @Test
    public void test02CreateTwoNodesWithSameUuidInSameTx() {

        try {

            final PropertyMap props = new PropertyMap();
            TestOne node = null;

            final String uuid = StringUtils.replace(UUID.randomUUID().toString(), "-", "");

            props.put(GraphObject.id, uuid);

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

                node = app.create(TestOne.class, props);

                assertTrue(node != null);
                assertTrue(node instanceof TestOne);
                assertEquals(node.getUuid(), uuid);

                node = app.create(TestOne.class, props);

                tx.success();

                fail("Validation failed!");
            }

        } catch (FrameworkException ex) {
        }

    }

    @Test
    public void test02CreateTwoNodesWithSameUuidInTwoTx() {

        try {

            final PropertyMap props = new PropertyMap();
            TestOne node = null;

            final String uuid = StringUtils.replace(UUID.randomUUID().toString(), "-", "");

            props.put(GraphObject.id, uuid);

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

                node = app.create(TestOne.class, props);

                assertTrue(node != null);
                assertTrue(node instanceof TestOne);
                assertEquals(node.getUuid(), uuid);

                tx.success();

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

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

                node = app.create(TestOne.class, props);

                tx.success();

                fail("Validation failed!");
            }

        } catch (FrameworkException ex) {

            // validate exception
            ex.printStackTrace();
        }

    }

    @Test
    public void test03CreateRelationship() {

        try {

            final List<GenericNode> nodes = createTestNodes(GenericNode.class, 2);
            final NodeInterface startNode = nodes.get(0);
            final NodeInterface endNode = nodes.get(1);
            NodeHasLocation rel = null;

            assertTrue(startNode != null);
            assertTrue(endNode != null);

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

                rel = app.create(startNode, endNode, NodeHasLocation.class);
                tx.success();
            }

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

                assertEquals(startNode.getUuid(), rel.getSourceNodeId());
                assertEquals(endNode.getUuid(), rel.getTargetNodeId());
                assertEquals(RelType.IS_AT.name(), rel.getType());
                assertEquals(NodeHasLocation.class, rel.getClass());
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    /**
     * Create a node for each configured entity class and check the type
     */
    @Test
    public void test04CheckNodeEntities() {

        AccessControlTest.clearResourceAccess();

        final PropertyMap props = new PropertyMap();

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

            List<Class> entityList = Collections.EMPTY_LIST;

            try {

                entityList = getClasses("org.structr.core.entity");

            } catch (IOException | ClassNotFoundException ex) {

                logger.error("", ex);
            }

            assertTrue(entityList.contains(AbstractNode.class));
            assertTrue(entityList.contains(GenericNode.class));
            assertTrue(entityList.contains(Location.class));
            assertTrue(entityList.contains(ResourceAccess.class));

            // Don't test these, it would fail due to violated constraints
            entityList.remove(TestTwo.class);
            entityList.remove(TestNine.class);
            entityList.remove(SchemaNode.class);
            entityList.remove(SchemaRelationshipNode.class);

            for (Class type : entityList) {

                // for (Entry<String, Class> entity : entities.entrySet()) {
                // Class entityClass = entity.getValue();
                if (AbstractNode.class.isAssignableFrom(type)) {

                    props.clear();

                    // For Group, fill mandatory fields
                    if (type.equals(Group.class)) {

                        props.put(Group.name, "Group-0");

                    }

                    // For TestSeven, fill mandatory fields
                    if (type.equals(TestSeven.class)) {

                        props.put(TestSeven.name, "TestSeven-0");

                    }

                    // For TestThirteen, fill mandatory fields
                    if (type.equals(TestThirteen.class)) {

                        props.put(TestThirteen.notNull, "some-value");

                    }

                    // For ResourceAccess, fill mandatory fields
                    if (type.equals(ResourceAccess.class)) {

                        props.put(ResourceAccess.signature, "/X");
                        props.put(ResourceAccess.flags, 6L);

                    }

                    // For DynamicResourceAccess, fill mandatory fields
                    if (type.equals(DynamicResourceAccess.class)) {

                        props.put(DynamicResourceAccess.signature, "/Y");
                        props.put(DynamicResourceAccess.flags, 6L);

                    }

                    // For Localization, fill mandatory fields
                    if (type.equals(Localization.class)) {

                        /*
                        props.put(Localization.name, "localizationKey");
                        props.put(Localization.locale, "de_DE");
                        */
                    }

                    // For Location, set coordinates
                    if (type.equals(Location.class)) {

                        props.put(StructrApp.key(Location.class, "latitude"), 12.34);
                        props.put(StructrApp.key(Location.class, "longitude"), 56.78);

                    }

                    logger.info("Creating node of type {}", type);

                    NodeInterface node = app.create(type, props);

                    assertTrue(type.getSimpleName().equals(node.getProperty(AbstractNode.type)));

                    // Remove mandatory fields for ResourceAccess from props map
                    if (type.equals(ResourceAccess.class)) {

                        props.remove(ResourceAccess.signature);
                        props.remove(ResourceAccess.flags);

                    }

                }
            }

            tx.success();

        } catch (FrameworkException ex) {

            logger.error(ex.toString());

            logger.warn("", ex);

            fail("Unexpected exception");
        }
    }

    /**
     * Create a node for each configured entity class and check the type
     */
    @Test
    public void test05CheckRelationshipEntities() {

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

            List<Class> entityList = null;

            try {

                entityList = getClasses("org.structr.core.entity");

            } catch (IOException | ClassNotFoundException ex) {

                logger.error("Unable to get list of entity classes", ex);
            }

            assertTrue(entityList.contains(AbstractRelationship.class));
            assertTrue(entityList.contains(GenericRelationship.class));

            for (Class entityClass : entityList) {

                // for (Entry<String, Class> entity : entities.entrySet()) {
                // Class entityClass = entity.getValue();
                if (AbstractRelationship.class.isAssignableFrom(entityClass)) {

                    String type = entityClass.getSimpleName();

                    logger.info("Creating relationship of type {}", type);

                    List<GenericNode> nodes = createTestNodes(GenericNode.class, 2);
                    final NodeInterface startNode = nodes.get(0);
                    final NodeInterface endNode = nodes.get(1);
                    final RelationshipType relType = RelType.IS_AT;
                    NodeHasLocation rel = app.create(startNode, endNode, NodeHasLocation.class);

                    assertTrue(rel != null);
                    assertTrue(rel.getType().equals(relType.name()));

                }
            }

            tx.success();

        } catch (Throwable ex) {

            logger.warn("", ex);

            logger.error(ex.toString());
            fail("Unexpected exception");
        }
    }

    @Test
    public void test06DuplicateRelationshipsOneToOne() {

        // Creating duplicate one-to-one relationships
        // is silently ignored, the relationship will
        // be replaced.

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

            final TestOne test1 = app.create(TestOne.class);
            final TestTwo test2 = app.create(TestTwo.class);

            // test duplicate prevention
            app.create(test1, test2, OneTwoOneToOne.class);
            app.create(test1, test2, OneTwoOneToOne.class);

            tx.success();

        } catch (FrameworkException ex) {

            fail("Creating duplicate relationships via app.create() should NOT throw an exception.");
        }

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

            final TestOne test1 = app.create(TestOne.class);
            final TestTwo test2 = app.create(TestTwo.class);

            test1.setProperty(TestOne.testTwo, test2);
            test1.setProperty(TestOne.testTwo, test2);

            tx.success();

        } catch (FrameworkException ex) {

            fail("Creating duplicate relationships via setProperty() should NOT throw an exception.");
        }
    }

    @Test
    public void test06DuplicateRelationshipsOneToMany() {

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

            final TestSix test1 = app.create(TestSix.class);
            final TestThree test2 = app.create(TestThree.class);

            // test duplicate prevention
            app.create(test1, test2, SixThreeOneToMany.class);
            app.create(test1, test2, SixThreeOneToMany.class);

            tx.success();

        } catch (FrameworkException ex) {

            fail("Creating duplicate relationships via app.create() should NOT throw an exception.");
        }

        // second test via setProperty will silently ignore
        // the duplicates in the list
        try (final Tx tx = app.tx()) {

            final TestSix test1 = app.create(TestSix.class);
            final TestThree test2 = app.create(TestThree.class);

            // test duplicate prevention
            final List<TestThree> list = new LinkedList<>();

            list.add(test2);
            list.add(test2);

            test1.setProperty(TestSix.oneToManyTestThrees, list);

            tx.success();

        } catch (FrameworkException ex) {

            fail("Creating duplicate relationships via setProperty() should NOT throw an exception.");
        }
    }

    @Test
    public void test06DuplicateRelationshipsManyToMany() {

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

            final TestSix test1 = app.create(TestSix.class);
            final TestOne test2 = app.create(TestOne.class);

            // test duplicate prevention
            app.create(test1, test2, SixOneManyToMany.class);
            app.create(test1, test2, SixOneManyToMany.class);

            fail("Creating duplicate relationships should throw an exception.");

            tx.success();

        } catch (FrameworkException ex) {
        }

        // second test via setProperty() should throw an exception
        // for manyToMany only.
        try (final Tx tx = app.tx()) {

            final TestSix test1 = app.create(TestSix.class);
            final TestOne test2 = app.create(TestOne.class);

            // test duplicate prevention
            final List<TestOne> list = new LinkedList<>();

            list.add(test2);
            list.add(test2);

            test1.setProperty(TestSix.manyToManyTestOnes, list);

            tx.success();

        } catch (FrameworkException ex) {

            fail("Creating duplicate relationships via setProperty() should NOT throw an exception.");
        }
    }

    @Test
    public void test01ModifyNode() {

        try {

            final PropertyMap props = new PropertyMap();
            final String type = "UnknownTestType";
            final String name = "GenericNode-name";

            NodeInterface node = null;

            props.put(AbstractNode.type, type);
            props.put(AbstractNode.name, name);

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

                node = app.create(GenericNode.class, props);
                tx.success();
            }

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

                // Check defaults
                assertEquals(GenericNode.class.getSimpleName(), node.getProperty(AbstractNode.type));
                assertTrue(node.getProperty(AbstractNode.name).equals(name));
                assertTrue(!node.getProperty(AbstractNode.hidden));
                assertTrue(!node.getProperty(AbstractNode.visibleToAuthenticatedUsers));
                assertTrue(!node.getProperty(AbstractNode.visibleToPublicUsers));
            }

            final String name2 = "GenericNode-name-";

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

                // Modify values
                node.setProperty(AbstractNode.name, name2);
                node.setProperty(AbstractNode.hidden, true);
                node.setProperty(AbstractNode.visibleToAuthenticatedUsers, true);
                node.setProperty(AbstractNode.visibleToPublicUsers, true);

                tx.success();
            }

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

                assertTrue(node.getProperty(AbstractNode.name).equals(name2));
                assertTrue(node.getProperty(AbstractNode.hidden));
                assertTrue(node.getProperty(AbstractNode.visibleToAuthenticatedUsers));
                assertTrue(node.getProperty(AbstractNode.visibleToPublicUsers));
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    /**
    * Test the results of setProperty and getProperty of a relationship
    */
    @Test
    public void test02ModifyRelationship() {

        try {

            final NodeHasLocation rel = (createTestRelationships(NodeHasLocation.class, 1)).get(0);
            final PropertyKey key1 = new StringProperty("jghsdkhgshdhgsdjkfgh");
            final String val1 = "54354354546806849870";

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

                rel.setProperty(key1, val1);
                tx.success();
            }

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

                assertTrue("Expected relationship to have a value for key '" + key1.dbName() + "'",
                        rel.getRelationship().hasProperty(key1.dbName()));

                assertEquals(val1, rel.getRelationship().getProperty(key1.dbName()));

                Object vrfy1 = rel.getProperty(key1);
                assertEquals(val1, vrfy1);
            }

            final String val2 = "ljkhoh8osdfhodhi";

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

                rel.setProperty(key1, val2);
                tx.success();
            }

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

                Object vrfy2 = rel.getProperty(key1);
                assertEquals(val2, vrfy2);
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");
        }
    }

    /**
     * Test the results of setProperty and getProperty of a node
     */
    @Test
    public void test03ModifyConstantBooleanProperty() {

        try {

            final Class groupType = StructrApp.getConfiguration().getNodeEntityClass("Group");
            final PropertyKey<Boolean> key = StructrApp.key(groupType, "isGroup");
            final PropertyMap props = new PropertyMap();
            final String type = "Group";
            final String name = "TestGroup-1";

            NodeInterface node = null;

            props.put(AbstractNode.type, type);
            props.put(AbstractNode.name, name);

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

                node = app.create(Group.class, props);
                tx.success();
            }

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

                // Check defaults
                assertEquals(Group.class.getSimpleName(), node.getProperty(AbstractNode.type));
                assertTrue(node.getProperty(AbstractNode.name).equals(name));
                assertTrue(node.getProperty(key));
            }

            final String name2 = "TestGroup-2";

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

                // Modify values
                node.setProperty(AbstractNode.name, name2);
                node.setProperty(key, false);

                fail("Should have failed with an exception: Group.isGroup is_read_only_property");

                tx.success();

            } catch (FrameworkException expected) {
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    @Test
    public void test01CompareAscending() {

        try {

            TestOne a = createTestNode(TestOne.class);
            TestOne b = createTestNode(TestOne.class);

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

                GraphObjectComparator comp = new GraphObjectComparator(TestOne.anInt,
                        GraphObjectComparator.ASCENDING);

                try {
                    comp.compare(null, null);
                    fail("Should have raised an NullPointerException");

                } catch (NullPointerException e) {
                }

                try {
                    comp.compare(a, null);
                    fail("Should have raised an NullPointerException");

                } catch (NullPointerException e) {
                }

                try {
                    comp.compare(null, b);
                    fail("Should have raised an NullPointerException");

                } catch (NullPointerException e) {
                }

                try {
                    comp.compare(null, b);
                    fail("Should have raised an NullPointerException");

                } catch (NullPointerException e) {
                }

                // a: null
                // b: null
                // a == b => 0
                assertEquals(0, comp.compare(a, b));

                // a: 0
                // b: null
                // a < b => -1
                setPropertyTx(a, TestOne.anInt, 0);
                assertEquals(-1, comp.compare(a, b));

                // a: null
                // b: 0
                // a > b => 1
                setPropertyTx(a, TestOne.anInt, null);
                setPropertyTx(b, TestOne.anInt, 0);
                assertEquals(1, comp.compare(a, b));

                // a: 1
                // b: 2
                // a < b => -1
                setPropertyTx(a, TestOne.anInt, 1);
                setPropertyTx(b, TestOne.anInt, 2);
                assertEquals(-1, comp.compare(a, b));

                // a: 2
                // b: 1
                // a > b => 1
                setPropertyTx(a, TestOne.anInt, 2);
                setPropertyTx(b, TestOne.anInt, 1);
                assertEquals(1, comp.compare(a, b));
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }

    }

    @Test
    public void test01CompareDescending() {

        try {

            TestOne a = createTestNode(TestOne.class);
            TestOne b = createTestNode(TestOne.class);

            GraphObjectComparator comp = new GraphObjectComparator(TestOne.anInt, GraphObjectComparator.DESCENDING);

            try {
                comp.compare(null, null);
                fail("Should have raised an NullPointerException");

            } catch (NullPointerException e) {
            }

            try {
                comp.compare(a, null);
                fail("Should have raised an NullPointerException");

            } catch (NullPointerException e) {
            }

            try {
                comp.compare(null, b);
                fail("Should have raised an NullPointerException");

            } catch (NullPointerException e) {
            }

            try {
                comp.compare(null, b);
                fail("Should have raised an NullPointerException");

            } catch (NullPointerException e) {
            }

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

                // a: null
                // b: null
                // a == b => 0
                assertEquals(0, comp.compare(a, b));
            }

            // a: 0
            // b: null
            // a > b => 1
            setPropertyTx(a, TestOne.anInt, 0);

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

                assertEquals(1, comp.compare(a, b));
            }

            // a: null
            // b: 0
            // a < b => -1
            setPropertyTx(a, TestOne.anInt, null);
            setPropertyTx(b, TestOne.anInt, 0);

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

                assertEquals(-1, comp.compare(a, b));
            }

            // a: 1
            // b: 2
            // a > b => 1
            setPropertyTx(a, TestOne.anInt, 1);
            setPropertyTx(b, TestOne.anInt, 2);

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

                assertEquals(1, comp.compare(a, b));
            }

            // a: 2
            // b: 1
            // a < b => -1
            setPropertyTx(a, TestOne.anInt, 2);
            setPropertyTx(b, TestOne.anInt, 1);

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

                assertEquals(-1, comp.compare(a, b));
            }

        } catch (FrameworkException ex) {

            logger.error(ex.toString());
            fail("Unexpected exception");

        }
    }

    @Test
    public void testNodeCacheInvalidationInRelationshipWrapper() {

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

            final TestOne testOne = createTestNode(TestOne.class);
            final TestTwo testTwo1 = createTestNode(TestTwo.class, new NodeAttribute<>(TestTwo.testOne, testOne));
            final OneTwoOneToOne rel = testOne.getOutgoingRelationship(OneTwoOneToOne.class);
            final TestTwo testTwo2 = rel.getTargetNode();

            testTwo1.setProperty(AbstractNode.name, "test");

            assertEquals("Cache invalidation failure!", "test", testTwo2.getProperty(AbstractNode.name));

            tx.success();

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

        // fill cache with other nodes
        try {

            createTestNodes(TestSix.class, 1000);

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

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

            final TestOne testOne = app.nodeQuery(TestOne.class).getFirst();
            final TestTwo testTwo1 = app.nodeQuery(TestTwo.class).getFirst();
            final OneTwoOneToOne rel = testOne.getOutgoingRelationship(OneTwoOneToOne.class);
            final TestTwo testTwo2 = rel.getTargetNode();

            testTwo1.setProperty(AbstractNode.name, "test2");

            assertEquals("Cache invalidation failure!", "test2", testTwo2.getProperty(AbstractNode.name));

            tx.success();

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

    @Test
    public void testNodeCacheInvalidationWithLongLivedReferences() {

        TestOne longLivedReference = null;

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

            longLivedReference = createTestNode(TestOne.class, "test1");

            tx.success();

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

        // fill cache with other nodes
        try {

            createTestNodes(TestSix.class, 1000);

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

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

            final TestOne freshReference = app.nodeQuery(TestOne.class).getFirst();

            freshReference.setProperty(AbstractNode.name, "test2");

            assertEquals("Cache invalidation failure!", "test2", longLivedReference.getProperty(AbstractNode.name));

            tx.success();

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

    @Test
    public void testRelationshipEndNodeTypeRestriction() {

        // this test makes sure that relationships with identical relationship
        // types are filtered according to the types of their end nodes
        try (final Tx tx = app.tx()) {

            // create two OWNS relationships with different end node types
            final TestOne testOne = app.create(TestOne.class, "testone");
            final TestThree testThree = app.create(TestThree.class, "testthree");
            final Principal testUser = app.create(Principal.class, "testuser");

            testOne.setProperty(TestOne.testThree, testThree);
            testThree.setProperty(TestThree.owner, testUser);

            tx.success();

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

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

            final List<OneThreeOneToOne> rels = app.relationshipQuery(OneThreeOneToOne.class).getAsList();

            assertEquals("Relationship query returns wrong number of results", 1, rels.size());

            for (final OneThreeOneToOne rel : rels) {
                assertEquals("Relationship query returns wrong type", OneThreeOneToOne.class, rel.getClass());
            }

            tx.success();

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

    @Test
    public void testRelationshipsOnNodeCreation() {

        Principal user = null;
        TestOne test = null;

        // create user
        try (final Tx tx = app.tx()) {

            user = app.create(Principal.class, "tester");

            tx.success();

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

        final SecurityContext ctx = SecurityContext.getInstance(user, AccessMode.Backend);
        final App app = StructrApp.getInstance(ctx);

        // create object with user context
        try (final Tx tx = app.tx()) {

            test = app.create(TestOne.class);

            tx.success();

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

        // query for relationships
        try (final Tx tx = app.tx()) {

            final List<? extends RelationshipInterface> rels1 = app.relationshipQuery()
                    .and(AbstractRelationship.sourceId, user.getUuid()).getAsList();
            final List<Class> classes1 = rels1.stream().map(r -> r.getClass()).collect(Collectors.toList());
            assertEquals("Invalid number of relationships after object creation", 2, rels1.size());
            assertTrue("Invalid relationship type after object creation", classes1.contains(Security.class));
            assertTrue("Invalid relationship type after object creation",
                    classes1.contains(PrincipalOwnsNode.class));

            final List<? extends RelationshipInterface> rels2 = app.relationshipQuery()
                    .and(AbstractRelationship.targetId, test.getUuid()).getAsList();
            final List<Class> classes2 = rels2.stream().map(r -> r.getClass()).collect(Collectors.toList());
            assertEquals("Invalid number of relationships after object creation", 2, rels2.size());
            assertTrue("Invalid relationship type after object creation", classes2.contains(Security.class));
            assertTrue("Invalid relationship type after object creation",
                    classes2.contains(PrincipalOwnsNode.class));

            final List<? extends RelationshipInterface> rels3 = Iterables.toList(test.getIncomingRelationships());
            final List<Class> classes3 = rels3.stream().map(r -> r.getClass()).collect(Collectors.toList());
            assertEquals("Invalid number of relationships after object creation", 2, rels3.size());
            assertTrue("Invalid relationship type after object creation", classes3.contains(Security.class));
            assertTrue("Invalid relationship type after object creation",
                    classes3.contains(PrincipalOwnsNode.class));

            final Security sec = app.relationshipQuery(Security.class).getFirst();
            assertNotNull("Relationship caching on node creation is broken", sec);

            final PrincipalOwnsNode owns = app.relationshipQuery(PrincipalOwnsNode.class).getFirst();
            assertNotNull("Relationship caching on node creation is broken", owns);

            tx.success();

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

    /**
     * This test makes sure that no graph objects that were created in a transaction which is rolled back
     * are visible/accessible in other tx.
     *
     * Before a bug fix this test was created for, we saw NotFoundExceptions with wrapped NoSuchRecordException
     * when trying to access stale nodes through relationships from the cache.
     */

    @Test
    public void testRelationshipsOnNodeCreationAfterRollback() {

        Principal user = null;
        TestThirteen test = null;

        // create user
        try (final Tx tx = app.tx()) {

            user = app.create(Principal.class, "tester");

            tx.success();

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

        final SecurityContext ctx = SecurityContext.getInstance(user, AccessMode.Backend);
        final App app = StructrApp.getInstance(ctx);

        String uuid = null;

        List<? extends RelationshipInterface> rels1 = Collections.EMPTY_LIST;
        List<? extends RelationshipInterface> rels2 = Collections.EMPTY_LIST;
        List<? extends RelationshipInterface> rels3 = Collections.EMPTY_LIST;
        List<? extends RelationshipInterface> rels4 = Collections.EMPTY_LIST;

        // create object with user context
        try (final Tx tx = app.tx()) {

            test = app.create(TestThirteen.class);
            uuid = test.getUuid();

            rels1 = app.relationshipQuery().and(AbstractRelationship.sourceId, user.getUuid()).getAsList();
            rels2 = app.relationshipQuery().and(AbstractRelationship.targetId, test.getUuid()).getAsList();
            rels3 = Iterables.toList(test.getIncomingRelationships());
            rels4 = Iterables.toList(user.getOutgoingRelationships());

            System.out.println("rels1: " + rels1.size());
            System.out.println("rels2: " + rels2.size());
            System.out.println("rels3: " + rels3.size());
            System.out.println("rels4: " + rels4.size());

            tx.success();

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

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

            // We shouldn't find a not-created node by its UUID
            final TestThirteen test2 = app.get(TestThirteen.class, uuid);
            assertNull(test2);

            rels1 = app.relationshipQuery().and(AbstractRelationship.sourceId, user.getUuid()).getAsList();
            rels4 = Iterables.toList(user.getOutgoingRelationships());

            System.out.println("rels1: " + rels1.size());
            System.out.println("rels4: " + rels4.size());

            for (final RelationshipInterface rel : rels1) {
                System.out.println("Source node: " + rel.getSourceNode() + ", target node: " + rel.getTargetNode());
            }

            for (final RelationshipInterface rel : rels4) {
                System.out.println("Source node: " + rel.getSourceNode() + ", target node: " + rel.getTargetNode());
            }

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

        // query for relationships
        try (final Tx tx = app.tx()) {

            rels1 = app.relationshipQuery().and(AbstractRelationship.sourceId, user.getUuid()).getAsList();
            final List<Class> classes1 = rels1.stream().map(r -> r.getClass()).collect(Collectors.toList());
            assertEquals("Invalid number of relationships after object creation", 0, rels1.size());

            rels4 = Iterables.toList(user.getOutgoingRelationships());
            final List<Class> classes4 = rels4.stream().map(r -> r.getClass()).collect(Collectors.toList());
            assertEquals("Invalid number of relationships after object creation", 0, rels4.size());

            final Security sec = app.relationshipQuery(Security.class).getFirst();
            assertNull("Relationship caching on node creation is broken", sec);

            final PrincipalOwnsNode owns = app.relationshipQuery(PrincipalOwnsNode.class).getFirst();
            assertNull("Relationship caching on node creation is broken", owns);

            tx.success();

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

    @Test
    public void testRelationshipsToListWithNullTimestamp() {

        String id = null;

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

            final TestSix testSix = createTestNode(TestSix.class);
            id = testSix.getUuid();

            // Create 1000 rels
            for (int i = 0; i < 10; i++) {

                final TestThree testThree = createTestNode(TestThree.class,
                        new NodeAttribute<>(TestThree.oneToManyTestSix, testSix));
                final RelationshipInterface rel = testThree.getRelationships(SixThreeOneToMany.class).iterator()
                        .next();

                if (i % 2 == 0) {
                    rel.getRelationship().setProperty("internalTimestamp", null);
                }
            }

            tx.success();

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

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

            final TestSix testSix = app.get(TestSix.class, id);

            // This calls NodeWrapper#toList internally
            List<OneTwoOneToOne> rels = Iterables.toList(testSix.getRelationships());

            assertEquals("Wrong number of relationships", 10, rels.size());

            for (RelationshipInterface rel : rels) {
                System.out.println(rel.getProperty(StructrApp.getConfiguration()
                        .getPropertyKeyForJSONName(RelationshipInterface.class, "internalTimestamp")));
            }

            tx.success();

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

    @Test
    public void testNodeCreationWithForcedUuid() {

        final String uuid = NodeServiceCommand.getNextUuid();
        TestOne test = null;

        // create object with user context
        try (final Tx tx = app.tx()) {

            test = app.create(TestOne.class, new NodeAttribute<>(AbstractNode.name, "test"),
                    new NodeAttribute<>(GraphObject.id, uuid));

            tx.success();

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

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

            final String uuid1 = test.getProperty(GraphObject.id);
            final String uuid2 = test.getUuid();

            assertEquals("Object creation does not accept provided UUID", uuid, uuid1);
            assertEquals("UUID mismatch in getProperty() and getUuid()", uuid1, uuid2);

            tx.success();

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

    @Test
    public void testNodeCreationWithForcedInvalidUuid() {

        TestOne test = null;

        // create object with user context
        try (final Tx tx = app.tx()) {

            test = app.create(TestOne.class, new NodeAttribute<>(AbstractNode.name, "test"),
                    new NodeAttribute<>(GraphObject.id, null));

            tx.success();

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

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

            final String uuid1 = test.getProperty(GraphObject.id);
            final String uuid2 = test.getUuid();

            assertEquals("UUID mismatch in getProperty() and getUuid()", uuid1, uuid2);

            tx.success();

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

    @Test
    public void testCreateNodeWithAdditionalProperties() {

        TestOne test = null;

        // create object with user context
        try (final Tx tx = app.tx()) {

            test = app.create(TestOne.class, new NodeAttribute<>(AbstractNode.name, "test"),
                    new NodeAttribute<>(GraphObject.visibleToPublicUsers, true),
                    new NodeAttribute<>(GraphObject.visibleToAuthenticatedUsers, true),
                    new NodeAttribute<>(AbstractNode.hidden, true));

            tx.success();

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

        // create object with user context
        try (final Tx tx = app.tx()) {

            assertEquals("Invalid create node result", "test", test.getProperty(AbstractNode.name));
            assertEquals("Invalid create node result", true, test.getProperty(GraphObject.visibleToPublicUsers));
            assertEquals("Invalid create node result", true,
                    test.getProperty(GraphObject.visibleToAuthenticatedUsers));
            assertEquals("Invalid create node result", true, test.getProperty(AbstractNode.hidden));

            tx.success();

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

    @Test
    public void testIdLifecycle() {

        final long wait = 1000 * 100L;

        cleanDatabaseAndSchema();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException ex) {
            logger.error("Thread sleep was interrupted", ex);
        }

        final int n = 10000; // number of CREATE iterations

        final Map<Long, String> results = new HashMap<>();

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

            System.out.println("Creating " + n + " triples with reltype A");

            for (int i = 0; i < n; i++) {

                //tx.run("CREATE (n)-[r:A]->(m)");
                final TestOne test1 = app.create(TestOne.class, "noname");
                final TestOne test2 = app.create(TestOne.class, "noname");
                app.create(test1, test2, AOneToOne.class);
            }

            System.out.println("done.");

            final List<RelationshipInterface> rels1 = app.cypher("MATCH ()-[r]-() RETURN DISTINCT r ORDER BY id(r)",
                    Collections.EMPTY_MAP);
            System.out.println("Storing relationships in map...");

            for (final RelationshipInterface rel : rels1) {

                Long id = rel.getId();
                String type = rel.getType();

                results.put(id, type);
            }
            System.out.println("done.");

            System.out.println("Deleting all A relationships through Cypher...");
            app.cypher("MATCH (n)-[r:A]-(m) DETACH DELETE n,r,m", Collections.EMPTY_MAP); // tx.run("MATCH (n)-[r]-(m) DETACH DELETE n,r,m");
            System.out.println("done.");

            System.out.println("Waiting " + wait / 1000 + " second(s)...");
            try {
                Thread.sleep(wait);
            } catch (InterruptedException ex) {
                logger.error("Thread sleep was interrupted", ex);
            }
            System.out.println("done.");

            System.out.println("Creating " + n + " triples with reltype B");
            for (int i = 0; i < n; i++) {

                //tx.run("CREATE (n)-[r:B]->(m)");
                final TestOne test1 = app.create(TestOne.class, "noname");
                final TestOne test2 = app.create(TestOne.class, "noname");
                app.create(test1, test2, BOneToOne.class);
            }

            System.out.println("done.");

            System.out.println("Comparing relationships with cache...");
            final List<RelationshipInterface> rels2 = app.cypher("MATCH ()-[r]-() RETURN DISTINCT r ORDER BY id(r)",
                    Collections.EMPTY_MAP);

            FixedSizeCache<Long, RelationshipWrapper> relCache = RelationshipWrapper.getCache();

            for (final RelationshipInterface rel : rels2) {

                Long id = rel.getId();
                String type = rel.getType();

                RelationshipWrapper relWrapper = relCache.get(id);

                if (relWrapper != null && !relWrapper.getType().name().equals(type)) {
                    fail("R" + id + " Relationship found in cache with type " + relWrapper.getType().name()
                            + " while type in database is " + type);
                }

                if (results.containsKey(id) && !results.get(id).equals(type)) {
                    fail("ERROR: Result contained relationship with same id but different type: R" + id
                            + ": stored type is " + results.get(id) + ", new type is " + type);
                }
            }
            System.out.println("done.");

            System.out.println("Comparing nodes with cache...");
            final List<NodeInterface> nodes = app.cypher("MATCH (n) RETURN DISTINCT n ORDER BY id(n)",
                    Collections.EMPTY_MAP);

            FixedSizeCache<Long, NodeWrapper> nodeCache = NodeWrapper.getCache();

            for (final NodeInterface node : nodes) {

                Long id = node.getId();
                String type = node.getType();

                NodeWrapper nodeWrapper = nodeCache.get(id);

                if (nodeWrapper != null && !nodeWrapper.getProperty("type").equals(type)) {
                    fail("N" + id + " Node found in cache with type " + nodeWrapper.getProperty("type")
                            + " while type in database is " + type);
                }

            }
            System.out.println("done.");

            tx.success();

        } catch (Throwable t) {
            t.printStackTrace();
            fail("Unexpected exception.");
        }

    }

    @Test
    public void testObjectCreationAfterRollback() {

        /**
         * 1. Test Setup:
         * - Clean Database
         * - Create "bad" type with error in onCreate
         * - Create "good" type
         * - Create a test user
         */

        cleanDatabaseAndSchema();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException ex) {
            logger.error("Thread sleep was interrupted", ex);
        }

        // setup: create dynamic type with onCreate() method
        try (final Tx tx = app.tx()) {

            SchemaNode badNodeType = createTestNode(SchemaNode.class, "BadNode");
            badNodeType.setProperty(new StringProperty("___onCreate"),
                    "{ return Structr.error('name', 'no node of this type can not be created!'); }");

            SchemaNode goodNodeType = createTestNode(SchemaNode.class, "GoodNode");

            tx.success();

        } catch (FrameworkException fex) {

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

        Principal user = null;

        // create user
        try (final Tx tx = app.tx()) {

            user = app.create(Principal.class, "tester");

            tx.success();

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

        final SecurityContext ctx = SecurityContext.getInstance(user, AccessMode.Backend);
        final App userApp = StructrApp.getInstance(ctx);

        /**
         * 2. Test Execution:
         * - Try to create one node of the type that fails ==> rollback
         * - Create HALF as many nodes of a type that can be created ==> the fail probability is quite high (fail meaning that a previously deleted relationship is still present in the cache and not marked as stale)
         */

        long maxId = 0;

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

            Class failingType = StructrApp.getConfiguration().getNodeEntityClass("BadNode");

            final NodeInterface x = userApp.create(failingType, "should fail");
            for (RelationshipInterface r : x.getIncomingRelationships()) {
                System.out.println(r.getRelType().name() + ": " + r.getId());
                maxId = Long.max(maxId, r.getId());
            }

            tx.success();

        } catch (FrameworkException fex) {

            // expected error
            assertEquals("BadNode.name no node of this type can not be created!", fex.toString());

        }

        /**
         * Sleep for some time - in the hopes that the database will re-use the previously deleted relationship ids
         */
        try {
            logger.info("Waiting for 20 seconds...");
            Thread.sleep(20000L);
        } catch (InterruptedException ex) {
            logger.error("Thread sleep was interrupted", ex);
        }

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

            Class goodType = StructrApp.getConfiguration().getNodeEntityClass("GoodNode");

            for (int cnt = 0; cnt < (maxId / 2); cnt++) {
                userApp.create(goodType, "Good node " + cnt);
            }

            tx.success();

        } catch (ClassCastException cce) {

            logger.warn("", cce);
            fail("ClassCastException occurred - this happens because a relationship cache entry was not set to stale after a rollback!");

        } catch (FrameworkException fex) {

            logger.warn("", fex);

            if (fex.getStatus() == 403) {
                fail("Previously existing relationship (which was deleted) but not set stale has been re-used in the cache... which is why the user is not allowed to modify his own node");
            }

            fail("Unexpected exception.");
        }

    }

    @Test
    public void testRelationshipCreationAfterRollback() {

        cleanDatabaseAndSchema();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException ex) {
            logger.error("Thread sleep was interrupted", ex);
        }

        // setup: create dynamic type with onCreate() method
        try (final Tx tx = app.tx()) {

            final SchemaNode source = app.create(SchemaNode.class, "Source");
            final SchemaNode target = app.create(SchemaNode.class, "Target");

            final SchemaRelationshipNode rel = app.create(SchemaRelationshipNode.class,
                    new NodeAttribute(SchemaRelationshipNode.relationshipType, "LINK"),
                    new NodeAttribute(SchemaRelationshipNode.sourceNode, source),
                    new NodeAttribute(SchemaRelationshipNode.targetNode, target),
                    new NodeAttribute(SchemaRelationshipNode.sourceMultiplicity, "1"),
                    new NodeAttribute(SchemaRelationshipNode.targetMultiplicity, "1"));

            rel.setProperty(new StringProperty("___onCreate"),
                    "{ return Structr.error('name', 'relationship can not be created!'); }");

            tx.success();

        } catch (FrameworkException fex) {

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

        Principal user = null;

        // create user
        try (final Tx tx = app.tx()) {

            user = app.create(Principal.class, "tester");

            tx.success();

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

        final SecurityContext ctx = SecurityContext.getInstance(user, AccessMode.Backend);
        final App userApp = StructrApp.getInstance(ctx);

        NodeInterface sourceNode = null;
        NodeInterface targetNode = null;

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

            Class sourceType = StructrApp.getConfiguration().getNodeEntityClass("Source");
            Class targetType = StructrApp.getConfiguration().getNodeEntityClass("Target");

            sourceNode = userApp.create(sourceType, "source node");
            targetNode = userApp.create(targetType, "target node");

            tx.success();

        } catch (FrameworkException fex) {

            fail("Unexpected exception.");

        }

        assertNotNull(sourceNode);
        assertNotNull(targetNode);

        long maxId = 0;

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

            final Class relClass = StructrApp.getConfiguration()
                    .getRelationClassForCombinedType(sourceNode.getType(), "LINK", targetNode.getType());

            userApp.create(sourceNode, targetNode, relClass);

            for (RelationshipInterface r : targetNode.getIncomingRelationships()) {
                System.out.println(r.getRelType().name() + ": " + r.getId());
                maxId = Long.max(maxId, r.getId());
            }

            tx.success();

        } catch (FrameworkException fex) {

            // expected error
            assertEquals("LINK.name relationship can not be created!", fex.toString());

        }

        maxId = Long.max(maxId, 100);

        /**
         * Sleep for some time - in the hopes that the database will re-use the previously deleted relationship ids
         */
        try {
            logger.info("Waiting for 20 seconds...");
            Thread.sleep(20000L);
        } catch (InterruptedException ex) {
            logger.error("Thread sleep was interrupted", ex);
        }

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

            Class goodType = StructrApp.getConfiguration().getNodeEntityClass("Source");

            for (int cnt = 0; cnt < (maxId); cnt++) {
                userApp.create(goodType, "Surce node " + cnt);
            }

            tx.success();

        } catch (ClassCastException cce) {

            logger.warn("", cce);
            fail("ClassCastException occurred - this happens because a relationship cache entry was not set to stale after a rollback!");

        } catch (FrameworkException fex) {

            logger.warn("", fex);

            if (fex.getStatus() == 403) {
                fail("Previously existing relationship (which was deleted) but not set stale has been re-used in the cache... which is why the user is not allowed to modify his own node");
            }

            fail("Unexpected exception.");
        }
    }

    // ----- private methods -----
    private AbstractRelationship cascadeRel(final Class type1, final Class type2, final int cascadeDeleteFlag)
            throws FrameworkException {

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

            NodeInterface start = createTestNode(type1);
            NodeInterface end = createTestNode(type2);
            AbstractRelationship rel = createTestRelationship(start, end, NodeHasLocation.class);

            rel.setProperty(AbstractRelationship.cascadeDelete, cascadeDeleteFlag);

            tx.success();

            return rel;
        }
    }

    private void deleteCascade(final NodeInterface node) throws FrameworkException {

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

            app.delete(node);
            tx.success();
        }
    }

    private void setPropertyTx(final GraphObject obj, final PropertyKey key, final Object value) {

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

            obj.setProperty(key, value);
            tx.success();

        } catch (FrameworkException ex) {
        }
    }
}