Author: alexparvulescu Date: Tue Nov 12 21:35:48 2013 New Revision: 1541250
URL: http://svn.apache.org/r1541250 Log: OAK-685 Enforce referential integrity for referenceable nodes - better patch, passes all the tests Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java (with props) jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java (with props) Modified: jackrabbit/oak/trunk/oak-core/pom.xml jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java jackrabbit/oak/trunk/oak-jcr/pom.xml jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java Modified: jackrabbit/oak/trunk/oak-core/pom.xml URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/pom.xml?rev=1541250&r1=1541249&r2=1541250&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/pom.xml (original) +++ jackrabbit/oak/trunk/oak-core/pom.xml Tue Nov 12 21:35:48 2013 @@ -54,6 +54,7 @@ org.apache.jackrabbit.oak.plugins.index.aggregate, org.apache.jackrabbit.oak.plugins.index.nodetype, org.apache.jackrabbit.oak.plugins.index.property, + org.apache.jackrabbit.oak.plugins.index.reference, org.apache.jackrabbit.oak.plugins.memory, org.apache.jackrabbit.oak.plugins.name, org.apache.jackrabbit.oak.plugins.nodetype, Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java?rev=1541250&r1=1541249&r2=1541250&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/identifier/IdentifierManager.java Tue Nov 12 21:35:48 2013 @@ -255,7 +255,7 @@ public class IdentifierManager { } @CheckForNull - private String resolveUUID(String uuid) { + public String resolveUUID(String uuid) { return resolveUUID(StringPropertyState.stringProperty("", uuid)); } Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java?rev=1541250&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java (added) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java Tue Nov 12 21:35:48 2013 @@ -0,0 +1,395 @@ +/* + * 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.plugins.index.reference; + +import static com.google.common.collect.Maps.newHashMap; +import static com.google.common.collect.Sets.newHashSet; +import static org.apache.jackrabbit.JcrConstants.JCR_UUID; +import static org.apache.jackrabbit.oak.api.CommitFailedException.INTEGRITY; +import static org.apache.jackrabbit.oak.api.Type.REFERENCE; +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; +import static org.apache.jackrabbit.oak.api.Type.WEAKREFERENCE; +import static org.apache.jackrabbit.oak.commons.PathUtils.concat; +import static org.apache.jackrabbit.oak.commons.PathUtils.elements; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.core.ImmutableRoot; +import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; +import org.apache.jackrabbit.oak.plugins.version.VersionConstants; +import org.apache.jackrabbit.oak.spi.commit.DefaultEditor; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * Index editor for keeping a references to a node up to date. + * + */ +class ReferenceEditor extends DefaultEditor { + + // TODO + // - look into using a storage strategy (trees) + // - what happens when you move a node? who updates the backlinks? + + private final static String REF_NAME = ":references"; + private final static String WEAK_REF_NAME = ":weakreferences"; + + /** Parent editor, or {@code null} if this is the root editor. */ + private final ReferenceEditor parent; + + /** Name of this node, or {@code null} for the root node. */ + private final String name; + + /** Path of this editor, built lazily in {@link #getPath()}. */ + private String path; + + /** The Id Manager, built lazily in {@link #getIdManager()}. */ + private IdentifierManager idManager; + + private final NodeBuilder builder; + + /** + * the uuid of the current node, null if the node doesn't have this + * property. + */ + private final String uuid; + + /** + * <UUID, Set<paths-pointing-to-the-uuid>> + */ + private final Map<String, Set<String>> newRefs; + + /** + * <UUID, Set<paths-pointing-to-the-uuid>> + */ + private final Map<String, Set<String>> rmRefs; + + /** + * <UUID, Set<paths-pointing-to-the-uuid>> + */ + private final Map<String, Set<String>> newWeakRefs; + + /** + * <UUID, Set<paths-pointing-to-the-uuid>> + */ + private final Map<String, Set<String>> rmWeakRefs; + + /** + * set of removed Ids of nodes that have a :reference property. These UUIDs + * need to be verified in the #after call + */ + private final Set<String> rmIds; + + /** + * set of ids that changed. This can happen when a node with the same name + * is deleted and added again + * + */ + private final Set<String> discardedIds; + + public ReferenceEditor(NodeBuilder builder) { + this.parent = null; + this.name = null; + this.path = "/"; + this.builder = builder; + this.uuid = null; + this.newRefs = newHashMap(); + this.rmRefs = newHashMap(); + this.newWeakRefs = newHashMap(); + this.rmWeakRefs = newHashMap(); + this.rmIds = newHashSet(); + this.discardedIds = newHashSet(); + } + + private ReferenceEditor(ReferenceEditor parent, String name, String uuid) { + this.parent = parent; + this.name = name; + this.path = null; + this.builder = parent.builder; + this.uuid = uuid; + this.newRefs = parent.newRefs; + this.rmRefs = parent.rmRefs; + this.newWeakRefs = parent.newWeakRefs; + this.rmWeakRefs = parent.rmWeakRefs; + this.rmIds = parent.rmIds; + this.discardedIds = parent.discardedIds; + } + + /** + * Returns the path of this node, building it lazily when first requested. + */ + private String getPath() { + if (path == null) { + path = concat(parent.getPath(), name); + } + return path; + } + + /** + * Returns the id manager, building it lazily when first requested. + */ + private IdentifierManager getIdManager() { + if (idManager == null) { + if (parent != null) { + return parent.getIdManager(); + } + this.idManager = new IdentifierManager(new ImmutableRoot( + this.builder.getNodeState())); + } + return idManager; + } + + @Override + public void enter(NodeState before, NodeState after) + throws CommitFailedException { + } + + @Override + public void leave(NodeState before, NodeState after) + throws CommitFailedException { + if (parent == null) { + Set<String> offending = newHashSet(rmIds); + offending.removeAll(rmRefs.keySet()); + if (!offending.isEmpty()) { + throw new CommitFailedException(INTEGRITY, 1, + "Unable to delete referenced node"); + } + rmIds.addAll(discardedIds); + + // local uuid-> nodebuilder cache + Map<String, NodeBuilder> builders = newHashMap(); + for (Entry<String, Set<String>> ref : rmRefs.entrySet()) { + String uuid = ref.getKey(); + if (rmIds.contains(uuid)) { + continue; + } + NodeBuilder child = resolveUUID(uuid, getIdManager(), builder, + builders); + if (child == null) { + throw new CommitFailedException(INTEGRITY, 2, + "Unable to resolve UUID " + uuid); + } + Set<String> rm = ref.getValue(); + Set<String> add = newHashSet(); + if (newRefs.containsKey(uuid)) { + add = newRefs.remove(uuid); + } + set(child, REF_NAME, add, rm); + } + for (Entry<String, Set<String>> ref : newRefs.entrySet()) { + String uuid = ref.getKey(); + if (rmIds.contains(uuid)) { + continue; + } + NodeBuilder child = resolveUUID(uuid, getIdManager(), builder, + builders); + if (child == null) { + throw new CommitFailedException(INTEGRITY, 3, + "Unable to resolve UUID " + uuid); + } + Set<String> add = ref.getValue(); + Set<String> rm = newHashSet(); + set(child, REF_NAME, add, rm); + } + for (Entry<String, Set<String>> ref : rmWeakRefs.entrySet()) { + String uuid = ref.getKey(); + if (rmIds.contains(uuid)) { + continue; + } + NodeBuilder child = resolveUUID(uuid, getIdManager(), builder, + builders); + if (child == null) { + // TODO log warning? + continue; + } + Set<String> rm = ref.getValue(); + Set<String> add = newHashSet(); + if (newWeakRefs.containsKey(uuid)) { + add = newWeakRefs.remove(uuid); + } + set(child, WEAK_REF_NAME, add, rm); + } + for (Entry<String, Set<String>> ref : newWeakRefs.entrySet()) { + String uuid = ref.getKey(); + if (rmIds.contains(uuid)) { + continue; + } + NodeBuilder child = resolveUUID(uuid, getIdManager(), builder, + builders); + if (child == null) { + // TODO log warning? + continue; + } + Set<String> add = ref.getValue(); + Set<String> rm = newHashSet(); + set(child, WEAK_REF_NAME, add, rm); + } + } + } + + @Override + public void propertyAdded(PropertyState after) { + propertyChanged(null, after); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + if (before != null) { + if (before.getType() == REFERENCE) { + put(rmRefs, before.getValue(STRING), getPath()); + } + if (before.getType() == WEAKREFERENCE) { + put(rmWeakRefs, before.getValue(STRING), getPath()); + } + if (JCR_UUID.equals(before.getName())) { + // node remove + add -> changed uuid + String beforeUuid = before.getValue(STRING); + if (beforeUuid != null && !beforeUuid.equals(uuid)) { + discardedIds.add(beforeUuid); + } + } + } + if (after != null) { + if (after.getType() == REFERENCE) { + put(newRefs, after.getValue(STRING), getPath()); + } + if (after.getType() == WEAKREFERENCE) { + put(newWeakRefs, after.getValue(STRING), getPath()); + } + } + } + + @Override + public void propertyDeleted(PropertyState before) { + propertyChanged(before, null); + } + + @Override + public Editor childNodeAdded(String name, NodeState after) { + String path = concat(getPath(), name); + if (isVersionStorePath(path)) { + return null; + } + return new ReferenceEditor(this, name, after.getString(JCR_UUID)); + } + + @Override + public Editor childNodeChanged(String name, NodeState before, + NodeState after) { + String path = concat(getPath(), name); + if (isVersionStorePath(path)) { + return null; + } + return new ReferenceEditor(this, name, after.getString(JCR_UUID)); + } + + @Override + public Editor childNodeDeleted(String name, NodeState before) + throws CommitFailedException { + String path = concat(getPath(), name); + if (isVersionStorePath(path)) { + return null; + } + String uuid = before.getString(JCR_UUID); + if (before.hasProperty(REF_NAME)) { + if (uuid != null) { + rmIds.add(uuid); + } + } + return new ReferenceEditor(this, name, uuid); + } + + // ---------- Utils ----------------------------------------- + + private static NodeBuilder resolveUUID(String uuid, + IdentifierManager idManager, NodeBuilder root, + Map<String, NodeBuilder> builders) { + if (builders.containsKey(uuid)) { + return builders.get(uuid); + } + String path = idManager.resolveUUID(uuid); + + if (path == null) { + return null; + } + NodeBuilder child = getChild(root, path); + if (child != null) { + builders.put(uuid, child); + } + return child; + } + + private static NodeBuilder getChild(NodeBuilder root, String path) { + NodeBuilder child = root; + for (String p : elements(path)) { + child = child.child(p); + } + return child; + } + + private static boolean isVersionStorePath(@Nonnull String oakPath) { + if (oakPath.indexOf(JcrConstants.JCR_SYSTEM) == 1) { + for (String p : VersionConstants.SYSTEM_PATHS) { + if (oakPath.startsWith(p)) { + return true; + } + } + } + return false; + } + + private static void put(Map<String, Set<String>> map, String key, + String value) { + Set<String> values = map.get(key); + if (values == null) { + values = newHashSet(); + } + values.add(value); + map.put(key, values); + } + + private static void set(NodeBuilder child, String name, Set<String> add, + Set<String> rm) { + // TODO should we optimize for the remove/add case? intersect the + // sets, work on the diffs? + + Set<String> vals; + PropertyState ref = child.getProperty(name); + if (ref != null) { + vals = newHashSet(ref.getValue(STRINGS)); + } else { + vals = newHashSet(); + } + vals.addAll(add); + vals.removeAll(rm); + if (!vals.isEmpty()) { + child.setProperty(name, vals, STRINGS); + } else { + child.removeProperty(name); + } + } + +} Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditor.java ------------------------------------------------------------------------------ svn:keywords = Author Date Id Revision Rev URL Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java?rev=1541250&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java (added) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java Tue Nov 12 21:35:48 2013 @@ -0,0 +1,38 @@ +/* + * 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.plugins.index.reference; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.commit.EditorProvider; +import org.apache.jackrabbit.oak.spi.commit.VisibleEditor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +@Component +@Service(EditorProvider.class) +public class ReferenceEditorProvider implements EditorProvider { + + @Override + public Editor getRootEditor(NodeState before, NodeState after, + NodeBuilder builder) throws CommitFailedException { + return new VisibleEditor(new ReferenceEditor(builder)); + } + +} Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceEditorProvider.java ------------------------------------------------------------------------------ svn:keywords = Author Date Id Revision Rev URL Modified: jackrabbit/oak/trunk/oak-jcr/pom.xml URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/pom.xml?rev=1541250&r1=1541249&r2=1541250&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-jcr/pom.xml (original) +++ jackrabbit/oak/trunk/oak-jcr/pom.xml Tue Nov 12 21:35:48 2013 @@ -37,9 +37,7 @@ org.apache.jackrabbit.test.api.SessionTest#testMoveConstraintViolationExceptionSrc <!-- OAK-132 --> org.apache.jackrabbit.test.api.SessionTest#testMoveConstraintViolationExceptionDest <!-- OAK-132 --> org.apache.jackrabbit.test.api.SessionTest#testMoveLockException - org.apache.jackrabbit.test.api.SessionUUIDTest#testSaveReferentialIntegrityException <!-- OAK-66 --> org.apache.jackrabbit.test.api.NodeTest#testRemoveNodeParentLocked - org.apache.jackrabbit.test.api.NodeUUIDTest#testSaveReferentialIntegrityException <!-- OAK-66 --> org.apache.jackrabbit.test.api.NodeSetPrimaryTypeTest#testLocked org.apache.jackrabbit.test.api.WorkspaceCopyVersionableTest#testCopyNodesVersionableAndCheckedIn <!-- OAK-118 --> org.apache.jackrabbit.test.api.WorkspaceMoveVersionableTest#testMoveNodesVersionableAndCheckedIn <!-- OAK-118 --> Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java?rev=1541250&r1=1541249&r2=1541250&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java (original) +++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/Jcr.java Tue Nov 12 21:35:48 2013 @@ -32,6 +32,7 @@ import org.apache.jackrabbit.oak.plugins import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider; import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider; import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexProvider; +import org.apache.jackrabbit.oak.plugins.index.reference.ReferenceEditorProvider; import org.apache.jackrabbit.oak.plugins.name.NameValidatorProvider; import org.apache.jackrabbit.oak.plugins.name.NamespaceValidatorProvider; import org.apache.jackrabbit.oak.plugins.nodetype.RegistrationEditorProvider; @@ -70,6 +71,7 @@ public class Jcr { with(new TypeEditorProvider()); with(new RegistrationEditorProvider()); with(new ConflictValidatorProvider()); + with(new ReferenceEditorProvider()); with(new PropertyIndexEditorProvider());
