package org.vertexium.cypher; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.vertexium.*; import org.vertexium.cypher.exceptions.VertexiumCypherNotImplemented; import org.vertexium.cypher.executor.ExpressionScope; import java.util.*; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkNotNull; public class VertexiumCypherScope implements VertexiumCypherResult, ExpressionScope { private final Collection<Item> items; private final LinkedHashSet<String> columnNames; private final VertexiumCypherScope parentScope; private VertexiumCypherScope(Collection<Item> items, LinkedHashSet<String> columnNames, VertexiumCypherScope parentScope) { this.items = items; for (Item item : items) { if (item.parentScope == null || parentScope == null) { item.parentScope = this; } } this.columnNames = columnNames; this.parentScope = parentScope; } @Override public int size() { return items.size(); } public VertexiumCypherScope getParentScope() { return parentScope; } @Override public VertexiumCypherScope getParentCypherScope() { return parentScope; } @Override public LinkedHashSet<String> getColumnNames() { return columnNames; } public Stream<Item> stream() { return items.stream(); } public static VertexiumCypherScope newSingleItemScope(Item item) { return newItemsScope(Lists.newArrayList(item), null); } public static VertexiumCypherScope newItemsScope(List<Item> items, VertexiumCypherScope parentScope) { return new VertexiumCypherScope(items, getColumnNames(items), parentScope); } public List<Object> getByName(String name) { List<Object> results = items.stream() .map(i -> i.getByName(name, false)) .filter(Objects::nonNull) .collect(Collectors.toList()); if (results.size() == 0 && getParentScope() != null) { return getParentScope().getByName(name); } return results; } @Override public boolean contains(String name) { boolean results = items.stream() .anyMatch(i -> i.contains(name, false)); if (results) { return true; } if (getParentScope() != null && getParentScope().contains(name)) { return true; } return false; } public VertexiumCypherScope concat(VertexiumCypherScope other, VertexiumCypherScope parentScope) { return concat(other, false, parentScope); } public VertexiumCypherScope concat(VertexiumCypherScope other, boolean distinct, VertexiumCypherScope parentScope) { Collection<Item> newItems = distinct ? new LinkedHashSet<>() : new ArrayList<>(); newItems.addAll(items); newItems.addAll(other.items); LinkedHashSet<String> allColumnNames = new LinkedHashSet<>(getColumnNames()); allColumnNames.addAll(other.getColumnNames()); return new VertexiumCypherScope(newItems, allColumnNames, parentScope); } public static Collector<VertexiumCypherScope, Builder, VertexiumCypherScope> concatStreams(VertexiumCypherScope parentScope) { return Collector.of( () -> new Builder(parentScope), Builder::add, Builder::concat, Builder::build ); } public static VertexiumCypherScope newEmpty() { return new VertexiumCypherScope(new ArrayList<>(), new LinkedHashSet<>(), null); } public VertexiumCypherScope cartesianProduct(VertexiumCypherScope other, VertexiumCypherScope parentScope) { List<Item> allItems = new ArrayList<>(); for (Item item : items) { for (Item itemOther : other.items) { allItems.add(item.concat(itemOther)); } } return newFromItems(allItems.stream(), parentScope); } public static VertexiumCypherScope newFromItems(Stream<Item> items, VertexiumCypherScope parentScope) { List<Item> itemsList = items.collect(Collectors.toList()); return newFromItems(itemsList.stream(), getColumnNames(itemsList), parentScope); } private static LinkedHashSet<String> getColumnNames(List<Item> itemsList) { LinkedHashSet<String> results = new LinkedHashSet<>(); for (Item item : itemsList) { results.addAll(item.getColumnNames()); } return results; } public static VertexiumCypherScope newFromItems(Stream<Item> items, LinkedHashSet<String> columnNames, VertexiumCypherScope parentScope) { return new VertexiumCypherScope(items.collect(Collectors.toList()), columnNames, parentScope); } public static Item newMapItem(String name, Object value, ExpressionScope parentScope) { checkNotNull(name, "name cannot be null"); LinkedHashMap<String, Object> map = new LinkedHashMap<>(); map.put(name, value); return newMapItem(map, parentScope); } public static Item newEmptyItem() { return new EmptyItem(null); } public static Item newMapItem(LinkedHashMap<String, ?> map, ExpressionScope parentScope) { return new MapItem(map, parentScope); } public static PathItem newEmptyPathItem(String pathName, ExpressionScope parentScope) { return new PathItem(pathName, new ArrayList<>(), parentScope); } public static abstract class Item implements VertexiumCypherResult.Row, ExpressionScope { private ExpressionScope parentScope; protected Item(ExpressionScope parentScope) { this.parentScope = parentScope; } public abstract LinkedHashSet<String> getColumnNames(); public Object getByName(String name) { return getByName(name, true); } @Override public boolean contains(String name) { return contains(name, true); } public abstract Object getByName(String name, boolean lookInParent); public abstract boolean contains(String name, boolean lookInParent); @Override public ExpressionScope getParentScope() { return parentScope; } public abstract Item concat(Item itemOther); public VertexiumCypherScope getParentCypherScope() { ExpressionScope parentScope = getParentScope(); while (parentScope != null) { if (parentScope instanceof VertexiumCypherScope) { return (VertexiumCypherScope) parentScope; } parentScope = parentScope.getParentScope(); } return null; } @Override public abstract boolean equals(Object o); @Override public abstract int hashCode(); public static List<Item> cartesianProduct(List<Item> item0, List<Item> item1) { List<Item> allItems = new ArrayList<>(); for (Item item : item0) { for (Item itemOther : item1) { allItems.add(item.concat(itemOther)); } } return allItems; } } public static class EmptyItem extends Item { public EmptyItem(VertexiumCypherScope parentScope) { super(parentScope); } @Override public LinkedHashSet<String> getColumnNames() { return new LinkedHashSet<>(); } @Override public Object getByName(String name, boolean lookInParent) { if (lookInParent && getParentScope() != null) { return getParentScope().getByName(name); } return null; } @Override public boolean contains(String name, boolean lookInParent) { if (lookInParent && getParentScope() != null) { return getParentScope().contains(name); } return false; } @Override public Item concat(Item itemOther) { return itemOther; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } return true; } @Override public int hashCode() { return 0; } } public static class PathItem extends VertexiumCypherScope.Item implements VertexiumCypherPath { private final String pathName; private final List<Entry> items; private PrintMode printMode; private PathItem(String pathName, List<Entry> entries, ExpressionScope parentScope) { super(parentScope); this.pathName = pathName; this.items = entries; } public PathItem setPrintMode(PrintMode printMode) { this.printMode = printMode; return this; } public PrintMode getPrintMode() { return printMode; } public Element getLastElement() { if (items.size() > 0) { return items.get(items.size() - 1).element; } throw new IndexOutOfBoundsException("list contains no items"); } public Element getFirstElement() { if (items.size() > 0) { return items.get(0).element; } throw new IndexOutOfBoundsException("list contains no items"); } public PathItem concat(String name, Element element) { List<Entry> allEntries = new ArrayList<>(); allEntries.addAll(this.items); allEntries.add(new Entry(name, element)); return new PathItem(pathName, allEntries, getParentScope()) .setPrintMode(getPrintMode()); } public PathItem concat(PathItem pathItem) { List<Entry> entries = new ArrayList<>(); entries.addAll(this.items); int offset = 0; if (entries.size() > 0 && pathItem.items.size() > 0 && entries.get(entries.size() - 1).element.equals(pathItem.items.get(0).element)) { offset = 1; } entries.addAll(pathItem.items.subList(offset, pathItem.items.size())); return new PathItem(pathName, entries, getParentScope()); } public boolean contains(Element element) { return items.stream().anyMatch(e -> e.element != null && e.element.equals(element)); } public List<Edge> getEdges() { return items.stream() .filter(e -> e.element instanceof Edge) .map(e -> (Edge) e.element) .collect(Collectors.toList()); } public List<Vertex> getVertices() { return items.stream() .filter(e -> e.element instanceof Vertex) .map(e -> (Vertex) e.element) .collect(Collectors.toList()); } public Element getElement(int index) { int calculatedIndex = index; if (calculatedIndex < 0) { calculatedIndex = items.size() + index; } if (calculatedIndex < 0 || calculatedIndex >= items.size()) { throw new IndexOutOfBoundsException("requested " + index + " but size is " + items.size()); } return items.get(calculatedIndex).element; } @Override public LinkedHashSet<String> getColumnNames() { throw new VertexiumCypherNotImplemented("getColumnNames"); } @Override public Object getByName(String name, boolean lookInParent) { throw new VertexiumCypherNotImplemented("getByName"); } @Override public boolean contains(String name, boolean lookInParent) { throw new VertexiumCypherNotImplemented("contains"); } @Override public VertexiumCypherScope.Item concat(VertexiumCypherScope.Item itemOther) { throw new VertexiumCypherNotImplemented("concat path items"); } public String toString(VertexiumCypherQueryContext ctx) { if (printMode == PrintMode.RELATIONSHIP_RANGE) { return "[" + items.stream() .filter(item -> item.element instanceof Edge) .map(item -> item.toString(ctx)) .collect(Collectors.joining(", ")) + "]"; } StringBuilder result = new StringBuilder(); result.append("<"); Vertex previousVertex = null; Element previousElement = null; for (Entry item : items) { Element element = item.element; if (element == null) { // do nothing } else if (element instanceof Edge) { Edge edge = (Edge) element; Direction direction = null; if (previousVertex != null) { direction = getDirection(previousVertex.getId(), edge); } if (direction != null && direction == Direction.IN) { result.append("<"); } result.append("-"); result.append(item.toString(ctx)); result.append("-"); if (direction != null && direction == Direction.OUT) { result.append(">"); } } else if (element instanceof Vertex) { Vertex vertex = (Vertex) element; if (previousElement == null && vertex.equals(previousVertex)) { // this is a result of a zero length path } else { result.append(item.toString(ctx)); previousVertex = vertex; } } else { throw new VertexiumException("unexpected element type: " + element.getClass().getName()); } previousElement = element; } result.append(">"); return result.toString(); } public String toString() { return toString(null); } private Direction getDirection(String previousVertexId, Edge element) { if (element.getVertexId(Direction.OUT).equals(previousVertexId)) { return Direction.OUT; } else { return Direction.IN; } } public int getLength() { return (int) items.stream() .filter(e -> e.element instanceof Edge) .map(e -> (Edge) e.element) .count(); } @Override public String getPathName() { return pathName; } @Override public List<VertexiumCypherPath.Item> getItems() { ImmutableList.Builder<VertexiumCypherPath.Item> builder = ImmutableList.builder(); return builder .addAll(items) .build(); } public boolean canVertexConnectOrFoundAtStartOrEnd(Vertex vertex) { return canVertexConnectOrFound(getFirstElement(), vertex) || canVertexConnectOrFound(getLastElement(), vertex); } private boolean canVertexConnectOrFound(Element element, Vertex vertex) { if (element instanceof Vertex) { return element.equals(vertex); } else if (element instanceof Edge) { Edge edge = (Edge) element; return edge.getVertexId(Direction.OUT).equals(vertex.getId()) || edge.getVertexId(Direction.IN).equals(vertex.getId()); } return false; } public enum PrintMode {RELATIONSHIP_RANGE} private static class Entry implements VertexiumCypherPath.Item { public final String name; public final Element element; private Entry(String name, Element element) { this.name = name; this.element = element; } @Override public String toString() { return "{" + "name='" + name + '\'' + ", " + (element instanceof Vertex ? "vertex" : "edge") + "Id=" + (element == null ? null : element.getId()) + '}'; } public String toString(VertexiumCypherQueryContext ctx) { if (ctx == null) { return element.toString(); } return ctx.getResultWriter().columnValueToString(ctx, element); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Entry entry = (Entry) o; if (name != null ? !name.equals(entry.name) : entry.name != null) { return false; } return element != null ? element.equals(entry.element) : entry.element == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (element != null ? element.hashCode() : 0); return result; } @Override public String getItemName() { return name; } @Override public Element getElement() { return element; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PathItem pathItem = (PathItem) o; if (pathName != null ? !pathName.equals(pathItem.pathName) : pathItem.pathName != null) { return false; } return items != null ? items.equals(pathItem.items) : pathItem.items == null; } @Override public int hashCode() { int result = pathName != null ? pathName.hashCode() : 0; result = 31 * result + (items != null ? items.hashCode() : 0); return result; } } public static class MapItem extends Item { private final LinkedHashMap<String, ?> map; public MapItem(LinkedHashMap<String, ?> map, ExpressionScope parentScope) { super(parentScope); this.map = map; } @Override public LinkedHashSet<String> getColumnNames() { LinkedHashSet<String> results = new LinkedHashSet<>(); for (Map.Entry<String, ?> entry : map.entrySet()) { results.add(entry.getKey()); } results.addAll(getParentScope().getColumnNames()); return results; } @Override public Object getByName(String name, boolean lookInParent) { if (map.containsKey(name)) { return map.get(name); } if (lookInParent && getParentScope() != null) { return getParentScope().getByName(name); } return null; } @Override public boolean contains(String name, boolean lookInParent) { if (map.containsKey(name)) { return true; } if (lookInParent && getParentScope() != null) { return getParentScope().contains(name); } return false; } @Override public Item concat(Item itemOther) { if (itemOther instanceof MapItem) { LinkedHashMap<String, Object> all = new LinkedHashMap<>(); all.putAll(map); all.putAll(((MapItem) itemOther).map); return new MapItem(all, getParentScope()); } throw new VertexiumCypherNotImplemented("concat map items with: " + itemOther); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MapItem mapItem = (MapItem) o; return map.equals(mapItem.map); } @Override public int hashCode() { return map.hashCode(); } } public static class Builder { private final VertexiumCypherScope parentScope; private final List<Item> items = new ArrayList<>(); public Builder(VertexiumCypherScope parentScope) { this.parentScope = parentScope; } public void add(VertexiumCypherScope other) { items.addAll(other.items); } public Builder concat(Builder other) { throw new VertexiumCypherNotImplemented("concat"); } public VertexiumCypherScope build() { return newItemsScope(items, parentScope); } } }