Changeset View
Changeset View
Standalone View
Standalone View
java/server/src/main/java/org/softwareheritage/graph/App.java
package org.softwareheritage.graph; | package org.softwareheritage.graph; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.util.HashMap; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | import java.util.Map; | ||||
import com.fasterxml.jackson.databind.ObjectMapper; | import com.fasterxml.jackson.databind.ObjectMapper; | ||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy; | import com.fasterxml.jackson.databind.PropertyNamingStrategy; | ||||
import com.martiansoftware.jsap.FlaggedOption; | import com.martiansoftware.jsap.FlaggedOption; | ||||
import com.martiansoftware.jsap.JSAP; | import com.martiansoftware.jsap.JSAP; | ||||
import com.martiansoftware.jsap.JSAPException; | import com.martiansoftware.jsap.JSAPException; | ||||
import com.martiansoftware.jsap.JSAPResult; | import com.martiansoftware.jsap.JSAPResult; | ||||
import com.martiansoftware.jsap.Parameter; | import com.martiansoftware.jsap.Parameter; | ||||
import com.martiansoftware.jsap.SimpleJSAP; | import com.martiansoftware.jsap.SimpleJSAP; | ||||
import com.martiansoftware.jsap.Switch; | |||||
import com.martiansoftware.jsap.UnflaggedOption; | import com.martiansoftware.jsap.UnflaggedOption; | ||||
import io.javalin.Javalin; | import io.javalin.Javalin; | ||||
import io.javalin.http.Context; | import io.javalin.http.Context; | ||||
import io.javalin.plugin.json.JavalinJackson; | import io.javalin.plugin.json.JavalinJackson; | ||||
import org.softwareheritage.graph.Endpoint; | import org.softwareheritage.graph.Endpoint; | ||||
import org.softwareheritage.graph.Graph; | import org.softwareheritage.graph.Graph; | ||||
import org.softwareheritage.graph.SwhId; | import org.softwareheritage.graph.SwhId; | ||||
Show All 17 Lines | public static void main(String[] args) throws IOException, JSAPException { | ||||
SimpleJSAP jsap = new SimpleJSAP( | SimpleJSAP jsap = new SimpleJSAP( | ||||
App.class.getName(), | App.class.getName(), | ||||
"Server to load and query a compressed graph representation of Software Heritage archive.", | "Server to load and query a compressed graph representation of Software Heritage archive.", | ||||
new Parameter[] { | new Parameter[] { | ||||
new FlaggedOption("port", JSAP.INTEGER_PARSER, "5009", JSAP.NOT_REQUIRED, 'p', "port", | new FlaggedOption("port", JSAP.INTEGER_PARSER, "5009", JSAP.NOT_REQUIRED, 'p', "port", | ||||
"Binding port of the server."), | "Binding port of the server."), | ||||
new UnflaggedOption("graphPath", JSAP.STRING_PARSER, JSAP.NO_DEFAULT, JSAP.REQUIRED, | new UnflaggedOption("graphPath", JSAP.STRING_PARSER, JSAP.NO_DEFAULT, JSAP.REQUIRED, | ||||
JSAP.NOT_GREEDY, "The basename of the compressed graph."), | JSAP.NOT_GREEDY, "The basename of the compressed graph."), | ||||
new Switch("timings", 't', "timings", "Show timings in API result metadata."), | |||||
} | } | ||||
); | ); | ||||
JSAPResult config = jsap.parse(args); | JSAPResult config = jsap.parse(args); | ||||
if (jsap.messagePrinted()) { | if (jsap.messagePrinted()) { | ||||
System.exit(1); | System.exit(1); | ||||
} | } | ||||
String graphPath = config.getString("graphPath"); | String graphPath = config.getString("graphPath"); | ||||
int port = config.getInt("port"); | int port = config.getInt("port"); | ||||
boolean showTimings = config.getBoolean("timings"); | |||||
startServer(graphPath, port); | startServer(graphPath, port, showTimings); | ||||
} | } | ||||
/** | /** | ||||
* Loads compressed graph and starts the web server to query it. | * Loads compressed graph and starts the web server to query it. | ||||
* | * | ||||
* @param graphPath basename of the compressed graph | * @param graphPath basename of the compressed graph | ||||
* @param port binding port of the server | * @param port binding port of the server | ||||
* @param showTimings true if timings should be in results metadata, false otherwise | |||||
*/ | */ | ||||
private static void startServer(String graphPath, int port) throws IOException { | private static void startServer(String graphPath, int port, boolean showTimings) throws IOException { | ||||
Graph graph = new Graph(graphPath); | Graph graph = new Graph(graphPath); | ||||
Stats stats = new Stats(graphPath); | Stats stats = new Stats(graphPath); | ||||
// Clean up on exit | // Clean up on exit | ||||
Runtime.getRuntime().addShutdownHook(new Thread() { | Runtime.getRuntime().addShutdownHook(new Thread() { | ||||
public void run() { | public void run() { | ||||
try { | try { | ||||
graph.cleanUp(); | graph.cleanUp(); | ||||
Show All 22 Lines | private static void startServer(String graphPath, int port, boolean showTimings) throws IOException { | ||||
// By default the traversal is a forward DFS using all edges | // By default the traversal is a forward DFS using all edges | ||||
app.get("/leaves/:src", ctx -> { | app.get("/leaves/:src", ctx -> { | ||||
SwhId src = new SwhId(ctx.pathParam("src")); | SwhId src = new SwhId(ctx.pathParam("src")); | ||||
String direction = ctx.queryParam("direction", "forward"); | String direction = ctx.queryParam("direction", "forward"); | ||||
String edgesFmt = ctx.queryParam("edges", "*"); | String edgesFmt = ctx.queryParam("edges", "*"); | ||||
Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | ||||
ctx.json(endpoint.leaves(src)); | Endpoint.Output output = endpoint.leaves(src); | ||||
ctx.json(formatEndpointOutput(output, showTimings)); | |||||
}); | }); | ||||
app.get("/neighbors/:src", ctx -> { | app.get("/neighbors/:src", ctx -> { | ||||
SwhId src = new SwhId(ctx.pathParam("src")); | SwhId src = new SwhId(ctx.pathParam("src")); | ||||
String direction = ctx.queryParam("direction", "forward"); | String direction = ctx.queryParam("direction", "forward"); | ||||
String edgesFmt = ctx.queryParam("edges", "*"); | String edgesFmt = ctx.queryParam("edges", "*"); | ||||
Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | ||||
ctx.json(endpoint.neighbors(src)); | Endpoint.Output output = endpoint.neighbors(src); | ||||
ctx.json(formatEndpointOutput(output, showTimings)); | |||||
}); | }); | ||||
app.get("/visit/nodes/:src", ctx -> { | app.get("/visit/nodes/:src", ctx -> { | ||||
SwhId src = new SwhId(ctx.pathParam("src")); | SwhId src = new SwhId(ctx.pathParam("src")); | ||||
String direction = ctx.queryParam("direction", "forward"); | String direction = ctx.queryParam("direction", "forward"); | ||||
String edgesFmt = ctx.queryParam("edges", "*"); | String edgesFmt = ctx.queryParam("edges", "*"); | ||||
Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | ||||
ctx.json(endpoint.visitNodes(src)); | Endpoint.Output output = endpoint.visitNodes(src); | ||||
ctx.json(formatEndpointOutput(output, showTimings)); | |||||
}); | }); | ||||
app.get("/visit/paths/:src", ctx -> { | app.get("/visit/paths/:src", ctx -> { | ||||
SwhId src = new SwhId(ctx.pathParam("src")); | SwhId src = new SwhId(ctx.pathParam("src")); | ||||
String direction = ctx.queryParam("direction", "forward"); | String direction = ctx.queryParam("direction", "forward"); | ||||
String edgesFmt = ctx.queryParam("edges", "*"); | String edgesFmt = ctx.queryParam("edges", "*"); | ||||
Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | ||||
ctx.json(endpoint.visitPaths(src)); | Endpoint.Output output = endpoint.visitPaths(src); | ||||
ctx.json(formatEndpointOutput(output, showTimings)); | |||||
}); | }); | ||||
app.get("/walk/:src/:dst", ctx -> { | app.get("/walk/:src/:dst", ctx -> { | ||||
SwhId src = new SwhId(ctx.pathParam("src")); | SwhId src = new SwhId(ctx.pathParam("src")); | ||||
String dstFmt = ctx.pathParam("dst"); | String dstFmt = ctx.pathParam("dst"); | ||||
String direction = ctx.queryParam("direction", "forward"); | String direction = ctx.queryParam("direction", "forward"); | ||||
String edgesFmt = ctx.queryParam("edges", "*"); | String edgesFmt = ctx.queryParam("edges", "*"); | ||||
String algorithm = ctx.queryParam("traversal", "dfs"); | String algorithm = ctx.queryParam("traversal", "dfs"); | ||||
Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | Endpoint endpoint = new Endpoint(graph, direction, edgesFmt); | ||||
ctx.json(endpoint.walk(src, dstFmt, algorithm)); | Endpoint.Output output = endpoint.walk(src, dstFmt, algorithm); | ||||
ctx.json(formatEndpointOutput(output, showTimings)); | |||||
}); | }); | ||||
app.exception(IllegalArgumentException.class, (e, ctx) -> { | app.exception(IllegalArgumentException.class, (e, ctx) -> { | ||||
ctx.status(400); | ctx.status(400); | ||||
ctx.result(e.getMessage()); | ctx.result(e.getMessage()); | ||||
}); | }); | ||||
} | } | ||||
/** | /** | ||||
* Checks query strings names provided to the REST API. | * Checks query strings names provided to the REST API. | ||||
* | * | ||||
* @param ctx Javalin HTTP request context | * @param ctx Javalin HTTP request context | ||||
* @param allowedFmt a regular expression describing allowed query strings names | * @param allowedFmt a regular expression describing allowed query strings names | ||||
* @throws IllegalArgumentException unknown query string provided | * @throws IllegalArgumentException unknown query string provided | ||||
*/ | */ | ||||
private static void checkQueryStrings(Context ctx, String allowedFmt) { | private static void checkQueryStrings(Context ctx, String allowedFmt) { | ||||
Map<String, List<String>> queryParamMap = ctx.queryParamMap(); | Map<String, List<String>> queryParamMap = ctx.queryParamMap(); | ||||
for (String key : queryParamMap.keySet()) { | for (String key : queryParamMap.keySet()) { | ||||
if (!key.matches(allowedFmt)) { | if (!key.matches(allowedFmt)) { | ||||
throw new IllegalArgumentException("Unknown query string: " + key); | throw new IllegalArgumentException("Unknown query string: " + key); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/** | |||||
* Formats endpoint result into final JSON for the REST API. | |||||
* <p> | |||||
* Removes unwanted information if necessary, such as timings (to prevent use of side channels | |||||
* attacks). | |||||
* | |||||
* @param output endpoint operation output which needs formatting | |||||
* @param showTimings true if timings should be in results metadata, false otherwise | |||||
* @return final Object with desired JSON format | |||||
*/ | |||||
private static Object formatEndpointOutput(Endpoint.Output output, boolean showTimings) { | |||||
if (showTimings) { | |||||
return output; | |||||
} else { | |||||
Map<String, Object> outputNoTimings = new HashMap<>(); | |||||
outputNoTimings.put("result", output.result); | |||||
return outputNoTimings; | |||||
} | |||||
} | |||||
} | } |