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 count endpoint #6

Merged
merged 1 commit into from
Oct 9, 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ curl -i -k -X POST https://api.paion-data.dev:8444/services/wilhelm-ws-expand/ro
--data name=wilhelm-ws-expand
```

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
Expand Down
92 changes: 92 additions & 0 deletions src/main/java/org/qubitpi/wilhelm/Language.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 jakarta.validation.constraints.NotNull;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.ThreadSafe;

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

/**
* A natural language represented in webservice that bridges the client request data format to database request format.
*/
@Immutable
@ThreadSafe
public enum Language {

/**
* German language.
*/
GERMAN("german", "German"),

/**
* Ancient Greek.
*/
ANCIENT_GREEK("ancientGreek", "Ancient Greek"),

/**
* Latin.
*/
LATIN("latin", "Latin");

private final String pathName;
private final String databaseName;

/**
* All-args constructor.
*
* @param pathName The client-side language name
* @param databaseName The database language name
*/
Language(@NotNull final String pathName, @NotNull final String databaseName) {
this.pathName = pathName;
this.databaseName = databaseName;
}

/**
* Constructs a {@link Language} from its 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
*/
public static Language ofClientValue(@NotNull final String language) throws IllegalArgumentException {
return Arrays.stream(values())
.filter(value -> value.pathName.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(", ")
)
)));
}

@NotNull
public String getPathName() {
return pathName;
}

@NotNull
public String getDatabaseName() {
return databaseName;
}
}
36 changes: 36 additions & 0 deletions src/main/java/org/qubitpi/wilhelm/LanguageCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 jakarta.ws.rs.NameBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* An annotation used exclusively by {@link org.qubitpi.wilhelm.web.filters.LanguageCheckFilter}.
*
* @see org.qubitpi.wilhelm.web.filters.LanguageCheckFilter
*/
@NameBinding
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LanguageCheck {

// intentionally left blank
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.qubitpi.wilhelm.web.filters.CorsFilter;

import org.glassfish.hk2.utilities.Binder;
import org.qubitpi.wilhelm.web.filters.LanguageCheckFilter;

import jakarta.inject.Inject;
import jakarta.ws.rs.ApplicationPath;
Expand All @@ -42,6 +43,7 @@ public ResourceConfig() {
packages(ENDPOINT_PACKAGE);

register(CorsFilter.class);
register(LanguageCheckFilter.class);

final Binder binder = new BinderFactory().buildBinder();
register(binder);
Expand Down
91 changes: 55 additions & 36 deletions src/main/java/org/qubitpi/wilhelm/web/endpoints/DataServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.neo4j.driver.QueryConfig;
import org.neo4j.driver.Value;
import org.neo4j.driver.internal.types.InternalTypeSystem;
import org.qubitpi.wilhelm.Language;
import org.qubitpi.wilhelm.LanguageCheck;
import org.qubitpi.wilhelm.config.ApplicationConfig;

import jakarta.inject.Inject;
Expand All @@ -43,7 +45,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
Expand All @@ -64,13 +65,6 @@ public class DataServlet {
private static final String NEO4J_PASSWORD = APPLICATION_CONFIG.neo4jPassword();
private static final String NEO4J_DATABASE = APPLICATION_CONFIG.neo4jDatabase();

private static final Map<String, String> LANGUAGES = Stream.of(
new AbstractMap.SimpleImmutableEntry<>("german", "German"),
new AbstractMap.SimpleImmutableEntry<>("ancientGreek", "Ancient Greek"),
new AbstractMap.SimpleImmutableEntry<>("latin", "Latin")
)
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));


/**
* Constructor for dependency injection.
Expand All @@ -93,59 +87,62 @@ public Response healthcheck() {
.build();
}

/**
* Returns the total number of terms of a specified langauges.
*
* @param language The language. Must be one of the valid return value of {@link Language#getPathName()}; otherwise
* a 400 response is returned
*
* @return a list of one map entry, whose key is 'count' and value is the total
*/
@GET
@LanguageCheck
@Path("/languages/{language}/count")
@Produces(MediaType.APPLICATION_JSON)
public Response getCountByLanguage(@NotNull @PathParam("language") final String language) {
final Language requestedLanguage = Language.ofClientValue(language);

final String query = String.format(
"MATCH (term:Term {language: '%s'}) RETURN count(*) as count", requestedLanguage.getDatabaseName()
);

return Response
.status(Response.Status.OK)
.entity(executeNonPathQuery(query))
.build();
}

/**
* Get paginated vocabularies of a language.
*
* @param language The language. Must be one of "german", "ancientGreek", or "latin". Otherwise a 404 response is
* returned
* @param language The language. Must be one of the valid return value of {@link Language#getPathName()}; otherwise
* a 400 response is returned
* @param perPage Requested number of words to be displayed on each page of results
* @param page Requested page of results desired
*
* @return the paginated Neo4J query results in JSON format
*/
@GET
@LanguageCheck
@Path("/languages/{language}")
@Produces(MediaType.APPLICATION_JSON)
public Response getVocabularyByLanguagePaged(
@NotNull @PathParam("language") final String language,
@NotNull @QueryParam("perPage") final String perPage,
@NotNull @QueryParam("page") final String page
) {
if (!LANGUAGES.containsKey(Objects.requireNonNull(language, "language"))) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity(
String.format(
"'%s' is not a recognized language. Acceptable ones are %s",
language,
String.join(", ", LANGUAGES.keySet())
)
)
.build();
}
final Language requestedLanguage = Language.ofClientValue(language);

final String query = String.format(
"MATCH (t:Term WHERE t.language = '%s')-[r]->(d:Definition) " +
"RETURN t.name AS term, d.name AS definition " +
"SKIP %s LIMIT %s",
LANGUAGES.get(language), (Integer.parseInt(page) - 1) * Integer.parseInt(perPage), perPage
requestedLanguage.getDatabaseName(), (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)
.entity(executeNonPathQuery(query))
.build();
}

Expand Down Expand Up @@ -214,6 +211,28 @@ RETURN path, length(path) AS hops
.build();
}

/**
* Runs a cypher query against Neo4J database and return result as a JSON-serializable.
* <p>
* Use this method only if the {@code query} does not involve path, because this method cannot handle query result
* that has path object nested in it
*
* @param query A standard cypher query string
*
* @return query's native result
*/
private Object executeNonPathQuery(@NotNull final String query) {
return executeNativeQuery(query).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());
}

/**
* Runs a cypher query against Neo4J database and return the query result unmodified.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.web.filters;

import org.qubitpi.wilhelm.Language;
import org.qubitpi.wilhelm.LanguageCheck;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import net.jcip.annotations.Immutable;
import net.jcip.annotations.ThreadSafe;

/**
* A {@link ContainerRequestFilter} that validates the {@code language} path param in endpoint requests and only applies
* to those with "languages/{language}" in the middle of its endpoint path, for example "/languages/{language}/count".
* <p>
* Endpoints wishing to be validated by this filter must satisfy 2 requirements:
* <ol>
* <li> having a {@link jakarta.ws.rs.PathParam "language" @PathParam}
* <li> the resource/endpoint method has been annotated with {@link LanguageCheck}
* </ol>
*/
@Immutable
@ThreadSafe
@Provider
@LanguageCheck
public class LanguageCheckFilter implements ContainerRequestFilter {

@Override
public void filter(final ContainerRequestContext containerRequestContext) {
final String requestedLanguage = containerRequestContext
.getUriInfo()
.getPathParameters()
.get("language")
.get(0);
try {
Language.ofClientValue(requestedLanguage);
} catch (final IllegalArgumentException exception) {
containerRequestContext.abortWith(
Response
.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build()
);
}
}
}
Loading
Loading