diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ProtectedPropertyTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ProtectedPropertyTest.java new file mode 100644 index 00000000000..3756b2b2ca6 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ProtectedPropertyTest.java @@ -0,0 +1,461 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; + +import org.apache.jackrabbit.oak.fixture.NodeStoreFixture; +import org.junit.Before; +import org.junit.Test; + +/** + * Test coverage for common JCR operations related to importing content. + * <p> + * Note that the purpose of these tests is not to check conformance with the JCR + * specification, but to observe the actual behavior of the implementation + * (which may be hard to change). + */ +public class ProtectedPropertyTest extends AbstractRepositoryTest { + + private Session session; + private Node testNode; + private static String TEST_NODE_NAME = "ImportOperationsTest"; + private static String TEST_NODE_NAME_REF = "ImportOperationsTest-Reference"; + private static String TEST_NODE_NAME_TMP = "ImportOperationsTest-Temp"; + + public ProtectedPropertyTest(NodeStoreFixture fixture) { + super(fixture); + } + + @Before + public void setup() throws RepositoryException { + this.session = getAdminSession(); + this.testNode = session.getRootNode().addNode("import-tests", NodeType.NT_UNSTRUCTURED); + session.save(); + } + + @Test + public void jcrMixinCreatedOnNtUnstructured() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + try { + test.setProperty(Property.JCR_CREATED, false); + session.save(); + test.addMixin(NodeType.MIX_CREATED); + session.save(); + assertEquals(false, test.getProperty(Property.JCR_CREATED).getBoolean()); + // in Oak, existing properties are left as-is (even the property + // type), which means that after adding the mixin:created type, the + // state of the node might be inconsistent with the mixin:created + // type. This may come as a surprise, but is allowed as + // implementation specific behavior, see + // https://developer.adobe.com/experience-manager/reference-materials/spec/jcr/2.0/3_Repository_Model.html#3.7.11.7%20mix:created + } finally { + test.remove(); + session.save(); + } + } + + @Test + public void jcrMixinReferenceableOnNtUnstructuredBeforeSettingMixin() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + try { + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + session.save(); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + // JCR spec + // (https://developer.adobe.com/experience-manager/reference-materials/spec/jcr/2.0/3_Repository_Model.html#3.8%20Referenceable%20Nodes) + // requests an "auto-created" property, so it might be a surprise + // that Oak actually keeps the application-assigned previous value. + assertEquals(testUuid, test.getProperty(Property.JCR_UUID).getString()); + } finally { + test.remove(); + session.save(); + } + } + + @Test + public void jcrMixinReferenceableOnNtUnstructuredBeforeSettingMixinButWithConflict() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + + try { + String testUuid = ref.getProperty(Property.JCR_UUID).getString(); + test.setProperty(Property.JCR_UUID, testUuid); + // note this fails even though test hasn't be set to mix:referenceable + session.save(); + fail("Attempt so set a UUID already in use should fail"); + } catch (ConstraintViolationException ex) { + // expected + } finally { + test.remove(); + ref.remove(); + session.save(); + } + } + + @Test + public void jcrMixinReferenceableOnNtUnstructuredAfterSettingMixin() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + try { + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + session.save(); + fail("Setting jcr:uuid after adding mixin:referenceable should fail"); + } catch (ConstraintViolationException ex) { + // expected + } finally { + test.remove(); + session.save(); + } + } + + @Test + public void setSameUuidOnTwoNtUnstructuredNodes() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + Node test2 = testNode.addNode(TEST_NODE_NAME + "2", NodeType.NT_UNSTRUCTURED); + session.save(); + try { + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + test2.setProperty(Property.JCR_UUID, testUuid); + session.save(); + fail("should not allow the same UUID on two different nodes"); + } catch (ConstraintViolationException ex) { + // expected + } finally { + test2.remove(); + test.remove(); + session.save(); + } + } + + @Test + public void setSameUuidOnTwoNtUnstructuredNodesTwoSessions() throws RepositoryException { + Session session2 = createAdminSession(); + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + Node test2 = session2.getNode(testNode.getParent().getPath()).addNode(TEST_NODE_NAME + "2", NodeType.NT_UNSTRUCTURED); + session.save(); + session2.save(); + try { + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + test2.setProperty(Property.JCR_UUID, testUuid); + session.save(); + session2.save(); + fail("should not allow the same UUID on two different nodes"); + } catch (ConstraintViolationException ex) { + // expected + } finally { + test2.remove(); + test.remove(); + session.save(); + session2.logout(); + } + } + + @Test + public void jcrMixinReferenceableOnNtUnstructuredAfterRemovingMixin() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + try { + // check jcr:uuid is there + String prevUuid = test.getProperty(Property.JCR_UUID).getString(); + test.removeMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + // ist jcr:uuid gone now? + try { + String newUuid = test.getProperty(Property.JCR_UUID).getString(); + fail("jcr:uuid should be gone after removing the mixin type, was " + prevUuid + ", now is " + newUuid); + } catch (PathNotFoundException ex) { + // expected + } + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + session.save(); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + assertEquals(testUuid, test.getProperty(Property.JCR_UUID).getString()); + Node check = session.getNodeByIdentifier(testUuid); + assertTrue(test.isSame(check)); + } finally { + test.remove(); + session.save(); + } + } + + @Test + public void jcrMixinReferenceableOnNtUnstructuredAfterRemovingMixinButDanglingReference() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.setProperty("reference", test.getIdentifier(), PropertyType.REFERENCE); + session.save(); + + try { + test.removeMixin(NodeType.MIX_REFERENCEABLE); + String testUuid = UUID.randomUUID().toString(); + test.setProperty(Property.JCR_UUID, testUuid); + session.save(); + fail("Changing jcr:uuid causing a dangling refence should fail"); + } catch (ReferentialIntegrityException ex) { + // expected + } finally { + ref.remove(); + test.remove(); + session.save(); + } + } + + @Test + public void changeUuidOnReferencedNodeWithOnlyMixin() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.setProperty("reference", test.getIdentifier(), PropertyType.REFERENCE); + session.save(); + + try { + String newUuid = UUID.randomUUID().toString(); + updateJcrUuidUsingRemoveMixin(test, newUuid); + assertEquals(newUuid, test.getProperty(Property.JCR_UUID).getString()); + session.save(); + assertEquals(newUuid, test.getProperty(Property.JCR_UUID).getString()); + assertTrue(test.isSame(session.getNodeByIdentifier(newUuid))); + } finally { + ref.remove(); + test.remove(); + session.save(); + } + } + + @Test + public void changeUuidOnReferencedNodeWithOnlyMixin2Sessions() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_UNSTRUCTURED); + test.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.setProperty("reference", test.getIdentifier(), PropertyType.REFERENCE); + session.save(); + + Session session2 = createAdminSession(); + Node testNode2 = session2.getNode(testNode.getPath()); + Node test2 = testNode2.addNode(TEST_NODE_NAME + "2", NodeType.NT_UNSTRUCTURED); + test2.addMixin(NodeType.MIX_REFERENCEABLE); + session2.save(); + Node ref2 = testNode.addNode(TEST_NODE_NAME_REF + "2", NodeType.NT_UNSTRUCTURED); + ref2.setProperty("reference", test2.getIdentifier(), PropertyType.REFERENCE); + session2.save(); + + try { + String newUuid = UUID.randomUUID().toString(); + updateJcrUuidUsingRemoveMixin(test, newUuid); + updateJcrUuidUsingRemoveMixin(test2, newUuid); + assertEquals(newUuid, test.getProperty(Property.JCR_UUID).getString()); + assertEquals(newUuid, test2.getProperty(Property.JCR_UUID).getString()); + session.save(); + assertTrue(test.isSame(session.getNodeByIdentifier(newUuid))); + session2.save(); + fail("saving 2nd session should fail"); + // SEGMENT_OK fails with the former, DOCUMENT_NS with the latter + } catch (ConstraintViolationException | ReferentialIntegrityException ex) { + // expected + } finally { + ref2.remove(); + test2.remove(); + ref.remove(); + test.remove(); + session2.logout(); + } + } + + @Test + public void changeUuidOnReferencedNodeWithInheritedMixin() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_RESOURCE); + test.setProperty(Property.JCR_DATA, session.getValueFactory().createBinary(new ByteArrayInputStream(new byte[0]))); + session.save(); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.setProperty("reference", test.getIdentifier(), PropertyType.REFERENCE); + session.save(); + + try { + String newUuid = UUID.randomUUID().toString(); + updateJcrUuidUsingRemoveMixin(test, newUuid); + fail("removing mixin:referenceable should fail on nt:resource"); + } catch (NoSuchNodeTypeException ex) { + // expected + } finally { + ref.remove(); + test.remove(); + session.save(); + } + } + + @Test + public void changeUuidOnReferencedNodeWithInheritedMixinByChangingNodeTypeTemporarily() throws RepositoryException { + Node test = testNode.addNode(TEST_NODE_NAME, NodeType.NT_RESOURCE); + test.setProperty(Property.JCR_DATA, session.getValueFactory().createBinary(new ByteArrayInputStream(new byte[0]))); + session.save(); + Node ref = testNode.addNode(TEST_NODE_NAME_REF, NodeType.NT_UNSTRUCTURED); + ref.setProperty("reference", test.getIdentifier(), PropertyType.REFERENCE); + session.save(); + + try { + String newUuid = UUID.randomUUID().toString(); + updateJcrUuidUsingNodeTypeManager(test, newUuid); + assertEquals(newUuid, test.getProperty(Property.JCR_UUID).getString()); + session.save(); + assertEquals(newUuid, test.getProperty(Property.JCR_UUID).getString()); + assertTrue(test.isSame(session.getNodeByIdentifier(newUuid))); + } finally { + ref.remove(); + test.remove(); + session.save(); + } + } + + private static void updateJcrUuidUsingRemoveMixin(Node target, String newUUID) throws RepositoryException { + // temporary node for rewriting the references + Node tmp = target.getParent().addNode(TEST_NODE_NAME_TMP, NodeType.NT_UNSTRUCTURED); + tmp.addMixin(NodeType.MIX_REFERENCEABLE); + + try { + // find all existing references to the node for which we want to rewrite the jcr:uuid + Set<Property> referrers = getReferrers(target); + + // move existing references to TEST_MODE_NAME to TEST_NODE_NAME_TMP + setReferrersTo(referrers, tmp.getIdentifier()); + + // rewrite jcr:uuid + target.removeMixin(NodeType.MIX_REFERENCEABLE); + target.setProperty(Property.JCR_UUID, newUUID); + target.addMixin(NodeType.MIX_REFERENCEABLE); + + // restore references + setReferrersTo(referrers, newUUID); + } finally { + tmp.remove(); + } + } + + private static void updateJcrUuidUsingNodeTypeManager(Node target, String newUUID) throws RepositoryException { + // temporary node for rewriting the references + Node tmp = target.getParent().addNode(TEST_NODE_NAME_TMP, NodeType.NT_UNSTRUCTURED); + tmp.addMixin(NodeType.MIX_REFERENCEABLE); + + try { + String previousType = target.getPrimaryNodeType().getName(); + + // find all existing references to the node for which we want to rewrite the jcr:uuid + Set<Property> referrers = getReferrers(target); + + // move existing references to TEST_MODE_NAME to TEST_NODE_NAME_TMP + setReferrersTo(referrers, tmp.getIdentifier()); + + // rewrite jcr:uuid + String temporaryType = registerPrimaryTypeExtendingAndUnprotectingJcrUUID(target); + target.setPrimaryType(temporaryType); + target.setProperty(Property.JCR_UUID, newUUID); + target.setPrimaryType(previousType); + unregisterPrimaryTypeExtendingAndUnprotectingJcrUUID(target, temporaryType); + + // restore references + setReferrersTo(referrers, newUUID); + + // assert temporary node type is gone + try { + target.getSession().getWorkspace().getNodeTypeManager().getNodeType(temporaryType); + fail("temporary node type should be removed"); + } catch (NoSuchNodeTypeException ex) { + // expected + } + } finally { + tmp.remove(); + } + } + + private static Set<Property> getReferrers(Node to) throws RepositoryException { + Set<Property> referrers = new HashSet<>(); + PropertyIterator pit = to.getReferences(); + while (pit.hasNext()) { + referrers.add(pit.nextProperty()); + } + return referrers; + } + + private static void setReferrersTo(Set<Property> referrers, String identifier) throws RepositoryException { + for (Property p : referrers) { + // add case for multivalued + p.getParent().setProperty(p.getName(), identifier); + } + } + + @SuppressWarnings("unchecked") + private static String registerPrimaryTypeExtendingAndUnprotectingJcrUUID(Node node) throws RepositoryException { + String tmpNodeTypeName = "tmp-" + UUID.randomUUID().toString(); + NodeTypeManager ntMgr = node.getSession().getWorkspace().getNodeTypeManager(); + + NodeTypeTemplate unprotectedNTT = ntMgr.createNodeTypeTemplate(); + unprotectedNTT.setName(tmpNodeTypeName); + unprotectedNTT.setDeclaredSuperTypeNames(new String[] {node.getPrimaryNodeType().getName()}); + PropertyDefinitionTemplate pdt = ntMgr.createPropertyDefinitionTemplate(); + pdt.setName(Property.JCR_UUID); + pdt.setProtected(false); + unprotectedNTT.getPropertyDefinitionTemplates().add(pdt); + ntMgr.registerNodeType(unprotectedNTT, true); + + return tmpNodeTypeName; + } + + private static void unregisterPrimaryTypeExtendingAndUnprotectingJcrUUID(Node node, String tmpType) throws RepositoryException { + NodeTypeManager ntMgr = node.getSession().getWorkspace().getNodeTypeManager(); + + ntMgr.unregisterNodeType(tmpType); + } +}