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

fix(RDF API): Incorrect internal IRIs #4383

Merged
merged 16 commits into from
Oct 28, 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import io.javalin.Javalin;
import io.javalin.http.Context;
import java.io.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -15,6 +16,7 @@
import org.molgenis.emx2.Schema;
import org.molgenis.emx2.Table;
import org.molgenis.emx2.rdf.RDFService;
import org.molgenis.emx2.utils.URLUtils;

public class RDFApi {
public static final String FORMAT = "format";
Expand Down Expand Up @@ -58,12 +60,11 @@ private static void defineApiRoutes(Javalin app, String apiLocation, RDFFormat f
app.head("{schema}" + apiLocation + "/{table}column/{column}/", (ctx) -> rdfHead(ctx, format));
}

private static String rdfHead(Context ctx, RDFFormat format) {
private static void rdfHead(Context ctx, RDFFormat format) {
ctx.contentType(selectFormat(ctx, format).getDefaultMIMEType());
return "";
}

private static int rdfForDatabase(Context ctx, RDFFormat format) throws IOException {
private static void rdfForDatabase(Context ctx, RDFFormat format) throws IOException {
format = selectFormat(ctx, format); // defines format if null

Database db = sessionManager.getSession(ctx.req()).getDatabase();
Expand All @@ -84,7 +85,7 @@ private static int rdfForDatabase(Context ctx, RDFFormat format) throws IOExcept
String[] schemaNamesArr = schemaNames.toArray(new String[schemaNames.size()]);
Schema[] schemas = new Schema[schemaNames.size()];

final String baseURL = extractBaseURL(ctx);
final String baseURL = URLUtils.extractBaseURL(ctx);

final RDFService rdf = new RDFService(ctx.url().split("/api/")[0], baseURL, format);
ctx.contentType(rdf.getMimeType());
Expand All @@ -99,17 +100,16 @@ private static int rdfForDatabase(Context ctx, RDFFormat format) throws IOExcept

outputStream.flush();
outputStream.close();
return 200;
}

private static int rdfForSchema(Context ctx, RDFFormat format) throws IOException {
private static void rdfForSchema(Context ctx, RDFFormat format) throws IOException {
format = selectFormat(ctx, format); // defines format if null

Schema schema = getSchema(ctx);
if (schema == null) {
throw new MolgenisException("Schema " + ctx.pathParam("schema") + " was not found");
}
final String baseURL = extractBaseURL(ctx);
final String baseURL = URLUtils.extractBaseURL(ctx);

RDFService rdf = new RDFService(baseURL, RDF_API_LOCATION, format);
ctx.contentType(rdf.getMimeType());
Expand All @@ -118,18 +118,17 @@ private static int rdfForSchema(Context ctx, RDFFormat format) throws IOExceptio
rdf.describeAsRDF(outputStream, null, null, null, schema);
outputStream.flush();
outputStream.close();
return 200;
}

private static int rdfForTable(Context ctx, RDFFormat format) throws IOException {
private static void rdfForTable(Context ctx, RDFFormat format) throws IOException {
format = selectFormat(ctx, format); // defines format if null

Table table = getTableByIdOrName(ctx);
String rowId = null;
if (ctx.queryString() != null && !ctx.queryString().isBlank()) {
rowId = ctx.queryString();
}
final String baseURL = extractBaseURL(ctx);
final String baseURL = URLUtils.extractBaseURL(ctx);

RDFService rdf = new RDFService(baseURL, RDF_API_LOCATION, format);
ctx.contentType(rdf.getMimeType());
Expand All @@ -138,32 +137,30 @@ private static int rdfForTable(Context ctx, RDFFormat format) throws IOException
rdf.describeAsRDF(outputStream, table, rowId, null, table.getSchema());
outputStream.flush();
outputStream.close();
return 200;
}

private static int rdfForRow(Context ctx, RDFFormat format) throws IOException {
private static void rdfForRow(Context ctx, RDFFormat format) throws IOException {
format = selectFormat(ctx, format); // defines format if null
Table table = getTableByIdOrName(ctx);
String rowId = sanitize(ctx.pathParam("row"));

final String baseURL = extractBaseURL(ctx);
final String baseURL = URLUtils.extractBaseURL(ctx);
RDFService rdf = new RDFService(baseURL, RDF_API_LOCATION, format);
ctx.contentType(rdf.getMimeType());

OutputStream outputStream = ctx.outputStream();
rdf.describeAsRDF(outputStream, table, rowId, null, table.getSchema());
outputStream.flush();
outputStream.close();
return 200;
}

private static int rdfForColumn(Context ctx, RDFFormat format) throws IOException {
private static void rdfForColumn(Context ctx, RDFFormat format) throws IOException {
format = selectFormat(ctx, format); // defines format if null

Table table = getTableByIdOrName(ctx);
String columnName = sanitize(ctx.pathParam("column"));

final String baseURL = extractBaseURL(ctx);
final String baseURL = URLUtils.extractBaseURL(ctx);

RDFService rdf = new RDFService(baseURL, RDF_API_LOCATION, format);
ctx.contentType(rdf.getMimeType());
Expand All @@ -172,30 +169,6 @@ private static int rdfForColumn(Context ctx, RDFFormat format) throws IOExceptio
rdf.describeAsRDF(outputStream, table, null, columnName, table.getSchema());
outputStream.flush();
outputStream.close();
return 200;
}

private static String extractBaseURL(Context ctx) {
// NOTE: The request.host() already includes the server port!
String scheme = ctx.scheme();
String port = null;
var parts = ctx.host().split(":", 2);
String host = parts[0];
if (parts.length == 2) {
if (!isWellKnownPort(scheme, parts[1])) {
port = parts[1];
}
}
return scheme
+ "://"
+ host
+ (port != null ? ":" + port : "")
+ (!ctx.path().isEmpty() ? "/" + ctx.path() + "/" : "/");
}

private static boolean isWellKnownPort(String scheme, String port) {
return (scheme.equals("http") && port.equals("80"))
|| (scheme.equals("https") && port.equals("443"));
}

private static RDFFormat selectFormat(Context ctx, RDFFormat format) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ public void testMolgenisWebservice_robotsDotTxt() {
}

@Test
public void testRdfApi() {
public void testRdfApiRequest() {
final String urlPrefix = "http://localhost:" + PORT;

final String defaultContentType = "text/turtle";
Expand Down Expand Up @@ -865,16 +865,40 @@ public void testRdfApi() {
.head(urlPrefix + "/pet store/api/jsonld");
rdfApiContentTypeRequest(200, jsonldContentType, ttlContentType)
.head(urlPrefix + "/pet store/api/ttl");
}

// Validate actual output.
String result =
@Test
void testRdfApiContent() {
// Output from global API call.
String resultBase =
given()
.sessionId(SESSION_ID)
.when()
.get("http://localhost:" + PORT + "/api/rdf?schemas=pet store")
.getBody()
.asString();
assertFalse(result.contains("CatalogueOntologies"));

// Output schema API call.
String resultSchema =
given()
.sessionId(SESSION_ID)
.when()
.get("http://localhost:" + PORT + "/pet store/api/rdf")
.getBody()
.asString();

assertAll(
// Validate base API.
() -> assertFalse(resultBase.contains("CatalogueOntologies")),
() ->
assertTrue(
resultBase.contains(
"http://localhost:" + PORT + "/pet%20store/api/rdf/Category/column/name")),
// Validate schema API.
() ->
assertTrue(
resultSchema.contains(
"http://localhost:" + PORT + "/pet%20store/api/rdf/Category/column/name")));
}

/**
Expand Down
1 change: 1 addition & 0 deletions backend/molgenis-emx2/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dependencies {
implementation 'org.graalvm.js:js:23.0.0'
implementation 'org.graalvm.js:js-scriptengine:23.0.0'
testImplementation 'io.javalin:javalin-testtools:6.2.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.molgenis.emx2.utils;

import io.javalin.http.Context;
import java.util.Objects;

public class URLUtils {
public static String extractBaseURL(Context ctx) {
String host = Objects.requireNonNull(ctx.host());
String[] hostSplit = host.split(":", 2);
if (hostSplit.length == 2 && isDefaultPort(ctx.scheme(), hostSplit[1])) {
host = hostSplit[0];
}

return ctx.scheme()
+ "://"
+ host
// ctx.contextPath() should start with "/"
+ (!ctx.contextPath().isEmpty() ? ctx.contextPath() + "/" : "/");
}

public static boolean isDefaultPort(String scheme, String port) {
return (scheme.equals("http") && port.equals("80"))
|| (scheme.equals("https") && port.equals("443"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.molgenis.emx2.utils;

import static org.junit.jupiter.api.Assertions.*;

import io.javalin.Javalin;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.testtools.JavalinTest;
import jakarta.servlet.ServletOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.Test;

/**
* Original test used `app.start(port)` instead to actually test when a port is set if the behaviour
* was correct or not, but simplified the tests as Azure failed to build due to no permission to use
* port 80. Current tests only validate by setting `config.contextResolver.host`.
*/
class URLUtilsTest {
private void runTestConfig(Handler handler, String expected, String host, String contextPath) {
Javalin app =
Javalin.create(
config -> {
config.router.ignoreTrailingSlashes = true;
config.router.treatMultipleSlashesAsSingleSlash = true;
if (host != null) config.contextResolver.host = ctx -> host;
if (contextPath != null) config.router.contextPath = contextPath;
});
app.get("/test", handler);

JavalinTest.test(
app,
((server, client) -> {
if (contextPath != null) {
assertEquals(expected, client.get(contextPath + "/test").body().string());
} else {
assertEquals(expected, client.get("/test").body().string());
}
}));
}

private void contextWrapper(Context ctx, byte[] bytes) {
ServletOutputStream outputStream = ctx.outputStream();
try {
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Test
void testBaseUrlMolgenis() {
runTestConfig(
(ctx) -> contextWrapper(ctx, URLUtils.extractBaseURL(ctx).getBytes()),
"http://molgenis.org/",
"molgenis.org",
null);
}

@Test
void testBaseUrlMolgenis80() {
runTestConfig(
(ctx) -> contextWrapper(ctx, URLUtils.extractBaseURL(ctx).getBytes()),
"http://molgenis.org/",
"molgenis.org:80",
null);
}

@Test
void testBaseUrlMolgenis8080() {
runTestConfig(
(ctx) -> contextWrapper(ctx, URLUtils.extractBaseURL(ctx).getBytes()),
"http://molgenis.org:8080/",
"molgenis.org:8080",
null);
}

/** Most complex scenario. */
@Test
void testBaseUrlMolgenisSubdir8080() {
runTestConfig(
(ctx) -> contextWrapper(ctx, URLUtils.extractBaseURL(ctx).getBytes()),
"http://molgenis.org:8080/subdir/",
"molgenis.org:8080",
"/subdir");
}

@Test
void testContextPathTrailingSlash() {
runTestConfig(
(ctx) -> contextWrapper(ctx, URLUtils.extractBaseURL(ctx).getBytes()),
"http://molgenis.org:8080/subdir/",
"molgenis.org:8080",
"/subdir/");
}

@Test
void testDefaultPort() {
assertAll(
() -> assertTrue(URLUtils.isDefaultPort("http", "80")),
() -> assertFalse(URLUtils.isDefaultPort("http", "8080")),
() -> assertTrue(URLUtils.isDefaultPort("https", "443")),
() -> assertFalse(URLUtils.isDefaultPort("https", "80")));
}
}