/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.test;

import junit.framework.TestResult;

import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.Session;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Repository;
import javax.jcr.NamespaceException;
import javax.jcr.RangeIterator;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.retention.RetentionManager;

import java.util.StringTokenizer;
import java.util.Random;
import java.util.List;
import java.util.Arrays;

/**
 * Abstract base class for all JCR test classes.
 */
public abstract class AbstractJCRTest extends JUnitTest {

    /**
     * Pool of helper objects to access repository transparently
     */
    private static final RepositoryHelperPool HELPER_POOL = RepositoryHelperPoolImpl.getInstance();

    /**
     * Namespace URI for jcr prefix.
     */
    public static final String NS_JCR_URI = "http://www.jcp.org/jcr/1.0";

    /**
     * Namespace URI for nt prefix.
     */
    public static final String NS_NT_URI = "http://www.jcp.org/jcr/nt/1.0";

    /**
     * Namespace URI for mix prefix.
     */
    public static final String NS_MIX_URI = "http://www.jcp.org/jcr/mix/1.0";

    /**
     * Namespace URI for sv prefix
     */
    public static final String NS_SV_URI = "http://www.jcp.org/jcr/sv/1.0";

    /**
     * The repository helper for this test.
     */
    private RepositoryHelper helper;

    /**
     * JCR Name jcr:primaryType using the namespace resolver of the current session.
     */
    protected String jcrPrimaryType;

    /**
     * JCR Name jcr:mixinTypes using the namespace resolver of the current session.
     */
    protected String jcrMixinTypes;

    /**
     * JCR Name jcr:predecessors using the namespace resolver of the current session.
     */
    protected String jcrPredecessors;

    /**
     * JCR Name jcr:successors using the namespace resolver of the current session.
     */
    protected String jcrSuccessors;

    /**
     * JCR Name jcr:created using the namespace resolver of the current session.
     */
    protected String jcrCreated;

    /**
     * JCR Name jcr:created using the namespace resolver of the current session.
     */
    protected String jcrVersionHistory;

    /**
     * JCR Name jcr:copiedFrom using the namespace resolver of the current session.
     */
    protected String jcrCopiedFrom;

    /**
     * JCR Name jcr:frozenNode using the namespace resolver of the current session.
     */
    protected String jcrFrozenNode;

    /**
     * JCR Name jcr:frozenUuid using the namespace resolver of the current session.
     */
    protected String jcrFrozenUuid;

    /**
     * JCR Name jcr:rootVersion using the namespace resolver of the current session.
     */
    protected String jcrRootVersion;

    /**
     * JCR Name jcr:isCheckedOut using the namespace resolver of the current session.
     */
    protected String jcrIsCheckedOut;

    /**
     * JCR Name jcr:baseVersion using the namespace resolver of the current session.
     */
    protected String jcrBaseVersion;

    /**
     * JCR Name jcr:uuid using the namespace resolver of the current session.
     */
    protected String jcrUUID;

    /**
     * JCR Name jcr:lockOwner using the namespace resolver of the current session.
     */
    protected String jcrLockOwner;

    /**
     * JCR Name jcr:lockIsDeep using the namespace resolver of the current session.
     */
    protected String jcrlockIsDeep;

    /**
     * JCR Name jcr:mergeFailed using the namespace resolver of the current session.
     */
    protected String jcrMergeFailed;

    /**
     * JCR Name jcr:system using the namespace resolver of the current session.
     */
    protected String jcrSystem;

    /**
     * JCR Name nt:base using the namespace resolver of the current session.
     */
    protected String ntBase;

    /**
     * JCR Name nt:unstructured using the namespace resolver of the current session.
     */
    protected String ntUnstructured;

    /**
     * JCR Name nt:version using the namespace resolver of the current session.
     */
    protected String ntVersion;

    /**
     * JCR Name nt:versionHistory using the namespace resolver of the current session.
     */
    protected String ntVersionHistory;

    /**
     * JCR Name nt:versionHistory using the namespace resolver of the current session.
     */
    protected String ntVersionLabels;

    /**
     * JCR Name nt:frozenNode using the namespace resolver of the current session.
     */
    protected String ntFrozenNode;

    /**
     * JCR Name mix:referenceable using the namespace resolver of the current session.
     */
    protected String mixReferenceable;

    /**
     * JCR Name mix:versionable using the namespace resolver of the current session.
     */
    protected String mixVersionable;

    /**
     * JCR Name mix:simpleVersionable using the namespace resolver of the current session.
     */
    protected String mixSimpleVersionable;

    /**
     * JCR Name mix:lockable using the namespace resolver of the current session.
     */
    protected String mixLockable;

    /**
     * JCR Name mix:title using the namespace resolver of the current session.
     */
    protected String mixTitle;

    /**
     * JCR Name mix:shareable using the namespace resolver of the current session.
     */
    protected String mixShareable;

    /**
     * JCR Name nt:query using the namespace resolver of the current session.
     */
    protected String ntQuery;

    /**
     * Relative path to the test root node.
     */
    protected String testPath;

    /**
     * Absolute path to the test root node.
     */
    protected String testRoot;

    /**
     * The node type name for newly created nodes.
     */
    protected String testNodeType;

    /**
     * The node type name for the test root node.
     */
    protected String testNodeTypeTestRoot;

    /**
     * A node type that does not allow any child nodes, such as nt:base.
     */
    protected String testNodeTypeNoChildren;

    /**
     * Name of a node that will be created during a test case.
     */
    protected String nodeName1;

    /**
     * Name of a node that will be created during a test case.
     */
    protected String nodeName2;

    /**
     * Name of a node that will be created during a test case.
     */
    protected String nodeName3;

    /**
     * Name of a node that will be created during a test case.
     */
    protected String nodeName4;

    /**
     * Name of a property that will be used during a test case.
     */
    protected String propertyName1;

    /**
     * Name of a property that will be used during a test case.
     */
    protected String propertyName2;

    /**
     * Name of a workspace to use instead of the default workspace.
     */
    protected String workspaceName;

    /**
     * The superuser session for the default workspace
     */
    protected Session superuser;

    /**
     * Flag that indicates if the current test is a read-only test, that is
     * no content is written to the workspace by the test.
     */
    protected boolean isReadOnly = false;

    /**
     * The root <code>Node</code> for testing
     */
    protected Node testRootNode;

    /**
     * The value factory for {@link #superuser}.
     */
    protected ValueFactory vf;

    protected void setUp() throws Exception {
        super.setUp();
        testRoot = getProperty(RepositoryStub.PROP_TESTROOT);
        if (testRoot == null) {
            fail("Property '" + RepositoryStub.PROP_TESTROOT + "' is not defined.");
        }

        // cut off '/' to build testPath
        testPath = testRoot.substring(1);
        testNodeType = getProperty(RepositoryStub.PROP_NODETYPE);
        testNodeTypeTestRoot = getProperty(RepositoryStub.PROP_NODETYPETESTROOT);
        if (testNodeTypeTestRoot == null) {
            testNodeTypeTestRoot = testNodeType; // backwards compatibility
        }
        testNodeTypeNoChildren = getProperty(RepositoryStub.PROP_NODETYPENOCHILDREN);
        // setup node names
        nodeName1 = getProperty(RepositoryStub.PROP_NODE_NAME1);
        if (nodeName1 == null) {
            fail("Property '" + RepositoryStub.PROP_NODE_NAME1 + "' is not defined.");
        }
        nodeName2 = getProperty(RepositoryStub.PROP_NODE_NAME2);
        if (nodeName2 == null) {
            fail("Property '" + RepositoryStub.PROP_NODE_NAME2 + "' is not defined.");
        }
        nodeName3 = getProperty(RepositoryStub.PROP_NODE_NAME3);
        if (nodeName3 == null) {
            fail("Property '" + RepositoryStub.PROP_NODE_NAME3 + "' is not defined.");
        }
        nodeName4 = getProperty(RepositoryStub.PROP_NODE_NAME4);
        if (nodeName4 == null) {
            fail("Property '" + RepositoryStub.PROP_NODE_NAME4 + "' is not defined.");
        }
        propertyName1 = getProperty(RepositoryStub.PROP_PROP_NAME1);
        if (propertyName1 == null) {
            fail("Property '" + RepositoryStub.PROP_PROP_NAME1 + "' is not defined.");
        }
        propertyName2 = getProperty(RepositoryStub.PROP_PROP_NAME2);
        if (propertyName2 == null) {
            fail("Property '" + RepositoryStub.PROP_PROP_NAME2 + "' is not defined.");
        }
        workspaceName = getProperty(RepositoryStub.PROP_WORKSPACE_NAME);
        if (workspaceName == null) {
            fail("Property '" + RepositoryStub.PROP_WORKSPACE_NAME + "' is not defined.");
        }

        superuser = getHelper().getSuperuserSession();

        // setup some common names
        jcrPrimaryType = superuser.getNamespacePrefix(NS_JCR_URI) + ":primaryType";
        jcrMixinTypes = superuser.getNamespacePrefix(NS_JCR_URI) + ":mixinTypes";
        jcrPredecessors = superuser.getNamespacePrefix(NS_JCR_URI) + ":predecessors";
        jcrSuccessors = superuser.getNamespacePrefix(NS_JCR_URI) + ":successors";
        jcrCreated = superuser.getNamespacePrefix(NS_JCR_URI) + ":created";
        jcrVersionHistory = superuser.getNamespacePrefix(NS_JCR_URI) + ":versionHistory";
        jcrCopiedFrom = superuser.getNamespacePrefix(NS_JCR_URI) + ":copiedFrom";
        jcrFrozenNode = superuser.getNamespacePrefix(NS_JCR_URI) + ":frozenNode";
        jcrFrozenUuid = superuser.getNamespacePrefix(NS_JCR_URI) + ":frozenUuid";
        jcrRootVersion = superuser.getNamespacePrefix(NS_JCR_URI) + ":rootVersion";
        jcrBaseVersion = superuser.getNamespacePrefix(NS_JCR_URI) + ":baseVersion";
        jcrIsCheckedOut = superuser.getNamespacePrefix(NS_JCR_URI) + ":isCheckedOut";
        jcrUUID = superuser.getNamespacePrefix(NS_JCR_URI) + ":uuid";
        jcrLockOwner = superuser.getNamespacePrefix(NS_JCR_URI) + ":lockOwner";
        jcrlockIsDeep = superuser.getNamespacePrefix(NS_JCR_URI) + ":lockIsDeep";
        jcrMergeFailed = superuser.getNamespacePrefix(NS_JCR_URI) + ":mergeFailed";
        jcrSystem = superuser.getNamespacePrefix(NS_JCR_URI) + ":system";
        ntBase = superuser.getNamespacePrefix(NS_NT_URI) + ":base";
        ntUnstructured = superuser.getNamespacePrefix(NS_NT_URI) + ":unstructured";
        ntVersion = superuser.getNamespacePrefix(NS_NT_URI) + ":version";
        ntVersionHistory = superuser.getNamespacePrefix(NS_NT_URI) + ":versionHistory";
        ntVersionLabels = superuser.getNamespacePrefix(NS_NT_URI) + ":versionLabels";
        ntFrozenNode = superuser.getNamespacePrefix(NS_NT_URI) + ":frozenNode";
        mixReferenceable = superuser.getNamespacePrefix(NS_MIX_URI) + ":referenceable";
        mixVersionable = superuser.getNamespacePrefix(NS_MIX_URI) + ":versionable";
        mixSimpleVersionable = superuser.getNamespacePrefix(NS_MIX_URI) + ":simpleVersionable";
        mixLockable = superuser.getNamespacePrefix(NS_MIX_URI) + ":lockable";
        mixShareable = superuser.getNamespacePrefix(NS_MIX_URI) + ":shareable";
        mixTitle = superuser.getNamespacePrefix(NS_MIX_URI) + ":title";
        ntQuery = superuser.getNamespacePrefix(NS_NT_URI) + ":query";

        // setup custom namespaces
        if (isSupported(Repository.LEVEL_2_SUPPORTED)) {
            NamespaceRegistry nsReg = superuser.getWorkspace().getNamespaceRegistry();
            String namespaces = getProperty(RepositoryStub.PROP_NAMESPACES);
            if (namespaces != null) {
                String[] prefixes = namespaces.split(" ");
                for (int i = 0; i < prefixes.length; i++) {
                    String uri = getProperty(RepositoryStub.PROP_NAMESPACES + "." + prefixes[i]);
                    if (uri != null) {
                        try {
                            nsReg.getPrefix(uri);
                        } catch (NamespaceException e) {
                            // not yet registered
                            nsReg.registerNamespace(prefixes[i], uri);
                        }
                    }
                }
            }
            vf = superuser.getValueFactory();
        }

        if (isReadOnly) {
            if (testPath.length() == 0) {
                // test root is the root node
                testRootNode = superuser.getRootNode();
            } else if (!superuser.getRootNode().hasNode(testPath)) {
                cleanUp();
                fail("Workspace does not contain test data at: " + testRoot);
            } else {
                testRootNode = superuser.getRootNode().getNode(testPath);
            }
        } else if (isSupported(Repository.LEVEL_2_SUPPORTED)) {
            testRootNode = cleanUpTestRoot(superuser);
            // also clean second workspace
            Session s = getHelper().getSuperuserSession(workspaceName);
            try {
                cleanUpTestRoot(s);
            } finally {
                s.logout();
            }
        } else {
            cleanUp();
            fail("Test case requires level 2 support.");
        }
    }

    protected void cleanUp() throws Exception {
        if (superuser != null) {
            try {
                if (!isReadOnly && isSupported(Repository.LEVEL_2_SUPPORTED)) {
                    cleanUpTestRoot(superuser);
                }
            } catch (Exception e) {
                log.println("Exception in tearDown: " + e.toString());
            } finally {
                superuser.logout();
                superuser = null;
                vf = null;
            }
        }
        testRootNode = null;
    }

    protected void tearDown() throws Exception {
        cleanUp();
        super.tearDown();
    }

    /**
     * Runs the test cases of this test class and reports the results to
     * <code>testResult</code>. In contrast to the default implementation of
     * <code>TestCase.run()</code> this method will suppress tests errors with
     * a {@link NotExecutableException}. That is, test cases that throw this
     * exception will still result as successful.
     * @param testResult the test result.
     */
    public void run(TestResult testResult) {
        try {
            helper = HELPER_POOL.borrowHelper();
            try {
                super.run(new JCRTestResult(testResult, log));
            } finally {
                HELPER_POOL.returnHelper(helper);
                helper = null;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @return the repository helper instance that is associated with this test.
     */
    protected RepositoryHelper getHelper() {
        return helper;
    }

    /**
     * Returns the value of the configuration property with <code>propName</code>.
     * The sequence how configuration properties are read is the follwoing:
     * <ol>
     * <li><code>javax.jcr.tck.&lt;testClassName>.&lt;testCaseName>.&lt;propName></code></li>
     * <li><code>javax.jcr.tck.&lt;testClassName>.&lt;propName></code></li>
     * <li><code>javax.jcr.tck.&lt;packageName>.&lt;propName></code></li>
     * <li><code>javax.jcr.tck.&lt;propName></code></li>
     * </ol>
     * Where:
     * <ul>
     * <li><code>&lt;testClassName></code> is the name of the test class without package prefix.</li>
     * <li><code>&lt;testMethodName></code> is the name of the test method</li>
     * <li><code>&lt;packageName></code> is the name of the package of the test class.
     * Example: packageName for <code>org.apache.jackrabbit.test.api.BooleanPropertyTest</code>: <code>api</code></li>
     * </ul>
     * @param propName the propName of the configration property.
     * @return the value of the property or <code>null</code> if the property
     *  does not exist.
     * @throws RepositoryException if an error occurs while reading from
     *  the configuration.
     */
    public String getProperty(String propName) throws RepositoryException {
        String testCaseName = getName();
        String testClassName = getClass().getName();
        String testPackName = "";
        int idx;
        if ((idx = testClassName.lastIndexOf('.')) > -1) {
            testPackName = testClassName.substring(testClassName.lastIndexOf('.', idx - 1) + 1, idx);
            testClassName = testClassName.substring(idx + 1);
        }

        // 1) test case specific property first
        String value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "."
                + testClassName + "." + testCaseName + "." + propName);
        if (value != null) {
            return value;
        }

        // 2) check test class property
        value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "."
                + testClassName + "." + propName);
        if (value != null) {
            return value;
        }

        // 3) check package property
        value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "."
                + testPackName + "." + propName);
        if (value != null) {
            return value;
        }

        // finally try global property
        return getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + propName);
    }

    /**
     * Returns the value of the configuration property with specified
     * <code>name</code>. If the property does not exist <code>defaultValue</code> is
     * returned.
     * <p>
     * Configuration properties are defined in the file:
     * <code>repositoryStubImpl.properties</code>.
     *
     * @param name the name of the property to retrieve.
     * @param defaultValue the default value if the property does not exist.
     * @return the value of the property or <code>defaultValue</code> if non existent.
     * @throws RepositoryException if the configuration file cannot be found.
     */
    public String getProperty(String name, String defaultValue) throws RepositoryException {
        String val = getProperty(name);
        if (val == null) {
            val = defaultValue;
        }
        return val;
    }

    /**
     * Create a JCR value based on the configuration.
     *
     * @param s
     * @param valueProp Name of the config property that contains the property value.
     * @param typeProp Name of the config property that contains the property type.
     * If the config parameter is missing, {@link PropertyType#STRING} is used
     * to create the JCR value.
     * @param defaultValue Default value to be used if the config does not define
     * the value property.
     * @return JCR value to be used for a test.
     * @throws RepositoryException
     */
    public Value getJcrValue(Session s, String valueProp, String typeProp, String defaultValue) throws RepositoryException {
        ValueFactory vf = s.getValueFactory();
        String val = getProperty(valueProp, defaultValue);
        int type = PropertyType.valueFromName(getProperty(typeProp, PropertyType.TYPENAME_STRING));
        return vf.createValue(val, type);
    }

    /**
     * Returns the size of the <code>RangeIterator</code> <code>it</code>.
     * Note, that the <code>RangeIterator</code> might get consumed, because
     * {@link RangeIterator#getSize()} might return -1 (information unavailable).
     * @param it a <code>RangeIterator</code>.
     * @return the size of the iterator (number of elements).
     */
    protected long getSize(RangeIterator it) {
        long size = it.getSize();
        if (size != -1) {
            return size;
        }
        size = 0;
        while (it.hasNext()) {
            it.next();
            size++;
        }
        return size;
    }

    /**
     * Returns the local name for the given <code>jcrName</code>.
     * 
     * @param jcrName
     *            the name.
     * @return the local name part.
     */
    protected static String getLocalName(String jcrName) {
        int idx = jcrName.indexOf(':');
        if (idx != -1) {
            return jcrName.substring(idx + 1);
        } else {
            return jcrName;
        }
    }

    /**
     * Returns the prefix for the given <code>jcrName</code>.
     * 
     * @param jcrName
     *            the name.
     * @return the prefix part (empty string when not prefixed)
     */
    protected static String getPrefix(String jcrName) {
        int idx = jcrName.indexOf(':');
        if (idx != -1) {
            return jcrName.substring(0, idx);
        } else {
            return "";
        }
    }

    /**
     * Returns the expanded name for the given <code>jcrName</code>.
     * 
     * @param jcrName
     *            the name.
     * @return the expanded name representation
     * @throws RepositoryException
     * @throws NamespaceException
     */
    protected static String getQualifiedName(Session session, String jcrName) throws RepositoryException {
        String prefix = getPrefix(jcrName);
        String namespace = session.getNamespaceURI(prefix);
        String localname = getLocalName(jcrName);
        return (namespace.length() > 0 ? "{" + namespace + "}" : "{}") + localname;
    }

    /**
     * Returns the name of a workspace that is not accessible from
     * <code>session</code>.
     * @param session the session.
     * @return name of a non existing workspace.
     * @throws RepositoryException if an error occurs.
     */
    protected String getNonExistingWorkspaceName(Session session) throws RepositoryException {
        List<String> names = Arrays.asList(session.getWorkspace().getAccessibleWorkspaceNames());
        String nonExisting = null;
        while (nonExisting == null) {
            String name = createRandomString(10);
            if (!names.contains(name)) {
                nonExisting = name;
            }
        }
        return nonExisting;
    }

    /**
     * Creates a <code>String</code> with a random sequence of characters
     * using 'a' - 'z'.
     * @param numChars number of characters.
     * @return the generated String.
     */
    protected String createRandomString(int numChars) {
        Random rand = new Random(System.currentTimeMillis());
        StringBuffer tmp = new StringBuffer(numChars);
        for (int i = 0; i < numChars; i++) {
            char c = (char) (rand.nextInt(('z' + 1) - 'a') + 'a');
            tmp.append(c);
        }
        return tmp.toString();
    }

    /**
     * Returns <code>true</code> if this repository support a certain optional
     * feature; otherwise <code>false</code> is returned. If there is no
     * such <code>descriptorKey</code> present in the repository, this method
     * also returns false.
     *
     * @param descriptorKey the descriptor key.
     * @return <code>true</code> if the option is supported.
     * @throws RepositoryException if an error occurs.
     */
    protected boolean isSupported(String descriptorKey) throws RepositoryException {
        return "true".equals(getHelper().getRepository().getDescriptor(descriptorKey));
    }

    /**
     * Throws a <code>NotExecutableException</code> if the repository does
     * not support the feature identified by the given <code>discriptorKey</code>.
     *
     * @param descriptorKey the descriptor key.
     * @throws RepositoryException if an error occurs.
     * @throws NotExecutableException If the feature is not supported.
     */
    protected void checkSupportedOption(String descriptorKey) throws RepositoryException, NotExecutableException {
        String value = getHelper().getRepository().getDescriptor(descriptorKey);
        if (value == null || ! Boolean.valueOf(value).booleanValue()) {
            throw new NotExecutableException (
                    "Repository feature not supported: " + descriptorKey);
        }
    }

    /**
     * Checks that the repository supports multiple workspace, otherwise aborts with
     * {@link NotExecutableException}.
     * @throws NotExecutableException when the repository only supports a single
     * workspace
     */
    protected void ensureMultipleWorkspacesSupported() throws RepositoryException, NotExecutableException {
        String workspacenames[] = superuser.getWorkspace().getAccessibleWorkspaceNames();
        if (workspacenames == null || workspacenames.length < 2) {
            throw new NotExecutableException("This repository does not seem to support multiple workspaces.");
        }
    }


    private boolean canSetProperty(NodeType nodeType, String propertyName, int propertyType, boolean isMultiple) {
        PropertyDefinition propDefs[] = nodeType.getPropertyDefinitions();

        for (int i = 0; i < propDefs.length; i++) {
            if (propDefs[i].getName().equals(propertyName) || propDefs[i].getName().equals("*")) {
                if ((propDefs[i].getRequiredType() == propertyType || propDefs[i].getRequiredType() == PropertyType.UNDEFINED)
                    && propDefs[i].isMultiple() == isMultiple) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean canSetProperty(Node node, String propertyName, int propertyType, boolean isMultiple) throws RepositoryException {

        if (canSetProperty(node.getPrimaryNodeType(), propertyName, propertyType, isMultiple)) {
            return true;
        }
        else {
            NodeType mixins[] = node.getMixinNodeTypes();
            boolean canSetIt = false;
            for (int i = 0; i < mixins.length && !canSetIt; i++) {
                canSetIt |= canSetProperty(mixins[i], propertyName, propertyType, isMultiple);
            }
            return canSetIt;
        }
    }

    /**
     * Checks that the repository can set the property to the required type, otherwise aborts with
     * {@link NotExecutableException}.
     * @throws NotExecutableException when setting the property to the required
     * type is not supported
     */
    protected void ensureCanSetProperty(Node node, String propertyName, int propertyType, boolean isMultiple) throws NotExecutableException, RepositoryException {

        if (! canSetProperty(node, propertyName, propertyType, isMultiple)) {
            throw new NotExecutableException("configured property name " + propertyName + " can not be set on node " + node.getPath());
        }
    }

    /**
     * Checks that the repository can set the property to the required type, otherwise aborts with
     * {@link NotExecutableException}.
     * @throws NotExecutableException when setting the property to the required
     * type is not supported
     */
    protected void ensureCanSetProperty(Node node, String propertyName, Value value) throws NotExecutableException, RepositoryException {
        ensureCanSetProperty(node, propertyName, value.getType(), false);
    }

    /**
     * Checks that the repository can set the property to the required type, otherwise aborts with
     * {@link NotExecutableException}.
     * @throws NotExecutableException when setting the property to the required
     * type is not supported
     */
    protected void ensureCanSetProperty(Node node, String propertyName, Value[] values) throws NotExecutableException, RepositoryException {

        int propertyType = values.length == 0 ? PropertyType.UNDEFINED : values[0].getType();

        if (! canSetProperty(node, propertyName, propertyType, true)) {
            throw new NotExecutableException("configured property name " + propertyName + " can not be set on node " + node.getPath());
        }
    }

    /**
     * Checks that the repository supports the specified node type, otherwise aborts with
     * {@link NotExecutableException}
     * @throws NotExecutableException when the specified node type is unknown
     */
    protected void ensureKnowsNodeType(Session session, String nodetype) throws NotExecutableException, RepositoryException {
        
        try {
            session.getWorkspace().getNodeTypeManager().getNodeType(nodetype);
        }
        catch (NoSuchNodeTypeException ex) {
            throw new NotExecutableException("Repository does not support node type " + nodetype);
        }
    }

    /**
     * Ensures that the given <code>node</code> is of the given mixin type.
     *
     * @param node  a node.
     * @param mixin the name of a mixin type.
     * @throws NotExecutableException if the node is not of type mixin and the
     *                                mixin cannot be added.
     * @throws RepositoryException    if an error occurs.
     */
    protected void ensureMixinType(Node node, String mixin)
            throws NotExecutableException, RepositoryException {
        if (!node.isNodeType(mixin)) {
            if (node.canAddMixin(mixin)) {
                node.addMixin(mixin);
            } else {
                throw new NotExecutableException(node.getPath() +
                        " does not support adding " + mixin);
            }
        }
    }

    /**
     * Checks whether the node already has the specified mixin node type
     */
    protected boolean needsMixin(Node node, String mixin) throws RepositoryException {
        return ! node.getSession().getWorkspace().getNodeTypeManager().getNodeType(node.getPrimaryNodeType().getName()).isNodeType(mixin);
    }

    /**
     * Reverts any pending changes made by <code>s</code> and deletes any nodes
     * under {@link #testRoot}. If there is no node at {@link #testRoot} then
     * the necessary nodes are created.
     *
     * @param s the session to clean up.
     * @return the {@link javax.jcr.Node} that represents the test root.
     * @throws RepositoryException if an error occurs.
     */
    protected Node cleanUpTestRoot(Session s) throws RepositoryException {
        // do a 'rollback'
        s.refresh(false);
        Node root = s.getRootNode();
        Node testRootNode;

        if (root.hasNode(testPath)) {
            RetentionManager rm;
            try {
                rm = s.getRetentionManager();
            } catch (UnsupportedRepositoryOperationException e) {
                rm = null;
            }

            // clean test root
            testRootNode = root.getNode(testPath);
            NodeIterator children = testRootNode.getNodes();
            while (children.hasNext()) {
                Node child = children.nextNode();

                // Remove retention policy if needed
                String childPath = child.getPath();
                if (rm != null && rm.getRetentionPolicy(childPath) != null) {
                    rm.removeRetentionPolicy(childPath);
                    s.save();
                }

                NodeDefinition nodeDef = child.getDefinition();
                if (!nodeDef.isMandatory() && !nodeDef.isProtected()) {
                    // try to remove child
                    try {
                        child.remove();
                    } catch (ConstraintViolationException e) {
                        log.println("unable to remove node: " + child.getPath());
                    }
                }
            }
        } else {
            // create nodes to testPath
            StringTokenizer names = new StringTokenizer(testPath, "/");
            Node currentNode = root;
            while (names.hasMoreTokens()) {
                String name = names.nextToken();
                if (currentNode.hasNode(name)) {
                    currentNode = currentNode.getNode(name);
                } else {
                    currentNode = currentNode.addNode(name, testNodeTypeTestRoot);
                }
            }
            testRootNode = currentNode;
        }
        s.save();
        return testRootNode;
    }
}
