Skip to content

Commit

Permalink
fix(RDF API): Incorrect internal IRIs (#4383)
Browse files Browse the repository at this point in the history
* Possible fix for RDF API using incorrect base URL

* Revert "Possible fix for RDF API using incorrect base URL"

This reverts commit 77c5c38.

* Removed unused returns

* fixed variable naming

* Updated tests to validate on bug

* fix for incorrect internal IRI + some refactoring

* Removed unneeded code (test on PR server shows no port in IRI while localhost did)

* Revert "fixed variable naming"

This reverts commit de0650c.

* Reverted wildcard imports done by IDE

* Added test (+ some refactoring to add tests properly)

* formatting

* auto-formatting

* Tests do not use actual ports but only override value due to failing Azure (no permission to use port 80)

* Some code adjustments for more coverage
  • Loading branch information
svandenhoek authored Oct 28, 2024
1 parent fddd2ad commit 90565b5
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 45 deletions.
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")));
}
}

0 comments on commit 90565b5

Please sign in to comment.