com.flexive.tests.embedded.FxTreeTest.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.tests.embedded.FxTreeTest.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library 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.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.tests.embedded;

import com.flexive.core.Database;
import com.flexive.core.storage.genericSQL.GenericTreeStorageSpreaded;
import com.flexive.shared.EJBLookup;
import com.flexive.shared.FxContext;
import com.flexive.shared.FxSharedUtils;
import com.flexive.shared.configuration.SystemParameters;
import com.flexive.shared.content.FxContent;
import com.flexive.shared.content.FxPK;
import com.flexive.shared.exceptions.*;
import com.flexive.shared.interfaces.ContentEngine;
import com.flexive.shared.interfaces.ScriptingEngine;
import com.flexive.shared.interfaces.TreeEngine;
import com.flexive.shared.interfaces.TypeEngine;
import com.flexive.shared.scripting.FxScriptEvent;
import com.flexive.shared.scripting.FxScriptInfo;
import com.flexive.shared.scripting.FxScriptInfoEdit;
import com.flexive.shared.security.ACLCategory;
import com.flexive.shared.structure.FxPropertyAssignmentEdit;
import com.flexive.shared.structure.FxType;
import com.flexive.shared.structure.FxTypeEdit;
import com.flexive.shared.tree.FxTreeMode;
import com.flexive.shared.tree.FxTreeNode;
import com.flexive.shared.tree.FxTreeNodeEdit;
import com.flexive.shared.tree.FxTreeRemoveOp;
import com.flexive.shared.value.FxReference;
import com.flexive.shared.value.FxString;
import com.flexive.shared.value.ReferencedContent;
import com.flexive.shared.workflow.StepDefinition;
import com.google.common.collect.Lists;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import java.sql.Connection;
import java.util.Collections;
import java.util.List;

import static com.flexive.shared.CacheAdmin.getEnvironment;
import static com.flexive.shared.EJBLookup.getContentEngine;
import static com.flexive.shared.EJBLookup.getTreeEngine;
import static com.flexive.tests.embedded.FxTestUtils.login;
import static com.flexive.tests.embedded.FxTestUtils.logout;
import static org.testng.Assert.*;

/**
 * Tree engine tests. Longer-running tests are not executed with "tests.all", but must be triggered explicitly
 * (group "tree-stress").
 *
 * @author Daniel Lichtenberger (daniel.lichtenberger@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public class FxTreeTest {
    TreeEngine tree;
    ScriptingEngine scripting;
    ContentEngine ce;
    TypeEngine ty;

    @BeforeClass(groups = { "tree", "tree-stress" })
    public void beforeClass() throws Exception {
        login(TestUsers.SUPERVISOR);
        tree = getTreeEngine();
        scripting = EJBLookup.getScriptingEngine();
        ce = getContentEngine();
        ty = EJBLookup.getTypeEngine();
        enableTreeChecks();
    }

    @AfterClass(groups = { "tree", "tree-stress" })
    public void afterClass() throws FxLogoutFailedException {
        try {
            clearTrees();
            disableTreeChecks();
        } catch (FxApplicationException e) {
            //ignore
        }
        logout();
    }

    private void enableTreeChecks() throws FxApplicationException {
        EJBLookup.getDivisionConfigurationEngine().put(SystemParameters.TREE_CHECKS_ENABLED, true);
    }

    private void disableTreeChecks() throws FxApplicationException {
        EJBLookup.getDivisionConfigurationEngine().put(SystemParameters.TREE_CHECKS_ENABLED, false);
    }

    /**
     * Helper to build a node name
     *
     * @param number running number
     * @return generated node name
     */
    private static String getNodeName(int number) {
        return "Node" + number;
    }

    /**
     * Helper to build a node label
     *
     * @param number running number
     * @return generated node label
     */
    private static FxString getNodeLabel(int number) {
        return new FxString(true, "Node" + number + "Description");
    }

    /**
     * Clear live and edit trees
     *
     * @throws FxApplicationException on errors
     */
    private void clearTrees() throws FxApplicationException {
        tree.clear(FxTreeMode.Live);
        tree.clear(FxTreeMode.Edit);
    }

    /**
     * Edit tests
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void treeTestEdit() throws FxApplicationException {
        treeCRUD(FxTreeMode.Edit);
        createPath(FxTreeMode.Edit);
    }

    /**
     * Test path creation
     *
     * @param mode FxTreeMode
     * @throws FxApplicationException on errors
     */
    private void createPath(FxTreeMode mode) throws FxApplicationException {
        final long[] nodes = tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, "my/virtual/directory");
        assertEquals(nodes.length, 3, "Should have created 3 nodes");
        assertEquals(tree.getNode(mode, nodes[0]).getName(), "my");
        assertEquals(tree.getNode(mode, nodes[1]).getName(), "virtual");
        assertEquals(tree.getNode(mode, nodes[2]).getName(), "directory");

        // create subfolder, reuse two folders
        final long[] tmpNodes = tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, "my/virtual/tmp");
        assertEquals(tmpNodes.length, 3, "Should have returned 3 nodes");
        assertEquals(tree.getNode(mode, tmpNodes[0]).getName(), "my");
        assertEquals(tree.getNode(mode, tmpNodes[1]).getName(), "virtual");
        assertEquals(tree.getNode(mode, tmpNodes[2]).getName(), "tmp");
        assertEquals(nodes[0], tmpNodes[0]);
        assertEquals(nodes[1], tmpNodes[1]);

        // use /my/virtual as root node
        final long[] nestedNodes = tree.createNodes(mode, nodes[1], 0, "/directory/tmp2");
        assertEquals(nestedNodes.length, 2);
        assertEquals(tree.getNode(mode, nestedNodes[0]).getName(), "directory");
        assertEquals(tree.getNode(mode, nestedNodes[1]).getName(), "tmp2");
        assertEquals(nestedNodes[0], nodes[2]); // check "directory" node
    }

    /**
     * Test most methods of the simplified API
     *
     * @param mode FxTreeMode to test
     * @throws FxApplicationException on errors
     */
    private void treeCRUD(FxTreeMode mode) throws FxApplicationException {
        //clear the tree
        clearTrees();
        assertTrue(tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount() == 0,
                "Expected to have 0 children, got: ["
                        + tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount() + "]");
        //create new node
        FxTreeNodeEdit node1 = FxTreeNodeEdit.createNew(getNodeName(1));
        node1.setLabel(getNodeLabel(1));
        node1.setMode(mode);
        long id1 = tree.save(node1);
        assertTrue(tree.exist(mode, id1));
        assertTrue(tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount() == 1,
                "Expected to have 1 child, got: [" + tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount()
                        + "]");
        //load and check if all well
        FxTreeNode node1_loaded = tree.getNode(mode, id1);
        assertTrue(node1_loaded.getName().equals(node1.getName()));
        assertEquals(node1_loaded.getLabel(), node1.getLabel());
        //rename name
        tree.save(new FxTreeNodeEdit(node1_loaded).setName("abcd"));
        node1_loaded = tree.getNode(mode, id1);
        assertTrue(node1_loaded.getName().equals("abcd"), "Expected [abcd] - got [" + node1_loaded.getName() + "]");
        assertEquals(node1_loaded.getLabel(), node1.getLabel());
        //rename label
        tree.save(new FxTreeNodeEdit(node1_loaded).setLabel(getNodeLabel(42)));
        node1_loaded = tree.getNode(mode, id1);
        assertTrue(node1_loaded.getName().equals("abcd"));
        assertTrue(node1_loaded.getLabel().equals(getNodeLabel(42)));
        //create child
        FxTreeNodeEdit node1_1 = FxTreeNodeEdit.createNewChildNode(node1_loaded).setName("1")
                .setLabel(getNodeLabel(1)).setMode(mode);
        long id1_1 = tree.save(node1_1);
        FxTreeNode node1_1_loaded = tree.getNode(mode, id1_1);
        assertTrue(node1_1_loaded.getParentNodeId() == node1_loaded.getId());
        //verify path
        assertTrue(node1_1_loaded.getPath().equals("/abcd/1"));
        //verify label
        assertEquals(tree.getLabels(mode, node1_1_loaded.getId()).get(0),
                "/" + getNodeLabel(42).getBestTranslation() + "/" + getNodeLabel(1).getBestTranslation());
        //create 2 other children for positioning tests
        FxTreeNodeEdit node1_2 = FxTreeNodeEdit.createNewChildNode(node1_loaded).setName("2")
                .setLabel(getNodeLabel(2)).setMode(mode);
        long id1_2 = tree.save(node1_2);
        FxTreeNode node1_2_loaded = tree.getNode(mode, id1_2);
        //verify path
        assertTrue(node1_2_loaded.getPath().equals("/abcd/2"),
                "Expected [/abcd/2] got: [" + node1_2_loaded.getPath() + "]");
        //verify label
        assertTrue(
                ("/" + getNodeLabel(42).getBestTranslation() + "/" + getNodeLabel(2).getBestTranslation())
                        .equals(tree.getLabels(mode, node1_2_loaded.getId()).get(0)),
                "Expected [/" + getNodeLabel(42).getBestTranslation() + "/" + getNodeLabel(2).getBestTranslation()
                        + "] got: [" + tree.getLabels(mode, node1_2_loaded.getId()).get(0) + "]");
        // verify unknown id return value
        final int invalidId = 999999;
        final String expectedOutcome = "/<unknown:" + invalidId + ">";
        assertEquals(tree.getLabels(mode, invalidId).get(0), expectedOutcome);
        FxTreeNodeEdit node1_3 = FxTreeNodeEdit.createNewChildNode(node1_loaded).setName("3")
                .setLabel(getNodeLabel(3)).setMode(mode);
        long id1_3 = tree.save(node1_3);
        FxTreeNode node1_3_loaded = tree.getNode(mode, id1_3);

        // TODO: check total child count with TreeEngine#getTotalChildCount (FX-690)

        //verify positions - should be 1-2-3
        assertTrue(node1_1_loaded.getPosition() == 0, "Expected [0] got: [" + node1_1_loaded.getPosition() + "]");
        assertTrue(node1_2_loaded.getPosition() == 1, "Expected [1] got: [" + node1_2_loaded.getPosition() + "]");
        assertTrue(node1_3_loaded.getPosition() == 2, "Expected [2] got: [" + node1_3_loaded.getPosition() + "]");

        //swap positions of 1 and 3 to net 3-2-1
        tree.save(new FxTreeNodeEdit(node1_3_loaded).setPosition(-1));
        tree.save(new FxTreeNodeEdit(node1_1_loaded).setPosition(100));
        node1_1_loaded = tree.getNode(mode, id1_1);
        node1_2_loaded = tree.getNode(mode, id1_2);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getPosition() == 2, "Expected [2] got: [" + node1_1_loaded.getPosition() + "]");
        assertTrue(node1_2_loaded.getPosition() == 1, "Expected [1] got: [" + node1_2_loaded.getPosition() + "]");
        assertTrue(node1_3_loaded.getPosition() == 0, "Expected [0] got: [" + node1_3_loaded.getPosition() + "]");
        //3-2-1 => 3-1-2
        tree.save(new FxTreeNodeEdit(node1_1_loaded).setPosition(1));
        node1_1_loaded = tree.getNode(mode, id1_1);
        node1_2_loaded = tree.getNode(mode, id1_2);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getPosition() == 1, "Expected [1] got: [" + node1_1_loaded.getPosition() + "]");
        assertTrue(node1_2_loaded.getPosition() == 2, "Expected [2] got: [" + node1_2_loaded.getPosition() + "]");
        assertTrue(node1_3_loaded.getPosition() == 0, "Expected [0] got: [" + node1_3_loaded.getPosition() + "]");
        //3-1-2 => 1-2-3
        tree.save(new FxTreeNodeEdit(node1_3_loaded).setPosition(4));
        node1_1_loaded = tree.getNode(mode, id1_1);
        node1_2_loaded = tree.getNode(mode, id1_2);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getPosition() == 0, "Expected [0] got: [" + node1_1_loaded.getPosition() + "]");
        assertTrue(node1_2_loaded.getPosition() == 1, "Expected [1] got: [" + node1_2_loaded.getPosition() + "]");
        assertTrue(node1_3_loaded.getPosition() == 2, "Expected [2] got: [" + node1_3_loaded.getPosition() + "]");
        //delete 1_2 and check positions
        tree.remove(new FxTreeNodeEdit(node1_2_loaded), FxTreeRemoveOp.Remove, false);
        assertTrue(!tree.exist(mode, id1_2));
        node1_1_loaded = tree.getNode(mode, id1_1);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getPosition() == 0, "Expected [0] got: [" + node1_1_loaded.getPosition() + "]");
        assertTrue(node1_3_loaded.getPosition() == 1, "Expected [1] got: [" + node1_3_loaded.getPosition() + "]");

        // TODO: check total child count with TreeEngine#getTotalChildCount (FX-690)

        if (mode == FxTreeMode.Live)
            return; //children are to be removed in live mode
        //delete parent but not children and check if they moved up in hierarchy
        tree.remove(new FxTreeNodeEdit(node1_loaded), FxTreeRemoveOp.Remove, false);
        node1_1_loaded = tree.getNode(mode, id1_1);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getParentNodeId() == FxTreeNode.ROOT_NODE);
        assertTrue(node1_3_loaded.getParentNodeId() == FxTreeNode.ROOT_NODE);
        //attach 1_3 as child to 1_2
        tree.save(new FxTreeNodeEdit(node1_3_loaded).setParentNodeId(node1_1_loaded.getId()));
        node1_1_loaded = tree.getTree(mode, id1_1, 3);
        node1_3_loaded = tree.getNode(mode, id1_3);
        assertTrue(node1_1_loaded.getChildren().size() == 1);
        assertTrue(node1_1_loaded.getChildren().get(0).getId() == node1_3_loaded.getId());
        assertTrue(node1_3_loaded.getPath().equals("/1/3"),
                "Expected [/1/3] got: [" + node1_3_loaded.getPath() + "]");
        //delete 1_1 with children and check that 1_3 is gone too
        tree.remove(new FxTreeNodeEdit(node1_1_loaded), FxTreeRemoveOp.Remove, true);
        assertTrue(!tree.exist(mode, id1_3));
        assertTrue(tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount() == 0,
                "Expected to have 0 children, got: ["
                        + tree.getNode(mode, FxTreeNode.ROOT_NODE).getDirectChildCount() + "]");
        clearTrees();

        //test changing a referenced content
        ContentEngine co = getContentEngine();
        FxPK testFolder = co.save(co.initialize(FxType.FOLDER));
        try {
            long nodeId = tree
                    .save(FxTreeNodeEdit.createNew("Test").setParentNodeId(FxTreeNode.ROOT_NODE).setMode(mode));
            FxTreeNode node = tree.getNode(mode, nodeId);
            assertNotSame(node.getReference(), testFolder, "Expected to have a different folder pk!");
            tree.save(node.asEditable().setReference(testFolder));
            node = tree.getNode(mode, nodeId);
            assertEquals(node.getReference(), testFolder, "Node reference should have been updated!");
        } finally {
            co.remove(testFolder);
        }
    }

    /**
     * Test node and subtree activation
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void activationTest() throws FxApplicationException {
        //clear live and edit tree
        clearTrees();
        //test activation without subtree
        long[] ids = tree.createNodes(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, 0, "/Test1/Test2/Test3");
        tree.activate(FxTreeMode.Edit, ids[0], false, true);
        checkTreeExists(FxTreeMode.Live, ids, new boolean[] { true, false, false });

        //test activation with subtree
        ids = tree.createNodes(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, 0, "/ATest1/ATest2/ATest3");
        tree.activate(FxTreeMode.Edit, ids[0], true, true);
        checkTreeExists(FxTreeMode.Live, ids, new boolean[] { true, true, true });

        // activate tree that has activated parts
        tree.activate(FxTreeMode.Edit, ids[0], true, true);
        checkTreeExists(FxTreeMode.Live, ids, new boolean[] { true, true, true });

        // set content without live step
        final long nodeId = ids[2];
        final FxContent content = getContentEngine().load(tree.getNode(FxTreeMode.Edit, ids[2]).getReference());

        content.setStepByDefinition(StepDefinition.EDIT_STEP_ID);
        getContentEngine().save(content);

        tree.activate(FxTreeMode.Edit, ids[0], true, false);
        checkTreeExists(FxTreeMode.Edit, nodeId, true);
        checkTreeExists(FxTreeMode.Live, nodeId, false);
        // activate content indirectly
        tree.activate(FxTreeMode.Edit, ids[1], true, true);
        checkTreeExists(FxTreeMode.Live, nodeId, true);

        //there should be 2 nodes not active (Test2 and Test3), see if they get activated after activating all
        tree.activate(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, true, true);
        //assertEquals(6, tree.getTree(FxTreeMode.Live, FxTreeNode.ROOT_NODE, 3).getTotalChildCount());

        // remove an activated node
        tree.remove(FxTreeMode.Live, ids[1], FxTreeRemoveOp.Unfile, false);
        tree.remove(FxTreeMode.Edit, ids[2], FxTreeRemoveOp.Unfile, true);
        checkTreeExists(FxTreeMode.Edit, new long[] { ids[1], ids[2] }, new boolean[] { true, false });
        //checkTreeExists(FxTreeMode.Live, new long[] { ids[1], ids[2] }, new boolean[] { false, true });

        // activate changes through parent node
        tree.activate(FxTreeMode.Edit, ids[1], false, false);
        checkTreeExists(FxTreeMode.Live, new long[] { ids[1], ids[2] }, new boolean[] { true, false });
    }

    private void checkTreeExists(FxTreeMode treeMode, long id, boolean exists) throws FxApplicationException {
        checkTreeExists(treeMode, new long[] { id }, new boolean[] { exists });
    }

    private void checkTreeExists(FxTreeMode treeMode, long[] ids, boolean[] exists) throws FxApplicationException {
        assertEquals(exists.length, ids.length, "Programming error: exists has " + exists.length
                + " elements, but there are " + ids.length + " ids.");
        for (int i = 0; i < ids.length; i++) {
            assertEquals(tree.exist(treeMode, ids[i]), exists[i],
                    "TreeEngine#exist returns unexpected result (i=" + i + ")");
        }
    }

    /**
     * Test behaviour when contents that are references of a tree node are removed
     * Expected behaviour: every tree node is a contentinstance itself. Removing the respective
     * content instance should therefore remove the relevant entry from the tree;
     * Live tree: childNodes are always removed!
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void contentRemoval() throws FxApplicationException {
        //clear live and edit tree
        clearTrees();
        FxPK contentPK;
        FxTreeMode mode = FxTreeMode.Edit;
        long[] nodes = createTestTree(mode);

        FxTreeNode n1 = tree.getNode(mode, nodes[0]);
        FxTreeNode n2 = tree.getNode(mode, nodes[1]);
        FxTreeNode n3 = tree.getNode(mode, nodes[2]);
        FxTreeNode childNode = tree.findChild(mode, nodes[1], "node_2_1");

        assertEquals(tree.getPathById(mode, n1.getId()), "/node_1");
        assertEquals(tree.getPathById(mode, n2.getId()), "/node_1/node_2");
        assertEquals(tree.getPathById(mode, n3.getId()), "/node_1/node_2/node_3");
        assertEquals(tree.getPathById(mode, childNode.getId()), "/node_1/node_2/node_2_1");

        // get the content reference of node2 and remove the content
        contentPK = n2.getReference();
        ce.remove(contentPK);

        // get the child node of nodes[0] which now must be "node_2_1"
        FxTreeNode n4 = tree.findChild(mode, nodes[0], "node_2_1");
        assertEquals(n4.getId(), childNode.getId());
        // check the new paths
        assertEquals(tree.getPathById(mode, n1.getId()), "/node_1");
        assertEquals(tree.getPathById(mode, n3.getId()), "/node_1/node_3");
        assertEquals(tree.getPathById(mode, childNode.getId()), "/node_1/node_2_1");
        assertEquals(tree.getPathById(mode, n2.getId()), "_"); // no path for the removed node

        try {
            tree.getNode(mode, nodes[1]);
            fail("Retrieving node " + nodes[1] + " should have thrown an exception");
        } catch (FxLoadException e) {
            // expected
        }

        // clean up
        tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);

        // perform tests in live tree: Removing a node from the live tree removes all its children (to avoid
        // inconsistencies with the edit tree)
        mode = FxTreeMode.Live;
        nodes = createTestTree(mode);

        n1 = tree.getNode(mode, nodes[0]);
        n2 = tree.getNode(mode, nodes[1]);
        n3 = tree.getNode(mode, nodes[2]);
        childNode = tree.findChild(mode, nodes[1], "node_2_1");

        assertEquals(tree.getPathById(mode, n1.getId()), "/node_1");
        assertEquals(tree.getPathById(mode, n2.getId()), "/node_1/node_2");
        assertEquals(tree.getPathById(mode, n3.getId()), "/node_1/node_2/node_3");
        assertEquals(tree.getPathById(mode, childNode.getId()), "/node_1/node_2/node_2_1");

        contentPK = n2.getReference();
        ce.remove(contentPK);

        try { // trying to find the same child node as in the edit tree should throw an exception
            tree.findChild(mode, nodes[0], "node_2_1");
            fail("Trying to retrieve node_2_1 should have thrown an exception");
        } catch (FxNotFoundException e) {
            // expected
        }
        assertEquals(tree.getPathById(mode, n1.getId()), "/node_1");
        assertEquals(tree.getPathById(mode, childNode.getId()), "_");

        // clean up
        tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
    }

    /**
     * Create a lot of nodes trying to provoke errors
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void massCreateTest() throws FxApplicationException {
        clearTrees();
        disableTreeChecks(); // disable to improve performance
        try {
            FxTreeMode mode = FxTreeMode.Edit;
            String path = "/t1/t2/t3/t4/t5/t6/t7/t8/t9/t10";
            for (int i = 0; i < 3; i++) {
                path = path + "/f" + (i + 1);
                tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, path);
                FxTreeNode parent = tree.getNode(mode, tree.getIdByPath(mode, path));
                createSubNodes(parent, 40);
            }
            // check consistency
            enableTreeChecks();
            triggerTreeCheck(mode);
        } finally {
            enableTreeChecks();
        }
    }

    private void triggerTreeCheck(FxTreeMode mode) throws FxApplicationException {
        final long testId = tree.save(FxTreeNodeEdit.createNew("test").setParentNodeId(FxTreeNode.ROOT_NODE));
        if (FxTreeMode.Live == mode) {
            tree.activate(FxTreeMode.Edit, testId, false, true);
        }
        tree.remove(FxTreeMode.Edit, testId, FxTreeRemoveOp.Remove, false);
    }

    /**
     * Create count subnodes attached to parent
     *
     * @param parent parent node
     * @param count  number of subnodes to create
     * @throws FxApplicationException on errors
     */
    private void createSubNodes(FxTreeNode parent, int count) throws FxApplicationException {
        FxTreeNodeEdit node;
        for (int i = 0; i < count; i++) {
            node = FxTreeNodeEdit.createNewChildNode(parent).setName("Node" + i);
            tree.save(node);
        }
    }

    public static long scriptCounter = 0;

    /**
     * Test add/remove node scripting
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void scriptingAddRemoveTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        scriptCounter = 0;
        String code = "println \"[Groovy script]=== After adding node ${node.id}: ${node.path} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter++";
        FxScriptInfo siAdd = EJBLookup.getScriptingEngine().createScript(new FxScriptInfoEdit(-1,
                FxScriptEvent.AfterTreeNodeAdded, "afterNodeAdded.gy", "Test script", code, true, true));
        code = "println \"[Groovy script]=== Before removing node ${node.id}: ${node.path} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter -= 2";
        FxScriptInfo siBeforeRemove = EJBLookup.getScriptingEngine().createScript(new FxScriptInfoEdit(-1,
                FxScriptEvent.BeforeTreeNodeRemoved, "beforeNodeRemoved.gy", "Test script", code, true, true));
        code = "println \"[Groovy script]=== After removing node ${node.id}: ${node.path} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter++";
        FxScriptInfo siAfterRemove = EJBLookup.getScriptingEngine().createScript(new FxScriptInfoEdit(-1,
                FxScriptEvent.AfterTreeNodeRemoved, "afterNodeRemoved.gy", "Test script", code, true, true));
        try {
            assertEquals(scriptCounter, 0);
            long nodeId = tree
                    .save(FxTreeNodeEdit.createNew("Test1").setMode(mode).setParentNodeId(FxTreeNode.ROOT_NODE));
            assertEquals(scriptCounter, 1);
            long topNode = tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, "/A/B/C")[0];
            assertEquals(scriptCounter, 4);
            tree.copy(mode, topNode, nodeId, 0, false);
            assertEquals(scriptCounter, 7);
            tree.remove(tree.getNode(mode, topNode), FxTreeRemoveOp.Unfile, true);
            assertEquals(scriptCounter, 4);
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.Unfile, true);
            assertEquals(scriptCounter, 0);
        } finally {
            scripting.remove(siAdd.getId());
            scripting.remove(siBeforeRemove.getId());
            scripting.remove(siAfterRemove.getId());
        }
    }

    /**
     * Test activate node scripting
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void scriptingActivateTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        scriptCounter = 0;
        String code = "println \"[Groovy script]=== Before activating node ${node.id}: ${node.path} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter+=2";
        FxScriptInfo siBeforeActivate = EJBLookup.getScriptingEngine().createScript(new FxScriptInfoEdit(-1,
                FxScriptEvent.BeforeTreeNodeActivated, "beforeNodeActivate.gy", "Test script", code, true, true));
        code = "println \"[Groovy script]=== After activating node ${node.id}: ${node.path} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter--";
        FxScriptInfo siAfterActivate = EJBLookup.getScriptingEngine().createScript(new FxScriptInfoEdit(-1,
                FxScriptEvent.AfterTreeNodeActivated, "afterNodeActivate.gy", "Test script", code, true, true));
        try {
            assertEquals(scriptCounter, 0);
            long nodeId = tree
                    .save(FxTreeNodeEdit.createNew("Test1").setMode(mode).setParentNodeId(FxTreeNode.ROOT_NODE));
            tree.activate(FxTreeMode.Edit, nodeId, false, true);
            assertEquals(scriptCounter, 1);

            long topNode = tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, "/A/B/C")[0];
            tree.activate(FxTreeMode.Edit, topNode, true, true);
            assertEquals(scriptCounter, 4);
        } finally {
            scripting.remove(siBeforeActivate.getId());
            scripting.remove(siAfterActivate.getId());
        }
    }

    /**
     * Test replacing a content with a folder scripting
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void scriptingFolderReplacementTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        scriptCounter = 0;
        ContentEngine co = getContentEngine();
        FxPK testContent = co.save(co.initialize(FxType.ROOT_ID));
        String code = "println \"[Groovy script]=== AfterTreeNodeFolderReplacement node ${node.id}: ${node.path}. co-pk: ${content}, folder-pk: ${node.reference} ===\"\n"
                + "com.flexive.tests.embedded.FxTreeTest.scriptCounter=42\n"
                + "if(content.id == node.reference.id) com.flexive.tests.embedded.FxTreeTest.scriptCounter=-1";
        FxScriptInfo siAfterTreeNodeFolderReplacement = EJBLookup.getScriptingEngine()
                .createScript(new FxScriptInfoEdit(-1, FxScriptEvent.AfterTreeNodeFolderReplacement,
                        "treeNodeFolderReplacement.gy", "Test script", code, true, true));
        try {
            assertEquals(scriptCounter, 0);
            tree.save(FxTreeNodeEdit.createNew("Test1").setMode(mode).setParentNodeId(FxTreeNode.ROOT_NODE)
                    .setReference(testContent));
            tree.createNodes(mode, FxTreeNode.ROOT_NODE, 0, "/Test1/A");
            co.remove(testContent);
            assertEquals(scriptCounter, 42);
        } finally {
            scripting.remove(siAfterTreeNodeFolderReplacement.getId());
            try {
                co.remove(testContent);
            } catch (FxApplicationException e) {
                //ignore
            }
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void treeIteratorTest() throws FxApplicationException {
        clearTrees();
        final String[] names = { "my", "virtual", "directory" };
        final long[] nodes = tree.createNodes(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, 0,
                StringUtils.join(names, "/"));
        int index = 0;
        for (FxTreeNode node : tree.getTree(FxTreeMode.Edit, nodes[0], 5)) {
            assertTrue(node.getName().equals(names[index]),
                    "Expected node name: " + names[index] + ", got: " + node.getName());
            index++;
        }

        // test mixed folder/node access
        tree.createNodes(FxTreeMode.Edit, nodes[1], 999, "new/directory");
        final String[] expected = { "my", "virtual", "directory", "new", "directory" };
        /* new folder structure:
        my/
            virtual/
                directory/
                new/
                    directory/
         */
        index = 0;
        for (FxTreeNode node : tree.getTree(FxTreeMode.Edit, nodes[0], 5)) {
            assertTrue(node.getName().equals(expected[index]),
                    "Expected node name: " + expected[index] + ", got: " + node.getName());
            index++;
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void treeCaptionPathToIdTest() throws FxApplicationException {
        clearTrees();
        FxTreeNodeEdit tn = FxTreeNodeEdit.createNew("NodeName");
        tn.setLabel(new FxString(false, "NodeLabel"));
        tn.setParentNodeId(FxTreeNode.ROOT_NODE);
        long nodeId = tree.save(tn);
        assertTrue(tree.getIdByLabelPath(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, "/NodeLabel") == nodeId);
        FxTreeNodeEdit tn2 = FxTreeNodeEdit.createNew("NodeName2");
        tn2.setLabel(new FxString(false, "NodeLabel2"));
        tn2.setParentNodeId(nodeId);
        long nodeId2 = tree.save(tn2);
        assertTrue(
                tree.getIdByLabelPath(FxTreeMode.Edit, FxTreeNode.ROOT_NODE, "/NodeLabel/NodeLabel2") == nodeId2);
    }

    /**
     * Tests #findChild and #getNodesWithReference methods
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void findChildrenTest() throws FxApplicationException {
        clearTrees();
        final String node2 = "node_2";
        FxTreeMode mode1, mode2;
        FxTreeNode result;
        List<FxTreeNode> nodesWithRefResults;
        FxPK pk;
        final String contentXPath = "TREETESTTYPE/TESTPROP";
        final String testData = "testdata, default lang 1234567890";
        mode1 = FxTreeMode.Edit;
        mode2 = FxTreeMode.Live;
        long[] nodes = createTestTree(mode1);

        // find by name
        result = tree.findChild(mode1, nodes[0], node2);
        assertEquals(result.getName(), node2);
        try { // let's cause an exception
            tree.findChild(mode2, nodes[0], node2);
            fail("Should not be able to find " + node2 + " in Live tree");
        } catch (FxNotFoundException e) {
            // expected
        }

        pk = result.getReference();
        // find by reference id
        result = tree.findChild(mode1, nodes[0], pk.getId());
        assertEquals(result.getName(), node2);
        try { // let's cause an exception
            tree.findChild(mode2, nodes[0], pk.getId());
            fail("Should not be able to find " + node2 + " in Live tree");
        } catch (FxNotFoundException e) {
            // expected
        }

        // find by pk (reference)
        result = tree.findChild(mode1, nodes[0], pk);
        assertEquals(result.getName(), node2);
        try { // let's cause an exception
            tree.findChild(mode2, nodes[0], pk);
            fail("Should not be able to find " + node2 + " in Live tree");
        } catch (FxNotFoundException e) {
            // expected
        }

        // find by FxReference
        // this also tests creating a content instance of a type and using it as a tree node
        FxTreeNode parent = tree.getNode(mode1, nodes[0]);
        long typeId = createTestType();
        FxPK contentPK = createTestContent(parent, typeId, testData);
        FxReference ref = new FxReference(false, new ReferencedContent(contentPK));
        result = tree.findChild(mode1, nodes[0], ref);
        assertEquals(result.getReference(), contentPK);
        try { // let's cause an exception
            tree.findChild(mode2, nodes[0], ref);
            fail("Should not be able to find " + contentPK + " in Live tree");
        } catch (FxNotFoundException e) {
            // expected
        }

        // retrieve all nodes with references
        nodesWithRefResults = tree.getNodesWithReference(mode1, ref.getBestTranslation().getId());
        int resultOK = 0;
        FxTreeNode tmp = null;
        for (FxTreeNode node : nodesWithRefResults) {
            if (node.getParentNodeId() == nodes[0]) {
                resultOK++;
                tmp = node;
            }
        }
        assertEquals(resultOK, 1); // should find exactly one node with a content reference
        try {
            FxContent co = ce.load(tmp.getReference());
            assertEquals(co.getValue(contentXPath).toString(), testData);
        } catch (NullPointerException e) { // Idea satisfaction guaranteed
            fail("The FxContent should have been found");
        }

        // clean up
        ce.remove(contentPK);
        ty.remove(typeId);
        tree.remove(mode1, nodes[0], FxTreeRemoveOp.Remove, true);
    }

    /**
     * Tests the #getPaths() and #getPathById methods
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void getPathsTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        long[] nodes = createTestTree(mode);
        final String node2_2 = "node_2_2";
        final String expectedPath1 = "/node_1/node_2/node_2_2";
        final String expectedPath2 = "/node_1/node_2/node_3";
        final long invalidNodeId = 1234567890;
        int pathsOK = 0;

        FxTreeNode node = tree.findChild(mode, nodes[1], node2_2);

        // retrieve the paths for node_2.2 and node_3
        for (String path : tree.getPaths(mode, nodes[2], node.getId())) {
            if (path.equals(expectedPath1) || path.equals(expectedPath2))
                pathsOK++;
        }
        assertEquals(pathsOK, 2); // should find 2 correct paths
        pathsOK = 0; // reset

        // assert that an invalid node id will result in an empty path (represented by "_")
        String returnedPath = "";
        for (String path : tree.getPaths(mode, invalidNodeId)) {
            if (path == null || path.equals("_")) // returns an underscore if no path was found
                returnedPath = path;
            pathsOK++;
        }
        assertTrue(pathsOK > 0);
        assertEquals(returnedPath, "_");

        // test getPathById()
        String path = tree.getPathById(mode, nodes[2]);
        assertEquals(path, expectedPath2);

        // clean up
        tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
    }

    /**
     * STUB TEST: #setData and #getData are not implemented yet!!
     * Tests the #setData and #getData methods
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void setAndGetDataTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        final String data1 = "Template A";
        final String data2 = "Template B";
        long nodes[] = createTestTree(mode);

        try {
            tree.setData(mode, nodes[0], data1);
            tree.setData(mode, nodes[1], data2);
        } catch (RuntimeException e) {
            // expected, method throws UnsupportedOperationException
        }

        try {
            tree.getDatas(mode, nodes[2]);
        } catch (RuntimeException e) {
            // expected, method throws UnsupportedOperationException
        }
        // clean up
        tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
    }

    /**
     * Tests the #getIdChain and #getReverseIdChainmethods
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void getIdsTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        long nodes[] = createTestTree(mode);
        long invalidId = 1234567890;

        long[] ids = tree.getIdChain(mode, nodes[2]); // should return 1, nodes[0], nodes[1], nodes[2]
        assertEquals(ids[0], 1);
        assertEquals(ids[1], nodes[0]);
        assertEquals(ids[2], nodes[1]);
        assertEquals(ids[3], nodes[2]);

        assertEquals(tree.getIdChain(mode, invalidId), null); // should be null

        ids = tree.getReverseIdChain(mode, nodes[2]); // should return nodes[2], nodes[1], nodes[0], 1
        assertEquals(ids[0], nodes[2]);
        assertEquals(ids[1], nodes[1]);
        assertEquals(ids[2], nodes[0]);
        assertEquals(ids[3], 1);

        // clean up
        tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
    }

    /**
     * This tests the #populate method
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = "tree-stress")
    public void populateTest() throws FxApplicationException {
        clearTrees();
        disableTreeChecks();
        try {
            // create the testdata in the live tree, assert that names of the root nodes
            FxTreeMode mode = FxTreeMode.Live;
            FxTreeNode n1, n2, n3;
            int maxLevel = 1;
            final String node1 = "Level2_0_";
            final String node2 = "Level3_0_9_";
            final String node3 = "Level4_0_9_99_";

            try {
                tree.populate(mode, maxLevel);
            } catch (FxApplicationException e) {
                fail(e.getMessage());
            }
            n1 = tree.findChild(mode, 1, node1);
            n2 = tree.findChild(mode, n1.getId(), node2);
            n3 = tree.findChild(mode, n2.getId(), node3);

            assertEquals(n1.getName(), node1);
            assertEquals(n2.getName(), node2);
            assertEquals(n3.getName(), node3);

            //clean up
            tree.remove(mode, n1.getId(), FxTreeRemoveOp.Remove, true);

            enableTreeChecks();
            triggerTreeCheck(mode);
        } finally {
            enableTreeChecks();
        }
    }

    /**
     * GenericTreeStorage: #setData method
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void genericTreeStorageSetDataTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        long[] nodes = createTestTree(mode);
        String data = "TEMPLATE A";
        TreeStorage treeStorage = new TreeStorage();

        Connection con = null;
        try {
            con = Database.getDbConnection();
            treeStorage.setData(con, mode, nodes[0], data);

            FxTreeNode n = tree.getNode(mode, nodes[0]);
            assertEquals(n.getData(), data);

        } catch (Throwable t) {
            fail("GenericTreeStorage #setData shouldn't throw an exception. Exception: " + t.getMessage());
            t.printStackTrace();
        } finally {
            Database.closeObjects(FxTreeTest.class, con, null);
            // clean up
            tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
        }
    }

    /**
     * GenericTreeStorage: #copy & #getCopyOfCount (explicitly)
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void genericTreeStorageGetCopyOfCountTest() throws FxApplicationException {
        clearTrees();
        FxTreeMode mode = FxTreeMode.Edit;
        long[] nodes = createTestTree(mode);
        long typeId = createTestType();
        FxTreeNode node = tree.getNode(mode, nodes[0]);
        final String testData = "TESTDATA TO BE COPIED";
        final int expectedCopyCount = 1; // expected copy count result - we will copy a node exactly once
        TreeStorage treeStorage = new TreeStorage();
        FxPK contentPK = createTestContent(node, typeId, testData);
        FxTreeNode contentNode1 = tree.findChild(mode, nodes[0], contentPK);

        long copyId = tree.copy(mode, contentNode1.getId(), nodes[2], 0, false);
        FxTreeNode contentNode2 = tree.findChild(mode, nodes[2], contentPK); // get the new tree node

        assertEquals(copyId, contentNode2.getId());
        assertEquals(contentNode2.getName(), contentNode1.getName());
        assertEquals(contentNode1.getReference(), contentNode2.getReference());

        Connection con = null;
        try {
            con = Database.getDbConnection();
            int actualCopyCount = treeStorage.getCopyOfCount(con, mode, "", nodes[2], copyId);
            assertEquals(actualCopyCount, expectedCopyCount);
        } catch (Exception e) {
            fail("GenericTreeStorage #getCopyOfCount shouldn't throw an exception. Exception: " + e.getMessage());
        } finally {
            Database.closeObjects(FxTreeTest.class, con, null);
            // clean up
            ce.remove(contentPK);
            ty.remove(typeId);
            tree.remove(mode, nodes[0], FxTreeRemoveOp.Remove, true);
        }
    }

    /**
     * GenericTreeStorage: #flagDirty method
     *
     * @throws FxApplicationException on errors
     */
    @Test(groups = { "ejb", "tree" })
    public void genericTreeStorageFlagDirtyTest() throws FxApplicationException {
        clearTrees();
        TreeStorage treeStorage = new TreeStorage();
        FxTreeMode mode = FxTreeMode.Edit;
        long nodeId = createTestTree(mode)[0];

        Connection con = null;
        try {
            con = Database.getDbConnection();
            treeStorage.flagDirty(con, mode, nodeId);
            FxTreeNode n = tree.getNode(mode, nodeId);
            assertTrue(n.isDirty());
        } catch (Exception e) {
            fail("Failed to flag node " + nodeId + " dirty. Exception: " + e.getMessage());
        } finally {
            Database.closeObjects(FxTreeTest.class, con, null);
            // clean up
            tree.remove(mode, nodeId, FxTreeRemoveOp.Remove, true);
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void manyMovesTest() throws FxApplicationException {
        // stress tree engine moves somewhat in reaction to bug FX-724, however, I haven't been able
        // to reproduce it yet.

        // create nodes in the root node and in one subfolder
        final List<Long> rootNodeIds = Lists.newArrayList(); // root node IDs
        final List<Long> folderNodeIds = Lists.newArrayList();

        try {
            disableTreeChecks();
            final long folderId = getTreeEngine()
                    .save(FxTreeNodeEdit.createNew("FX-724").setParentNodeId(FxTreeNode.ROOT_NODE));
            final int numNodes = 30;
            for (int i = 0; i < numNodes; i++) {
                rootNodeIds.add(getTreeEngine()
                        .save(FxTreeNodeEdit.createNew("test" + i).setParentNodeId(FxTreeNode.ROOT_NODE)));
                folderNodeIds.add(
                        getTreeEngine().save(FxTreeNodeEdit.createNew("nested" + i).setParentNodeId(folderId)));
            }
            try {
                randomMoves(FxTreeNode.ROOT_NODE, numNodes / 2);
                randomMoves(folderId, numNodes / 2);
            } finally {
                enableTreeChecks();
                getTreeEngine().remove(FxTreeMode.Edit, folderId, FxTreeRemoveOp.Remove, true);
                for (long nodeId : rootNodeIds) {
                    getTreeEngine().remove(FxTreeMode.Edit, nodeId, FxTreeRemoveOp.Remove, false);
                }
            }
        } finally {
            enableTreeChecks();
        }
    }

    private void randomMoves(long parentNodeId, int times) throws FxApplicationException {
        // create client-side copy to check if the moves are implemented correctly
        final List<FxTreeNode> children = Lists
                .newArrayList(getTreeEngine().getTree(FxTreeMode.Edit, parentNodeId, 1).getChildren());
        assertTrue(children.size() > 0);

        for (int i = 0; i < times; i++) {
            final FxTreeNode child = children.get(RandomUtils.nextInt(children.size()));
            final int oldPosition = children.indexOf(child);
            int newPosition = oldPosition;
            while (newPosition == oldPosition) {
                newPosition = RandomUtils.nextInt(children.size());
            }

            assertEquals(getTreeEngine().getNode(FxTreeMode.Edit, child.getId()).getPosition(), oldPosition,
                    "Invalid old node position.");

            getTreeEngine().move(FxTreeMode.Edit, child.getId(), parentNodeId, newPosition);

            children.remove(oldPosition);
            children.add(newPosition, child);

            assertEquals(
                    FxSharedUtils.getSelectableObjectIdList(
                            getTreeEngine().getTree(FxTreeMode.Edit, parentNodeId, 1).getChildren()),
                    FxSharedUtils.getSelectableObjectIdList(children), "Tree children order mixed up.");
        }
    }

    /**
     * Helper class for testing GenericTreeStorage methods
     */
    class TreeStorage extends GenericTreeStorageSpreaded {
        public TreeStorage() {
            super();
        }

        @Override
        public int getCopyOfCount(Connection con, FxTreeMode mode, String copyOfPrefix, long parentNodeId,
                long nodeId) throws FxTreeException {
            return super.getCopyOfCount(con, mode, copyOfPrefix, parentNodeId, nodeId);
        }

        @Override
        public void setData(Connection con, FxTreeMode mode, long nodeId, String data)
                throws FxApplicationException {
            super.setData(con, mode, nodeId, data);
        }

        @Override
        public void flagDirty(Connection con, FxTreeMode mode, long nodeId) throws FxUpdateException {
            super.flagDirty(con, mode, nodeId);
        }
    }

    /**
     * Helper method creates a test tree
     *
     * @param mode FxTreeMode (e.g. .Edit)
     * @return long[] array of tree node ids
     * @throws FxApplicationException on errors
     */
    private long[] createTestTree(FxTreeMode mode) throws FxApplicationException {
        FxTreeMode _mode = mode;
        if (mode == FxTreeMode.Live)
            _mode = FxTreeMode.Edit;
        final long[] nodes = tree.createNodes(_mode, FxTreeNode.ROOT_NODE, 0, "node_1/node_2/node_3");
        tree.createNodes(_mode, (int) nodes[1], 0, "node_2_1");
        tree.createNodes(_mode, (int) nodes[1], 1, "node_2_2");
        tree.createNodes(_mode, (int) nodes[2], 0, "node_3_1");
        if (mode == FxTreeMode.Live)
            tree.activate(_mode, nodes[0], true, true);
        return nodes;
    }

    /**
     * Creates a test type
     *
     * @return the id of the test type
     * @throws FxApplicationException on errors
     */
    private long createTestType() throws FxApplicationException {
        long typeId = EJBLookup.getTypeEngine()
                .save(FxTypeEdit.createNew("TREETESTTYPE", new FxString("Tree test type"),
                        getEnvironment().getACL(ACLCategory.STRUCTURE.getDefaultId()), null));

        /*EJBLookup.getAssignmentEngine().createProperty(typeId, FxPropertyEdit.createNew(
            "TESTPROP", new FxString(true, FxLanguage.ENGLISH, "TESTPROP"), new FxString(true, FxLanguage.ENGLISH, "TESTPROP"),
            new FxMultiplicity(0, 5), CacheAdmin.getEnvironment().getACL(ACLCategory.STRUCTURE.getDefaultId()),
            FxDataType.String1024).setMultiLang(false), "/");*/
        EJBLookup.getAssignmentEngine()
                .save(FxPropertyAssignmentEdit.reuse("ROOT/FQN", "TREETESTTYPE", "/", "TESTPROP"), false);
        return typeId;
    }

    /**
     * Creates a test content instance for the reference search and attaches the content to a given FxTreeNode
     *
     * @param parentNode the parent FxTreeNode to which the content should be attached
     * @param typeId     the id of the type for which the content instance will be created
     * @param testData   a String value for test data
     * @return FxPK the content instance PK
     * @throws FxApplicationException on errors
     */
    private FxPK createTestContent(FxTreeNode parentNode, long typeId, String testData)
            throws FxApplicationException {
        // create content for testing - will produce console output
        FxString data = new FxString(false, testData);

        FxContent co = ce.initialize(typeId);
        FxPK contentPK = ce.save(co);
        co = ce.load(contentPK);
        co.setValue("/TESTPROP[1]", data);
        contentPK = ce.save(co);
        tree.save(FxTreeNodeEdit.createNew(String.valueOf(contentPK)).setParentNodeId(parentNode.getId())
                .setReference(contentPK));

        return contentPK;
    }

    @Test(groups = { "ejb", "tree" })
    public void removeOpTest() throws FxApplicationException {
        long typeId = createTestType();

        try {
            clearTrees();
            TreeStorage treeStorage = new TreeStorage();
            FxTreeMode mode = FxTreeMode.Edit;
            FxTreeNodeEdit tne = FxTreeNodeEdit.createNew("TestRemove").setMode(mode);

            FxTreeNode rootNode = tree.getNode(mode, FxTreeNode.ROOT_NODE);

            FxPK pk = createTestContent(rootNode, typeId, "TestData123");
            long nodeId = tree.getIdByPath(mode, "/TestData123");
            assertEquals(tree.getNode(mode, nodeId).getReference().getId(), pk.getId());

            //Unfile: remove node but keep content
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.Unfile, false);
            assertFalse(tree.exist(mode, nodeId), "Node should have been removed!");
            try {
                ce.getContentVersionInfo(pk);
            } catch (FxApplicationException e) {
                fail("Expected pk to exist!", e);
            }

            nodeId = tree.save(FxTreeNodeEdit.createNew("TestData4").setReference(pk).setMode(mode)
                    .setParentNodeId(FxTreeNode.ROOT_NODE));
            assertEquals(tree.getNode(mode, nodeId).getReference().getId(), pk.getId()); //check exists and pk is correct

            //Remove: remove node AND content
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.Remove, false);
            assertFalse(tree.exist(mode, nodeId), "Node should have been removed!");
            try {
                ce.getContentVersionInfo(pk);
                fail("Content should no longer exist!");
            } catch (FxApplicationException e) {
                //expected
            }

            pk = createTestContent(rootNode, typeId, "TestData123");
            nodeId = tree.getIdByPath(mode, "/TestData123");
            assertEquals(tree.getNode(mode, nodeId).getReference().getId(), pk.getId()); //check exists and pk is correct

            //RemoveSingleFiled: remove node AND content if content is not referenced from other nodes
            //test1: like Remove the content should be removed
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.RemoveSingleFiled, false);
            assertFalse(tree.exist(mode, nodeId), "Node should have been removed!");
            try {
                ce.getContentVersionInfo(pk);
                fail("Content should no longer exist!");
            } catch (FxApplicationException e) {
                //expected
            }

            pk = createTestContent(rootNode, typeId, "TestData123");
            nodeId = tree.getIdByPath(mode, "/TestData123");
            long nodeId2 = tree.save(FxTreeNodeEdit.createNew("TestData5").setReference(pk).setMode(mode)
                    .setParentNodeId(FxTreeNode.ROOT_NODE));
            assertEquals(tree.getNode(mode, nodeId).getReference().getId(), pk.getId()); //check exists and pk is correct

            //RemoveSingleFiled: remove node AND content if content is not referenced from other nodes
            //test2: content should still exist since it is referenced by 2 nodes
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.RemoveSingleFiled, false);
            assertFalse(tree.exist(mode, nodeId), "Node should have been removed!");
            assertTrue(tree.exist(mode, nodeId2), "Node2 should still exist!");
            try {
                ce.getContentVersionInfo(pk);
            } catch (FxApplicationException e) {
                fail("Expected pk to exist!", e);
            }
            final long folderId = tree.save(
                    FxTreeNodeEdit.createNew("TestDataFolder").setMode(mode).setParentNodeId(FxTreeNode.ROOT_NODE));
            tree.save(FxTreeNodeEdit.createNew("TestData5_Child").setReference(pk).setMode(mode)
                    .setParentNodeId(folderId));
            tree.remove(tree.getNode(mode, folderId), FxTreeRemoveOp.RemoveSingleFiled, true);
            // content should still exist
            assertTrue(tree.exist(mode, nodeId2), "Node2 should still exist!");

            nodeId = tree.save(FxTreeNodeEdit.createNew("TestData6").setReference(pk).setMode(mode)
                    .setParentNodeId(FxTreeNode.ROOT_NODE));
            assertEquals(tree.getTree(mode, FxTreeNode.ROOT_NODE, 10).getDirectChildCount(), 2, "");

            //UnfileAll: remove both nodes but not the content
            tree.remove(tree.getNode(mode, nodeId), FxTreeRemoveOp.UnfileAll, false);
            assertFalse(tree.exist(mode, nodeId), "Node should have been removed!");
            assertFalse(tree.exist(mode, nodeId2), "Node2 should have been removed!");
            try {
                ce.getContentVersionInfo(pk);
            } catch (FxApplicationException e) {
                fail("Expected pk to exist!", e);
            }
        } finally {
            // clean up
            ce.removeForType(typeId);
            ty.remove(typeId);
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void testEscapedTreePathLookup_FX796() throws FxApplicationException {
        final String[] tests = { "name with spaces", "name-with-dash", "name with colon:",
                "name-with-umlaut: \u00f6" };
        for (String test : tests) {
            final long nodeId = getTreeEngine()
                    .save(FxTreeNodeEdit.createNew(test).setParentNodeId(FxTreeNode.ROOT_NODE));
            try {
                assertEquals(getTreeEngine().getIdByPath(FxTreeMode.Edit, "/" + test), nodeId,
                        "Invalid node returned for path: " + test);
            } finally {
                getTreeEngine().remove(FxTreeMode.Edit, nodeId, FxTreeRemoveOp.Remove, false);
            }
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void testLongPaths() throws FxApplicationException {
        if ("Oracle".equals(FxContext.get().getDivisionData().getDbVendor()))
            return; //skip this test with oracle as the stored procedures currently can not pass parameters longer than 4k characters ...
        // push FQN path limits
        long firstFolderId = -1;
        long folderId = FxTreeNode.ROOT_NODE;
        try {
            final List<String> paths = Lists.newArrayList();
            final List<String> captions = Lists.newArrayList();

            for (int i = 0; i < 10; i++) {
                final String path = RandomStringUtils.randomAlphanumeric(1024);
                final String caption = RandomStringUtils.randomAlphanumeric(1024);
                folderId = getTreeEngine().save(FxTreeNodeEdit.createNew(path).setLabel(new FxString(true, caption))
                        .setParentNodeId(folderId));
                if (firstFolderId == -1) {
                    firstFolderId = folderId;
                }
                paths.add(path);
                captions.add(caption);
            }

            for (int i = 1; i < 10; i++) {
                getTreeEngine().getIdByPath(FxTreeMode.Edit, "/" + StringUtils.join(paths.subList(0, i), '/'));
                getTreeEngine().getIdByLabelPath(FxTreeMode.Edit, FxTreeNode.ROOT_NODE,
                        "/" + StringUtils.join(captions.subList(0, i), '/'));
            }
        } finally {
            getTreeEngine().remove(FxTreeMode.Edit, firstFolderId, FxTreeRemoveOp.Remove, true);
        }
    }

    @Test(groups = { "ejb", "tree" })
    public void testReorgParent() throws FxApplicationException {
        // trigger a bug that occured when triggering a parent reorg through TreeEngine#move
        treeFolderStressTest(10, 10);
    }

    @Test(groups = "tree-stress")
    public void testDeepTree() throws FxApplicationException {
        treeFolderStressTest(500, 50);
    }

    @Test(groups = "tree-stress")
    public void testDeepTree2() throws FxApplicationException {
        treeFolderStressTest(5, 500);
    }

    private void treeFolderStressTest(int testInstanceCount, int maxDepth) throws FxApplicationException {
        long folderId = FxTreeNode.ROOT_NODE;
        long firstFolderId = -1;
        long instancesFolderId = -1;
        try {
            // create some test nodes that will be moved
            instancesFolderId = getTreeEngine()
                    .save(FxTreeNodeEdit.createNew("testinstances").setParentNodeId(FxTreeNode.ROOT_NODE));
            final FxContent co = getContentEngine().initialize("CMIS_PERSON");
            final FxPK pk = getContentEngine().save(co);
            for (int i = 0; i < testInstanceCount; i++) {
                getTreeEngine().save(FxTreeNodeEdit.createNew("instance" + i).setReference(pk)
                        .setParentNodeId(instancesFolderId));
            }
            assertEquals(getTreeEngine().getNode(FxTreeMode.Edit, instancesFolderId).getDirectChildCount(),
                    testInstanceCount);
            // create nested folders
            final List<Long> folderIds = Lists.newArrayListWithCapacity(maxDepth);
            for (int i = 0; i < maxDepth; i++) {
                folderId = getTreeEngine()
                        .save(FxTreeNodeEdit.createNew("deep_l" + i).setParentNodeId(folderId).setPosition(-1));
                if (firstFolderId == -1) {
                    firstFolderId = folderId;
                }
                folderIds.add(folderId);
                getTreeEngine().move(FxTreeMode.Edit, instancesFolderId, folderId, -1);
            }

            // bubble test folder back to the root node
            Collections.reverse(folderIds);
            for (long id : folderIds) {
                getTreeEngine().move(FxTreeMode.Edit, instancesFolderId, id, -1);
            }
        } finally {
            if (instancesFolderId != -1) {
                getTreeEngine().remove(FxTreeMode.Edit, instancesFolderId, FxTreeRemoveOp.Remove, true);
            }
            if (firstFolderId != -1) {
                getTreeEngine().remove(FxTreeMode.Edit, firstFolderId, FxTreeRemoveOp.Remove, true);
            }
        }
    }

}