diff --git a/checkstyle.xml b/checkstyle.xml index 758eb2a..2b46383 100755 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -140,7 +140,6 @@ - diff --git a/pom.xml b/pom.xml index cce952c..b701ac9 100644 --- a/pom.xml +++ b/pom.xml @@ -280,13 +280,13 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.2 + 3.4.0 com.puppycrawl.tools checkstyle - 8.30 + 10.18.1 diff --git a/src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java b/src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java index 2515dd9..14b5254 100644 --- a/src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java +++ b/src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java @@ -39,6 +39,9 @@ import net.jcip.annotations.ThreadSafe; import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -113,7 +116,8 @@ public Response getVocabularyByLanguagePaged( .status(Response.Status.BAD_REQUEST) .entity( String.format( - "'language' path parameter has to be one of %s", + "'%s' is not a recognized language. Acceptable ones are %s", + language, String.join(", ", LANGUAGES.keySet()) ) ) @@ -127,42 +131,104 @@ public Response getVocabularyByLanguagePaged( LANGUAGES.get(language), (Integer.parseInt(page) - 1) * Integer.parseInt(perPage), perPage ); + final EagerResult result = executeNativeQuery(query); + + final Object responseBody = result.records() + .stream() + .map( + record -> record.keys() + .stream() + .map(key -> new AbstractMap.SimpleImmutableEntry<>(key, expand(record.get(key)))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ) + .collect(Collectors.toList()); + + return Response + .status(Response.Status.OK) + .entity(responseBody) + .build(); + } + + /** + * Recursively find all related terms and definitions of a word. + * + * @param word The word to expand + * + * @return a JSON representation of the expanded sub-graph + */ + @GET + @Path("/expand/{word}") + @Produces(MediaType.APPLICATION_JSON) + @SuppressWarnings("MultipleStringLiterals") + public Response expand(@NotNull @PathParam("word") final String word) { + final String query = String.format( + """ + MATCH (term:Term{name:'%s'}) + CALL apoc.path.expand(term, "RELATED|DEFINITION", null, 1, -1) + YIELD path + RETURN path, length(path) AS hops + ORDER BY hops; + """, + word + ); + + final EagerResult result = executeNativeQuery(query); + + final Map>> responseBody = Map.of( + "nodes", new ArrayList<>(), + "links", new ArrayList<>() + ); + + result.records().stream() + .map(record -> record.get("path").asPath()) + .forEach(path -> { + path.nodes().forEach(node -> responseBody.get("nodes").add( + Stream.of( + node.asMap(), + Collections.singletonMap("id", node.elementId()) + ) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + )); + path.relationships().forEach(relationship -> responseBody.get("links").add( + Stream.of( + relationship.asMap(), + Collections.singletonMap( + "sourceNodeId", + relationship.startNodeElementId() + ), + Collections.singletonMap( + "targetNodeId", + relationship.endNodeElementId() + ) + ) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)) + )); + }); + + return Response .status(Response.Status.OK) - .entity(executeNativeQuery(query)) + .entity(responseBody) .build(); } /** - * Runs a cypher query against Neo4J database and return the query result as a JSON-serializable object. + * Runs a cypher query against Neo4J database and return the query result unmodified. * * @param query A standard cypher query string * - * @return query result + * @return query's native result */ @NotNull - private Object executeNativeQuery(@NotNull final String query) { + private EagerResult executeNativeQuery(@NotNull final String query) { try (Driver driver = GraphDatabase.driver(NEO4J_URL, AuthTokens.basic(NEO4J_USERNAME, NEO4J_PASSWORD))) { driver.verifyConnectivity(); - final EagerResult result = driver.executableQuery(query) + return driver.executableQuery(query) .withConfig(QueryConfig.builder().withDatabase(NEO4J_DATABASE).build()) .execute(); - - return result - .records() - .stream() - .map( - record -> record.keys() - .stream() - .map(key -> new AbstractMap.SimpleImmutableEntry<>( - key, - expand(record.get(key)) - )) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) - - ) - .collect(Collectors.toList()); } } diff --git a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/DataServletITSpec.groovy b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/DataServletITSpec.groovy index 65e2a6e..e6fc5c4 100644 --- a/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/DataServletITSpec.groovy +++ b/src/test/groovy/org/qubitpi/wilhelm/web/endpoints/DataServletITSpec.groovy @@ -16,6 +16,8 @@ package org.qubitpi.wilhelm.web.endpoints import static org.hamcrest.Matchers.equalTo +import static org.hamcrest.Matchers.hasKey +import static org.hamcrest.Matchers.matchesPattern import org.hamcrest.Description import org.hamcrest.Matcher @@ -49,7 +51,8 @@ class DataServletITSpec extends Specification { .withExposedPorts(7474, 7687) .withEnv([ NEO4J_AUTH: "none", - NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes" + NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes", + NEO4JLABS_PLUGINS: "[\"apoc\"]" ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*INFO Started.*")) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) @@ -88,7 +91,7 @@ class DataServletITSpec extends Specification { .statusCode(200) } - def "Get vocabulary by language and retrieve the very first word"() { + def "Get vocabulary by language returns a list of map, with each entry containing 'term' and 'definition' keys"() { expect: RestAssured .given() @@ -99,6 +102,24 @@ class DataServletITSpec extends Specification { .get("/data/languages/german") .then() .statusCode(200) - .body("[0]", equalTo([term: "dieser", definition: "this"])) + .body("[0]", hasKey("term")) + .body("[0]", hasKey("definition")) + } + + def "Expand a word returns a map of two keys - 'nodes' & 'links'"() { + expect: + RestAssured + .given() + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .when() + .get("/data/expand/nämlich") + .then() + .statusCode(200) + .body("", hasKey("nodes")) + .body("", hasKey("links")) + .body("nodes[0]", hasKey("id")) + .body("links[0]", hasKey("sourceNodeId")) + .body("links[0]", hasKey("targetNodeId")) } }