org.neo4j.graphdb.impl.StandardExpander Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of neo4j-graphdb-api Show documentation
Show all versions of neo4j-graphdb-api Show documentation
Graph Database API for Neo4j.
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.neo4j.graphdb.impl;
import static java.util.Arrays.asList;
import static org.neo4j.graphdb.traversal.Paths.singleNodePath;
import static org.neo4j.internal.helpers.collection.ResourceClosingIterator.newResourceIterator;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PathExpander;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ResourceIterator;
import org.neo4j.graphdb.traversal.BranchState;
import org.neo4j.internal.helpers.collection.AbstractResourceIterable;
import org.neo4j.internal.helpers.collection.ArrayIterator;
import org.neo4j.internal.helpers.collection.FilteringIterator;
import org.neo4j.internal.helpers.collection.MappingResourceIterator;
import org.neo4j.internal.helpers.collection.NestingResourceIterator;
import org.neo4j.internal.helpers.collection.ResourceClosingIterator;
public abstract class StandardExpander implements PathExpander {
private StandardExpander() {}
abstract static class StandardExpansion extends AbstractResourceIterable {
final StandardExpander expander;
final Path path;
final BranchState state;
StandardExpansion(StandardExpander expander, Path path, BranchState state) {
this.expander = expander;
this.path = path;
this.state = state;
}
String stringRepresentation(String nodesORrelationships) {
return "Expansion[" + path + ".expand( " + expander + " )." + nodesORrelationships + "()]";
}
abstract StandardExpansion createNew(StandardExpander expander);
public StandardExpansion including(RelationshipType type) {
return createNew(expander.add(type));
}
public StandardExpansion including(RelationshipType type, Direction direction) {
return createNew(expander.add(type, direction));
}
public StandardExpansion excluding(RelationshipType type) {
return createNew(expander.remove(type));
}
public T getSingle() {
try (ResourceIterator expanded = iterator()) {
if (expanded.hasNext()) {
final T result = expanded.next();
if (expanded.hasNext()) {
throw new NotFoundException("More than one relationship found for " + this);
}
return result;
}
}
return null;
}
public boolean isEmpty() {
return !expander.doExpand(path, state).hasNext();
}
public StandardExpansion nodes() {
return new NodeExpansion(expander, path, state);
}
public StandardExpansion relationships() {
return new RelationshipExpansion(expander, path, state);
}
}
private static final class RelationshipExpansion extends StandardExpansion {
RelationshipExpansion(StandardExpander expander, Path path, BranchState state) {
super(expander, path, state);
}
@Override
public String toString() {
return stringRepresentation("relationships");
}
@Override
StandardExpansion createNew(StandardExpander expander) {
return new RelationshipExpansion(expander, path, state);
}
@Override
public StandardExpansion relationships() {
return this;
}
@Override
protected ResourceIterator newIterator() {
return expander.doExpand(path, state);
}
}
private static final class NodeExpansion extends StandardExpansion {
NodeExpansion(StandardExpander expander, Path path, BranchState state) {
super(expander, path, state);
}
@Override
public String toString() {
return stringRepresentation("nodes");
}
@Override
StandardExpansion createNew(StandardExpander expander) {
return new NodeExpansion(expander, path, state);
}
@Override
public StandardExpansion nodes() {
return this;
}
@Override
protected ResourceIterator newIterator() {
final Node node = path.endNode();
return new MappingResourceIterator<>(expander.doExpand(path, state)) {
@Override
protected Node map(Relationship rel) {
return rel.getOtherNode(node);
}
};
}
}
private static class AllExpander extends StandardExpander {
private final Direction direction;
AllExpander(Direction direction) {
this.direction = direction;
}
@Override
void buildString(StringBuilder result) {
if (direction != Direction.BOTH) {
result.append(direction);
result.append(':');
}
result.append('*');
}
@Override
ResourceIterator doExpand(Path path, BranchState state) {
return ResourceClosingIterator.fromResourceIterable(path.endNode().getRelationships(direction));
}
@Override
public StandardExpander add(RelationshipType type, Direction dir) {
return this;
}
@Override
public StandardExpander remove(RelationshipType type) {
Map exclude = new HashMap<>();
exclude.put(type.name(), Exclusion.ALL);
return new ExcludingExpander(Exclusion.include(direction), exclude);
}
@Override
public StandardExpander reversed() {
return reverse();
}
@Override
public StandardExpander reverse() {
return new AllExpander(direction.reverse());
}
}
private enum Exclusion {
ALL(null, "!") {
@Override
public boolean accept(Node start, Relationship rel) {
return false;
}
},
INCOMING(Direction.OUTGOING) {
@Override
Exclusion reversed() {
return OUTGOING;
}
},
OUTGOING(Direction.INCOMING) {
@Override
Exclusion reversed() {
return INCOMING;
}
},
NONE(Direction.BOTH, "") {
@Override
boolean includes(Direction direction) {
return true;
}
};
private final String string;
private final Direction direction;
Exclusion(Direction direction, String string) {
this.direction = direction;
this.string = string;
}
Exclusion(Direction direction) {
this.direction = direction;
this.string = "!" + name() + ":";
}
@Override
public final String toString() {
return string;
}
boolean accept(Node start, Relationship rel) {
return matchDirection(direction, start, rel);
}
Exclusion reversed() {
return this;
}
boolean includes(Direction dir) {
return this.direction == dir;
}
static Exclusion include(Direction direction) {
return switch (direction) {
case INCOMING -> OUTGOING;
case OUTGOING -> INCOMING;
default -> NONE;
};
}
}
private static final class ExcludingExpander extends StandardExpander {
private final Exclusion defaultExclusion;
private final Map exclusion;
ExcludingExpander(Exclusion defaultExclusion, Map exclusion) {
this.defaultExclusion = defaultExclusion;
this.exclusion = exclusion;
}
@Override
void buildString(StringBuilder result) {
// FIXME: not really correct
result.append(defaultExclusion);
result.append('*');
for (Map.Entry entry : exclusion.entrySet()) {
result.append(',');
result.append(entry.getValue());
result.append(entry.getKey());
}
}
@Override
ResourceIterator doExpand(Path path, BranchState state) {
final Node node = path.endNode();
ResourceIterator resourceIterator =
ResourceClosingIterator.fromResourceIterable(node.getRelationships());
return newResourceIterator(
new FilteringIterator<>(resourceIterator, rel -> {
Exclusion exclude = exclusion.get(rel.getType().name());
exclude = (exclude == null) ? defaultExclusion : exclude;
return exclude.accept(node, rel);
}),
resourceIterator);
}
@Override
public StandardExpander add(RelationshipType type, Direction direction) {
Exclusion excluded = exclusion.get(type.name());
final Map newExclusion;
if ((excluded == null ? defaultExclusion : excluded).includes(direction)) {
return this;
} else {
excluded = Exclusion.include(direction);
if (excluded == defaultExclusion) {
if (exclusion.size() == 1) {
return new AllExpander(defaultExclusion.direction);
} else {
newExclusion = new HashMap<>(exclusion);
newExclusion.remove(type.name());
}
} else {
newExclusion = new HashMap<>(exclusion);
newExclusion.put(type.name(), excluded);
}
}
return new ExcludingExpander(defaultExclusion, newExclusion);
}
@Override
public StandardExpander remove(RelationshipType type) {
Exclusion excluded = exclusion.get(type.name());
if (excluded == Exclusion.ALL) {
return this;
}
Map newExclusion = new HashMap<>(exclusion);
newExclusion.put(type.name(), Exclusion.ALL);
return new ExcludingExpander(defaultExclusion, newExclusion);
}
@Override
public StandardExpander reversed() {
return reverse();
}
@Override
public StandardExpander reverse() {
Map newExclusion = new HashMap<>();
for (Map.Entry entry : exclusion.entrySet()) {
newExclusion.put(entry.getKey(), entry.getValue().reversed());
}
return new ExcludingExpander(defaultExclusion.reversed(), newExclusion);
}
}
public static final StandardExpander DEFAULT = new AllExpander(Direction.BOTH) {
@Override
public StandardExpander add(RelationshipType type, Direction direction) {
return create(type, direction);
}
};
public static final StandardExpander EMPTY = new RegularExpander(Collections.emptyMap());
private record DirectionAndTypes(Direction direction, RelationshipType[] types) {}
static class RegularExpander extends StandardExpander {
final Map typesMap;
final DirectionAndTypes[] directions;
RegularExpander(Map types) {
this.typesMap = types;
this.directions = new DirectionAndTypes[types.size()];
int i = 0;
for (Map.Entry entry : types.entrySet()) {
this.directions[i++] = new DirectionAndTypes(entry.getKey(), entry.getValue());
}
}
@Override
void buildString(StringBuilder result) {
result.append(typesMap);
}
@Override
ResourceIterator doExpand(Path path, BranchState state) {
final Node node = path.endNode();
if (directions.length == 1) {
DirectionAndTypes direction = directions[0];
return ResourceClosingIterator.fromResourceIterable(
node.getRelationships(direction.direction, direction.types));
} else {
return new NestingResourceIterator<>(new ArrayIterator<>(directions)) {
@Override
protected ResourceIterator createNestedIterator(DirectionAndTypes item) {
return ResourceClosingIterator.fromResourceIterable(
node.getRelationships(item.direction, item.types));
}
};
}
}
StandardExpander createNew(Map types) {
if (types.isEmpty()) {
return new AllExpander(Direction.BOTH);
}
return new RegularExpander(types);
}
@Override
public StandardExpander add(RelationshipType type, Direction direction) {
Map> tempMap = temporaryTypeMapFrom(typesMap);
tempMap.get(direction).add(type);
return createNew(toTypeMap(tempMap));
}
@Override
public StandardExpander remove(RelationshipType type) {
Map> tempMap = temporaryTypeMapFrom(typesMap);
for (Direction direction : Direction.values()) {
tempMap.get(direction).remove(type);
}
return createNew(toTypeMap(tempMap));
}
@Override
public StandardExpander reversed() {
return reverse();
}
@Override
public StandardExpander reverse() {
Map> tempMap = temporaryTypeMapFrom(typesMap);
Collection out = tempMap.get(Direction.OUTGOING);
Collection in = tempMap.get(Direction.INCOMING);
tempMap.put(Direction.OUTGOING, in);
tempMap.put(Direction.INCOMING, out);
return createNew(toTypeMap(tempMap));
}
}
private static final class FilteringExpander extends StandardExpander {
private final StandardExpander expander;
private final Filter[] filters;
FilteringExpander(StandardExpander expander, Filter... filters) {
this.expander = expander;
this.filters = filters;
}
@Override
void buildString(StringBuilder result) {
expander.buildString(result);
result.append("; filter:");
for (Filter filter : filters) {
result.append(' ');
result.append(filter);
}
}
@Override
ResourceIterator doExpand(final Path path, BranchState state) {
ResourceIterator resourceIterator = expander.doExpand(path, state);
return newResourceIterator(
new FilteringIterator<>(resourceIterator, item -> {
Path extendedPath = ExtendedPath.extend(path, item);
for (Filter filter : filters) {
if (filter.exclude(extendedPath)) {
return false;
}
}
return true;
}),
resourceIterator);
}
@Override
public StandardExpander addNodeFilter(Predicate super Node> filter) {
return new FilteringExpander(expander, append(filters, new NodeFilter(filter)));
}
@Override
public StandardExpander addRelationshipFilter(Predicate super Relationship> filter) {
return new FilteringExpander(expander, append(filters, new RelationshipFilter(filter)));
}
@Override
public StandardExpander add(RelationshipType type, Direction direction) {
return new FilteringExpander(expander.add(type, direction), filters);
}
@Override
public StandardExpander remove(RelationshipType type) {
return new FilteringExpander(expander.remove(type), filters);
}
@Override
public StandardExpander reversed() {
return reverse();
}
@Override
public StandardExpander reverse() {
return new FilteringExpander(expander.reversed(), filters);
}
}
private abstract static class Filter {
abstract boolean exclude(Path path);
}
private static final class NodeFilter extends Filter {
private final Predicate super Node> predicate;
NodeFilter(Predicate super Node> predicate) {
this.predicate = predicate;
}
@Override
public String toString() {
return predicate.toString();
}
@Override
boolean exclude(Path path) {
return !predicate.test(path.endNode());
}
}
private static final class RelationshipFilter extends Filter {
private final Predicate super Relationship> predicate;
RelationshipFilter(Predicate super Relationship> predicate) {
this.predicate = predicate;
}
@Override
public String toString() {
return predicate.toString();
}
@Override
boolean exclude(Path path) {
return !predicate.test(path.lastRelationship());
}
}
public final StandardExpansion expand(Node node) {
return new RelationshipExpansion(this, singleNodePath(node), BranchState.NO_STATE);
}
@Override
public final StandardExpansion expand(Path path, BranchState state) {
return new RelationshipExpansion(this, path, state);
}
@SuppressWarnings("unchecked")
static T[] append(T[] array, T item) {
T[] result = (T[]) Array.newInstance(array.getClass().getComponentType(), array.length + 1);
System.arraycopy(array, 0, result, 0, array.length);
result[array.length] = item;
return result;
}
private static boolean matchDirection(Direction dir, Node start, Relationship rel) {
return switch (dir) {
case INCOMING -> rel.getEndNode().equals(start);
case OUTGOING -> rel.getStartNode().equals(start);
case BOTH -> true;
};
}
abstract ResourceIterator doExpand(Path path, BranchState state);
@Override
public final String toString() {
StringBuilder result = new StringBuilder("Expander[");
buildString(result);
result.append(']');
return result.toString();
}
abstract void buildString(StringBuilder result);
public final StandardExpander add(RelationshipType type) {
return add(type, Direction.BOTH);
}
public abstract StandardExpander add(RelationshipType type, Direction direction);
public abstract StandardExpander remove(RelationshipType type);
@Override
public abstract StandardExpander reverse();
public abstract StandardExpander reversed();
public StandardExpander addNodeFilter(Predicate super Node> filter) {
return new FilteringExpander(this, new NodeFilter(filter));
}
public StandardExpander addRelationshipFilter(Predicate super Relationship> filter) {
return new FilteringExpander(this, new RelationshipFilter(filter));
}
public static StandardExpander create(Direction direction) {
return new AllExpander(direction);
}
public static StandardExpander create(RelationshipType type, Direction dir) {
Map types = new EnumMap<>(Direction.class);
types.put(dir, new RelationshipType[] {type});
return new RegularExpander(types);
}
private static Map toTypeMap(Map> tempMap) {
// Remove OUT/IN where there is a BOTH
Collection both = tempMap.get(Direction.BOTH);
tempMap.get(Direction.OUTGOING).removeAll(both);
tempMap.get(Direction.INCOMING).removeAll(both);
// Convert into a final map
Map map = new EnumMap<>(Direction.class);
for (Map.Entry> entry : tempMap.entrySet()) {
if (!entry.getValue().isEmpty()) {
map.put(entry.getKey(), entry.getValue().toArray(new RelationshipType[0]));
}
}
return map;
}
private static Map> temporaryTypeMap() {
Map> map = new EnumMap<>(Direction.class);
for (Direction direction : Direction.values()) {
map.put(direction, new ArrayList<>());
}
return map;
}
private static Map> temporaryTypeMapFrom(
Map typeMap) {
Map> map = new EnumMap<>(Direction.class);
for (Direction direction : Direction.values()) {
List types = new ArrayList<>();
map.put(direction, types);
RelationshipType[] existing = typeMap.get(direction);
if (existing != null) {
types.addAll(asList(existing));
}
}
return map;
}
public static StandardExpander create(
RelationshipType type1, Direction dir1, RelationshipType type2, Direction dir2, Object... more) {
Map> tempMap = temporaryTypeMap();
tempMap.get(dir1).add(type1);
tempMap.get(dir2).add(type2);
for (int i = 0; i < more.length; i++) {
RelationshipType type = (RelationshipType) more[i++];
Direction direction = (Direction) more[i];
tempMap.get(direction).add(type);
}
return new RegularExpander(toTypeMap(tempMap));
}
}