Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 2 types of expand - apoc & plain DFS #7

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading