Skip to content

Commit

Permalink
Support 2 types of expand - apoc & plain DFS (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
QubitPi authored Nov 6, 2024
1 parent d8a41b8 commit a1e001c
Show file tree
Hide file tree
Showing 15 changed files with 924 additions and 97 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
target/
logback/
.DS_Store
jetty-base/
jetty-home-11.0.15/
application.properties
jetty-home-11.0.15.tar.gz
1 change: 1 addition & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ file:///*
https://querydsl.com/*
https://uel.java.net/
https://www.openapis.org/
https://api.paion-data.dev/*
50 changes: 32 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ Aristotle
=========

![Java Version Badge][Java Version Badge]
![HashiCorp Packer Badge][HashiCorp Packer Badge]
![HashiCorp Terraform Badge][HashiCorp Terraform Badge]
[![Apache License Badge]][Apache License, Version 2.0]

Aristotle is a [JSR 370] [JAX-RS] webservice of CRUD operations against a graph database. It supports Neo4J now.

Configuration
-------------
Start Locally in Jetty
----------------------

- `NEO4J_URI`
- `NEO4J_USERNAME`
- `NEO4J_PASSWORD`
- `NEO4J_DATABASE`
Navigate to a dedicated directory; make sure port 8080 is not occupied and the following environment variables are set:

```console
export NEO4J_URI=
export NEO4J_USERNAME=
export NEO4J_PASSWORD=
export NEO4J_DATABASE=
```

Then start webservice with:

```bash
./jetty-start.sh
```

Press `Ctr-C` to stop the webservice and delete generated directories if needed when done.

Test
----
Expand All @@ -26,25 +36,31 @@ mvn clean verify
Deployment
----------

```bash
mvn clean package
```
This is a one-person project. Agility outplays team scaling, so deployment is manual and pretty much follows
[jetty-start.sh](./jetty-start.sh)

### Sending Logs to ELK Cloud

Simply add Logstash integration and install agent on the production server. The logs will be available on integration
dashboard.

### Gateway Registration

```bash
export GATEWAY_PUBLIC_IP=52.53.186.26

# vocabulary paged & count
curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
--data name=wilhelm-ws-languages \
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/data/languages"
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/neo4j/languages"
curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-languages/routes \
--data "paths[]=/wilhelm/languages" \
--data name=wilhelm-ws-languages

# expand
curl -v -i -s -k -X POST https://api.paion-data.dev:8444/services \
--data name=wilhelm-ws-expand \
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/data/expand"
--data url="http://${GATEWAY_PUBLIC_IP}:8080/v1/neo4j/expand"
curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-expand/routes \
--data "paths[]=/wilhelm/expand" \
--data name=wilhelm-ws-expand
Expand All @@ -54,8 +70,9 @@ We should see `HTTP/1.1 201 Created` as signs of success.

#### Example requests:

- https://api.paion-data.dev/wilhelm/languages/german?perPage=100&page=1
- https://api.paion-data.dev/wilhelm/expand/nämlich
- 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

License
-------
Expand All @@ -65,9 +82,6 @@ The use and distribution terms for [Aristotle]() are covered by the [Apache Lice
[Apache License Badge]: https://img.shields.io/badge/Apache%202.0-F25910.svg?style=for-the-badge&logo=Apache&logoColor=white
[Apache License, Version 2.0]: https://www.apache.org/licenses/LICENSE-2.0

[HashiCorp Packer Badge]: https://img.shields.io/badge/Packer-02A8EF?style=for-the-badge&logo=Packer&logoColor=white
[HashiCorp Terraform Badge]: https://img.shields.io/badge/Terraform-7B42BC?style=for-the-badge&logo=terraform&logoColor=white

[Java Version Badge]: https://img.shields.io/badge/Java-17-brightgreen?style=for-the-badge&logo=OpenJDK&logoColor=white
[JAX-RS]: https://jcp.org/en/jsr/detail?id=370
[JSR 370]: https://jcp.org/en/jsr/detail?id=370
30 changes: 30 additions & 0 deletions jetty-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -x
set -e

# Copyright Jiaqi Liu
#
# 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.

mvn clean package -Dcheckstyle.skip -DskipTests

wget -O jetty-home-11.0.15.tar.gz https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/11.0.15/jetty-home-11.0.15.tar.gz
tar -xzvf jetty-home-11.0.15.tar.gz
export JETTY_HOME=$(pwd)/jetty-home-11.0.15

mkdir -p jetty-base
cd jetty-base
java -jar $JETTY_HOME/start.jar --add-module=annotations,server,http,deploy,servlet,webapp,resources,jsp

mv ../target/wilhelm-ws-1.0-SNAPSHOT.war webapps/ROOT.war
java -jar $JETTY_HOME/start.jar
155 changes: 155 additions & 0 deletions src/main/java/org/qubitpi/wilhelm/Graph.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright Jiaqi Liu
*
* 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 org.qubitpi.wilhelm;

import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.validation.constraints.NotNull;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.ThreadSafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A JSON-serializable object representation of a knowledge graph in wilhelm-ws.
*/
@Immutable
@ThreadSafe
@SuppressWarnings("ClassCanBeRecord")
@JsonIncludeProperties({ "nodes", "links" })
public class Graph {

private static final Logger LOG = LoggerFactory.getLogger(Graph.class);
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

private final Set<Node> nodes;
private final Set<Link> links;

/**
* All-args constructor.
*
* @param nodes The set of all nodes contained in this Graph, cannot be {@code null}
* @param links The set of all links contained in this Graph, cannot be {@code null}
*/
public Graph(@NotNull final Set<Node> nodes, @NotNull final Set<Link> links) {
this.nodes = new HashSet<>(Objects.requireNonNull(nodes));
this.links = new HashSet<>(Objects.requireNonNull(links));
}

/**
* Creates a new {@link Graph} instance with no initial nodes or links in it.
*
* @return a new instance
*/
public static Graph emptyGraph() {
return new Graph(new HashSet<>(), new HashSet<>());
}

/**
* Returns whether or not this {@link Graph} has neither nodes noe links.
*
* @return {@code true} if no nodes or links exist in this {@link Graph}, or {@code false} otherwise.
*/
public boolean isEmpty() {
return getNodes().isEmpty() && getLinks().isEmpty();
}

/**
* Returns all weakly connected neighbors of a specified node.
* <p>
* If the node has no such neighrbors, this method returns an empty list
*
* @param node a node from this {@link Graph}
*
* @return all nodes each of which has a link between it and the provided node.
*/
@NotNull
public Set<Node> getUndirectedNeighborsOf(final Node node) {
final Set<String> neighborIds = getLinks().stream()
.filter(link ->
node.getId().equals(link.getSourceNodeId()) || node.getId().equals(link.getTargetNodeId())
)
.flatMap(link -> Stream.of(link.getSourceNodeId(), link.getTargetNodeId()))
.filter(id -> !node.getId().equals(id))
.collect(Collectors.toUnmodifiableSet());

return getNodes().stream()
.filter(it -> neighborIds.contains(it.getId()))
.collect(Collectors.toUnmodifiableSet());
}

/**
* Combines the nodes and links from this {@link Graph} instance and the other one and returns a new {@link Graph}.
*
* @param that the other {@link Graph} instance to be merged with this {@link Graph}
*
* @return a new instance
*/
public Graph merge(@NotNull final Graph that) {
return new Graph(
Stream.of(this.getNodes(), that.getNodes()).flatMap(Set::stream).collect(Collectors.toSet()),
Stream.of(this.getLinks(), that.getLinks()).flatMap(Set::stream).collect(Collectors.toSet())
);
}

/**
* Returns an unmodifiable view of all the nodes in this Graph instance.
*
* @return an immutable list of nodes
*/
@NotNull
public Set<Node> getNodes() {
return Collections.unmodifiableSet(nodes);
}

/**
* Returns an unmodifiable view of all the links in this Graph instance.
*
* @return an immutable list of links
*/
@NotNull
public Set<Link> getLinks() {
return Collections.unmodifiableSet(links);
}

/**
* Returns a JSON serialization of this Graph instance. It contains 2 fields: nodes and links, each of which is a
* list of nodes and links respectively. Each list element is itself a JSON object whose structure are defined by
* Jackson's serialization on {@link Node} and {@link Link}.
*
* @return a JSON string
*/
@NotNull
@Override
public String toString() {
try {
return JSON_MAPPER.writeValueAsString(this);
} catch (final JsonProcessingException exception) {
LOG.error(exception.getMessage());
throw new IllegalStateException(exception);
}
}
}
52 changes: 47 additions & 5 deletions src/main/java/org/qubitpi/wilhelm/Language.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import net.jcip.annotations.ThreadSafe;

import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -59,25 +60,61 @@ public enum Language {
}

/**
* Constructs a {@link Language} from its client-side name.
* Constructs a {@link Language} from its {@link #getDatabaseName() database name}.
*
* @param language The client-side requested language name
*
* @return a new instance
*
* @throws IllegalArgumentException if the language name is not a valid one
*/
@NotNull
public static Language ofDatabaseName(@NotNull final String language) {
return valueOf(language, Language::getDatabaseName);
}

/**
* Constructs a {@link Language} from its {@link #getPathName() client-side name}.
*
* @param language The client-side requested language name
*
* @return a new instance
*
* @throws IllegalArgumentException if the language name is not a valid one
*/
@NotNull
public static Language ofClientValue(@NotNull final String language) throws IllegalArgumentException {
return valueOf(language, Language::getPathName);
}

/**
* Converts a string value to a {@link Language} object.
* <p>
* The string value must match one of the {@link #getDatabaseName() database name} or
* {@link #getPathName() client API name}.
*
* @param language A string whose value is equal to either {@link #getDatabaseName() database name} or
* {@link #getPathName() client API name}
* @param nameExtractor If {@code language} matches {@link #getDatabaseName()}, use {@link #getDatabaseName()};
* otherwise use {@link #getPathName()}
*
* @return a new instance
*
* @throws IllegalArgumentException if the language name is not a valid one
*/
private static Language valueOf(@NotNull final String language, final Function<Language, String> nameExtractor) {
return Arrays.stream(values())
.filter(value -> value.pathName.equals(language))
.filter(value -> nameExtractor.apply(value).equals(language))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
String.format(
"'%s' is not a recognized language. Acceptable ones are %s",
language,
Arrays.stream(values()).map(Language::getPathName).collect(Collectors.joining(", ")
)
)));
Arrays.stream(values())
.map(nameExtractor)
.collect(Collectors.joining(", "))
)
));
}

@NotNull
Expand All @@ -89,4 +126,9 @@ public String getPathName() {
public String getDatabaseName() {
return databaseName;
}

@Override
public String toString() {
return getDatabaseName();
}
}
Loading

0 comments on commit a1e001c

Please sign in to comment.