Refactored tools services endpoints system and fixed json formatter. (#91)

Co-authored-by: Artur Kołecki <koleckiartur@icloud.com>
Co-authored-by: Adam Bem <adam.bem@zoho.eu>
Reviewed-on: R11/release11-tools-web#91
This commit is contained in:
2023-03-02 11:49:21 +01:00
parent a90cbb938f
commit b0b930926c
34 changed files with 1000 additions and 444 deletions

View File

@@ -96,23 +96,23 @@ def process_xml(request: request, type: str) -> str:
return json.dumps(response_json), code
@app.route("/xpathpost", methods=["POST"])
@app.route("/xpath", methods=["POST"])
def xpath():
return process_xml(request, "xpath")
@app.route("/xsdpost", methods=["POST"])
@app.route("/xsd", methods=["POST"])
def xsd():
return process_xml(request, "xsd")
@app.route("/xsltpost", methods=["POST"])
@app.route("/xslt", methods=["POST"])
def xslt():
return process_xml(request, "xslt")
@app.route("/prettifypost", methods=["POST"])
@app.route("/prettify", methods=["POST"])
def prettify():
return process_xml(request, "prettify")
@app.route("/minimizepost", methods=["POST"])
@app.route("/minimize", methods=["POST"])
def minimize():
return process_xml(request, "minimize")

View File

@@ -19,7 +19,7 @@
<packaging>pom</packaging>
<modules>
<module>xslt-rest</module>
<module>tools-services</module>
<module>mocked-services</module>
</modules>
</project>

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.r11.tools</groupId>
<artifactId>xslt-rest</artifactId>
<artifactId>tools-services</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
@@ -15,24 +15,11 @@
<jackson.version>2.14.1</jackson.version>
<slf4j.version>2.0.6</slf4j.version>
<log4j.version>2.19.0</log4j.version>
<gson.version>2.10.1</gson.version>
</properties>
<build>
<plugins>
<!-- <plugin>-->
<!-- <artifactId>maven-jar-plugin</artifactId>-->
<!-- <version>3.1.0</version>-->
<!-- <configuration>-->
<!-- <archive>-->
<!-- <manifest>-->
<!-- <addClasspath>true</addClasspath>-->
<!-- <classpathPrefix>lib/</classpathPrefix>-->
<!-- <mainClass>com.r11.tools.xslt.Main</mainClass>-->
<!-- </manifest>-->
<!-- </archive>-->
<!-- </configuration>-->
<!-- </plugin>-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
@@ -51,7 +38,7 @@
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.r11.tools.xslt.Main</mainClass>
<mainClass>com.r11.tools.SparkInitializer</mainClass>
</manifest>
</archive>
<descriptorRefs>
@@ -82,6 +69,11 @@
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
@@ -142,7 +134,5 @@
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,39 @@
package com.r11.tools;
import com.r11.tools.controller.JsonController;
import com.r11.tools.controller.ProcessorInfoController;
import com.r11.tools.controller.XPathController;
import com.r11.tools.controller.XsdController;
import com.r11.tools.controller.XsltController;
import com.r11.tools.controller.internal.RestControllerRegistry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import spark.Spark;
public class SparkApplication {
public static void run() {
// TODO: read port from config
Spark.port(8081);
Spark.after((request, response) -> {
response.header("Access-Control-Allow-Origin", "*");
response.header("access-control-allow-headers", "*");
response.header("access-control-expose-headers", "*");
response.header("Access-Control-Allow-Methods", "POST");
});
Logger logger = LogManager.getLogger(SparkApplication.class);
RestControllerRegistry registry = new RestControllerRegistry();
registry.registerController(new ProcessorInfoController(logger));
registry.registerController(new XsdController(logger));
registry.registerController(new XPathController(logger));
registry.registerController(new XsltController(logger));
registry.registerController(new JsonController());
registry.register();
logger.info("Server is online at port: " + Spark.port());
}
}

View File

@@ -0,0 +1,7 @@
package com.r11.tools;
public class SparkInitializer {
public static void main(String[] args) {
SparkApplication.run();
}
}

View File

@@ -0,0 +1,58 @@
package com.r11.tools.controller;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.r11.tools.controller.internal.GlobalControllerManifest;
import com.r11.tools.controller.internal.HandlerType;
import com.r11.tools.controller.internal.RestController;
import com.r11.tools.controller.internal.ScopedControllerManifest;
import spark.Request;
import spark.Response;
@GlobalControllerManifest(path = "/json")
public class JsonController implements RestController {
private final Gson prettyGson = new GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create();
private final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.create();
@ScopedControllerManifest(method = HandlerType.POST, path = "/formatting")
public void formatting(Request request, Response response) {
try {
JsonObject jsonObject = this.gson.fromJson(request.body(), JsonObject.class);
response.status(200);
response.body(this.prettyGson.toJson(jsonObject));
} catch (Exception e) {
response.status(500);
Throwable cause = e.getCause();
if (cause == null) {
response.body(e.getMessage());
} else {
response.body(cause.getMessage());
}
}
}
@ScopedControllerManifest(method = HandlerType.POST, path = "/minimize")
public void minimize(Request request, Response response) {
try {
JsonObject jsonObject = this.prettyGson.fromJson(request.body(), JsonObject.class);
response.status(200);
response.body(this.gson.toJson(jsonObject));
} catch (Exception e) {
response.status(500);
Throwable cause = e.getCause();
if (cause == null) {
response.body(e.getMessage());
} else {
response.body(cause.getMessage());
}
}
}
}

View File

@@ -0,0 +1,34 @@
package com.r11.tools.controller;
import com.r11.tools.controller.internal.GlobalControllerManifest;
import com.r11.tools.controller.internal.HandlerType;
import com.r11.tools.controller.internal.RestController;
import com.r11.tools.controller.internal.ScopedControllerManifest;
import com.r11.tools.xml.Saxon;
import org.apache.logging.log4j.Logger;
import spark.Request;
import spark.Response;
@GlobalControllerManifest
public class ProcessorInfoController implements RestController {
private final Logger logger;
public ProcessorInfoController(Logger logger) {
this.logger = logger;
}
/**
* Handler that returns processor version
*/
@ScopedControllerManifest(method = HandlerType.GET, path = "/procinfo")
public void processorInfo(Request request, Response response) {
try {
response.header("processor", "Saxon " + Saxon.getVersion() + " over s9api");
response.body(Saxon.getVersion());
} catch (Exception ex) {
this.logger.error("Error on retrieving engine version. " + ex);
response.body(ex.getMessage());
}
}
}

View File

@@ -0,0 +1,104 @@
package com.r11.tools.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r11.tools.controller.internal.GlobalControllerManifest;
import com.r11.tools.controller.internal.HandlerType;
import com.r11.tools.controller.internal.RestController;
import com.r11.tools.controller.internal.ScopedControllerManifest;
import com.r11.tools.xml.Saxon;
import com.r11.tools.xml.Xalan;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.Logger;
import spark.Request;
import spark.Response;
@GlobalControllerManifest
public class XPathController implements RestController {
private final Logger logger;
public XPathController(Logger logger) {
this.logger = logger;
}
@ScopedControllerManifest(method = HandlerType.POST, path = "/xpath")
public void transform(Request request, Response response) throws JsonProcessingException {
String body = request.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonProcessingException ex) {
this.logger.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
response.status(400);
response.body(mapper.writeValueAsString(responseMap));
return;
}
String data = requestMap.get("data");
String query = requestMap.get("process");
String processor = requestMap.get("processor");
String version = requestMap.get("version");
String tmp = "";
long timeStart;
long duration;
if (processor == null) {
response.body("saxon, xalan");
return;
}
switch (processor) {
case "saxon":
response.header("processor", "Saxon " + Saxon.getVersion() + " " + version + " over s9api");
timeStart = System.currentTimeMillis();
try {
tmp = Saxon.processXPath(data, query, version).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
this.logger.error("Error on processing XPath using Saxon. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
response.status(400);
}
duration = System.currentTimeMillis() - timeStart;
this.logger.info("Request" + body + " processed in " + duration + " ms.");
responseMap.put("processor", "Saxon " + Saxon.getVersion() + " " + version + " over s9api");
responseMap.put("time", "" + duration);
response.body(mapper.writeValueAsString(responseMap));
return;
case "xalan":
response.header("processor", Xalan.getVersion());
timeStart = System.currentTimeMillis();
try {
tmp = Xalan.processXPath(data, query).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
this.logger.error("Error on processing XPath using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
response.status(400);
}
duration = System.currentTimeMillis() - timeStart;
this.logger.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", Long.toString(duration));
response.body(mapper.writeValueAsString(responseMap));
return;
default:
response.body("saxon, xalan");
}
}
}

View File

@@ -0,0 +1,70 @@
package com.r11.tools.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r11.tools.controller.internal.GlobalControllerManifest;
import com.r11.tools.controller.internal.HandlerType;
import com.r11.tools.controller.internal.RestController;
import com.r11.tools.controller.internal.ScopedControllerManifest;
import com.r11.tools.xml.Xalan;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.Logger;
import spark.Request;
import spark.Response;
@GlobalControllerManifest
public class XsdController implements RestController {
private final Logger logger;
public XsdController(Logger logger) {
this.logger = logger;
}
@ScopedControllerManifest(method = HandlerType.POST, path = "/xsd")
public Response transform(Request req, Response resp) throws JsonProcessingException {
String body = req.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonProcessingException ex) {
this.logger.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
resp.status(400);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
}
String data = requestMap.get("data");
String xsd = requestMap.get("process");
resp.header("processor", Xalan.getVersion());
long timeStart = System.currentTimeMillis();
String tmp;
try {
tmp = Xalan.validate(data, xsd).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
this.logger.error("Error on validation against XSD using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
long duration = System.currentTimeMillis() - timeStart;
this.logger.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", "" + duration);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
}
}

View File

@@ -0,0 +1,105 @@
package com.r11.tools.controller;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r11.tools.controller.internal.GlobalControllerManifest;
import com.r11.tools.controller.internal.HandlerType;
import com.r11.tools.controller.internal.RestController;
import com.r11.tools.controller.internal.ScopedControllerManifest;
import com.r11.tools.xml.Saxon;
import com.r11.tools.xml.Xalan;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.Logger;
import spark.Request;
import spark.Response;
@GlobalControllerManifest
public class XsltController implements RestController {
private final Logger logger;
public XsltController(Logger logger) {
this.logger = logger;
}
@ScopedControllerManifest(method = HandlerType.POST, path = "/xslt")
public void transform(Request request, Response response) throws JsonProcessingException {
String body = request.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonMappingException | JsonParseException ex) {
this.logger.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
response.status(400);
response.body(mapper.writeValueAsString(responseMap));
return;
}
String data = requestMap.get("data");
String query = requestMap.get("process");
String processor = requestMap.get("processor");
String version = requestMap.get("version");
if (processor == null) {
response.body("saxon, xalan");
return;
}
String tmp;
long timeStart;
long duration;
switch (processor) {
case "saxon":
timeStart = System.currentTimeMillis();
try {
tmp = Saxon.processXSLT(data, query);
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
this.logger.error("Error on processing XSLT using Saxon. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
response.status(400);
}
duration = System.currentTimeMillis() - timeStart;
this.logger.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", "Saxon " + Saxon.getVersion() + " " + version);
responseMap.put("time", Long.toString(duration));
response.body(mapper.writeValueAsString(responseMap));
return;
case "xalan":
timeStart = System.currentTimeMillis();
try {
tmp = Xalan.processXSLT(data, query);
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
this.logger.error("Error on processing XSLT using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
response.status(400);
}
duration = System.currentTimeMillis() - timeStart;
this.logger.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", Long.toString(duration));
response.body(mapper.writeValueAsString(responseMap));
return;
default:
response.body("saxon, xalan");
}
}
}

View File

@@ -0,0 +1,14 @@
package com.r11.tools.controller.internal;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface GlobalControllerManifest {
String path() default "";
}

View File

@@ -0,0 +1,7 @@
package com.r11.tools.controller.internal;
public enum HandlerType {
GET, POST, PUT, DELETE
}

View File

@@ -0,0 +1,5 @@
package com.r11.tools.controller.internal;
public interface RestController {
}

View File

@@ -0,0 +1,59 @@
package com.r11.tools.controller.internal;
import com.r11.tools.controller.internal.path.PathBuilder;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import spark.Spark;
public class RestControllerRegistry {
private final Set<RestController> registeredControllers;
public RestControllerRegistry() {
this.registeredControllers = new HashSet<>();
}
public void registerController(RestController restController) {
this.registeredControllers.add(restController);
}
public void register() {
this.registeredControllers.forEach(controller -> {
if (controller.getClass().isAnnotationPresent(GlobalControllerManifest.class)) {
for (Method method : controller.getClass().getMethods()) {
this.registerAssignableHandlers(controller.getClass(), controller, method);
}
}
});
}
private void registerAssignableHandlers(Class<? extends RestController> parent, RestController parentValue, Method method) {
if (
(parent.isAnnotationPresent(GlobalControllerManifest.class)) &&
(method.isAnnotationPresent(ScopedControllerManifest.class))
) {
HandlerType handlerType = method.getAnnotation(ScopedControllerManifest.class).method();
String path = PathBuilder.resolvePathOf(
parent.getAnnotation(GlobalControllerManifest.class).path(),
method.getAnnotation(ScopedControllerManifest.class).path()
);
switch (handlerType) {
case GET:
Spark.get(path, (request, response) -> method.invoke(parentValue, request, response));
break;
case PUT:
Spark.put(path, (request, response) -> method.invoke(parentValue, request, response));
break;
case POST:
Spark.post(path, (request, response) -> method.invoke(parentValue, request, response));
break;
case DELETE:
Spark.delete(path, (request, response) -> method.invoke(parentValue, request, response));
break;
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.r11.tools.controller.internal;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ScopedControllerManifest {
HandlerType method();
String path();
}

View File

@@ -0,0 +1,30 @@
package com.r11.tools.controller.internal.path;
public final class PathBuilder {
private static final String PATH_SEPARATOR = "/";
private PathBuilder() {
}
public static String resolvePathOf(String globalPath, String scopedPath) {
String resolvedPath =
PathBuilder.removeTrailingPathSeparator(globalPath) +
PathBuilder.removeTrailingPathSeparator(scopedPath);
if (resolvedPath.endsWith(PATH_SEPARATOR)) {
resolvedPath = resolvedPath.substring(0, resolvedPath.length() - 1);
}
return PATH_SEPARATOR + resolvedPath;
}
private static String removeTrailingPathSeparator(String path) {
if (path.endsWith(PATH_SEPARATOR)) {
return path.substring(0, path.length() - 1);
}
return path;
}
}

View File

@@ -1,4 +1,4 @@
package com.r11.tools.xslt.processors;
package com.r11.tools.xml;
import net.sf.saxon.om.NamespaceMap;
import net.sf.saxon.s9api.XPathCompiler;

View File

@@ -1,8 +1,6 @@
package com.r11.tools.xslt.processors;
package com.r11.tools.xml;
import net.sf.saxon.s9api.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;

View File

@@ -1,8 +1,5 @@
package com.r11.tools.xslt.processors;
package com.r11.tools.xml;
import net.sf.saxon.lib.RawResult;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -19,10 +16,6 @@ import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import java.io.*;
/**

View File

@@ -1,4 +1,4 @@
package com.r11.tools.xslt.processors;
package com.r11.tools.xml;
import org.w3c.dom.*;

View File

@@ -11,7 +11,7 @@
</File>
</Appenders>
<Loggers>
<Logger name="com.r11.tools.xslt.SparkInitializer" level="info" additivity="true">
<Logger name="com.r11.tools.SparkApplication" level="info" additivity="true">
<AppenderRef ref="Console"/>
</Logger>
<Root level="info">

View File

@@ -1,15 +0,0 @@
package com.r11.tools.xslt;
/**
* Application initializer
* @author Wojciech Czop
*/
public class Main {
/**
* Initializes the application
* @param args
*/
public static void main(String[] args) {
SparkInitializer.run();
}
}

View File

@@ -1,260 +0,0 @@
package com.r11.tools.xslt;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r11.tools.xslt.processors.Saxon;
import com.r11.tools.xslt.processors.Xalan;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import spark.*;
import java.util.HashMap;
import java.util.Map;
/**
* Class contains spark configuration and method initializing spark framework
* @author Wojciech Czop
*/
public class SparkInitializer {
private static final Logger LOG = LogManager.getLogger(SparkInitializer.class);
/**
* Initializes spark framework
*/
public static void run(){
// TODO: Port value as property
Spark.port(8081);
Spark.after((request, response) -> {
response.header("Access-Control-Allow-Origin", "*");
response.header("access-control-allow-headers", "*");
response.header("access-control-expose-headers", "*");
response.header("Access-Control-Allow-Methods", "POST");
});
Spark.post("/xsltpost", xsltHandler);
Spark.post("/xpathpost", xpathHandler);
Spark.post("/xsdpost", xsdHandler);
Spark.get("/procinfo", procinfoHandler);
LOG.info("Server is online at port: " + Spark.port());
}
/**
* Handler that returns processor version
*/
private static final Route procinfoHandler = (Request req, Response resp) -> {
try {
resp.header("processor", "Saxon " + Saxon.getVersion() + " over s9api");
return Saxon.getVersion();
} catch (Exception ex) {
LOG.error("Error on retrieving engine version. " + ex);
return ex.getMessage();
}
};
/**
* Handler that returns info if document is valid
* Also provides info about request time and processor
*/
private static final Route xsdHandler = (Request req, Response resp) -> {
String body = req.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonMappingException | JsonParseException ex) {
LOG.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
resp.status(400);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
}
String data = requestMap.get("data");
String xsd = requestMap.get("process");
resp.header("processor", Xalan.getVersion());
long timeStart = System.currentTimeMillis();
String tmp;
try {
tmp = Xalan.validate(data, xsd).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
LOG.error("Error on validation against XSD using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
long duration = System.currentTimeMillis() - timeStart;
LOG.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", "" + duration);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
};
/**
* Handler that returns output of xpath query and processor data
*/
private static final Route xpathHandler = (Request req, Response resp) -> {
String body = req.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonMappingException | JsonParseException ex) {
LOG.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
resp.status(400);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
}
String data = requestMap.get("data");
String query = requestMap.get("process");
String processor = requestMap.get("processor");
String version = requestMap.get("version");
String tmp = "";
long timeStart;
long duration;
if (processor == null) {
return "saxon, xalan";
}
switch (processor) {
case "saxon":
resp.header("processor", "Saxon " + Saxon.getVersion() + " " + version + " over s9api");
timeStart = System.currentTimeMillis();
try {
tmp = Saxon.processXPath(data, query, version).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
LOG.error("Error on processing XPath using Saxon. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
duration = System.currentTimeMillis() - timeStart;
LOG.info("Request" + body + " processed in " + duration + " ms.");
responseMap.put("processor", "Saxon " + Saxon.getVersion() + " " + version + " over s9api");
responseMap.put("time", "" + duration);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
case "xalan":
resp.header("processor", Xalan.getVersion());
timeStart = System.currentTimeMillis();
try {
tmp = Xalan.processXPath(data, query).trim();
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
LOG.error("Error on processing XPath using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
duration = System.currentTimeMillis() - timeStart;
LOG.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", Long.toString(duration));
resp.body(mapper.writeValueAsString(responseMap));
return resp;
default:
return "saxon, xalan";
}
};
/**
* Handler that returns outcome of xslt transformation and processor data
*/
private static final Route xsltHandler = (Request req, Response resp) -> {
String body = req.body();
ObjectMapper mapper = new ObjectMapper();
Map<String, String> requestMap = new HashMap<>();
Map<String, String> responseMap = new HashMap<>();
try {
requestMap = mapper.readValue(body, Map.class);
} catch (JsonMappingException | JsonParseException ex) {
LOG.error("Request JSON error. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("processor", "N/A");
responseMap.put("status", "ERR");
responseMap.put("time", "N/A");
resp.status(400);
resp.body(mapper.writeValueAsString(responseMap));
return resp;
}
String data = requestMap.get("data");
String query = requestMap.get("process");
String processor = requestMap.get("processor");
String version = requestMap.get("version");
if (processor == null) {
return "saxon, xalan";
}
String tmp;
long timeStart;
long duration;
switch (processor) {
case "saxon":
timeStart = System.currentTimeMillis();
try {
tmp = Saxon.processXSLT(data, query);
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
LOG.error("Error on processing XSLT using Saxon. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
duration = System.currentTimeMillis() - timeStart;
LOG.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", "Saxon " + Saxon.getVersion() + " " + version);
responseMap.put("time", Long.toString(duration));
resp.body(mapper.writeValueAsString(responseMap));
return resp;
case "xalan":
timeStart = System.currentTimeMillis();
try {
tmp = Xalan.processXSLT(data, query);
responseMap.put("result", tmp);
responseMap.put("status", "OK");
} catch (Exception ex) {
LOG.error("Error on processing XSLT using Xalan. " + ex);
responseMap.put("result", ex.getMessage());
responseMap.put("status", "ERR");
resp.status(400);
}
duration = System.currentTimeMillis() - timeStart;
LOG.info("Request: " + body + " processed in " + duration + " ms.");
responseMap.put("processor", Xalan.getVersion());
responseMap.put("time", Long.toString(duration));
resp.body(mapper.writeValueAsString(responseMap));
return resp;
default:
return "saxon, xalan";
}
};
}

View File

@@ -1,39 +1,63 @@
function formatAndValidateJson(errorElement) {
const input = document.querySelector('#jsonBlock');
const processInfo = document.getElementById(errorElement);
const start = new Date();
try {
const start = new Date();
const address = window.location.protocol + "//" + window.location.hostname + ":" + 8081 + "/json/formatting"
const obj = JSON.parse(input.textContent);
input.innerHTML = JSON.stringify(obj, null, 2);
fetch(address, {
method: 'POST',
body: input.textContent
})
.then(async (response) => {
if (!response.ok) {
throw Error(await response.text());
}
return response.text();
})
.then((data) => {
input.innerText = data;
processInfo.innerText = "";
hljs.highlightElement(input);
const end = new Date();
processInfo.innerHTML = "<b style='color: black'>Validation and formatting time:</b> <span style='color: green'>" + (end.getMilliseconds() - start.getMilliseconds()) + "ms</span>";
} catch (error) {
})
.catch((error) => {
processInfo.innerHTML = "<b style='color: red'>" + error + "</b>";
console.error("Error: ", error)
}
console.error('Error:', error);
});
}
function minimizeJson(errorElement) {
const input = document.querySelector('#jsonBlock');
const processInfo = document.getElementById(errorElement);
try {
const start = new Date();
const start = new Date();
const address = window.location.protocol + "//" + window.location.hostname + ":" + 8081 + "/json/minimize"
const obj = JSON.parse(input.textContent);
input.innerHTML = JSON.stringify(obj);
fetch(address, {
method: 'POST',
body: input.textContent
})
.then(async (response) => {
if (!response.ok) {
throw Error(await response.text());
}
return response.text();
})
.then((data) => {
input.innerText = data;
processInfo.innerText = "";
hljs.highlightElement(input);
const end = new Date();
processInfo.innerHTML = "<b style='color: black'>Validation and formatting time:</b> <span style='color: green'>" + (end.getMilliseconds() - start.getMilliseconds()) + "ms</span>";
} catch (error) {
})
.catch((error) => {
processInfo.innerHTML = "<b style='color: red'>" + error + "</b>";
console.error("Error: ", error)
}
console.error('Error:', error);
});
}

View File

@@ -1,54 +1,251 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
<link rel="stylesheet" href="../assets/css/tools/r11form.css">
<link rel="stylesheet" href="../assets/css/json.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script src="../assets/scripts/tools/scripts.js"></script>
<script src="../assets/scripts/tools/json.js"></script>
<script>hljs.highlightAll();</script>
</head>
<link rel="stylesheet" href="../assets/css/tools/r11form.css">
<link rel="stylesheet" href="../assets/css/json.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script src="../assets/scripts/tools/scripts.js"></script>
<script src="../assets/scripts/tools/json.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class="container">
<div id="tool" class="tool rwd-expandable">
<div class="tool-context">
<div class="headline">
<h1>Online JSON Formatter</h1>
<body>
<div class="container">
<div id="tool" class="tool rwd-expandable">
<div class="tool-context">
<div class="headline">
<h1>Online JSON Formatter</h1>
</div>
<p style="margin-bottom: -30px" id="processInfo"></p>
<pre>
<code class="hightlight-json json-block" id="jsonBlock" contenteditable="True">{"enter": "your", "json": "here"}</code>
</pre>
<button style="margin-top: 20px"
class="max-width block-label action-button active"
onclick="formatAndValidateJson('processInfo')"
>Prettify JSON</button>
<button class="max-width block-label action-button active"
onclick="minimizeJson('processInfo')"
>Minimize JSON</button>
</div>
<p style="margin-bottom: -30px" id="processInfo"></p>
<pre>
<code class="hightlight-json json-block" id="jsonBlock" contenteditable="True">{"enter": "your", "json": "here"}</code>
</pre>
<button style="margin-top: 20px"
class="max-width block-label action-button active"
onclick="formatAndValidateJson('processInfo')"
>Prettify JSON</button>
<button class="max-width block-label action-button active"
onclick="minimizeJson('processInfo')"
>Minimize JSON</button>
</div>
<div class="tooltip-window rwd-hideable">
<h2>What is this?</h2>
<p>This tool has 2 main functions:
<ul>
<li><strong>Prettify JSON</strong> to make it human-readable (add indentation etc.)</li>
<li><strong>Minimize JSON</strong> to make it more compact (exactly opposite to above)</li>
</ul>
</p>
</div>
</div>
<div class="tooltip-window rwd-hideable">
<h2>What is this?</h2>
<p>This tool has 2 main functions:
<ul>
<li><strong>Prettify JSON</strong> to make it human-readable (add indentation etc.)</li>
<li><strong>Minimize JSON</strong> to make it more compact (exactly opposite to above)</li>
</ul>
</p>
</div>
<script>
const mergeHTMLPlugin = (function () {
'use strict';
</div>
</body>
var originalStream;
/**
* @param {string} value
* @returns {string}
*/
function escapeHTML(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
/* plugin itself */
/** @type {HLJSPlugin} */
const mergeHTMLPlugin = {
// preserve the original HTML token stream
"before:highlightElement": ({el}) => {
originalStream = nodeStream(el);
},
// merge it afterwards with the highlighted token stream
"after:highlightElement": ({el, result, text}) => {
if (!originalStream.length) {
return;
}
const resultNode = document.createElement('div');
resultNode.innerHTML = result.value;
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
el.innerHTML = result.value;
}
};
/**
* @param {Node} node
*/
function tag(node) {
return node.nodeName.toLowerCase();
}
/**
* @param {Node} node
*/
function nodeStream(node) {
/** @type Event[] */
const result = [];
(function _nodeStream(node, offset) {
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType === 3) {
offset += child.nodeValue.length;
} else if (child.nodeType === 1) {
result.push({
event: 'start',
offset: offset,
node: child
});
offset = _nodeStream(child, offset);
if (!tag(child).match(/br|hr|img|input/)) {
result.push({
event: 'stop',
offset: offset,
node: child
});
}
}
}
return offset;
})(node, 0);
return result;
}
/**
* @param {any} original - the original stream
* @param {any} highlighted - stream of the highlighted source
* @param {string} value - the original source itself
*/
function mergeStreams(original, highlighted, value) {
let processed = 0;
let result = '';
const nodeStack = [];
function selectStream() {
if (!original.length || !highlighted.length) {
return original.length ? original : highlighted;
}
if (original[0].offset !== highlighted[0].offset) {
return (original[0].offset < highlighted[0].offset) ? original : highlighted;
}
return highlighted[0].event === 'start' ? original : highlighted;
}
/**
* @param {Node} node
*/
function open(node) {
/** @param {Attr} attr */
function attributeString(attr) {
return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
}
// @ts-ignore
result += '<' + tag(node) + [].map.call(node.attributes, attributeString).join('')
+ '>';
}
/**
* @param {Node} node
*/
function close(node) {
result += '</' + tag(node) + '>';
}
/**
* @param {Event} event
*/
function render(event) {
(event.event === 'start' ? open : close)(event.node);
}
while (original.length || highlighted.length) {
let stream = selectStream();
result += escapeHTML(value.substring(processed, stream[0].offset));
processed = stream[0].offset;
if (stream === original) {
/*
On any opening or closing tag of the original markup we first close
the entire highlighted node stack, then render the original tag along
with all the following original tags at the same offset and then
reopen all the tags on the highlighted stack.
*/
nodeStack.reverse().forEach(close);
do {
render(stream.splice(0, 1)[0]);
stream = selectStream();
} while (stream === original && stream.length && stream[0].offset === processed);
nodeStack.reverse().forEach(open);
} else {
if (stream[0].event === 'start') {
nodeStack.push(stream[0].node);
} else {
nodeStack.pop();
}
render(stream.splice(0, 1)[0]);
}
}
return result + escapeHTML(value.substr(processed));
}
return mergeHTMLPlugin;
}());
hljs.addPlugin(mergeHTMLPlugin);
const editorEle = document.getElementById('jsonBlock');
// Handle the `paste` event
editorEle.addEventListener('paste', function (e) {
// Prevent the default action
e.preventDefault();
// Get the copied text from the clipboard
const text = e.clipboardData
? (e.originalEvent || e).clipboardData.getData('text/plain')
: // For IE
window.clipboardData
? window.clipboardData.getData('Text')
: '';
if (document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, text);
} else {
// Insert text at the current position of caret
const range = document.getSelection().getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(text);
range.insertNode(textNode);
range.selectNodeContents(textNode);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
});
</script>
</body>
</html>

View File

@@ -1,79 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<!-- <link rel="stylesheet" href="styles.css"> -->
<link rel="stylesheet" href="../assets/css/tools/r11form.css">
<script src="../assets/scripts/tools/scripts.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
</head>
<head>
<!-- <link rel="stylesheet" href="styles.css"> -->
<link rel="stylesheet" href="../assets/css/tools/r11form.css">
<script src="../assets/scripts/tools/scripts.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
</head>
<body onload="init();">
<div class="container">
<div id="tool" class="tool rwd-expandable">
<div class="tool-context">
<div class="headline">
<h1>Online XML Formatter</h1>
</div>
<select name="processors" id="processors" class="hidden">
<option value="libxml">libXML</option>
</select>
<div class="display-space-between">
<div>
<b><span id="formatinfo"></span></b><br>
<label for="xmlArea"><b>Insert your XML:</b></label>
<body onload="init();">
<div class="container">
<div id="tool" class="tool rwd-expandable">
<div class="tool-context">
<div class="headline">
<h1>Online XML Formatter</h1>
</div>
<div>
<button class="action-button active" id="clearXMLButton" style="padding: 3px 10px;"
onclick="clearDataField()">Clear</button>
<button class="action-button active" id="defaultXMLButton" style="padding: 3px 10px;"
onclick="fillDefaultXML(this)">Insert default XML</button>
<select name="processors" id="processors" class="hidden">
<option value="libxml">libXML</option>
</select>
<div class="display-space-between">
<div>
<b><span id="formatinfo"></span></b><br>
<label for="xmlArea"><b>Insert your XML:</b></label>
</div>
<div>
<button class="action-button active" id="clearXMLButton" style="padding: 3px 10px;"
onclick="clearDataField()">Clear</button>
<button class="action-button active" id="defaultXMLButton" style="padding: 3px 10px;"
onclick="fillDefaultXML(this)">Insert default XML</button>
</div>
</div>
<textarea id="xmlArea" name="xmlArea" rows="15"
class="textarea-700 bordered-field vertically-resizeable max-width"
onblur="setDefaultContent(this, 'Insert XML here');"
onfocus="clearDefaultContent(this, 'Insert XML here');"></textarea>
<br><br>
<button id="prettifyButton" class="max-width block-label action-button active"
onclick="performFormatRequest('prettify', true, 'xmlArea', 'xmlArea')">Prettify XML</button>
<button id="minimizeButton" class="max-width block-label action-button active"
onclick="performFormatRequest('minimize', true, 'xmlArea', 'xmlArea')">Minimize XML</button>
</div>
<textarea id="xmlArea" name="xmlArea" rows="15"
class="textarea-700 bordered-field vertically-resizeable max-width"
onblur="setDefaultContent(this, 'Insert XML here');"
onfocus="clearDefaultContent(this, 'Insert XML here');"></textarea>
<br><br>
<button id="prettifyButton" class="max-width block-label action-button active"
onclick="performFormatRequest('prettifypost', true, 'xmlArea', 'xmlArea')">Prettify XML</button>
<button id="minimizeButton" class="max-width block-label action-button active"
onclick="performFormatRequest('minimizepost', true, 'xmlArea', 'xmlArea')">Minimize XML</button>
</div>
</div>
<div class="tooltip-window rwd-hideable">
<h2>What is this?</h2>
<p>This tool has 2 main functions:
<ul>
<li><strong>Prettify XML</strong> to make it human-readable (add indentation etc.)</li>
<li><strong>Minimize XML</strong> to make it more compact (exactly opposite to above)</li>
</ul>
</p>
<div class="tooltip-window rwd-hideable">
<h2>What is this?</h2>
<p>This tool has 2 main functions:
<ul>
<li><strong>Prettify XML</strong> to make it human-readable (add indentation etc.)</li>
<li><strong>Minimize XML</strong> to make it more compact (exactly opposite to above)</li>
</ul>
</p>
</div>
</div>
<script>
function getProcessor() {
return "libxml";
}
function getVersion() {
return "1.0"
}
</div>
function init() {
setDefaultContent(document.getElementById("xmlArea"), 'Insert XML here');
}
</script>
<script>
function getProcessor() {
return "libxml";
}
function getVersion() {
return "1.0"
}
function init() {
setDefaultContent(document.getElementById("xmlArea"), 'Insert XML here');
}
</script>
</body>
</body>
</html>

View File

@@ -40,7 +40,7 @@
<button class="action-button active" id="clearXMLButton" style="padding: 3px 10px;"
onclick="clearDataField()">Clear</button>
<button class="action-button active" id="prettyXMLButton" style="padding: 3px 10px;"
onclick="performFormatRequest('prettifypost', true, 'xmlArea', 'xmlArea')">Format XML</button>
onclick="performFormatRequest('prettify', true, 'xmlArea', 'xmlArea')">Format XML</button>
<button class="action-button active" id="defaultXMLButton" style="padding: 3px 10px;"
onclick="fillDefaultXML(this)">Insert default XML</button>
</div>
@@ -59,7 +59,7 @@
onfocus="clearDefaultContent(this, 'Insert XPath expression here');"></textarea>
<br>
<button id="requestButton" class="max-width block-label action-button active"
onclick="performRequest('xpathpost', false, true)">Execute XPath
onclick="performRequest('xpath', false, true)">Execute XPath
expression</button>
<br><br>
<label for="resultArea"><b>Transform result:<span id="procinfo"></span></b></label>

View File

@@ -28,7 +28,7 @@
<button class="action-button active" id="clearXMLButton" style="padding: 3px 10px;"
onclick="clearDataField()">Clear</button>
<button class="action-button active" id="prettyXMLButton" style="padding: 3px 10px;"
onclick="performFormatRequest('prettifypost', true, 'xmlArea', 'xmlArea')">Format XML</button>
onclick="performFormatRequest('prettify', true, 'xmlArea', 'xmlArea')">Format XML</button>
<button class="action-button active" id="defaultXMLButton" style="padding: 3px 10px;"
onclick="fillDefaultXML(this)">Insert default XML</button>
</div>
@@ -50,7 +50,7 @@
onfocus="clearDefaultContent(this, 'Insert XSD here');"></textarea>
<br>
<button id="requestButton" class="max-width block-label action-button active"
onclick="performRequest('xsdpost', true, true)">Verify XSD</button>
onclick="performRequest('xsd', true, true)">Verify XSD</button>
<br><br>
<label for="resultArea"><b>Result:<span id="procinfo"></span></b></label>

View File

@@ -29,7 +29,7 @@
<button class="action-button active" id="clearXMLButton" style="padding: 3px 10px;"
onclick="clearDataField()">Clear</button>
<button class="action-button active" id="prettyXMLButton" style="padding: 3px 10px;"
onclick="performFormatRequest('prettifypost', true, 'xmlArea', 'xmlArea')">Format XML</button>
onclick="performFormatRequest('prettify', true, 'xmlArea', 'xmlArea')">Format XML</button>
<button class="action-button active" id="defaultXMLButton" style="padding: 3px 10px;"
onclick="fillDefaultXML(this)">Insert default XML</button>
</div>
@@ -52,7 +52,7 @@
onfocus="clearDefaultContent(this, 'Insert XSLT here');"></textarea>
<br>
<button id="requestButton" class="max-width block-label action-button active"
onclick="performRequest('xsltpost', true, true)">Execute XSLT transform</button>
onclick="performRequest('xslt', true, true)">Execute XSLT transform</button>
<br><br>
<label for="resultArea"><b>Transform result:<span id="procinfo"></span></b></label>

View File

@@ -423,7 +423,89 @@
}
}
},
"/xpathpost": {
"/json/formatting": {
"post": {
"tags": [
"JSON"
],
"summary": "Json validation and formatting.",
"description": "",
"operationId": "json",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "request",
"description": "A transform that is to be performed",
"required": true,
"schema": {
"example": "{'a': 'b', 'c': 'd'}"
}
}
],
"responses": {
"200": {
"description": "Formatted JSON",
"schema": {
"type": "string"
}
},
"500": {
"description": "Error message from json formatting.",
"schema": {
"type": "string"
}
}
}
}
},
"/json/minimize": {
"post": {
"tags": [
"JSON"
],
"summary": "Json validation and minimize formatting.",
"description": "",
"operationId": "json",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "request",
"description": "A transform that is to be performed",
"required": true,
"schema": {
"example": "{'a': 'b', 'c': 'd'}"
}
}
],
"responses": {
"200": {
"description": "Formatted JSON",
"schema": {
"type": "string"
}
},
"500": {
"description": "Error message from json formatting.",
"schema": {
"type": "string"
}
}
}
}
},
"/xpath": {
"post": {
"tags": [
"XPath"
@@ -470,7 +552,7 @@
}
}
},
"/xsltpost": {
"/xslt": {
"post": {
"tags": [
"XSLT"
@@ -517,7 +599,7 @@
}
}
},
"/xsdpost": {
"/xsd": {
"post": {
"tags": [
"XSD"

View File

@@ -13,7 +13,7 @@ services:
- 8086:80
xmltools-backend:
build: ./Backend/xslt-rest
build: ./Backend/tools-services
container_name: xmltools-backend
image: xmltools-backend
ports:

View File

@@ -11,7 +11,7 @@ Service is split into three microservices.
Main page is hosted on port 8086.
## API Documentation
API documentation is available on port 8080/swagger/
API documentation is available on port 8000/swagger/
## JavaSpark backend
Backend is hosted by default on port 8081. Rest api documentation is contained in OpenApi document