diff --git a/README.md b/README.md index 2807ad2..c968bbc 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ We should see `HTTP/1.1 201 Created` as signs of success. - vocabulary count: https://api.paion-data.dev/wilhelm/languages/german?perPage=100&page=1 - query vocabulary paged: https://api.paion-data.dev/wilhelm/languages/german/count - expand: https://api.paion-data.dev/wilhelm/expand/nämlich +- search: https://api.paion-data.dev/wilhelm/search/das License ------- diff --git a/src/main/java/org/qubitpi/wilhelm/Link.java b/src/main/java/org/qubitpi/wilhelm/Link.java index 3f69838..0e45f5e 100644 --- a/src/main/java/org/qubitpi/wilhelm/Link.java +++ b/src/main/java/org/qubitpi/wilhelm/Link.java @@ -47,6 +47,13 @@ @JsonIncludeProperties({ "label", "sourceNodeId", "targetNodeId", "attributes" }) public class Link { + /** + * The database node attribute name whose value is used for displaying the relationship caption on UI. + *

+ * For example, for Neo4J database, this would correspond to a relationship property. + */ + public static final String LABEL_ATTRIBUTE = "label"; + private static final Logger LOG = LoggerFactory.getLogger(Link.class); private final String label; @@ -93,18 +100,17 @@ private Link( * @throws IllegalStateException if {@code relationship} is missing a "name" property */ public static Link valueOf(final Relationship relationship) { - final String labelKey = "name"; - if (!Objects.requireNonNull(relationship).asMap().containsKey(labelKey)) { - LOG.error("Neo4J relationship does not contain '{}' attribute: {}", labelKey, relationship.asMap()); + if (!Objects.requireNonNull(relationship).asMap().containsKey(LABEL_ATTRIBUTE)) { + LOG.error("Neo4J relationship does not contain '{}' attribute: {}", LABEL_ATTRIBUTE, relationship.asMap()); throw new IllegalStateException( "There seems to be a data format mismatch between Wilhelm webservice and Neo4J database. " + "Please file an issue at https://github.com/QubitPi/wilhelm-ws/issues for a fix" ); } - final String label = relationship.asMap().get(labelKey).toString(); + final String label = relationship.asMap().get(LABEL_ATTRIBUTE).toString(); final Map attributes = relationship.asMap().entrySet().stream() - .filter(entry -> !labelKey.equals(entry.getKey())) + .filter(entry -> !LABEL_ATTRIBUTE.equals(entry.getKey())) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); return new Link(label, relationship.startNodeElementId(), relationship.endNodeElementId(), attributes); diff --git a/src/main/java/org/qubitpi/wilhelm/Node.java b/src/main/java/org/qubitpi/wilhelm/Node.java index 82554ce..ad99242 100644 --- a/src/main/java/org/qubitpi/wilhelm/Node.java +++ b/src/main/java/org/qubitpi/wilhelm/Node.java @@ -45,6 +45,13 @@ @JsonIncludeProperties({ "id", "label", "attributes" }) public class Node { + /** + * The database node attribute name whose value is used for displaying the node caption on UI. + *

+ * For example, for Neo4J database, this would correspond to a node property. + */ + public static final String LABEL_ATTRIBUTE = "label"; + private static final Logger LOG = LoggerFactory.getLogger(Node.class); private final String id; @@ -82,18 +89,17 @@ private Node(@NotNull final String id, @NotNull final String label, @NotNull fin * @throws IllegalStateException if {@code node} is missing a "name" property */ public static Node valueOf(@NotNull final org.neo4j.driver.types.Node node) { - final String labelKey = "name"; - if (!Objects.requireNonNull(node).asMap().containsKey(labelKey)) { - LOG.error("Neo4J node does not contain '{}' attribute: {}", labelKey, node.asMap()); + if (!Objects.requireNonNull(node).asMap().containsKey(LABEL_ATTRIBUTE)) { + LOG.error("Neo4J node does not contain '{}' attribute: {}", LABEL_ATTRIBUTE, node.asMap()); throw new IllegalStateException( "There seems to be a data format mismatch between Wilhelm webservice and Neo4J database. " + "Please file an issue at https://github.com/QubitPi/wilhelm-ws/issues for a fix" ); } - final String label = node.asMap().get(labelKey).toString(); + final String label = node.asMap().get(LABEL_ATTRIBUTE).toString(); final Map attributes = node.asMap().entrySet().stream() - .filter(entry -> !labelKey.equals(entry.getKey())) + .filter(entry -> !LABEL_ATTRIBUTE.equals(entry.getKey())) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); return new Node(node.elementId(), label, attributes); diff --git a/src/main/java/org/qubitpi/wilhelm/web/endpoints/Neo4JServlet.java b/src/main/java/org/qubitpi/wilhelm/web/endpoints/Neo4JServlet.java index cb7a7f4..7308cf7 100644 --- a/src/main/java/org/qubitpi/wilhelm/web/endpoints/Neo4JServlet.java +++ b/src/main/java/org/qubitpi/wilhelm/web/endpoints/Neo4JServlet.java @@ -126,9 +126,12 @@ public Response getVocabularyByLanguagePaged( final String query = String.format( "MATCH (t:Term WHERE t.language = '%s')-[r]->(d:Definition) " + - "RETURN t.name AS term, d.name AS definition " + + "RETURN t.%s AS term, d.%s AS definition " + "SKIP %s LIMIT %s", - requestedLanguage.getDatabaseName(), (Integer.parseInt(page) - 1) * Integer.parseInt(perPage), perPage + requestedLanguage.getDatabaseName(), + Node.LABEL_ATTRIBUTE, + Node.LABEL_ATTRIBUTE, + (Integer.parseInt(page) - 1) * Integer.parseInt(perPage), perPage ); return Response @@ -142,14 +145,17 @@ public Response getVocabularyByLanguagePaged( * * @param keyword The provided keyword * - * @return all nodes whose "name" attribute contains the search keyword + * @return all nodes whose {@link Node#LABEL_ATTRIBUTE "label"} attribute contains the search keyword */ @GET @Path("/search/{keyword}") @Produces(MediaType.APPLICATION_JSON) @SuppressWarnings("MultipleStringLiterals") public Response search(@NotNull @PathParam("keyword") final String keyword) { - final String query = String.format("MATCH (node) WHERE node.name =~ '.*%s.*' RETURN node", keyword); + final String query = String.format( + "MATCH (node) WHERE node.%s =~ '.*%s.*' RETURN node", + Node.LABEL_ATTRIBUTE, keyword + ); return Response .status(Response.Status.OK) @@ -313,12 +319,13 @@ public Response expandApoc( final String query = String.format( """ - MATCH (node{name:'%s'}) + MATCH (node{%s:'%s'}) CALL apoc.path.expand(node, "LINK", null, 1, %s) YIELD path RETURN path, length(path) AS hops ORDER BY hops; """, + Node.LABEL_ATTRIBUTE, word.replace("'", "\\'"), maxHops ); diff --git a/src/test/groovy/org/qubitpi/wilhelm/LinkSpec.groovy b/src/test/groovy/org/qubitpi/wilhelm/LinkSpec.groovy index 0b85ed8..945bfa0 100644 --- a/src/test/groovy/org/qubitpi/wilhelm/LinkSpec.groovy +++ b/src/test/groovy/org/qubitpi/wilhelm/LinkSpec.groovy @@ -47,7 +47,7 @@ class LinkSpec extends Specification { _ | "attributes" } - def "when a Neo4J relationship does not contain 'name' property, an error is thrown"() { + def "when a Neo4J relationship does not contain 'label' property, an error is thrown"() { when: "a Neo4J node has no properties" Link.valueOf(Mock(Relationship) {asMap() >> [:]}) @@ -62,7 +62,7 @@ class LinkSpec extends Specification { when: "a happy path Neo4J relationship is being converted to a transparent link" Link actual = Link.valueOf(Mock(Relationship) { asMap() >> [ - name: "my node", + label: "my node", type: "follows" ] startNodeElementId() >> "node1" diff --git a/src/test/groovy/org/qubitpi/wilhelm/NodeSpec.groovy b/src/test/groovy/org/qubitpi/wilhelm/NodeSpec.groovy index 9a1d3d2..e73e898 100644 --- a/src/test/groovy/org/qubitpi/wilhelm/NodeSpec.groovy +++ b/src/test/groovy/org/qubitpi/wilhelm/NodeSpec.groovy @@ -44,7 +44,7 @@ class NodeSpec extends Specification { _ | "attributes" } - def "when a Neo4J node does not contain 'name' property, an error is thrown"() { + def "when a Neo4J node does not contain 'label' property, an error is thrown"() { when: "a Neo4J node has no properties" Node.valueOf(Mock(org.neo4j.driver.types.Node) {asMap() >> [:]}) @@ -59,7 +59,7 @@ class NodeSpec extends Specification { when: "a happy path Neo4J node is being converted to a transparent node" Node actual = Node.valueOf(Mock(org.neo4j.driver.types.Node) { asMap() >> [ - name: "my node", + label: "my node", color: "blue", size: "medium" ] diff --git a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletITSpec.groovy b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletITSpec.groovy index 7a94b98..4b40387 100644 --- a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletITSpec.groovy +++ b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletITSpec.groovy @@ -140,7 +140,7 @@ class Neo4JServletITSpec extends Specification { .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .when() - .get("/neo4j/expand/nämlich") + .get("/neo4j/expand/dreißig") .then() .statusCode(200) .body("", hasKey("nodes")) diff --git a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletSpec.groovy b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletSpec.groovy index 3dbb781..d6a48bf 100644 --- a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletSpec.groovy +++ b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/Neo4JServletSpec.groovy @@ -32,8 +32,8 @@ class Neo4JServletSpec extends Specification { keys() >> ["term", "definition"] get("term") >> Mock(Value) { type() >> InternalTypeSystem.TYPE_SYSTEM.NODE() - keys() >> ["name", "language"] - get("name") >> Mock(Value) { + keys() >> ["label", "language"] + get("label") >> Mock(Value) { type() >> InternalTypeSystem.TYPE_SYSTEM.STRING() asString() >> "Hallo" } @@ -44,8 +44,8 @@ class Neo4JServletSpec extends Specification { } get("definition") >> Mock(Value) { type() >> InternalTypeSystem.TYPE_SYSTEM.NODE() - keys() >> ["name"] - get("name") >> Mock(Value) { + keys() >> ["label"] + get("label") >> Mock(Value) { type() >> InternalTypeSystem.TYPE_SYSTEM.STRING() asString() >> "Hello" } @@ -55,11 +55,11 @@ class Neo4JServletSpec extends Specification { expect: Neo4JServlet.expand(value) == [ term: [ - name: "Hallo", + label: "Hallo", language: "German" ], definition: [ - name: "Hello" + label: "Hello" ] ] }