blob: 151ec3bdfca644894a01f73c396faec08d9279f7 [file] [log] [blame]
/*
* 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.base.Suppliers.memoize;
import static com.google.common.collect.ImmutableSet.of;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.Collections.emptySet;
import static javax.jcr.PropertyType.REFERENCE;
import static javax.jcr.PropertyType.WEAKREFERENCE;
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.STRING;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.REF_NAME;
import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.WEAK_REF_NAME;
import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
import static org.apache.jackrabbit.oak.spi.version.VersionConstants.VERSION_STORE_PATH;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.base.Supplier;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.plugins.index.IndexEditor;
import org.apache.jackrabbit.oak.plugins.index.property.Multiplexers;
import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
import org.apache.jackrabbit.oak.spi.commit.DefaultEditor;
import org.apache.jackrabbit.oak.spi.commit.Editor;
import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
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 implements IndexEditor {
/** 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;
private final NodeState root;
private final NodeBuilder definition;
/**
* <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 were added during this commit. we need it to reconcile
* moves
*/
private final Set<String> newIds;
private final MountInfoProvider mountInfoProvider;
/**
* flag marking a reindex, case in which we don't need to keep track of the
* newIds set
*/
private boolean isReindex;
public ReferenceEditor(NodeBuilder definition, NodeState root,MountInfoProvider mountInfoProvider) {
this.parent = null;
this.name = null;
this.path = "/";
this.definition = definition;
this.root = root;
this.newRefs = newHashMap();
this.rmRefs = newHashMap();
this.newWeakRefs = newHashMap();
this.rmWeakRefs = newHashMap();
this.rmIds = newHashSet();
this.newIds = newHashSet();
this.mountInfoProvider = mountInfoProvider;
}
private ReferenceEditor(ReferenceEditor parent, String name) {
this.parent = parent;
this.name = name;
this.path = null;
this.definition = parent.definition;
this.root = parent.root;
this.newRefs = parent.newRefs;
this.rmRefs = parent.rmRefs;
this.newWeakRefs = parent.newWeakRefs;
this.rmWeakRefs = parent.rmWeakRefs;
this.rmIds = parent.rmIds;
this.newIds = parent.newIds;
this.isReindex = parent.isReindex;
this.mountInfoProvider = parent.mountInfoProvider;
}
/**
* 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;
}
@Override
public void enter(NodeState before, NodeState after)
throws CommitFailedException {
if (MISSING_NODE == before && parent == null) {
isReindex = true;
}
}
@Override
public void leave(NodeState before, NodeState after)
throws CommitFailedException {
if (parent == null) {
Set<IndexStoreStrategy> refStores = getStrategies(false, REF_NAME);
Set<IndexStoreStrategy> weakRefStores = getStrategies(false, WEAK_REF_NAME);
// update references
for (Entry<String, Set<String>> ref : rmRefs.entrySet()) {
String uuid = ref.getKey();
Set<String> rm = ref.getValue();
Set<String> add = emptySet();
if (newRefs.containsKey(uuid)) {
add = newRefs.remove(uuid);
}
update(refStores, definition, REF_NAME, uuid, add, rm);
}
for (Entry<String, Set<String>> ref : newRefs.entrySet()) {
String uuid = ref.getKey();
if (rmIds.contains(uuid)) {
continue;
}
Set<String> add = ref.getValue();
Set<String> rm = emptySet();
update(refStores, definition, REF_NAME, uuid, add, rm);
}
checkReferentialIntegrity(refStores, root, definition.getNodeState(),
Sets.difference(rmIds, newIds));
// update weak references
for (Entry<String, Set<String>> ref : rmWeakRefs.entrySet()) {
String uuid = ref.getKey();
Set<String> rm = ref.getValue();
Set<String> add = emptySet();
if (newWeakRefs.containsKey(uuid)) {
add = newWeakRefs.remove(uuid);
}
update(weakRefStores, definition, WEAK_REF_NAME, uuid, add, rm);
}
for (Entry<String, Set<String>> ref : newWeakRefs.entrySet()) {
String uuid = ref.getKey();
Set<String> add = ref.getValue();
Set<String> rm = emptySet();
update(weakRefStores, definition, WEAK_REF_NAME, uuid, add, rm);
}
}
}
Set<IndexStoreStrategy> getStrategies(boolean unique, String index) {
return Multiplexers.getStrategies(unique, mountInfoProvider,
definition, index);
}
@Override
public void propertyAdded(PropertyState after) {
propertyChanged(null, after);
}
@Override
public void propertyChanged(PropertyState before, PropertyState after) {
if (before != null) {
if (before.getType().tag() == REFERENCE) {
if (!isVersionStorePath(getPath())) {
put(rmRefs, before.getValue(STRINGS),
concat(getPath(), before.getName()));
}
}
if (before.getType().tag() == WEAKREFERENCE) {
put(rmWeakRefs, before.getValue(STRINGS),
concat(getPath(), before.getName()));
}
if (JCR_UUID.equals(before.getName())) {
// node remove + add -> changed uuid
rmIds.add(before.getValue(STRING));
}
}
if (after != null) {
if (after.getType().tag() == REFERENCE) {
if (!isVersionStorePath(getPath())) {
put(newRefs, after.getValue(STRINGS),
concat(getPath(), after.getName()));
}
}
if (after.getType().tag() == WEAKREFERENCE) {
put(newWeakRefs, after.getValue(STRINGS),
concat(getPath(), after.getName()));
}
if (JCR_UUID.equals(after.getName())) {
// node remove + add -> changed uuid
newIds.add(after.getValue(STRING));
}
}
}
@Override
public void propertyDeleted(PropertyState before) {
propertyChanged(before, null);
}
@Override
public Editor childNodeAdded(String name, NodeState after) {
String uuid = after.getString(JCR_UUID);
if (!isReindex && uuid != null) {
newIds.add(uuid);
}
return new ReferenceEditor(this, name);
}
@Override
public Editor childNodeChanged(String name, NodeState before,
NodeState after) {
return new ReferenceEditor(this, name);
}
@Override
public Editor childNodeDeleted(String name, NodeState before)
throws CommitFailedException {
String uuid = before.getString(JCR_UUID);
if (uuid != null) {
rmIds.add(uuid);
}
return new ReferenceEditor(this, name);
}
// ---------- Utils -----------------------------------------
private static boolean isVersionStorePath(String oakPath) {
return oakPath != null
&& oakPath.startsWith(VERSION_STORE_PATH);
}
private static void put(Map<String, Set<String>> map,
Iterable<String> keys, String value) {
String asRelative = isAbsolute(value) ? value.substring(1) : value;
for (String key : keys) {
Set<String> values = map.get(key);
if (values == null) {
values = newHashSet();
}
values.add(asRelative);
map.put(key, values);
}
}
private void update(Set<IndexStoreStrategy> refStores,
NodeBuilder definition, String name, String key, Set<String> add,
Set<String> rm) throws CommitFailedException {
for (IndexStoreStrategy store : refStores) {
Set<String> empty = of();
for (String p : rm) {
Supplier<NodeBuilder> index = memoize(() -> definition.child(store.getIndexNodeName()));
store.update(index, p, name, definition, of(key), empty);
}
for (String p : add) {
// TODO do we still need to encode the values?
Supplier<NodeBuilder> index = memoize(() -> definition.child(store.getIndexNodeName()));
store.update(index, p, name, definition, empty, of(key));
}
}
}
private static boolean hasReferences(IndexStoreStrategy refStore,
NodeState root,
NodeState definition,
String name,
String key) {
return definition.hasChildNode(name)
&& refStore.count(root, definition, of(key), 1) > 0;
}
private static void checkReferentialIntegrity(Set<IndexStoreStrategy> refStores,
NodeState root,
NodeState definition,
Set<String> idsOfRemovedNodes)
throws CommitFailedException {
for (IndexStoreStrategy store : refStores) {
for (String id : idsOfRemovedNodes) {
if (hasReferences(store, root, definition, REF_NAME, id)) {
throw new CommitFailedException(INTEGRITY, 1,
"Unable to delete referenced node: " + id);
}
}
}
}
}