/*
 * 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.oak.jcr;

import static java.util.Arrays.asList;
import static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW;
import static javax.jcr.Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED;
import static org.apache.jackrabbit.commons.JcrUtils.getChildNodes;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.jcr.Binary;
import javax.jcr.GuestCredentials;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.nodetype.PropertyDefinitionTemplate;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AppenderBase;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.JackrabbitNode;
import org.apache.jackrabbit.api.JackrabbitRepository;
import org.apache.jackrabbit.api.ReferenceBinary;
import org.apache.jackrabbit.commons.cnd.CndImporter;
import org.apache.jackrabbit.commons.cnd.ParseException;
import org.apache.jackrabbit.commons.jackrabbit.SimpleReferenceBinary;
import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
import org.apache.jackrabbit.oak.commons.testing.RandomInputStream;
import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
import org.apache.jackrabbit.oak.jcr.repository.RepositoryImpl;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl;
import org.apache.jackrabbit.spi.commons.value.QValueValue;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.LoggerFactory;

public class RepositoryTest extends AbstractRepositoryTest {

    private static final String TEST_NODE = "test_node";
    private static final String TEST_PATH = '/' + TEST_NODE;

    public RepositoryTest(NodeStoreFixture fixture) {
        super(fixture);
    }

    @Before
    public void setup() throws RepositoryException {
        Session session = getAdminSession();
        ValueFactory valueFactory = session.getValueFactory();
        Node root = session.getRootNode();
        Node foo = root.addNode("foo");
        foo.setProperty("stringProp", "stringVal");
        foo.setProperty("intProp", 42);
        foo.setProperty("mvProp", new Value[]{
                valueFactory.createValue(1),
                valueFactory.createValue(2),
                valueFactory.createValue(3),
        });
        root.addNode("bar");
        root.addNode(TEST_NODE);
        session.save();
    }

    @Test
    public void createRepository() throws RepositoryException {
        Repository repository = getRepository();
        assertNotNull(repository);
    }

    @Test
    public void login() throws RepositoryException {
        Session session = getAdminSession();
        assertNotNull(session);
        Set<Principal> principals = (Set<Principal>) session.getAttribute(RepositoryImpl.BOUND_PRINCIPALS);
        assertNotNull(principals);
        Set<String> expectedPrincipalNames = Set.of("admin", "everyone");
        assertEquals(expectedPrincipalNames, principals.stream().map(Principal::getName).collect(Collectors.toSet()));
    }

    @Test
    public void loginWithAttribute() throws RepositoryException {
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(RepositoryImpl.REFRESH_INTERVAL, 42);
        attributes.put(RepositoryImpl.BOUND_PRINCIPALS, Collections.singleton(new AdminPrincipal("admin")));
        Session session = ((JackrabbitRepository) getRepository()).login(
                new GuestCredentials(), null, attributes);

        String[] attributeNames = session.getAttributeNames();
        assertTrue(Arrays.asList(attributeNames).contains(RepositoryImpl.REFRESH_INTERVAL));
        assertEquals(42L, session.getAttribute(RepositoryImpl.REFRESH_INTERVAL));
        Set<Principal> principals = (Set<Principal>) session.getAttribute(RepositoryImpl.BOUND_PRINCIPALS);
        assertNotNull(principals);
        // admin must not be contained in the principals (altough added in the login attributes)
        Set<String> expectedPrincipalNames = Set.of("anonymous", "everyone");
        assertEquals(expectedPrincipalNames, principals.stream().map(Principal::getName).collect(Collectors.toSet()));
        session.logout();
    }

    @Test
    public void loginWithCredentialsAttribute() throws RepositoryException {
        SimpleCredentials sc = getAdminCredentials();
        sc.setAttribute("attr", "val");
        Session session = null;

        try {
            session = getRepository().login(sc, null);
            String[] attributeNames = session.getAttributeNames();
            assertTrue(Arrays.asList(attributeNames).contains("attr"));
            assertEquals("val", session.getAttribute("attr"));
        } finally {
            if (session != null) {
                session.logout();
            }
        }
    }

    @Test(expected = NoSuchWorkspaceException.class)
    public void loginInvalidWorkspace() throws RepositoryException {
        Repository repository = getRepository();
        repository.login(new GuestCredentials(), "invalid");
    }

    @Ignore("OAK-118") // TODO OAK-118: implement workspace management
    @Test
    public void getWorkspaceNames() throws RepositoryException {
        String[] workspaces = getAdminSession().getWorkspace().getAccessibleWorkspaceNames();

        Set<String> names = new HashSet<String>() {{
            add("default");
        }};

        assertTrue(asList(workspaces).containsAll(names));
        assertTrue(names.containsAll(asList(workspaces)));
    }

    @Ignore("OAK-118") // TODO OAK-118: implement workspace management
    @Test
    public void createDeleteWorkspace() throws RepositoryException {
        getAdminSession().getWorkspace().createWorkspace("new");

        Session session2 = createAdminSession();
        try {
            String[] workspaces = session2.getWorkspace().getAccessibleWorkspaceNames();
            assertTrue(asList(workspaces).contains("new"));
            Session session3 = getRepository().login("new");
            assertEquals("new", session3.getWorkspace().getName());
            session3.logout();
            session2.getWorkspace().deleteWorkspace("new");
        } finally {
            session2.logout();
        }

        Session session4 = createAdminSession();
        try {
            String[] workspaces = session4.getWorkspace().getAccessibleWorkspaceNames();
            assertFalse(asList(workspaces).contains("new"));
        } finally {
            session4.logout();
        }
    }

    @Test
    public void getRoot() throws RepositoryException {
        Node root = getAdminSession().getRootNode();
        assertNotNull(root);
        assertEquals("", root.getName());
        assertEquals("/", root.getPath());
    }

    @Test
    public void getRootFromPath() throws RepositoryException {
        Node root = getNode("/");
        assertEquals("", root.getName());
        assertEquals("/", root.getPath());
    }

    @Test(expected = PathNotFoundException.class)
    public void getPropertyDot() throws RepositoryException {
        Node node = getNode("/foo");
        node.getProperty(".");
    }

    @Test(expected = PathNotFoundException.class)
    public void getPropertyDotDot() throws RepositoryException {
        Node node = getNode("/foo");
        node.getProperty("..");
    }

    @Test
    public void hasPropertyDot() throws RepositoryException {
        Node root = getNode("/");
        assertFalse((root.hasProperty(".")));
        Node node = getNode("/foo");
        assertFalse((node.hasProperty(".")));
    }

    @Test
    public void hasPropertyDotDot() throws RepositoryException {
        Node root = getNode("/");
        assertFalse((root.hasProperty("..")));
        Node node = getNode("/foo");
        assertFalse((node.hasProperty("..")));
    }

    @Test
    public void getNodeDot() throws RepositoryException {
        Node node = getNode("/foo");
        Node same = node.getNode(".");
        assertNotNull(same);
        assertEquals("foo", same.getName());
        assertTrue(same.isSame(node));
    }

    @Test
    public void getNodeDotDot() throws RepositoryException {
        Node node = getNode("/foo");
        Node root = node.getNode("..");
        assertNotNull(root);
        assertEquals("", root.getName());
        assertTrue(root.isSame(node.getParent()));
    }

    @Test(expected = PathNotFoundException.class)
    public void getRootGetDotDot() throws RepositoryException {
        Node root = getNode("/");
        assertNotNull(root);
        root.getNode("..");
    }

    @Test
    public void hasNodeDot() throws RepositoryException {
        Node root = getNode("/");
        assertTrue(root.hasNode("."));
        Node node = getNode("/foo");
        assertTrue(node.hasNode("."));
    }

    @Test
    public void hasNodeDotDot() throws RepositoryException {
        Node root = getNode("/");
        assertFalse(root.hasNode(".."));
        Node node = getNode("/foo");
        assertTrue(node.hasNode(".."));
    }

    @Test(expected = ItemExistsException.class)
    public void testAddNodeDot() throws RepositoryException {
        Node node = getNode("/foo");
        // add a node with '..' should fail...
        node.addNode("..");
    }

    @Test(expected = ItemExistsException.class)
    public void testAddNodeDotDot() throws RepositoryException {
        Node node = getNode("/foo");
        node.addNode(".");
    }

    @Test
    public void getNode() throws RepositoryException {
        Node node = getNode("/foo");
        assertNotNull(node);
        assertEquals("foo", node.getName());
        assertEquals("/foo", node.getPath());
    }

    /**
     * Test SNS 1-based indexed path.
     * JCR 2.0, Chapter 22.2:
     * <em>
     *     A name in a content repository path that does not explicitly specify an index implies an index of 1.
     *     For example, /a/b/c is equivalent to /a[1]/b[1]/c[1].
     * </em>
     *
     * @throws RepositoryException
     */
    @Test
    public void getNodeSNS() throws RepositoryException {
        Node node = getNode("/foo[1]");
        assertNotNull(node);
        assertEquals("foo", node.getName());
        assertEquals("/foo", node.getPath());

        node.addNode("bar");
        Node bar = getNode("/foo[1]/bar[1]");
        assertEquals("/foo/bar", bar.getPath());

        try {
            getNode("/foo[1]/bar[2]");
            fail("retrieving wrong SNS index should throw PathNotFoundException");
        } catch (PathNotFoundException e) {
            // expected.
        }

        try {
            getProperty("/foo[1]/bar[2]/jcr:primaryType");
            fail("retrieving wrong SNS index should throw PathNotFoundException");
        } catch (PathNotFoundException e) {
            // expected.
        }

        assertTrue(getAdminSession().nodeExists("/foo[1]/bar[1]"));
        assertTrue(node.hasNode("bar[1]"));
        assertTrue(node.hasProperty("bar[1]/jcr:primaryType"));
        assertTrue(getAdminSession().propertyExists("/foo[1]/bar[1]/jcr:primaryType"));
        assertFalse(getAdminSession().nodeExists("/foo[1]/bar[2]"));
        assertFalse(node.hasNode("bar[2]"));
        assertFalse(node.hasProperty("bar[2]/jcr:primaryType"));
        assertFalse(getAdminSession().propertyExists("/foo[1]/bar[2]/jcr:primaryType"));
    }

    @Test(expected = RepositoryException.class)
    public void getNodeAbsolutePath() throws RepositoryException {
        Node root = getNode("/");
        root.getNode("/foo");
    }

    @Test
    public void getNodeByIdentifier() throws RepositoryException {
        Node node = getNode("/foo");
        String id = node.getIdentifier();
        Node node2 = getAdminSession().getNodeByIdentifier(id);
        assertTrue(node.isSame(node2));
    }

    @Test
    public void getNodeByIncorrectIdentifier() throws RepositoryException {
        Node node = getNode("/foo");
        String id = node.getIdentifier() + "foofoofoo";
        try {
            getAdminSession().getNodeByIdentifier(id);
            fail("should not get here");
        } catch (ItemNotFoundException expected) {
        }
    }

    @Test
    public void getNodeByEmptyIdentifier() throws RepositoryException {
        try {
            getAdminSession().getNodeByIdentifier("");
            fail("should not get here");
        } catch (RepositoryException expected) {
        }
    }

    @Test
    public void getNodeByUUID() throws RepositoryException {
        Node node = getNode("/foo").addNode("boo");
        node.addMixin(JcrConstants.MIX_REFERENCEABLE);

        assertTrue(node.isNodeType(JcrConstants.MIX_REFERENCEABLE));
        String uuid = node.getUUID();
        assertNotNull(uuid);
        assertEquals(uuid, node.getIdentifier());

        Node nAgain = node.getSession().getNodeByUUID(uuid);
        assertTrue(nAgain.isSame(node));
        assertTrue(nAgain.isSame(node.getSession().getNodeByIdentifier(uuid)));

        Node childNode = node.addNode("boohoo");
        childNode.addMixin(JcrConstants.MIX_REFERENCEABLE);

        assertTrue(childNode.isNodeType(JcrConstants.MIX_REFERENCEABLE));
        String childUuid = childNode.getUUID();
        assertNotNull(childUuid);
        assertEquals(childUuid, childNode.getIdentifier());

        nAgain = childNode.getSession().getNodeByUUID(childUuid);
        assertTrue(nAgain.isSame(childNode));
        assertTrue(nAgain.isSame(childNode.getSession().getNodeByIdentifier(childUuid)));

        node.getSession().move("/foo/boo", "/foo/boohoo");
        node = getNode("/foo/boohoo");
        nAgain = node.getSession().getNodeByUUID(uuid);
        assertTrue(nAgain.isSame(node));
        assertTrue(nAgain.isSame(node.getSession().getNodeByIdentifier(uuid)));
    }

    @Test
    public void getRootChildByUUID() throws RepositoryException {
        Node node = getNode("/").addNode("boo");
        node.addMixin(JcrConstants.MIX_REFERENCEABLE);

        assertTrue(node.isNodeType(JcrConstants.MIX_REFERENCEABLE));
        String uuid = node.getUUID();
        assertNotNull(uuid);
        assertEquals(uuid, node.getIdentifier());

        Node nAgain = node.getSession().getNodeByUUID(uuid);
        assertTrue(nAgain.isSame(node));
        assertTrue(nAgain.isSame(node.getSession().getNodeByIdentifier(uuid)));

        Node childNode = node.addNode("boohoo");
        childNode.addMixin(JcrConstants.MIX_REFERENCEABLE);

        assertTrue(childNode.isNodeType(JcrConstants.MIX_REFERENCEABLE));
        String childUuid = childNode.getUUID();
        assertNotNull(childUuid);
        assertEquals(childUuid, childNode.getIdentifier());

        nAgain = childNode.getSession().getNodeByUUID(childUuid);
        assertTrue(nAgain.isSame(childNode));
        assertTrue(nAgain.isSame(childNode.getSession().getNodeByIdentifier(childUuid)));

        node.getSession().move("/boo", "/boohoo");
        node = getNode("/boohoo");
        nAgain = node.getSession().getNodeByUUID(uuid);
        assertTrue(nAgain.isSame(node));
        assertTrue(nAgain.isSame(node.getSession().getNodeByIdentifier(uuid)));
    }

    @Test
    public void getNodeFromNode() throws RepositoryException {
        Node root = getNode("/");
        Node node = root.getNode("foo");
        assertNotNull(node);
        assertEquals("foo", node.getName());
        assertEquals("/foo", node.getPath());

        Node nodeAgain = getNode("/foo");
        assertTrue(node.isSame(nodeAgain));
    }

    @Test
    public void getNodes() throws RepositoryException {
        Node test = getNode(TEST_PATH);
        test.addNode("foo");
        test.addNode("bar");
        test.addNode("baz");
        getAdminSession().save();

        Set<String> nodeNames = new HashSet<String>() {{
            add("bar");
            add("added");
            add("baz");
        }};

        test = getNode(TEST_PATH);
        test.addNode("added");         // transiently added
        test.getNode("foo").remove();  // transiently removed
        test.getNode("bar").remove();  // transiently removed and...
        test.addNode("bar");           // ... added again

        NodeIterator nodes = test.getNodes();
        assertEquals(3, nodes.getSize());

        while (nodes.hasNext()) {
            String name = nodes.nextNode().getName();
            assertTrue(nodeNames.remove(name));
        }

        assertTrue(nodeNames.isEmpty());
    }

    @Test
    public void getProperties() throws RepositoryException {
        Set<String> propertyNames = new HashSet<String>() {{
            add("intProp");
            add("mvProp");
            add("added");
        }};

        Set<String> values = new HashSet<String>() {{
            add("added");
            add("1");
            add("2");
            add("3");
            add("42");
        }};

        Node node = getNode("/foo");
        node.setProperty("added", "added");        // transiently added
        node.getProperty("stringProp").remove();   // transiently removed
        node.getProperty("intProp").remove();      // transiently removed...
        node.setProperty("intProp", 42);           // ...and added again
        PropertyIterator properties = node.getProperties();
        assertEquals(4, properties.getSize());
        while (properties.hasNext()) {
            Property p = properties.nextProperty();
            if (JcrConstants.JCR_PRIMARYTYPE.equals(p.getName())) {
                continue;
            }
            assertTrue(propertyNames.remove(p.getName()));
            if (p.isMultiple()) {
                for (Value v : p.getValues()) {
                    assertTrue(values.remove(v.getString()));
                }
            } else {
                assertTrue(values.remove(p.getString()));
            }
        }

        assertTrue(propertyNames.isEmpty());
        assertTrue(values.isEmpty());
    }

    @Test(expected = PathNotFoundException.class)
    public void getNonExistingNode() throws RepositoryException {
        getNode("/qoo");
    }

    @Test
    public void getProperty() throws RepositoryException {
        Property property = getProperty("/foo/stringProp");
        assertNotNull(property);
        assertEquals("stringProp", property.getName());
        assertEquals("/foo/stringProp", property.getPath());

        Value value = property.getValue();
        assertNotNull(value);
        assertEquals(PropertyType.STRING, value.getType());
        assertEquals("stringVal", value.getString());
    }

    @Test
    public void getPropertyFromNode() throws RepositoryException {
        Node node = getNode("/foo");
        Property property = node.getProperty("stringProp");
        assertNotNull(property);
        assertEquals("stringProp", property.getName());
        assertEquals("/foo/stringProp", property.getPath());

        Value value = property.getValue();
        assertNotNull(value);
        assertEquals(PropertyType.STRING, value.getType());
        assertEquals("stringVal", value.getString());

        Property propertyAgain = getProperty("/foo/stringProp");
        assertTrue(property.isSame(propertyAgain));
    }

    @Test
    public void addNode() throws RepositoryException {
        Session session = getAdminSession();
        String newNode = TEST_PATH + "/new";
        assertFalse(session.nodeExists(newNode));

        Node node = getNode(TEST_PATH);
        Node added = node.addNode("new");
        assertFalse(node.isNew());
        assertTrue(node.isModified());
        assertTrue(added.isNew());
        assertFalse(added.isModified());
        session.save();

        Session session2 = createAnonymousSession();
        try {
            assertTrue(session2.nodeExists(newNode));
            added = session2.getNode(newNode);
            assertFalse(added.isNew());
            assertFalse(added.isModified());
        } finally {
            session2.logout();
        }
    }

    @Test
    public void testIsNew() throws RepositoryException, InterruptedException {
        Session session = getAdminSession();
        Node root = session.getRootNode();
        Node node1 = root.addNode("node1");
        session.save();

        node1.remove();
        Node node2 = root.addNode("node2");
        assertTrue("The Node is just added", node2.isNew());
        Node node1Again = root.addNode("node1");
        assertTrue("The Node is just added but has a remove in same commit", node1Again.isNew());
    }

    @Test
    public void testAddNodeWithExpandedName() throws RepositoryException {
        Session session = getAdminSession();

        session.getWorkspace().getNamespaceRegistry().registerNamespace("foo", "http://foo");

        Node node = getNode(TEST_PATH);
        Node added = node.addNode("{http://foo}new");
        assertEquals("foo:new", added.getName());
        session.save();

        added = session.getNode(TEST_PATH + "/{http://foo}new");
        assertEquals("foo:new", added.getName());
    }

    @Test
    public void addNodeWithSpecialChars() throws RepositoryException {
        Session session = getAdminSession();
        String nodeName = "foo{";

        String newNode = TEST_PATH + '/' + nodeName;
        assertFalse(session.nodeExists(newNode));

        Node node = getNode(TEST_PATH);
        node.addNode(nodeName);
        session.save();

        Session session2 = createAnonymousSession();
        try {
            assertTrue(session2.nodeExists(newNode));
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addNodeWithNodeType() throws RepositoryException {
        Session session = getAdminSession();

        Node node = getNode(TEST_PATH);
        node.addNode("new", "nt:folder");
        session.save();
    }

    @Test
    public void addNodeToRootNode() throws RepositoryException {
        Session session = getAdminSession();
        Node root = session.getRootNode();
        String newNode = "newNodeBelowRoot";
        assertFalse(root.hasNode(newNode));
        root.addNode(newNode);
        session.save();
    }

    @Test
    public void addStringProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "string", getAdminSession().getValueFactory().createValue("string value"));
    }

    @Test
    public void testGetItemReturnsNodeBeforeProperty() throws RepositoryException {
        String snnpSupported = getRepository().getDescriptor(OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED);
        assumeTrue(Boolean.valueOf(snnpSupported));

        String newNodeName = "getItemTest";
        String nodeAndPropertyName = "subnode";

        Session session = getAdminSession();
        Node root = session.getRootNode();
        assertFalse(root.hasNode(newNodeName));
        Node newNode = root.addNode(newNodeName, NodeTypeConstants.NT_OAK_UNSTRUCTURED);

        newNode.setProperty(nodeAndPropertyName, "prop value");
        newNode.addNode(nodeAndPropertyName);
        session.save();

        Item item = session.getItem("/" + newNodeName + "/" + nodeAndPropertyName);

        assertTrue("should retrieve Node before property", item.isNode());
    }


    @Test
    public void addMultiValuedString() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue("one");
        values[1] = getAdminSession().getValueFactory().createValue("two");

        parentNode.setProperty("multi string", values);
        parentNode.getSession().save();

        Session session2 = createAdminSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi string");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.STRING, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addLongProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "long", getAdminSession().getValueFactory().createValue(42L));
    }

    @Test
    public void addMultiValuedLong() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(42L);
        values[1] = getAdminSession().getValueFactory().createValue(84L);

        parentNode.setProperty("multi long", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi long");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.LONG, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addDoubleProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "double", getAdminSession().getValueFactory().createValue(42.2D));
    }

    @Test
    public void addMultiValuedDouble() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(42.1d);
        values[1] = getAdminSession().getValueFactory().createValue(99.0d);

        parentNode.setProperty("multi double", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi double");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.DOUBLE, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addBooleanProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "boolean", getAdminSession().getValueFactory().createValue(true));
    }

    @Test
    public void addMultiValuedBoolean() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(true);
        values[1] = getAdminSession().getValueFactory().createValue(false);

        parentNode.setProperty("multi boolean", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi boolean");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.BOOLEAN, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addDecimalProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "decimal", getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(21)));
    }

    @Test
    public void addMultiValuedDecimal() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(42));
        values[1] = getAdminSession().getValueFactory().createValue(BigDecimal.valueOf(99));

        parentNode.setProperty("multi decimal", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi decimal");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.DECIMAL, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addDateProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "date", getAdminSession().getValueFactory().createValue(Calendar.getInstance()));
    }

    @Test
    public void addMultiValuedDate() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        Calendar calendar = Calendar.getInstance();
        values[0] = getAdminSession().getValueFactory().createValue(calendar);
        calendar.add(Calendar.DAY_OF_MONTH, 1);
        values[1] = getAdminSession().getValueFactory().createValue(calendar);

        parentNode.setProperty("multi date", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi date");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.DATE, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addURIProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "uri", getAdminSession().getValueFactory().createValue("http://www.day.com/", PropertyType.URI));
    }

    @Test
    public void addMultiValuedURI() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue("http://www.day.com", PropertyType.URI);
        values[1] = getAdminSession().getValueFactory().createValue("file://var/dam", PropertyType.URI);

        parentNode.setProperty("multi uri", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi uri");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.URI, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addNameProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "name", getAdminSession().getValueFactory().createValue("jcr:something\"", PropertyType.NAME));
    }

    @Test
    public void addMultiValuedName() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue("jcr:foo", PropertyType.NAME);
        values[1] = getAdminSession().getValueFactory().createValue("bar", PropertyType.NAME);

        parentNode.setProperty("multi name", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi name");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.NAME, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addEmptyMultiValue() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[0];

        parentNode.setProperty("multi value", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi value");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.STRING, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(0, values2.length);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addEmptyMultiValueName() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[0];

        parentNode.setProperty("multi name", values, PropertyType.NAME);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi name");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.NAME, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(0, values2.length);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addPathProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "path", getAdminSession().getValueFactory().createValue("/jcr:foo/bar\"", PropertyType.PATH));
    }

    @Test
    public void addMultiValuedPath() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue("/nt:foo/jcr:bar", PropertyType.PATH);
        values[1] = getAdminSession().getValueFactory().createValue("/", PropertyType.PATH);

        parentNode.setProperty("multi path", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi path");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.PATH, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addBinaryProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        InputStream is = new ByteArrayInputStream("foo\"".getBytes());
        Binary bin = getAdminSession().getValueFactory().createBinary(is);
        addProperty(parentNode, "binary", getAdminSession().getValueFactory().createValue(bin));
    }

    @Test
    public void addSmallBinaryProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        InputStream is = new NumberStream(1234);
        Binary bin = getAdminSession().getValueFactory().createBinary(is);
        addProperty(parentNode, "bigBinary", getAdminSession().getValueFactory().createValue(bin));
    }

    @Test
    public void addBigBinaryProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        InputStream is = new NumberStream(123456);
        Binary bin = getAdminSession().getValueFactory().createBinary(is);
        addProperty(parentNode, "bigBinary", getAdminSession().getValueFactory().createValue(bin));
    }

    @Test
    public void addAlienBinaryProperty() throws RepositoryException, IOException {
        Session session = getAdminSession();
        QValue qValue = QValueFactoryImpl.getInstance().create("binaryValue".getBytes());
        Value value = new QValueValue(qValue, new DefaultNamePathResolver(session));
        getNode(TEST_PATH).setProperty("binary", value);
        session.save();

        Value valueAgain = getNode(TEST_PATH).getProperty("binary").getValue();
        assertEqualStream(value.getBinary().getStream(), valueAgain.getBinary().getStream());
    }

    @Test
    public void addMultiValuedBinary() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];

        InputStream is = new ByteArrayInputStream("foo".getBytes());
        Binary bin = getAdminSession().getValueFactory().createBinary(is);
        values[0] = getAdminSession().getValueFactory().createValue(bin);

        is = new ByteArrayInputStream("bar".getBytes());
        bin = getAdminSession().getValueFactory().createBinary(is);
        values[1] = getAdminSession().getValueFactory().createValue(bin);

        parentNode.setProperty("multi binary", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi binary");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.BINARY, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addReferenceProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        Node referee = getAdminSession().getNode("/foo");
        referee.addMixin("mix:referenceable");
        getAdminSession().save();

        addProperty(parentNode, "reference", getAdminSession().getValueFactory().createValue(referee));
    }

    @Test
    public void addMultiValuedReference() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Node referee = getAdminSession().getNode("/foo");
        referee.addMixin("mix:referenceable");
        getAdminSession().save();

        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(referee);
        values[1] = getAdminSession().getValueFactory().createValue(referee);

        parentNode.setProperty("multi reference", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi reference");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.REFERENCE, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addWeakReferenceProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        Node referee = getAdminSession().getNode("/foo");
        referee.addMixin("mix:referenceable");
        getAdminSession().save();

        addProperty(parentNode, "weak reference", getAdminSession().getValueFactory().createValue(referee, true));
    }

    @Test
    public void addMultiValuedWeakReference() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Node referee = getAdminSession().getNode("/foo");
        referee.addMixin("mix:referenceable");
        getAdminSession().save();

        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue(referee, true);
        values[1] = getAdminSession().getValueFactory().createValue(referee, true);

        parentNode.setProperty("multi weak reference", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi weak reference");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.WEAKREFERENCE, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length, values2.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addEmptyMultiValuedProperty() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[0];

        parentNode.setProperty("multi empty", values, PropertyType.BOOLEAN);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi empty");
            assertTrue(property.isMultiple());
            Value[] values2 = property.getValues();
            assertEquals(0, values2.length);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void addMultiValuedStringWithNull() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[3];
        values[0] = getAdminSession().getValueFactory().createValue(true);
        values[2] = getAdminSession().getValueFactory().createValue(false);

        parentNode.setProperty("multi with null", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi with null");
            assertTrue(property.isMultiple());
            assertEquals(PropertyType.BOOLEAN, property.getType());
            Value[] values2 = property.getValues();
            assertEquals(values.length - 1, values2.length);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void transientChanges() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);

        Node node = parentNode.addNode("test");

        assertFalse(node.hasProperty("p"));
        node.setProperty("p", "pv");
        assertTrue(node.hasProperty("p"));

        assertFalse(node.hasNode("n"));
        node.addNode("n");
        assertTrue(node.hasNode("n"));

        assertTrue(node.hasProperties());
        assertTrue(node.hasNodes());
    }

    @Test
    public void setStringProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "string", getAdminSession().getValueFactory().createValue("string \" value"));

        Property property = parentNode.getProperty("string");
        property.setValue("new value");
        assertTrue(parentNode.isModified());
        assertTrue(property.isModified());
        assertFalse(property.isNew());
        property.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property2 = session2.getProperty(TEST_PATH + "/string");
            assertEquals("new value", property2.getString());
        } finally {
            session2.logout();
        }
    }

    @Test
    public void setPropertyAgain() throws RepositoryException {
        Session session = getAdminSession();
        Property p1 = session.getProperty("/foo/stringProp");

        Property p2 = p1.getParent().setProperty("stringProp", "newValue");
        Property p3 = session.getProperty("/foo/stringProp");

        assertEquals("newValue", p1.getString());
        assertEquals("newValue", p2.getString());
        assertEquals("newValue", p3.getString());
    }

    // OAK-6410
    @Test
    public void setInexistentProperty() throws RepositoryException {
        Node node = getNode(TEST_PATH);
        node.addMixin(JcrConstants.MIX_VERSIONABLE);
        node.getSession().save();
        node.getSession().getWorkspace().getVersionManager().checkin(TEST_PATH);
        node.setProperty("inexistent", (Value) null);
    }

    @Test
    public void setDoubleNaNProperty() throws RepositoryException, IOException {
        Node parentNode = getNode(TEST_PATH);
        addProperty(parentNode, "NaN", getAdminSession().getValueFactory().createValue(Double.NaN));

        Session session2 = createAnonymousSession();
        try {
            Property property2 = session2.getProperty(TEST_PATH + "/NaN");
            assertTrue(Double.isNaN(property2.getDouble()));
        } finally {
            session2.logout();
        }
    }

    @Test
    public void setMultiValuedProperty() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        Value[] values = new Value[2];
        values[0] = getAdminSession().getValueFactory().createValue("one");
        values[1] = getAdminSession().getValueFactory().createValue("two");

        parentNode.setProperty("multi string2", values);
        parentNode.getSession().save();

        values[0] = getAdminSession().getValueFactory().createValue("eins");
        values[1] = getAdminSession().getValueFactory().createValue("zwei");
        parentNode.setProperty("multi string2", values);
        parentNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            Property property = session2.getProperty(TEST_PATH + "/multi string2");
            assertTrue(property.isMultiple());
            Value[] values2 = property.getValues();
            assertEquals(2, values.length);
            assertEquals(values[0], values2[0]);
            assertEquals(values[1], values2[1]);
        } finally {
            session2.logout();
        }
    }

    @Test
    public void nullProperty() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        parentNode.setProperty("newProperty", "some value");
        parentNode.getSession().save();

        Session session2 = createAdminSession();
        try {
            session2.getProperty(TEST_PATH + "/newProperty").setValue((String) null);
            session2.save();
        } finally {
            session2.logout();
        }

        Session session3 = createAnonymousSession();
        try {
            assertFalse(session3.propertyExists(TEST_PATH + "/newProperty"));
        } finally {
            session3.logout();
        }
    }

    @Test
    public void setPropertyWithConversion() throws RepositoryException {
        Node n = getNode(TEST_PATH);
        Node file = n.addNode("file", "nt:file");
        Node content = file.addNode("jcr:content", "nt:resource");

        long value = System.currentTimeMillis();
        Property property = content.setProperty("jcr:lastModified", value);
        assertEquals(value, property.getDate().getTimeInMillis());

        content.setProperty("jcr:data", new ByteArrayInputStream("foo".getBytes()));
        content.setProperty("jcr:mimeType", "text/plain");
        n.getSession().save();
    }

    @Test
    public void removeProperty() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        parentNode.setProperty("newProperty", "some value");
        parentNode.getSession().save();

        Session session2 = createAdminSession();
        try {
            session2.getProperty(TEST_PATH + "/newProperty").remove();
            session2.save();
        } finally {
            session2.logout();
        }

        Session session3 = createAnonymousSession();
        try {
            assertFalse(session3.propertyExists(TEST_PATH + "/newProperty"));
        } finally {
            session3.logout();
        }
    }

    @Test
    public void removeNode() throws RepositoryException {
        Node parentNode = getNode(TEST_PATH);
        parentNode.addNode("newNode");
        parentNode.getSession().save();

        Session session2 = createAdminSession();
        try {
            Node removeNode = session2.getNode(TEST_PATH + "/newNode");
            removeNode.remove();

            try {
                removeNode.getParent();
                fail("Cannot retrieve the parent from a transiently removed item.");
            } catch (InvalidItemStateException expected) {
            }

            assertTrue(session2.getNode(TEST_PATH).isModified());
            session2.save();
        } finally {
            session2.logout();
        }

        Session session3 = createAnonymousSession();
        try {
            assertFalse(session3.nodeExists(TEST_PATH + "/newNode"));
            assertFalse(session3.getNode(TEST_PATH).isModified());
        } finally {
            session3.logout();
        }
    }

    @Test
    public void removeNode2() throws RepositoryException {
        Node foo = getNode("/foo");
        getAdminSession().removeItem(foo.getPath());
        try {
            foo.getParent();
            fail("Cannot retrieve the parent from a transiently removed item.");
        } catch (InvalidItemStateException e) {
            // success
        }
    }

    @Test
    public void accessRemovedItem() throws RepositoryException {
        Node foo = getNode("/foo");
        Node bar = foo.addNode("bar");
        Property p = bar.setProperty("name", "value");
        foo.remove();
        try {
            bar.getPath();
            fail("Expected InvalidItemStateException");
        }
        catch (InvalidItemStateException expected) {
        }
        try {
            p.getPath();
            fail("Expected InvalidItemStateException");
        }
        catch (InvalidItemStateException expected) {
        }
    }

    @Test
    public void accessRemovedProperty() throws RepositoryException {
        Node foo = getNode("/foo");
        Property p = foo.setProperty("name", "value");
        p.remove();
        try {
            p.getPath();
            fail("Expected InvalidItemStateException");
        }
        catch (InvalidItemStateException expected) {
        }
    }

    @Test
    public void getReferences() throws RepositoryException {
        Session session = getAdminSession();
        Node referee = getNode("/foo");
        referee.addMixin("mix:referenceable");
        getNode(TEST_PATH).setProperty("reference", session.getValueFactory().createValue(referee));
        session.save();

        PropertyIterator refs = referee.getReferences();
        assertTrue(refs.hasNext());
        Property p = refs.nextProperty();
        assertEquals("reference", p.getName());
        assertFalse(refs.hasNext());
    }

    @Test
    public void getNamedReferences() throws RepositoryException {
        Session session = getAdminSession();
        Node referee = getNode("/foo");
        referee.addMixin("mix:referenceable");
        Value value = session.getValueFactory().createValue(referee);
        getNode(TEST_PATH).setProperty("reference1", value);
        getNode("/bar").setProperty("reference2", value);
        session.save();

        PropertyIterator refs = referee.getReferences("reference1");
        assertTrue(refs.hasNext());
        Property p = refs.nextProperty();
        assertEquals("reference1", p.getName());
        assertFalse(refs.hasNext());
    }

    @Test
    public void getWeakReferences() throws RepositoryException {
        Session session = getAdminSession();
        Node referee = getNode("/foo");
        referee.addMixin("mix:referenceable");
        getNode(TEST_PATH).setProperty("weak-reference", session.getValueFactory().createValue(referee, true));
        session.save();

        PropertyIterator refs = referee.getWeakReferences();
        assertTrue(refs.hasNext());
        Property p = refs.nextProperty();
        assertEquals("weak-reference", p.getName());
        assertFalse(refs.hasNext());
    }

    @Test
    public void getNamedWeakReferences() throws RepositoryException {
        Session session = getAdminSession();
        Node referee = getNode("/foo");
        referee.addMixin("mix:referenceable");
        Value value = session.getValueFactory().createValue(referee, true);
        getNode(TEST_PATH).setProperty("weak-reference1", value);
        getNode("/bar").setProperty("weak-reference2", value);
        session.save();

        PropertyIterator refs = referee.getWeakReferences("weak-reference1");
        assertTrue(refs.hasNext());
        Property p = refs.nextProperty();
        assertEquals("weak-reference1", p.getName());
        assertFalse(refs.hasNext());
    }

    @Test
    public void sessionSave() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            // Add some items and ensure they are accessible through this session
            session1.getNode("/").addNode("node1");
            session1.getNode("/node1").addNode("node2");
            session1.getNode("/").addNode("node1/node3");

            Node node1 = session1.getNode("/node1");
            assertEquals("/node1", node1.getPath());

            Node node2 = session1.getNode("/node1/node2");
            assertEquals("/node1/node2", node2.getPath());

            Node node3 = session1.getNode("/node1/node3");
            assertEquals("/node1/node3", node3.getPath());

            node3.setProperty("property1", "value1");
            Item property1 = session1.getProperty("/node1/node3/property1");
            assertFalse(property1.isNode());
            assertEquals("value1", ((Property) property1).getString());

            // Make sure these items are not accessible through another session
            assertFalse(session2.itemExists("/node1"));
            assertFalse(session2.itemExists("/node1/node2"));
            assertFalse(session2.itemExists("/node1/node3"));
            assertFalse(session2.itemExists("/node1/node3/property1"));

            session1.save();

            // Make sure they are accessible through another session
            assertTrue(session2.itemExists("/node1"));
            assertTrue(session2.itemExists("/node1/node2"));
            assertTrue(session2.itemExists("/node1/node3"));
            assertTrue(session2.itemExists("/node1/node3/property1"));

            session2.refresh(false);

            // Make sure they are accessible through another session after refresh
            assertTrue(session2.itemExists("/node1"));
            assertTrue(session2.itemExists("/node1/node2"));
            assertTrue(session2.itemExists("/node1/node3"));
            assertTrue(session2.itemExists("/node1/node3/property1"));
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void sessionRefresh() throws RepositoryException {
        Session session = getAdminSession();
        // Add some items and ensure they are accessible through this session
        session.getNode("/").addNode("node1");
        session.getNode("/node1").addNode("node2");
        session.getNode("/").addNode("node1/node3");

        Node node1 = session.getNode("/node1");
        assertEquals("/node1", node1.getPath());

        Node node2 = session.getNode("/node1/node2");
        assertEquals("/node1/node2", node2.getPath());

        Node node3 = session.getNode("/node1/node3");
        assertEquals("/node1/node3", node3.getPath());

        node3.setProperty("property1", "value1");
        Item property1 = session.getProperty("/node1/node3/property1");
        assertFalse(property1.isNode());
        assertEquals("value1", ((Property) property1).getString());

        // Make sure these items are still accessible after refresh(true);
        session.refresh(true);
        assertTrue(session.itemExists("/node1"));
        assertTrue(session.itemExists("/node1/node2"));
        assertTrue(session.itemExists("/node1/node3"));
        assertTrue(session.itemExists("/node1/node3/property1"));

        session.refresh(false);
        // Make sure these items are not accessible after refresh(false);
        assertFalse(session.itemExists("/node1"));
        assertFalse(session.itemExists("/node1/node2"));
        assertFalse(session.itemExists("/node1/node3"));
        assertFalse(session.itemExists("/node1/node3/property1"));
    }

    @Test
    public void sessionRefreshFalse() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            Node foo = session1.getNode("/foo");
            foo.addNode("added");

            session2.getNode("/foo").addNode("bar");
            session2.save();

            session1.refresh(false);
            assertFalse(foo.hasNode("added"));
            assertTrue(foo.hasNode("bar"));
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void sessionRefreshTrue() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            Node foo = session1.getNode("/foo");
            Node added = foo.addNode("added");
            assertTrue(added.isNew());

            session2.getNode("/foo").addNode("bar");
            session2.save();

            session1.refresh(true);
            assertTrue(foo.hasNode("added"));
            assertTrue(foo.getNode("added").isNew());
            assertTrue(foo.hasNode("bar"));
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void sessionIsolation() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            session1.getRootNode().addNode("node1");
            session2.getRootNode().addNode("node2");
            assertTrue(session1.getRootNode().hasNode("node1"));
            assertTrue(session2.getRootNode().hasNode("node2"));
            assertFalse(session1.getRootNode().hasNode("node2"));
            assertFalse(session2.getRootNode().hasNode("node1"));

            session1.save();
            session2.save();
            assertTrue(session1.getRootNode().hasNode("node1"));
            assertTrue(session1.getRootNode().hasNode("node2"));
            assertTrue(session2.getRootNode().hasNode("node1")); // save refreshes
            assertTrue(session2.getRootNode().hasNode("node2"));
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void inThreadSessionSynchronisation() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        Session session3 = createAdminSession();
        try {
            session1.getRootNode().addNode("newNode");
            session1.save();
            assertTrue(session2.nodeExists("/newNode"));
            assertTrue(session3.nodeExists("/newNode"));
        } finally {
            session1.logout();
            session2.logout();
            session3.logout();
        }
    }

    @Test
    public void saveRefreshConflict() throws RepositoryException {
        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            session1.getRootNode().addNode("node").setProperty("p", "v1");
            session2.getRootNode().addNode("node").setProperty("p", "v2");
            assertTrue(session1.getRootNode().hasNode("node"));
            assertTrue(session2.getRootNode().hasNode("node"));

            session1.save();
            assertTrue(session1.getRootNode().hasNode("node"));
            assertTrue(session2.getRootNode().hasNode("node"));

            session2.refresh(true);
            assertTrue(session1.getRootNode().hasNode("node"));
            assertTrue(session2.getRootNode().hasNode("node"));

            try {
                session2.save();
                fail("Expected InvalidItemStateException");
            } catch (InvalidItemStateException expected) {
            }
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void saveConflict() throws RepositoryException {
        getAdminSession().getRootNode().addNode("node");
        getAdminSession().save();

        Session session1 = createAdminSession();
        Session session2 = createAdminSession();
        try {
            session1.getNode("/node").remove();
            session2.getNode("/node").addNode("2");
            assertFalse(session1.getRootNode().hasNode("node"));
            assertTrue(session2.getRootNode().hasNode("node"));
            assertTrue(session2.getRootNode().getNode("node").hasNode("2"));

            session1.save();
            assertFalse(session1.getRootNode().hasNode("node"));
            assertFalse(session2.getRootNode().hasNode("node"));

            try {
                session2.save();
                fail("Expected InvalidItemStateException");
            } catch (InvalidItemStateException expected) {
            }
        } finally {
            session1.logout();
            session2.logout();
        }
    }

    @Test
    public void liveNodes() throws RepositoryException {
        Session session = getAdminSession();
        Node n1 = (Node) session.getItem(TEST_PATH);
        Node n2 = (Node) session.getItem(TEST_PATH);

        Node c1 = n1.addNode("c1");
        Node c2 = n2.addNode("c2");
        assertTrue(n1.hasNode("c1"));
        assertTrue(n1.hasNode("c2"));
        assertTrue(n2.hasNode("c1"));
        assertTrue(n2.hasNode("c2"));

        c1.remove();
        assertFalse(n1.hasNode("c1"));
        assertTrue(n1.hasNode("c2"));
        assertFalse(n2.hasNode("c1"));
        assertTrue(n2.hasNode("c2"));

        c2.remove();
        assertFalse(n1.hasNode("c1"));
        assertFalse(n1.hasNode("c2"));
        assertFalse(n2.hasNode("c1"));
        assertFalse(n2.hasNode("c2"));
    }

    @Test
    public void move() throws RepositoryException {
        Session session = getAdminSession();

        Node node = getNode(TEST_PATH);
        Node source = node.addNode("source").addNode("node");
        node.addNode("target");
        session.save();

        session.refresh(true);
        session.move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved");
        assertEquals(TEST_PATH + "/target/moved", source.getPath());

        assertFalse(node.hasNode("source/node"));
        assertTrue(node.hasNode("source"));
        assertTrue(node.hasNode("target/moved"));

        session.save();

        assertFalse(node.hasNode("source/node"));
        assertTrue(node.hasNode("source"));
        assertTrue(node.hasNode("target/moved"));
    }

    @Test
    public void renameNonOrderable() throws RepositoryException {
        Session session = getAdminSession();
        Node root = session.getRootNode();
        Node parent = root.addNode("parent", NodeTypeConstants.NT_OAK_UNSTRUCTURED);
        parent.addNode("fo");
        Node foo = parent.addNode("foo");
        session.save();

        ((JackrabbitNode) foo).rename("renamed");
        assertEquals("renamed", foo.getName());
        assertFalse(session.nodeExists("/parent/foo"));
        assertTrue(session.nodeExists("/parent/renamed"));
    }

    @Test(expected = RepositoryException.class)
    public void moveToDescendant() throws RepositoryException {
        Session session = getAdminSession();

        Node node = getNode(TEST_PATH);
        Node source = node.addNode("s");
        session.save();

        session.refresh(true);
        session.move(TEST_PATH + "/s", TEST_PATH + "/s/t");
    }

    @Test
    public void oak962() throws RepositoryException {
        Session session = getAdminSession();
        Node root = session.getRootNode().addNode("root");
        root.addNode("N3");
        root.addNode("N6").addNode("N7");
        session.save();
        session.move("/root/N6/N7", "/root/N3/N12");
        root.getNode("N3").getNode("N12").remove();
        root.getNode("N6").remove();
        session.save();
    }

    @Test
    public void moveReferenceable() throws RepositoryException {
        Session session = getAdminSession();

        Node node = getNode(TEST_PATH);
        node.addNode("source").addNode("node").addMixin("mix:referenceable");
        node.addNode("target");
        session.save();

        session.refresh(true);
        session.move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved");

        assertFalse(node.hasNode("source/node"));
        assertTrue(node.hasNode("source"));
        assertTrue(node.hasNode("target/moved"));

        session.save();

        assertFalse(node.hasNode("source/node"));
        assertTrue(node.hasNode("source"));
        assertTrue(node.hasNode("target/moved"));
    }

    @Test
    public void workspaceMove() throws RepositoryException {
        Session session = getAdminSession();

        Node node = getNode(TEST_PATH);
        node.addNode("source").addNode("node");
        node.addNode("target");
        session.save();

        session.getWorkspace().move(TEST_PATH + "/source/node", TEST_PATH + "/target/moved");

        assertFalse(node.hasNode("source/node"));
        assertTrue(node.hasNode("source"));
        assertTrue(node.hasNode("target/moved"));
    }

    @Test
    public void invalidItemStateExceptionOnRemovedNode() throws Exception {
        Session session = getAdminSession();

        for (String parentPath : new String[] {"/", TEST_PATH}) {
            Node parent = session.getNode(parentPath);
            Node child = parent.addNode("child");
            String childPath = child.getPath();

            child.remove();
            try {
                child.getPath();
                fail();
            }
            catch (InvalidItemStateException expected) { }

            session.save();
            try {
                child.getPath();
                fail();
            }
            catch (InvalidItemStateException expected) { }

            parent.addNode("child");
            assertEquals(childPath, child.getPath());
        }
    }

    @Test
    public void setPrimaryType() throws RepositoryException {
        Node testNode = getNode(TEST_PATH);
        assertEquals("nt:unstructured", testNode.getPrimaryNodeType().getName());
        assertEquals("nt:unstructured", testNode.getProperty("jcr:primaryType").getString());

        testNode.setPrimaryType("nt:folder");
        getAdminSession().save();

        Session session2 = createAnonymousSession();
        try {
            testNode = session2.getNode(TEST_PATH);
            assertEquals("nt:folder", testNode.getPrimaryNodeType().getName());
            assertEquals("nt:folder", testNode.getProperty("jcr:primaryType").getString());
        } finally {
            session2.logout();
        }
    }

    @Test
    public void setPrimaryTypeExpandedName() throws RepositoryException {
        Node testNode = getNode(TEST_PATH);
        assertEquals("nt:unstructured", testNode.getPrimaryNodeType().getName());
        assertEquals("nt:unstructured", testNode.getProperty("jcr:primaryType").getString());

        testNode.setPrimaryType("{http://www.jcp.org/jcr/nt/1.0}folder");
        getAdminSession().save();

        Session session2 = createAnonymousSession();
        try {
            testNode = session2.getNode(TEST_PATH);
            assertEquals("nt:folder", testNode.getPrimaryNodeType().getName());
            assertEquals("nt:folder", testNode.getProperty("jcr:primaryType").getString());
        } finally {
            session2.logout();
        }
    }

    @Test
    public void setPrimaryTypeShouldFail() throws RepositoryException {
        Node testNode = getNode(TEST_PATH);
        assertEquals("nt:unstructured", testNode.getPrimaryNodeType().getName());
        assertEquals("nt:unstructured", testNode.getProperty("jcr:primaryType").getString());

        // create subnode that would not be allowed with nt:folder
        testNode.addNode("unstructured_child", NodeType.NT_UNSTRUCTURED);
        getAdminSession().save();

        testNode.setPrimaryType("nt:folder");
        try {
            getAdminSession().save();
            fail("Changing the primary type to nt:folder should fail.");
        } catch (RepositoryException e) {
            // ok
            getAdminSession().refresh(false);
        }

        Session session2 = createAnonymousSession();
        try {
            testNode = session2.getNode(TEST_PATH);
            assertEquals("nt:unstructured", testNode.getPrimaryNodeType().getName());
            assertEquals("nt:unstructured", testNode.getProperty("jcr:primaryType").getString());
        } finally {
            session2.logout();
        }
    }

    @Test
    public void nodeTypeRegistry() throws RepositoryException {
        NodeTypeManager ntMgr = getAdminSession().getWorkspace().getNodeTypeManager();
        assertFalse(ntMgr.hasNodeType("foo"));

        NodeTypeTemplate ntd = ntMgr.createNodeTypeTemplate();
        ntd.setName("foo");
        ntMgr.registerNodeType(ntd, false);
        assertTrue(ntMgr.hasNodeType("foo"));

        ntMgr.unregisterNodeType("foo");
        assertFalse(ntMgr.hasNodeType("foo"));
    }

    @Test
    public void testNamespaceRegistry() throws RepositoryException {
        NamespaceRegistry nsReg =
                getAdminSession().getWorkspace().getNamespaceRegistry();
        assertFalse(asList(nsReg.getPrefixes()).contains("foo"));
        assertFalse(asList(nsReg.getURIs()).contains("file:///foo"));

        nsReg.registerNamespace("foo", "file:///foo");
        assertTrue(asList(nsReg.getPrefixes()).contains("foo"));
        assertTrue(asList(nsReg.getURIs()).contains("file:///foo"));

        nsReg.unregisterNamespace("foo");
        assertFalse(asList(nsReg.getPrefixes()).contains("foo"));
        assertFalse(asList(nsReg.getURIs()).contains("file:///foo"));
    }

    @Test
    public void testUnregisterNamespaceWhenUsed() throws RepositoryException {

        // see OAK-11138

        String testNamespaceName1 = "file:///foo";
        String testNamespaceName2 = "file:///bar";
        Session session = getAdminSession();

        NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry();
        assertFalse(asList(nsReg.getPrefixes()).contains("foo"));
        assertFalse(asList(nsReg.getURIs()).contains(testNamespaceName1));

        nsReg.registerNamespace("foo", testNamespaceName1);
        assertTrue(asList(nsReg.getPrefixes()).contains("foo"));
        assertTrue(asList(nsReg.getURIs()).contains(testNamespaceName1));

        // check that nodes are "same" independent of how retrieved
        Node x1 = session.getRootNode().addNode("foo:test");
        session.save();
        Node x2 = session.getRootNode().getNode("{" + testNamespaceName1 + "}test");
        assertTrue(x1.isSame(x2));

        // unregister and add with a new name
        nsReg.unregisterNamespace("foo");
        assertFalse(asList(nsReg.getPrefixes()).contains("foo"));
        assertFalse(asList(nsReg.getURIs()).contains(testNamespaceName1));
        nsReg.registerNamespace("foo", testNamespaceName2);
        assertTrue(asList(nsReg.getPrefixes()).contains("foo"));
        assertTrue(asList(nsReg.getURIs()).contains(testNamespaceName2));

        try {
            session.getRootNode().getNode("{" + testNamespaceName1 + "}test");
        } catch (PathNotFoundException expected) {
            // we expect that this fails as the namespace name has been removed
        }

        // after remapping, the node created earlier is stil accessible with the
        // name prefix, but the expanded name has changed
        Node x3 = session.getRootNode().getNode("{" + testNamespaceName2 + "}test");
        Node x4 = session.getRootNode().getNode("foo:test");
        assertTrue(x3.isSame(x4));

        nsReg.unregisterNamespace("foo");

        session.getRootNode().getNode("foo:test").remove();
        session.save();
    }

    @Test
    public void sessionRemappedNamespace() throws RepositoryException {
        NamespaceRegistry nsReg =
                getAdminSession().getWorkspace().getNamespaceRegistry();
        nsReg.registerNamespace("foo", "file:///foo");

        getAdminSession().getRootNode().addNode("foo:test");
        getAdminSession().save();

        Session s = createAdminSession();
        s.setNamespacePrefix("bar", "file:///foo");
        assertTrue(s.getRootNode().hasNode("bar:test"));
        Node n = s.getRootNode().getNode("bar:test");
        assertEquals("bar:test", n.getName());
        s.logout();

        getAdminSession().getRootNode().getNode("foo:test").remove();
        getAdminSession().save();
        nsReg.unregisterNamespace("foo");
    }

    @Test
    public void registryRemappedNamespace() throws RepositoryException {
        NamespaceRegistry nsReg =
                getAdminSession().getWorkspace().getNamespaceRegistry();
        nsReg.registerNamespace("foo", "file:///foo");

        getAdminSession().getRootNode().addNode("foo:test");
        getAdminSession().save();

        try {
            nsReg.registerNamespace("bar", "file:///foo");
            fail("Remapping namespace through NamespaceRegistry must not be allowed");
        } catch (NamespaceException e) {
            // expected
        } finally {
            getAdminSession().getRootNode().getNode("foo:test").remove();
            getAdminSession().save();

            nsReg.unregisterNamespace("foo");
        }
    }

    @Test  // Regression test for OAK-299
    public void importNodeType() throws RepositoryException, IOException, ParseException {
        Session session = getAdminSession();
        NodeTypeManager manager = session.getWorkspace().getNodeTypeManager();
        if (!manager.hasNodeType("myNodeType")) {
            StringBuilder defs = new StringBuilder();
            defs.append("[\"myNodeType\"]\n");
            defs.append("  - prop1\n");
            defs.append("  + * (nt:base) = nt:unstructured \n");
            Reader cndReader = new InputStreamReader(new ByteArrayInputStream(defs.toString().getBytes()));
            CndImporter.registerNodeTypes(cndReader, session);
        }

        NodeType myNodeType = manager.getNodeType("myNodeType");
        assertTrue(myNodeType.isNodeType("nt:base"));
    }

    @Test
    public void mixin() throws RepositoryException {
        NodeTypeManager ntMgr = getAdminSession().getWorkspace().getNodeTypeManager();
        NodeTypeTemplate mixTest = ntMgr.createNodeTypeTemplate();
        mixTest.setName("mix:test");
        mixTest.setMixin(true);
        ntMgr.registerNodeType(mixTest, false);

        Node testNode = getNode(TEST_PATH);
        NodeType[] mix = testNode.getMixinNodeTypes();
        assertEquals(0, mix.length);

        testNode.addMixin("mix:test");
        testNode.getSession().save();

        Session session2 = createAnonymousSession();
        try {
            mix = session2.getNode(TEST_PATH).getMixinNodeTypes();
            assertEquals(1, mix.length);
            assertEquals("mix:test", mix[0].getName());
        } finally {
            session2.logout();
        }

        testNode.removeMixin("mix:test");
    }

    @Test
    public void liveNode() throws RepositoryException {
        Session session = getAdminSession();

        Node n1 = session.getNode(TEST_PATH);
        Node n2 = session.getNode(TEST_PATH);
        assertTrue(n1.isSame(n2));

        Node c1 = n1.addNode("c1");
        n1.setProperty("p1", "v1");
        c1.setProperty("pc1", "vc1");

        Node c2 = n2.addNode("c2");
        n2.setProperty("p2", "v2");
        c2.setProperty("pc2", "vc2");

        assertTrue(c1.isSame(n2.getNode("c1")));
        assertTrue(c2.isSame(n1.getNode("c2")));

        assertTrue(n1.hasNode("c1"));
        assertTrue(n1.hasNode("c2"));
        assertTrue(n1.hasProperty("p1"));
        assertTrue(n1.hasProperty("p2"));
        assertTrue(c1.hasProperty("pc1"));
        assertFalse(c1.hasProperty("pc2"));

        assertTrue(n2.hasNode("c1"));
        assertTrue(n2.hasNode("c2"));
        assertTrue(n2.hasProperty("p1"));
        assertTrue(n2.hasProperty("p2"));
        assertFalse(c2.hasProperty("pc1"));
        assertTrue(c2.hasProperty("pc2"));
    }

    @Test
    public void expandedName() throws RepositoryException {
        Session session = getAdminSession();
        session.setNamespacePrefix("foo", "http://example.com/");
        session.getRootNode().addNode("{0} test");
        session.save();
        assertTrue(session.nodeExists("/{0} test"));
    }

    @Test
    public void testGetDefinitionWithSNS() throws RepositoryException, IOException {
        Session session = getAdminSession();
        Node node = session.getNode("/jcr:system/jcr:nodeTypes/nt:file");
        // TODO: use getNode("jcr:childNodeDefinition[1]") once that works
        for (Node child : getChildNodes(node, "jcr:childNodeDefinition")) {
            NodeDefinition definition = child.getDefinition(); // OAK-829
            definition.getDefaultPrimaryType();                // OAK-826
            definition.getRequiredPrimaryTypes();              // OAK-826
        }
    }

    @Test
    public void workspaceCopyWithReferences() throws RepositoryException {
        Session session = getAdminSession();
        ValueFactory vf = session.getValueFactory();
        Node root = session.getRootNode();
        Node other = root.addNode("other");
        other.addMixin("mix:referenceable");
        Node src = root.addNode("src");
        Node test = src.addNode("test");
        test.addMixin("mix:referenceable");
        src.setProperty("test", test);
        src.setProperty("other", other);
        src.setProperty("multi", new Value[]{vf.createValue(test), vf.createValue(other)});
        session.save();
        session.getWorkspace().copy("/src", "/dest");
        Node dest = root.getNode("dest");
        assertEquals("/dest/test", dest.getProperty("test").getNode().getPath());
        assertEquals("/other", dest.getProperty("other").getNode().getPath());
        Value[] refs = dest.getProperty("multi").getValues();
        assertEquals(2, refs.length);
        Set<String> paths = new HashSet<String>();
        for (Value v : refs) {
            paths.add(session.getNodeByIdentifier(v.getString()).getPath());
        }
        assertTrue(paths.contains("/other"));
        assertTrue(paths.contains("/dest/test"));
    }

    @Test // OAK-1244
    public void importUUIDCreateNew() throws Exception {
        Session session = getAdminSession();
        Node node = session.getRootNode().addNode("node");
        node.addMixin("mix:referenceable");
        session.save();
        String uuid = node.getIdentifier();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        session.exportSystemView("/node", out, true, false);
        node.remove();
        session.save();
        session.importXML("/", new ByteArrayInputStream(out.toByteArray()),
                IMPORT_UUID_CREATE_NEW);
        session.save();
        node = session.getNode("/node");
        assertFalse(uuid.equals(node.getIdentifier()));
    }

    @Test
    public void testReferenceBinary() throws RepositoryException {
        ValueFactory valueFactory = getAdminSession().getValueFactory();
        Binary binary = valueFactory.createBinary(new RandomInputStream(1, 256 * 1024));

        String reference = binary instanceof ReferenceBinary
            ? ((ReferenceBinary) binary).getReference()
            : null;

        assumeTrue(reference != null);
        Session session = createAdminSession();
        try {
            valueFactory = session.getValueFactory();
            assertEquals(binary, valueFactory.createValue(
                    new SimpleReferenceBinary(reference)).getBinary());
        } finally {
            session.logout();
        }
    }

    @Test
    public void manyTransientChanges() throws RepositoryException {
        // test for OAK-1670, run with -Dupdate.limit=100
        Session session = getAdminSession();
        Node test = session.getRootNode().getNode(TEST_NODE);
        Node foo = test.addNode("foo");
        session.save();
        test.setProperty("p", "value");
        for (int i = 0; i < 76; i++) {
            test.addNode("n" + i);
        }
        Node t = foo.addNode("test");
        t.setProperty("prop", "value");
        session.getNode(TEST_PATH + "/foo/test").setProperty("prop", "value");
        session.save();

        session.logout();
    }

    @Test // OAK-2038
    public void importWithRegisteredType() throws Exception {
        Session session = getAdminSession();
        NodeTypeManager ntMgr = getAdminSession().getWorkspace().getNodeTypeManager();
        NodeTypeTemplate ntd = ntMgr.createNodeTypeTemplate();
        ntd.setName("fooType");
        PropertyDefinitionTemplate propDefTemplate = ntMgr.createPropertyDefinitionTemplate();
        propDefTemplate.setName("fooProp");
        propDefTemplate.setRequiredType(PropertyType.STRING);
        ntd.getPropertyDefinitionTemplates().add(propDefTemplate);
        ntMgr.registerNodeType(ntd, false);

        Node node = session.getRootNode().addNode("node", "fooType");
        node.setProperty("fooProp", "fooValue");
        session.save();

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        session.exportDocumentView("/node", out, true, false);
        node.remove();
        session.save();

        session.getWorkspace().importXML(
                "/", new ByteArrayInputStream(out.toByteArray()), IMPORT_UUID_CREATE_NEW);
        session.save();
        assertEquals("fooValue", session.getProperty("/node/fooProp").getString());
    }

    private void internalShadedNamespaceMappingsInImport(String xml, String ns1, String ns2) throws Exception {
        Session session = getAdminSession();
        session.getWorkspace().importXML(
                "/", new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)) , IMPORT_UUID_CREATE_NEW);
        session.save();

        String pref1 = session.getNamespacePrefix(ns1);
        String pref2 = session.getNamespacePrefix(ns2);
        assertNotEquals("prefixes should be different - " + pref1 + " vs " + pref2, pref1, pref2);

        Node nPrefFoo = session.getNode("/" + pref1 + ":foo");
        Node nExpFoo = session.getNode("/{" + ns1 + "}foo");
        assertTrue(nPrefFoo.isSame(nExpFoo));

        Node nPrefQux = nPrefFoo.getNode(pref2 + ":qux");
        Node nExpQux = nExpFoo.getNode("{" + ns2 + "}qux");
        assertTrue(nPrefQux.isSame(nExpQux));

        Node nPrefXyz = nPrefQux.getNode(pref1 + ":xyz");
        Node nExpXyz = nExpQux.getNode("{" + ns1 + "}xyz");
        assertTrue(nPrefXyz.isSame(nExpXyz));

        Node nPrefBar = nPrefFoo.getNode( pref1 + ":" + "bar");
        Node nExpBar = nExpFoo.getNode("{" + ns1 + "}bar");
        assertTrue(nPrefBar.isSame(nExpBar));
    }

    // tests import with namespace prefixes re-used for different namespace names
    @Test
    public void reusedNameSpacePrefixesInDocViewImport() throws Exception {
        String ns1 = "urn:uuid:" + UUID.randomUUID();
        String ns2 = "urn:uuid:" + UUID.randomUUID();

        String docView =
                "<a:foo xmlns:a=\"" + ns1 + "\">" +
                "  <a:qux xmlns:a=\"" + ns2 + "\">" +
                "    <a:xyz xmlns:a=\"" + ns1 + "\"/>" +
                "  </a:qux>" +
                "  <a:bar/>" +
                "</a:foo>";
        String sysView =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<sv:node xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\"" +
                "         xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\"" +
                "         xmlns:jcr=\"http://www.jcp.org/jcr/1.0\"" +
                "         xmlns:a=\"" + ns1 + "\"" +
                "         sv:name=\"a:foo\">" +
                "    <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
                "        <sv:value>nt:unstructured</sv:value>" +
                "    </sv:property>" +
                "    <sv:node xmlns:a=\"" + ns2 + "\" sv:name=\"a:qux\">" +
                "        <sv:node xmlns:a=\"" + ns1 + "\" sv:name=\"a:xyz\"/>" +
                "    </sv:node>" +
                "    <sv:node sv:name=\"a:bar\"/>" +
                "</sv:node>";

        internalShadedNamespaceMappingsInImport(docView, ns1, ns2);
        internalShadedNamespaceMappingsInImport(sysView, ns1, ns2);
    }

    // tests import with no namespace prefixes
    @Test
    public void noNameSpacePrefixesInImport() throws Exception {
        String ns1 = "urn:uuid:" + UUID.randomUUID();
        String ns2 = "urn:uuid:" + UUID.randomUUID();

        String docView =
                "<foo xmlns=\"" + ns1 + "\">" +
                        "  <qux xmlns=\"" + ns2 + "\">" +
                        "    <xyz xmlns=\"" + ns1 + "\"/>" +
                        "  </qux>" +
                        "  <bar/>" +
                        "</foo>";
        String sysView =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                        "<sv:node xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\"" +
                        "         xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\"" +
                        "         xmlns:jcr=\"http://www.jcp.org/jcr/1.0\"" +
                        "         xmlns=\"" + ns1 + "\"" +
                        "         sv:name=\"foo\">" +
                        "    <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
                        "        <sv:value>nt:unstructured</sv:value>" +
                        "    </sv:property>" +
                        "    <sv:node xmlns=\"" + ns2 + "\" sv:name=\"qux\">" +
                        "        <sv:node xmlns=\"" + ns1 + "\" sv:name=\"xyz\"/>" +
                        "    </sv:node>" +
                        "    <sv:node sv:name=\"bar\"/>" +
                        "</sv:node>";

        internalShadedNamespaceMappingsInImport(docView, ns1, ns2);
        internalShadedNamespaceMappingsInImport(sysView, ns1, ns2);
    }

    @Test
    public void largeMultiValueProperty() throws Exception{
        final List<String> logMessages = new ArrayList<>();
        Appender<ILoggingEvent> a = new AppenderBase<ILoggingEvent>() {
            @Override
            protected void append(ILoggingEvent e) {
                if (Level.WARN.isGreaterOrEqual(e.getLevel())){
                    logMessages.add(e.getFormattedMessage());
                }
            }
        };
        a.start();
        rootLogger().addAppender(a);
        Session session = getAdminSession();
        Node node = session.getRootNode().addNode("largeMultiValueProperty", "nt:unstructured");
        String[] largeArray = new String[1000+1]; //ItemImpl.MV_PROPERTY_WARN_THRESHOLD - 1000
        Arrays.fill(largeArray, "x");
        Property p = node.setProperty("fooProp", largeArray);
        Property p2 = node.setProperty("barProp", new String[] {"x"});
        p2.setValue(largeArray);
        session.save();

        rootLogger().detachAppender(a);
        a.stop();

        assertTrue(logMessages.size() >= 2);
        assertThat("Warn log message must contains a reference to the large array property path",
                logMessages.toString(), containsString(p.getPath()));
        assertThat("Warn log message must contains a reference to the large array property path",
                logMessages.toString(), containsString(p2.getPath()));
    }

    private static ch.qos.logback.classic.Logger rootLogger() {
        return ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
    }

    //------------------------------------------------------------< private >---

    private Node getNode(String path) throws RepositoryException {
        return getAdminSession().getNode(path);
    }

    private Property getProperty(String path) throws RepositoryException {
        return getAdminSession().getProperty(path);
    }

    private void addProperty(Node parentNode, String name, Value value) throws RepositoryException, IOException {
        String propertyPath = parentNode.getPath() + '/' + name;
        assertFalse(getAdminSession().propertyExists(propertyPath));

        Property added = parentNode.setProperty(name, value);
        assertTrue(parentNode.isModified());
        assertFalse(added.isModified());
        assertTrue(added.isNew());
        getAdminSession().save();

        Session session2 = createAnonymousSession();
        try {
            assertTrue(session2.propertyExists(propertyPath));
            Value value2 = session2.getProperty(propertyPath).getValue();
            assertEquals(value.getType(), value2.getType());
            if (value.getType() == PropertyType.BINARY) {
                assertEqualStream(value.getStream(), value2.getStream());
            } else {
                assertEquals(value.getString(), value2.getString());
            }

            if (value2.getType() == PropertyType.REFERENCE || value2.getType() == PropertyType.WEAKREFERENCE) {
                String ref = value2.getString();
                assertNotNull(getAdminSession().getNodeByIdentifier(ref));
            }
        } finally {
            session2.logout();
        }
    }

    private static void assertEqualStream(InputStream is1, InputStream is2) throws IOException {
        byte[] buf1 = new byte[65536];
        byte[] buf2 = new byte[65536];

        int c = 0;
        while (c != -1) {
            assertEquals(c = is1.read(buf1), is2.read(buf2));
            for (int i = 0; i < c; i++) {
                assertEquals(buf1[i], buf2[i]);
            }
        }
    }

    //------------------------------------------------------------< NumberStream >---

    /**
     * Dummy stream class used by the binary property tests.
     */
    private static class NumberStream extends InputStream {

        private final int limit;

        private int counter;

        public NumberStream(int limit) {
            this.limit = limit;
        }

        @Override
        public int read() throws IOException {
            return counter < limit
                ? counter++ & 0xff
                : -1;
        }

    }

}
