Skip to content

Commit

Permalink
Implement path expand endpoint (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi authored Oct 3, 2024
1 parent a646f01 commit cdc428a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 27 deletions.
1 change: 0 additions & 1 deletion checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@
<module name="JavadocMethod">
<property name="allowedAnnotations" value="Override"/>
<property name="validateThrows" value="true"/>
<property name="scope" value="anoninner"/>
<property name="allowMissingParamTags" value="false"/>
<property name="allowMissingReturnTag" value="false"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,13 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
<version>3.4.0</version>
<dependencies>
<!-- override default checkstyle version -->
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.30</version>
<version>10.18.1</version>
</dependency>
</dependencies>
<configuration>
Expand Down
108 changes: 87 additions & 21 deletions src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
)
)
Expand All @@ -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<String, List<Map<String, Object>>> 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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand All @@ -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"))
}
}

0 comments on commit cdc428a

Please sign in to comment.