From f4d373895b8344f4ace6d6f559c610f903787320 Mon Sep 17 00:00:00 2001 From: tb06904 <141412860+tb06904@users.noreply.github.com> Date: Fri, 26 Jul 2024 09:42:47 +0000 Subject: [PATCH] Gh-3253 Fix custom types in gremlin (#3255) * add conversions so gafferpop edges and vertexes are graphson compliant * update testing * update testing * javadoc * make sure cypher types can be used correctly * typo --- library/tinkerpop/pom.xml | 2 - .../gchq/gaffer/tinkerpop/GafferPopGraph.java | 49 +++--- .../generator/GafferEdgeGenerator.java | 27 ++-- .../generator/GafferEntityGenerator.java | 10 +- .../generator/GafferPopEdgeGenerator.java | 22 +-- .../generator/GafferPopVertexGenerator.java | 20 ++- .../step/util/GafferPopHasContainer.java | 5 +- .../util/GafferCustomTypeFactory.java | 149 ++++++++++++++++++ .../util/GafferPredicateFactory.java | 7 +- .../util/TypeSubTypeValueFactory.java | 66 -------- .../gaffer/tinkerpop/GafferPopGraphTest.java | 3 +- .../GafferPopElementGeneratorTest.java | 17 +- .../traversal/step/GafferPopGraphStepIT.java | 15 +- .../traversal/step/GafferPopHasStepIT.java | 12 +- .../util/GafferCustomTypeFactoryTest.java | 108 +++++++++++++ .../util/TypeSubTypeValueFactoryTest.java | 66 -------- .../util/GafferPopTstvTestUtils.java | 15 +- pom.xml | 2 + rest-api/spring-rest/pom.xml | 5 + .../rest/controller/GremlinController.java | 12 +- .../rest/handler/GremlinWebSocketHandler.java | 6 + .../controller/GremlinControllerTest.java | 35 ++++ .../handler/GremlinWebSocketIT.java | 18 +++ 23 files changed, 449 insertions(+), 222 deletions(-) create mode 100644 library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactory.java delete mode 100644 library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactory.java create mode 100644 library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactoryTest.java delete mode 100644 library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactoryTest.java diff --git a/library/tinkerpop/pom.xml b/library/tinkerpop/pom.xml index 7612fc4c477..645ec6d4963 100644 --- a/library/tinkerpop/pom.xml +++ b/library/tinkerpop/pom.xml @@ -28,8 +28,6 @@ 4.2.3 7.15.0 - - 1.0.0 !GafferPopGraphStructureStandardTest,!GafferPopFeatureTest,!GafferPopGraphProcessStandardTest diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraph.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraph.java index 42f55e88f25..89d4af3397d 100755 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraph.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraph.java @@ -57,7 +57,7 @@ import uk.gov.gchq.gaffer.tinkerpop.generator.GafferPopElementGenerator; import uk.gov.gchq.gaffer.tinkerpop.process.traversal.strategy.optimisation.GafferPopGraphStepStrategy; import uk.gov.gchq.gaffer.tinkerpop.process.traversal.strategy.optimisation.GafferPopHasStepStrategy; -import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.TypeSubTypeValueFactory; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; import uk.gov.gchq.gaffer.tinkerpop.service.GafferPopNamedOperationServiceFactory; import uk.gov.gchq.gaffer.user.User; import uk.gov.gchq.koryphe.iterable.MappedIterable; @@ -728,6 +728,32 @@ public T execute(final OperationChain opChain) { } } + /** + * Sets the {@link GafferPopGraphVariables} to default values for this + * graph + * + * @param variables The variables + */ + public void setDefaultVariables(final GafferPopGraphVariables variables) { + LOGGER.debug("Resetting graph variables to defaults"); + variables.set(GafferPopGraphVariables.OP_OPTIONS, Collections.unmodifiableMap(opOptions)); + variables.set(GafferPopGraphVariables.USER, defaultUser); + variables.set(GafferPopGraphVariables.GET_ALL_ELEMENTS_LIMIT, + configuration().getInteger(GET_ALL_ELEMENTS_LIMIT, DEFAULT_GET_ALL_ELEMENTS_LIMIT)); + variables.set(GafferPopGraphVariables.HAS_STEP_FILTER_STAGE, + configuration().getString(HAS_STEP_FILTER_STAGE, DEFAULT_HAS_STEP_FILTER_STAGE.toString())); + variables.set(GafferPopGraphVariables.LAST_OPERATION_CHAIN, new OperationChain()); + } + + /** + * Get the underlying Gaffer graph this GafferPop graph is connected to. + * + * @return The Gaffer Graph. + */ + public Graph getGafferGraph() { + return graph; + } + private Iterator verticesWithSeedsAndView(final List seeds, final View view) { final boolean getAll = null == seeds || seeds.isEmpty(); final LinkedList idVertices = new LinkedList<>(); @@ -933,9 +959,9 @@ private List getElementSeeds(final Iterable ids) { // Check if contains label in edge ID if (edgeIDMatcher.matches()) { seeds.add(new EdgeSeed(edgeIDMatcher.group("src"), edgeIDMatcher.group("dest"))); + // If not then check if a custom type e.g. TSTV } else { - // If not then check if a TSTV ID - seeds.add(new EntitySeed(TypeSubTypeValueFactory.parseAsTstvIfValid(id))); + seeds.add(new EntitySeed(GafferCustomTypeFactory.parseAsCustomTypeIfValid(id))); } // Assume entity ID as a fallback } else { @@ -988,23 +1014,6 @@ private IncludeIncomingOutgoingType getInOutType(final Direction direction) { return inOutType; } - /** - * Sets the {@link GafferPopGraphVariables} to default values for this - * graph - * - * @param variables The variables - */ - public void setDefaultVariables(final GafferPopGraphVariables variables) { - LOGGER.info("Resetting graph variables to defaults"); - variables.set(GafferPopGraphVariables.OP_OPTIONS, Collections.unmodifiableMap(opOptions)); - variables.set(GafferPopGraphVariables.USER, defaultUser); - variables.set(GafferPopGraphVariables.GET_ALL_ELEMENTS_LIMIT, - configuration().getInteger(GET_ALL_ELEMENTS_LIMIT, DEFAULT_GET_ALL_ELEMENTS_LIMIT)); - variables.set(GafferPopGraphVariables.HAS_STEP_FILTER_STAGE, - configuration().getString(HAS_STEP_FILTER_STAGE, DEFAULT_HAS_STEP_FILTER_STAGE.toString())); - variables.set(GafferPopGraphVariables.LAST_OPERATION_CHAIN, new OperationChain()); - } - /** * Gets the next ID to assign to a supplied vertex based on the currently configured ID manager. * diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEdgeGenerator.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEdgeGenerator.java index 4ed6f1bfece..640ffb54f9a 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEdgeGenerator.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEdgeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Crown Copyright + * Copyright 2016-2024 Crown Copyright * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,27 @@ package uk.gov.gchq.gaffer.tinkerpop.generator; -import org.apache.tinkerpop.gremlin.structure.Property; - import uk.gov.gchq.gaffer.data.element.Edge; import uk.gov.gchq.gaffer.data.generator.OneToOneElementGenerator; import uk.gov.gchq.gaffer.tinkerpop.GafferPopEdge; - -import java.util.Iterator; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; public class GafferEdgeGenerator implements OneToOneElementGenerator { @Override public Edge _apply(final GafferPopEdge gafferPopEdge) { - final Edge edge = new Edge(gafferPopEdge.label(), gafferPopEdge.outVertex().id(), - gafferPopEdge.inVertex().id(), true); - final Iterator> propItr = gafferPopEdge.properties(); - while (propItr.hasNext()) { - final Property prop = propItr.next(); - if (null != prop.key()) { - edge.putProperty(prop.key(), prop.value()); + // Add edge + final Edge edge = new Edge( + gafferPopEdge.label(), + GafferCustomTypeFactory.parseAsCustomTypeIfValid(gafferPopEdge.outVertex().id()), + GafferCustomTypeFactory.parseAsCustomTypeIfValid(gafferPopEdge.inVertex().id()), + true); + + // Add properties + gafferPopEdge.properties().forEachRemaining(prop -> { + if (prop.key() != null) { + edge.putProperty(prop.key(), GafferCustomTypeFactory.parseAsCustomTypeIfValid(prop.value())); } - } + }); return edge; } } diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEntityGenerator.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEntityGenerator.java index 550bd60f9d6..b574770744f 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEntityGenerator.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferEntityGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Crown Copyright + * Copyright 2016-2024 Crown Copyright * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import uk.gov.gchq.gaffer.data.element.Entity; import uk.gov.gchq.gaffer.data.generator.OneToOneElementGenerator; import uk.gov.gchq.gaffer.tinkerpop.GafferPopVertex; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; public class GafferEntityGenerator implements OneToOneElementGenerator { @Override @@ -27,12 +28,13 @@ public Entity _apply(final GafferPopVertex vertex) { throw new IllegalArgumentException("Unable to convert a null GafferPopVertex Object"); } - final Entity entity = new Entity(vertex.label(), vertex.id()); + final Entity entity = new Entity(vertex.label(), GafferCustomTypeFactory.parseAsCustomTypeIfValid(vertex.id())); // Tinkerpop allows nested properties under a key for Gaffer we need to flatten these so only one property per key vertex.properties().forEachRemaining(vertProp -> { - entity.putProperty(vertProp.key(), vertProp.value()); - vertProp.properties().forEachRemaining(prop -> entity.putProperty(prop.key(), prop.value())); + entity.putProperty(vertProp.key(), GafferCustomTypeFactory.parseAsCustomTypeIfValid(vertProp.value())); + vertProp.properties().forEachRemaining( + prop -> entity.putProperty(prop.key(), GafferCustomTypeFactory.parseAsCustomTypeIfValid(prop.value()))); }); return entity; diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopEdgeGenerator.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopEdgeGenerator.java index efc798ae581..4ca1840fde0 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopEdgeGenerator.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopEdgeGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Crown Copyright + * Copyright 2016-2024 Crown Copyright * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,7 @@ import uk.gov.gchq.gaffer.data.generator.OneToOneObjectGenerator; import uk.gov.gchq.gaffer.tinkerpop.GafferPopEdge; import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph; - -import java.util.Map.Entry; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; public class GafferPopEdgeGenerator implements OneToOneObjectGenerator { private final GafferPopGraph graph; @@ -48,14 +47,19 @@ public GafferPopEdge _apply(final Element element) { } final Edge edge = ((Edge) element); - final GafferPopEdge gafferPopEdge = new GafferPopEdge(edge.getGroup(), - edge.getSource(), edge.getDestination(), graph); + final GafferPopEdge gafferPopEdge = new GafferPopEdge( + edge.getGroup(), + GafferCustomTypeFactory.parseForGraphSONv3(edge.getSource()), + GafferCustomTypeFactory.parseForGraphSONv3(edge.getDestination()), + graph); - for (final Entry entry : edge.getProperties().entrySet()) { - if (null != entry.getValue()) { - gafferPopEdge.propertyWithoutUpdate(entry.getKey(), entry.getValue()); + // Add the properties + edge.getProperties().forEach((k, v) -> { + if (v != null) { + gafferPopEdge.propertyWithoutUpdate(k, GafferCustomTypeFactory.parseForGraphSONv3(v)); } - } + }); + if (gafferPopReadOnly) { gafferPopEdge.setReadOnly(); } diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopVertexGenerator.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopVertexGenerator.java index 18ad7aed0d2..33b5e42c6d3 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopVertexGenerator.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopVertexGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 Crown Copyright + * Copyright 2016-2024 Crown Copyright * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ import uk.gov.gchq.gaffer.data.generator.OneToOneObjectGenerator; import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph; import uk.gov.gchq.gaffer.tinkerpop.GafferPopVertex; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; -import java.util.Map.Entry; public class GafferPopVertexGenerator implements OneToOneObjectGenerator { private final GafferPopGraph graph; @@ -49,12 +49,18 @@ public GafferPopVertex _apply(final Element element) { } final Entity entity = ((Entity) element); - final GafferPopVertex vertex = new GafferPopVertex(entity.getGroup(), entity.getVertex(), graph); - for (final Entry entry : entity.getProperties().entrySet()) { - if (null != entry.getValue()) { - vertex.propertyWithoutUpdate(Cardinality.list, entry.getKey(), entry.getValue()); + final GafferPopVertex vertex = new GafferPopVertex( + entity.getGroup(), + GafferCustomTypeFactory.parseForGraphSONv3(entity.getVertex()), + graph); + + // Add the properties + entity.getProperties().forEach((k, v) -> { + if (v != null) { + vertex.propertyWithoutUpdate(Cardinality.list, k, GafferCustomTypeFactory.parseForGraphSONv3(v)); } - } + }); + if (gafferPopReadOnly) { vertex.setReadOnly(); } diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/util/GafferPopHasContainer.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/util/GafferPopHasContainer.java index 2d2c088b95b..1b98260cc5e 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/util/GafferPopHasContainer.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/util/GafferPopHasContainer.java @@ -20,6 +20,7 @@ import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Property; +import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferCustomTypeFactory; import uk.gov.gchq.gaffer.tinkerpop.process.traversal.util.GafferPredicateFactory; import java.util.function.Predicate; @@ -45,7 +46,7 @@ public Predicate getGafferPredicate() { @Override protected boolean testId(final Element element) { - return gafferPredicate.test(element.id()); + return gafferPredicate.test(GafferCustomTypeFactory.parseAsCustomTypeIfValid(element.id())); } @Override @@ -60,7 +61,7 @@ protected boolean testLabel(final Element element) { @Override protected boolean testValue(final Property property) { - return gafferPredicate.test(property.value()); + return gafferPredicate.test(GafferCustomTypeFactory.parseAsCustomTypeIfValid(property.value())); } @Override diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactory.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactory.java new file mode 100644 index 00000000000..e13817615ee --- /dev/null +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactory.java @@ -0,0 +1,149 @@ +/* + * Copyright 2024 Crown Copyright + * + * Licensed 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 uk.gov.gchq.gaffer.tinkerpop.process.traversal.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import uk.gov.gchq.gaffer.types.TypeSubTypeValue; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class that helps the conversion between Gaffer custom types and + * standard types that Tinkerpop supports. + * + * Tinkerpop only supports a limited number of types so any custom Gaffer + * ones can only be passed as String representations via Gremlin so need + * conversion before using in Gaffer Operation chains. + */ +public final class GafferCustomTypeFactory { + /* + * List of supported standard types that GraphSON v3 can serialise + */ + public static final List> GRAPHSONV3_TYPES = Collections.unmodifiableList( + Arrays.asList( + Boolean.class, + Byte[].class, + Date.class, + Double.class, + Float.class, + Integer.class, + List.class, + Long.class, + Map.class, + Set.class, + String.class, + Timestamp.class, + UUID.class)); + + private static final Logger LOGGER = LoggerFactory.getLogger(GafferCustomTypeFactory.class); + private static final Pattern TSTV_REGEX = Pattern.compile(".*\\[type=(?.*),\\s*subType=(?.*),\\s*value=(?.*)\\]$"); + + private GafferCustomTypeFactory() { + // Utility class + } + + /** + * Returns a the relevant Object e.g. {@link TypeSubTypeValue} from the + * supplied value or ID, usually by parsing a specifically formatted string. + * As a fallback will give back the original value if no relevant type + * was found. + * + *
+     * "TypeSubTypeValue[type=alpha,subType=beta,value=gamma]" // returns TypeSubTypeValue object
+     * "[type=alpha,subType=beta,value=gamma]" // returns TypeSubTypeValue object
+     * "normalvalue" // returns itself
+     * 
+ * + * @param value The value. + * @return The value as its relevant type. + */ + public static Object parseAsCustomTypeIfValid(final Object value) { + if (value instanceof String) { + Matcher tstvMatcher = TSTV_REGEX.matcher((String) value); + if (tstvMatcher.matches()) { + // Split into a TSTV via matcher + LOGGER.debug("Parsing string as a TSTV: {}", value); + return new TypeSubTypeValue( + tstvMatcher.group("type"), + tstvMatcher.group("stype"), + tstvMatcher.group("val")); + } + } + + // If value is collection e.g. list or set then check the values inside it + if (value instanceof Collection) { + List converted = new ArrayList<>(); + ((Collection) value).forEach(v -> converted.add(parseAsCustomTypeIfValid(v))); + // Return a set if needed + if (value instanceof Set) { + return new HashSet<>(converted); + } + return converted; + } + + return value; + } + + /** + * Parses the given value to make sure it can be used with Tinkerpops + * GraphSONv3 types. Will convert the value to String representation + * if type is not compatible or if value is a collection it will convert + * all the values inside it. + * + * @param value The value to parse. + * @return The value in compatible format. + */ + public static Object parseForGraphSONv3(final Object value) { + if (value == null) { + return value; + } + + // If value is collection e.g. list or set then check the values inside it + if (value instanceof Collection) { + List converted = new ArrayList<>(); + ((Collection) value).forEach(v -> converted.add(parseForGraphSONv3(v))); + // Return a set if needed + if (value instanceof Set) { + return new HashSet<>(converted); + } + return converted; + } + + // Check if the value can be used with GraphSON v3 + if (!GRAPHSONV3_TYPES.contains(value.getClass())) { + LOGGER.debug("Converting value to string {}", value); + return value.toString(); + } + + return value; + } + +} diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferPredicateFactory.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferPredicateFactory.java index a7c73b08e69..af22d5f5359 100644 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferPredicateFactory.java +++ b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferPredicateFactory.java @@ -42,7 +42,6 @@ import java.util.stream.Collectors; public final class GafferPredicateFactory { - private static final String COULD_NOT_TRANSLATE_ERROR = "Could not translate Gremlin predicate: "; private GafferPredicateFactory() { @@ -60,7 +59,7 @@ private GafferPredicateFactory() { * @param p the Gremlin predicate to convert * @return the equivalent {@link KoryphePredicate} * - * @see TypeSubTypeValueFactory#parseAsTstvIfValid(Object) + * @see GafferCustomTypeFactory#parseAsCustomTypeIfValid(Object) */ public static Predicate convertGremlinPredicate(final P p) { if (p == null) { @@ -76,12 +75,12 @@ public static Predicate convertGremlinPredicate(final P p) { BiPredicate biPredicate = p.getBiPredicate(); if (biPredicate instanceof Compare) { - Object value = TypeSubTypeValueFactory.parseAsTstvIfValid(p.getValue()); + Object value = GafferCustomTypeFactory.parseAsCustomTypeIfValid(p.getValue()); return getComparePredicate((Compare) biPredicate, value); } else if (biPredicate instanceof Contains) { Collection value = (Collection) p.getValue(); Collection mappedValues = value.stream() - .map(v -> TypeSubTypeValueFactory.parseAsTstvIfValid(v)) + .map(GafferCustomTypeFactory::parseAsCustomTypeIfValid) .collect(Collectors.toList()); return getContainsPredicate((Contains) biPredicate, mappedValues); } else if (biPredicate instanceof Text) { diff --git a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactory.java b/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactory.java deleted file mode 100644 index 4a43c13e32e..00000000000 --- a/library/tinkerpop/src/main/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactory.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2024 Crown Copyright - * - * Licensed 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 uk.gov.gchq.gaffer.tinkerpop.process.traversal.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import uk.gov.gchq.gaffer.types.TypeSubTypeValue; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class TypeSubTypeValueFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(TypeSubTypeValueFactory.class); - private static final Pattern TSTV_REGEX = Pattern.compile("^t:(?.*)\\|st:(?.*)\\|v:(?.*)$"); - - private TypeSubTypeValueFactory() { - // Utility class - } - - /** - * Returns a the relevant Object e.g. {@link TypeSubTypeValue} from the - * supplied value or ID, usually by parsing a specifically formatted string. - * As a fallback will give back the original value if no relevant type - * was found. - * - *
-     * "t:type|st:subtype|v:value" // returns TypeSubTypeValue object
-     * "notATstv" // returns itself
-     * 
- * - * @param value The value. - * @return The value as its relevant type. - */ - public static Object parseAsTstvIfValid(final Object value) { - if (value instanceof String) { - Matcher tstvMatcher = TSTV_REGEX.matcher((String) value); - if (tstvMatcher.matches()) { - // Split into a TSTV via matcher - LOGGER.debug("Parsing string as a TSTV: {}", value); - return new TypeSubTypeValue( - tstvMatcher.group("type"), - tstvMatcher.group("stype"), - tstvMatcher.group("val")); - } - } - - return value; - } - -} diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraphTest.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraphTest.java index 25ef6aafc7e..29db09fc4b3 100644 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraphTest.java +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/GafferPopGraphTest.java @@ -50,7 +50,6 @@ import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTestUtil.TEST_CONFIGURATION_2; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTestUtil.TEST_CONFIGURATION_3; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTestUtil.getTestUser; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID_STRING; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.CREATED; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.JOSH; @@ -322,7 +321,7 @@ void shouldGetVerticesWithTSTV() { assertThat(result).toIterable() .hasSize(1) .extracting(r -> r.id()) - .containsExactly(TSTV_ID); + .containsExactly(TSTV_ID_STRING); } @Test diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopElementGeneratorTest.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopElementGeneratorTest.java index 731060cdf3a..64ee051c2b1 100644 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopElementGeneratorTest.java +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/generator/GafferPopElementGeneratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Crown Copyright + * Copyright 2023-2024 Crown Copyright * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,10 @@ import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph; import uk.gov.gchq.gaffer.tinkerpop.GafferPopVertex; -public class GafferPopElementGeneratorTest { +class GafferPopElementGeneratorTest { @Test - public void shouldReturnAGafferPopVertex() { + void shouldReturnAGafferPopVertex() { // Given final GafferPopGraph graph = mock(GafferPopGraph.class); final Element element = new Entity.Builder().group(TestGroups.ENTITY).build(); @@ -49,10 +49,15 @@ public void shouldReturnAGafferPopVertex() { } @Test - public void shouldReturnAGafferPopEdge() { + void shouldReturnAGafferPopEdge() { // Given final GafferPopGraph graph = mock(GafferPopGraph.class); - final Element element = new Edge.Builder().group(TestGroups.EDGE).build(); + final String source = "source"; + final String dest = "dest"; + final Element element = new Edge.Builder() + .group(TestGroups.EDGE) + .source(source) + .dest(dest).build(); final GafferPopElementGenerator generator = new GafferPopElementGenerator(graph, true); @@ -66,7 +71,7 @@ public void shouldReturnAGafferPopEdge() { } @Test - public void shouldThrowExceptionForInvalidElement() { + void shouldThrowExceptionForInvalidElement() { // Given final GafferPopGraph graph = mock(GafferPopGraph.class); final Element element = mock(Element.class); diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopGraphStepIT.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopGraphStepIT.java index 3a74cd1bd2e..c50c213c9f9 100644 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopGraphStepIT.java +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopGraphStepIT.java @@ -34,12 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatRuntimeException; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.OTHER_TSTV_PROPERTY; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.OTHER_TSTV_PROPERTY_STRING; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID_STRING; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY_STRING; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.AGE; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.JOSH; @@ -93,7 +90,7 @@ void shouldGetVertexByTSTVSeed() { assertThat(result) .extracting(r -> r.id()) - .containsExactlyInAnyOrder(TSTV_ID); + .containsExactlyInAnyOrder(TSTV_ID_STRING); } @@ -141,7 +138,7 @@ void shouldFilterVerticesByLabelAndTstvProperty() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(TSTV_PROPERTY); + .containsExactlyInAnyOrder(TSTV_PROPERTY_STRING); } @Test @@ -161,7 +158,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyLessThan() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(TSTV_PROPERTY); + .containsExactlyInAnyOrder(TSTV_PROPERTY_STRING); } @Test @@ -181,7 +178,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyMoreThan() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING); } @Test @@ -201,7 +198,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyWithin() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY, TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING, TSTV_PROPERTY_STRING); } @Test @@ -238,7 +235,7 @@ void shouldFilterVerticesByTstvPropertyWithin() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY, TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING, TSTV_PROPERTY_STRING); } @Test diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopHasStepIT.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopHasStepIT.java index 0b3f0e4e0be..e817be0f64e 100644 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopHasStepIT.java +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/step/GafferPopHasStepIT.java @@ -31,10 +31,8 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.OTHER_TSTV_PROPERTY; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.OTHER_TSTV_PROPERTY_STRING; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV; -import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY; import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY_STRING; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.AGE; import static uk.gov.gchq.gaffer.tinkerpop.util.modern.GafferPopModernTestUtils.JOSH; @@ -80,7 +78,7 @@ void shouldFilterVerticesByLabelAndTstvProperty() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(TSTV_PROPERTY); + .containsExactlyInAnyOrder(TSTV_PROPERTY_STRING); } @Test @@ -100,7 +98,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyLessThan() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(TSTV_PROPERTY); + .containsExactlyInAnyOrder(TSTV_PROPERTY_STRING); } @Test @@ -120,7 +118,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyMoreThan() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING); } @Test @@ -141,7 +139,7 @@ void shouldFilterVerticesByLabelAndTstvPropertyWithin() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY, TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING, TSTV_PROPERTY_STRING); } @Test @@ -179,7 +177,7 @@ void shouldFilterVerticesByTstvPropertyWithin() { assertThat(result) .extracting(r -> r.value(NAME)) - .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY, TSTV_PROPERTY); + .containsExactlyInAnyOrder(OTHER_TSTV_PROPERTY_STRING, TSTV_PROPERTY_STRING); } @Test diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactoryTest.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactoryTest.java new file mode 100644 index 00000000000..1d7ffb808c8 --- /dev/null +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/GafferCustomTypeFactoryTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 Crown Copyright + * + * Licensed 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 uk.gov.gchq.gaffer.tinkerpop.process.traversal.util; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import uk.gov.gchq.gaffer.types.TypeSubTypeValue; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.COMPLEX_TSTV_ID; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.COMPLEX_TSTV_ID_STRING; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_ID_STRING; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY_SET; +import static uk.gov.gchq.gaffer.tinkerpop.util.GafferPopTstvTestUtils.TSTV_PROPERTY_SET_STRING; + +class GafferCustomTypeFactoryTest { + + @Test + void shouldConvertTypeSubTypeValues() { + // When + Object stringParsed = GafferCustomTypeFactory.parseAsCustomTypeIfValid(TSTV_ID_STRING); + Object tstvParsed = GafferCustomTypeFactory.parseForGraphSONv3(TSTV_ID); + + // Then + assertThat(stringParsed) + .isInstanceOf(TypeSubTypeValue.class) + .extracting(r -> (TypeSubTypeValue) r) + .isEqualTo(TSTV_ID); + + assertThat(tstvParsed) + .isInstanceOf(String.class) + .extracting(r -> (String) r) + .isEqualTo(TSTV_ID_STRING); + } + + @Test + void shouldConvertComplexTypeSubTypeValues() { + // When + Object stringParsed = GafferCustomTypeFactory.parseAsCustomTypeIfValid(COMPLEX_TSTV_ID_STRING); + Object tstvParsed = GafferCustomTypeFactory.parseForGraphSONv3(COMPLEX_TSTV_ID); + + // Then + assertThat(stringParsed) + .isInstanceOf(TypeSubTypeValue.class) + .extracting(r -> (TypeSubTypeValue) r) + .isEqualTo(COMPLEX_TSTV_ID); + + assertThat(tstvParsed) + .isInstanceOf(String.class) + .extracting(r -> (String) r) + .isEqualTo(COMPLEX_TSTV_ID_STRING); + } + + @Test + void shouldParseSetOfTypeSubTypeValues() { + Object stringParsed = GafferCustomTypeFactory.parseAsCustomTypeIfValid(TSTV_PROPERTY_SET_STRING); + Object tstvParsed = GafferCustomTypeFactory.parseForGraphSONv3(TSTV_PROPERTY_SET); + + // Then + assertThat(stringParsed) + .isInstanceOf(Collection.class) + .asInstanceOf(InstanceOfAssertFactories.COLLECTION) + .containsExactlyInAnyOrderElementsOf(TSTV_PROPERTY_SET); + + assertThat(tstvParsed) + .isInstanceOf(Collection.class) + .asInstanceOf(InstanceOfAssertFactories.COLLECTION) + .containsExactlyInAnyOrderElementsOf(TSTV_PROPERTY_SET_STRING); + } + + @Test + void shouldNotParseStringAsTstv() { + String notATstv = "not|a|tstv"; + Object result = GafferCustomTypeFactory.parseAsCustomTypeIfValid(notATstv); + assertThat(result) + .isInstanceOf(String.class) + .extracting(r -> (String) r) + .isEqualTo("not|a|tstv"); + } + + @Test + void shouldNotParseObjectAsTstv() { + Object notATstv = 1; + Object result = GafferCustomTypeFactory.parseAsCustomTypeIfValid(notATstv); + assertThat(result) + .isInstanceOf(Integer.class) + .extracting(r -> (Integer) r) + .isEqualTo(1); + } +} diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactoryTest.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactoryTest.java deleted file mode 100644 index b5b2267ec7a..00000000000 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/process/traversal/util/TypeSubTypeValueFactoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2024 Crown Copyright - * - * Licensed 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 uk.gov.gchq.gaffer.tinkerpop.process.traversal.util; - -import org.junit.jupiter.api.Test; - -import uk.gov.gchq.gaffer.types.TypeSubTypeValue; - -import static org.assertj.core.api.Assertions.assertThat; - -class TypeSubTypeValueFactoryTest { - - @Test - void shouldParseStringAsTstv() { - String tstv = "t:one|st:two|v:three"; - Object result = TypeSubTypeValueFactory.parseAsTstvIfValid(tstv); - assertThat(result) - .isInstanceOf(TypeSubTypeValue.class) - .extracting(r -> (TypeSubTypeValue) r) - .isEqualTo(new TypeSubTypeValue("one", "two", "three")); - } - - @Test - void shouldParseComplexStringAsTstv() { - String tstv = "t:one|one|st:two|two|v:three|three"; - Object result = TypeSubTypeValueFactory.parseAsTstvIfValid(tstv); - assertThat(result) - .isInstanceOf(TypeSubTypeValue.class) - .extracting(r -> (TypeSubTypeValue) r) - .isEqualTo(new TypeSubTypeValue("one|one", "two|two", "three|three")); - } - - @Test - void shouldNotParseStringAsTstv() { - String notATstv = "not|a|tstv"; - Object result = TypeSubTypeValueFactory.parseAsTstvIfValid(notATstv); - assertThat(result) - .isInstanceOf(String.class) - .extracting(r -> (String) r) - .isEqualTo("not|a|tstv"); - } - - @Test - void shouldNotParseObjectAsTstv() { - Object notATstv = 1; - Object result = TypeSubTypeValueFactory.parseAsTstvIfValid(notATstv); - assertThat(result) - .isInstanceOf(Integer.class) - .extracting(r -> (Integer) r) - .isEqualTo(1); - } -} diff --git a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/util/GafferPopTstvTestUtils.java b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/util/GafferPopTstvTestUtils.java index 824741329ed..c3c9e342eb6 100644 --- a/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/util/GafferPopTstvTestUtils.java +++ b/library/tinkerpop/src/test/java/uk/gov/gchq/gaffer/tinkerpop/util/GafferPopTstvTestUtils.java @@ -24,18 +24,27 @@ import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph; import uk.gov.gchq.gaffer.types.TypeSubTypeValue; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + public final class GafferPopTstvTestUtils { public static final String TSTV = "tstv"; public static final String NAME = "name"; public static final String EDGE = "test"; public static final TypeSubTypeValue TSTV_ID = new TypeSubTypeValue("alpha", "beta", "gamma"); - public static final String TSTV_ID_STRING = "t:alpha|st:beta|v:gamma"; public static final TypeSubTypeValue OTHER_TSTV_ID = new TypeSubTypeValue("delta", "epsilon", "zeta"); + public static final TypeSubTypeValue COMPLEX_TSTV_ID = new TypeSubTypeValue("de|lt-a", "eps|i|l|o=n", "zet|09!//a"); public static final TypeSubTypeValue TSTV_PROPERTY = new TypeSubTypeValue("eta", "theta", "iota"); - public static final String TSTV_PROPERTY_STRING = "t:eta|st:theta|v:iota"; public static final TypeSubTypeValue OTHER_TSTV_PROPERTY = new TypeSubTypeValue("kappa", "lambda", "mu"); - public static final String OTHER_TSTV_PROPERTY_STRING = "t:kappa|st:lambda|v:mu"; + public static final String TSTV_ID_STRING = "TypeSubTypeValue[type=alpha,subType=beta,value=gamma]"; + public static final String OTHER_TSTV_ID_STRING = "TypeSubTypeValue[type=delta,subType=epsilon,value=zeta]"; + public static final String COMPLEX_TSTV_ID_STRING = "TypeSubTypeValue[type=de|lt-a,subType=eps|i|l|o=n,value=zet|09!//a]"; + public static final String TSTV_PROPERTY_STRING = "TypeSubTypeValue[type=eta,subType=theta,value=iota]"; + public static final String OTHER_TSTV_PROPERTY_STRING = "TypeSubTypeValue[type=kappa,subType=lambda,value=mu]"; + public static final Set TSTV_PROPERTY_SET = new HashSet<>(Arrays.asList(TSTV_PROPERTY, OTHER_TSTV_PROPERTY)); + public static final Set TSTV_PROPERTY_SET_STRING = new HashSet<>(Arrays.asList(TSTV_PROPERTY_STRING, OTHER_TSTV_PROPERTY_STRING)); public static final Configuration TSTV_CONFIGURATION = new BaseConfiguration() { { diff --git a/pom.xml b/pom.xml index 25626464c92..dca3aecbdfd 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,8 @@ 5.15.0 1.36.0 3.7.1 + + 1.0.0 2.17 diff --git a/rest-api/spring-rest/pom.xml b/rest-api/spring-rest/pom.xml index 521ab6fb6e3..085e10e198a 100644 --- a/rest-api/spring-rest/pom.xml +++ b/rest-api/spring-rest/pom.xml @@ -121,6 +121,11 @@ springdoc-openapi-ui ${springdoc.version} + + org.opencypher.gremlin + cypher-gremlin-server-plugin + ${cypher-gremlin.version} + diff --git a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GremlinController.java b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GremlinController.java index af6d5e0c449..ba763b88d22 100644 --- a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GremlinController.java +++ b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/controller/GremlinController.java @@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import org.json.JSONObject; +import org.opencypher.gremlin.server.jsr223.CypherPlugin; import org.opencypher.gremlin.translation.CypherAst; import org.opencypher.gremlin.translation.translator.Translator; import org.springframework.beans.factory.annotation.Autowired; @@ -43,7 +44,9 @@ import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.LinkedList; +import java.util.Map; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; @@ -61,12 +64,15 @@ public class GremlinController { private final ConcurrentBindings bindings = new ConcurrentBindings(); private final AbstractUserFactory userFactory; private final Graph graph; + private final Map> plugins = new HashMap<>(); @Autowired public GremlinController(final GraphTraversalSource g, final AbstractUserFactory userFactory) { bindings.putIfAbsent("g", g); graph = g.getGraph(); this.userFactory = userFactory; + // Add cypher plugin so cypher functions can be used in queries + plugins.put(CypherPlugin.class.getName(), new HashMap<>()); } /** @@ -163,13 +169,15 @@ private JSONObject runGremlinAndGetExplain(final String gremlinQuery, final Http } else { gafferPopGraph = (GafferPopGraph) graph; } + gafferPopGraph.setDefaultVariables((GafferPopGraphVariables) gafferPopGraph.variables()); // Hooks for user auth userFactory.setHttpHeaders(httpHeaders); graph.variables().set(GafferPopGraphVariables.USER, userFactory.createUser()); JSONObject explain = new JSONObject(); - try (GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(bindings).create()) { - gafferPopGraph.setDefaultVariables((GafferPopGraphVariables) gafferPopGraph.variables()); + try (GremlinExecutor gremlinExecutor = GremlinExecutor.build() + .addPlugins("gremlin-groovy", plugins) + .globalBindings(bindings).create()) { // Execute the query note this will actually run the query which we need // as Gremlin will skip steps if there is no input from the previous ones gremlinExecutor.eval(gremlinQuery).join(); diff --git a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/handler/GremlinWebSocketHandler.java b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/handler/GremlinWebSocketHandler.java index 1564a5bfd4a..abd2885bf63 100644 --- a/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/handler/GremlinWebSocketHandler.java +++ b/rest-api/spring-rest/src/main/java/uk/gov/gchq/gaffer/rest/handler/GremlinWebSocketHandler.java @@ -40,6 +40,7 @@ import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializer; import org.apache.tinkerpop.gremlin.util.ser.SerTokens; import org.json.JSONObject; +import org.opencypher.gremlin.server.jsr223.CypherPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.socket.BinaryMessage; @@ -57,6 +58,7 @@ import java.nio.charset.StandardCharsets; import java.util.AbstractMap.SimpleEntry; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -88,6 +90,7 @@ public class GremlinWebSocketHandler extends BinaryWebSocketHandler { private final ConcurrentBindings bindings = new ConcurrentBindings(); private final AbstractUserFactory userFactory; private final Graph graph; + private final Map> plugins = new HashMap<>(); /** * Constructor @@ -99,6 +102,8 @@ public GremlinWebSocketHandler(final GraphTraversalSource g, final AbstractUserF bindings.putIfAbsent("g", g); graph = g.getGraph(); this.userFactory = userFactory; + // Add cypher plugin so cypher functions can be used in queries + plugins.put(CypherPlugin.class.getName(), new HashMap<>()); } @Override @@ -143,6 +148,7 @@ private ResponseMessage handleGremlinRequest(final WebSocketSession session, fin try (Scope scope = span.makeCurrent(); GremlinExecutor gremlinExecutor = GremlinExecutor.build() .globalBindings(bindings) + .addPlugins("gremlin-groovy", plugins) .executorService(executorService) .create()) { // Set current headers for potential authorisation then set the user diff --git a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GremlinControllerTest.java b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GremlinControllerTest.java index d5cd4e79169..92c94f4a980 100644 --- a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GremlinControllerTest.java +++ b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/controller/GremlinControllerTest.java @@ -32,6 +32,8 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import uk.gov.gchq.gaffer.operation.impl.Limit; +import uk.gov.gchq.gaffer.operation.impl.get.GetAllElements; import uk.gov.gchq.gaffer.operation.impl.get.GetElements; import uk.gov.gchq.gaffer.rest.factory.spring.AbstractUserFactory; import uk.gov.gchq.gaffer.rest.factory.spring.UnknownUserFactory; @@ -125,6 +127,7 @@ void shouldRejectMalformedGremlinQuery() throws Exception { void shouldReturnExplainOfValidCypherQuery() throws Exception { // Given String cypherString = "MATCH (p:person) WHERE ID(p) = '" + MARKO.getId() + "' RETURN p"; + List expectedOperations = Arrays.asList(GetElements.class.getName()); // When @@ -152,6 +155,38 @@ void shouldReturnExplainOfValidCypherQuery() throws Exception { .containsExactlyElementsOf(expectedOperations); } + @Test + void shouldReturnExplainOfCypherQueryWithExtensions() throws Exception { + // Given (uses the toInteger custom function) + String cypherString = "MATCH (p:person) WHERE p.age > toInteger(22) RETURN p"; + + List expectedOperations = Arrays.asList(GetAllElements.class.getName(), Limit.class.getName()); + + // When + MvcResult result = mockMvc + .perform(MockMvcRequestBuilders + .post(CYPHER_EXPLAIN_ENDPOINT) + .content(cypherString) + .contentType(TEXT_PLAIN_VALUE)) + .andReturn(); + + // Then + // Ensure OK response + assertThat(result.getResponse().getStatus()).isEqualTo(200); + + // Get and check response + JSONObject jsonResponse = new JSONObject(result.getResponse().getContentAsString()); + assertThat(jsonResponse.has(GremlinController.EXPLAIN_OVERVIEW_KEY)).isTrue(); + assertThat(jsonResponse.has(GremlinController.EXPLAIN_OP_CHAIN_KEY)).isTrue(); + assertThat(jsonResponse.has(GremlinController.EXPLAIN_GREMLIN_KEY)).isTrue(); + + // Check the operations that ran are as expected + JSONArray operations = jsonResponse.getJSONObject("chain").getJSONArray("operations"); + assertThat(operations) + .map(json -> ((JSONObject) json).getString("class")) + .containsExactlyElementsOf(expectedOperations); + } + @Test void shouldRejectMalformedCypherQuery() throws Exception { // Given diff --git a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/integration/handler/GremlinWebSocketIT.java b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/integration/handler/GremlinWebSocketIT.java index 2dba6d0a30c..c3f72fff2f6 100644 --- a/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/integration/handler/GremlinWebSocketIT.java +++ b/rest-api/spring-rest/src/test/java/uk/gov/gchq/gaffer/rest/integration/handler/GremlinWebSocketIT.java @@ -145,4 +145,22 @@ void shouldAcceptQueryWithCypher() { .containsExactly(MARKO.getName()); } + @Test + void shouldAcceptGremlinQueryUsingCustomCypherFunctions() { + // Given + String query = "g.V().hasLabel('person').values('age').map(cypherToString()).toList()"; + + // When + List results = client.submit(query).stream().collect(Collectors.toList()); + + // Then + assertThat(results) + .map(result -> result.getObject()) + .containsExactlyInAnyOrder( + String.valueOf(MARKO.getAge()), + String.valueOf(VADAS.getAge()), + String.valueOf(PETER.getAge()), + String.valueOf(JOSH.getAge())); + } + }