diff --git a/src/main/java/org/intellimate/izou/sdk/AddOnImpl.java b/src/main/java/org/intellimate/izou/sdk/AddOnImpl.java
new file mode 100644
index 0000000..6878fef
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/AddOnImpl.java
@@ -0,0 +1,62 @@
+package org.intellimate.izou.sdk;
+
+
+import org.intellimate.izou.activator.ActivatorModel;
+import org.intellimate.izou.events.EventsControllerModel;
+import org.intellimate.izou.output.OutputControllerModel;
+import org.intellimate.izou.output.OutputExtensionModel;
+import org.intellimate.izou.output.OutputPluginModel;
+import org.intellimate.izou.sdk.addon.AddOn;
+import org.intellimate.izou.sdk.contentgenerator.ContentGenerator;
+import org.intellimate.izou.sdk.server.SDKRouter;
+import ro.fortsoft.pf4j.Extension;
+
+/**
+ * @author LeanderK
+ * @version 1.0
+ */
+@Extension
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class AddOnImpl extends AddOn {
+ public AddOnImpl() {
+ super(AddOnImpl.class.getCanonicalName());
+ }
+
+ /**
+ * This method gets called before registering
+ */
+ @Override
+ public void prepare() {
+ setRouter(new SDKRouter(getContext()));
+ }
+
+ @Override
+ public ActivatorModel[] registerActivator() {
+ return new ActivatorModel[0];
+ }
+
+ @Override
+ public ContentGenerator[] registerContentGenerator() {
+ return new ContentGenerator[0];
+ }
+
+ @Override
+ public EventsControllerModel[] registerEventController() {
+ return new EventsControllerModel[0];
+ }
+
+ @Override
+ public OutputPluginModel[] registerOutputPlugin() {
+ return new OutputPluginModel[0];
+ }
+
+ @Override
+ public OutputExtensionModel[] registerOutputExtension() {
+ return new OutputExtensionModel[0];
+ }
+
+ @Override
+ public OutputControllerModel[] registerOutputController() {
+ return new OutputControllerModel[0];
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/Context.java b/src/main/java/org/intellimate/izou/sdk/Context.java
index 8166135..d6750bd 100644
--- a/src/main/java/org/intellimate/izou/sdk/Context.java
+++ b/src/main/java/org/intellimate/izou/sdk/Context.java
@@ -145,6 +145,16 @@ public AddOns getAddOns() {
return context.getAddOns();
}
+ /**
+ * Returns the API, which contains information bout to the server and the connection
+ *
+ * @return ServerInformation
+ */
+ @Override
+ public ServerInformation getServerInformation() {
+ return context.getServerInformation();
+ }
+
private class ContentGeneratorsImpl implements ContentGenerators {
/**
diff --git a/src/main/java/org/intellimate/izou/sdk/addon/AddOn.java b/src/main/java/org/intellimate/izou/sdk/addon/AddOn.java
index 3723def..d28c914 100644
--- a/src/main/java/org/intellimate/izou/sdk/addon/AddOn.java
+++ b/src/main/java/org/intellimate/izou/sdk/addon/AddOn.java
@@ -11,9 +11,12 @@
import org.intellimate.izou.sdk.contentgenerator.ContentGenerator;
import org.intellimate.izou.sdk.output.OutputController;
import org.intellimate.izou.sdk.output.OutputExtension;
+import org.intellimate.izou.sdk.server.Router;
+import org.intellimate.izou.sdk.server.properties.PropertiesRouter;
import org.intellimate.izou.sdk.util.ContextProvider;
import org.intellimate.izou.sdk.util.Loggable;
import org.intellimate.izou.sdk.util.LoggedExceptionCallback;
+import org.intellimate.izou.server.Request;
import ro.fortsoft.pf4j.PluginWrapper;
/**
@@ -26,6 +29,7 @@ public abstract class AddOn implements AddOnModel, ContextProvider, Loggable, Lo
private final String addOnID;
private Context context;
private PluginWrapper plugin;
+ private Router router;
/**
* The default constructor for AddOns
@@ -41,6 +45,7 @@ public AddOn(String addOnID) {
*/
@Override
public void register() {
+ this.router = new PropertiesRouter(getContext());
prepare();
ContentGenerator[] contentGenerators = registerContentGenerator();
if (contentGenerators != null) {
@@ -190,6 +195,29 @@ public PluginWrapper getPlugin() {
return plugin;
}
+ /**
+ * sets the router to use when handling requests
+ * @param router the router to use
+ */
+ @SuppressWarnings("unused")
+ protected void setRouter(Router router) {
+ this.router = router;
+ }
+
+ /**
+ * this method handles the HTTP-Requests from the server.
+ *
+ * this method should not be overridden, to change behaviour please user setRouter.
+ *
+ *
+ * @param request the request to process
+ * @return the response
+ */
+ @Override
+ public org.intellimate.izou.server.Response handleRequest(Request request) {
+ return router.handle(request);
+ }
+
/**
* Sets the Plugin IF it is not already set.
*
diff --git a/src/main/java/org/intellimate/izou/sdk/server/BadRequestException.java b/src/main/java/org/intellimate/izou/sdk/server/BadRequestException.java
new file mode 100644
index 0000000..f88206c
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/BadRequestException.java
@@ -0,0 +1,38 @@
+package org.intellimate.izou.sdk.server;
+
+/**
+ * represents an BadRequest (400)
+ * @author LeanderK
+ * @version 1.0
+ */
+public class BadRequestException extends RuntimeException {
+ /**
+ * Constructs a new runtime exception with the specified detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ *
+ * @param message the detail message. The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public BadRequestException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new runtime exception with the specified detail message and
+ * cause.
Note that the detail message associated with
+ * {@code cause} is not automatically incorporated in
+ * this runtime exception's detail message.
+ *
+ * @param message the detail message (which is saved for later retrieval
+ * by the {@link #getMessage()} method).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A null value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ * @since 1.4
+ */
+ public BadRequestException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/DefaultHandler.java b/src/main/java/org/intellimate/izou/sdk/server/DefaultHandler.java
new file mode 100644
index 0000000..3e115d7
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/DefaultHandler.java
@@ -0,0 +1,50 @@
+package org.intellimate.izou.sdk.server;
+
+import org.intellimate.izou.sdk.Context;
+import org.intellimate.izou.sdk.util.AddOnModule;
+import org.intellimate.izou.sdk.util.FireEvent;
+import org.intellimate.izou.util.IzouModule;
+
+import java.util.Optional;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+
+/**
+ * handles a request on a specific route
+ * @author LeanderK
+ * @version 1.0
+ */
+@SuppressWarnings("WeakerAccess")
+public class DefaultHandler extends AddOnModule implements Handler, FireEvent {
+ private final Method method;
+ private final Function function;
+ private final boolean internal;
+
+ /**
+ * initializes the Module
+ * @param context the current context
+ * @param method the registered method
+ * @param function the function to execute
+ */
+ public DefaultHandler(Context context, String addOnPackageName, String route, Method method, boolean internal, Function function) {
+ super(context, addOnPackageName+"."+route+method.name());
+ this.method = method;
+ this.function = function;
+ this.internal = internal;
+ }
+
+ @Override
+ public Optional handle(Request request) {
+ if (!request.getMethod().toLowerCase().equals(method.name().toLowerCase())) {
+ return Optional.empty();
+ }
+ if (internal && !request.getToken().isPresent()) {
+ return Optional.empty();
+ }
+ Response response = function.apply(request);
+ if (response == null || !response.isValidResponse()) {
+ throw new InternalServerErrorException("App returned illegal response");
+ }
+ return Optional.of(response);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Handler.java b/src/main/java/org/intellimate/izou/sdk/server/Handler.java
new file mode 100644
index 0000000..449d673
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Handler.java
@@ -0,0 +1,17 @@
+package org.intellimate.izou.sdk.server;
+
+import java.util.Optional;
+
+/**
+ * handles a request on a specific route
+ * @author LeanderK
+ * @version 1.0
+ */
+public interface Handler extends HandlerHelper {
+ /**
+ * maybe handles a request
+ * @param request the request
+ * @return empty if not responsible for the request or response
+ */
+ Optional handle(Request request);
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/HandlerHelper.java b/src/main/java/org/intellimate/izou/sdk/server/HandlerHelper.java
new file mode 100644
index 0000000..ab06047
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/HandlerHelper.java
@@ -0,0 +1,152 @@
+package org.intellimate.izou.sdk.server;
+
+import org.intellimate.izou.identification.AddOnInformation;
+import org.intellimate.izou.sdk.Context;
+
+import javax.activation.MimetypesFileTypeMap;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.*;
+
+/**
+ * @author LeanderK
+ * @version 1.0
+ */
+public interface HandlerHelper {
+ MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
+
+ /**
+ * creates a permanent 308 redirect
+ * @param url the url to redirecto to
+ * @return a redirect
+ */
+ default Response createRedirect(String url) {
+ Map> header = new HashMap<>();
+ LinkedList list = new LinkedList<>();
+ list.add(url);
+ header.put("Location", list);
+ String body = "This page is located here.";
+ return new Response(308, header, "text/html", body);
+ }
+
+ /**
+ * constructs a link, falling back to localhost port 80 if no valid izou-server link was found
+ * @param route the route to add to the link, e.g. apps/1
+ * @return a Link
+ */
+ @SuppressWarnings("unused")
+ default String constructLinkToServer(String route) {
+ String id = getContext().getAddOns().getAddOn().getID();
+ Optional addOnInformation = getContext().getAddOns().getAddOnInformation(id);
+ String base = getContext().getServerInformation()
+ .getIzouServerURL()
+ .map(url -> {
+ if (url.endsWith("/")) {
+ return url.substring(0, url.length() - 1);
+ } else {
+ return url;
+ }
+ }).orElse("locahost:80");
+ if (!route.startsWith("/")) {
+ base = base + "/";
+ }
+ return base + route;
+ }
+
+ /**
+ * constructs a link, falling back to localhost port 80 if no valid izou-server link was found
+ * @param addOnInformation the addon to link to
+ * @param route the route to add to the link, e.g. apps/1
+ * @return a Link
+ */
+ @SuppressWarnings("unused")
+ default String constructLinkToAddon(AddOnInformation addOnInformation, String route) {
+ String base = getContext().getServerInformation()
+ .getIzouServerURL()
+ .map(url -> {
+ if (url.endsWith("/")) {
+ return url.substring(0, url.length() - 1);
+ } else {
+ return url;
+ }
+ })
+ .flatMap(url ->
+ getContext().getServerInformation().getIzouRoute()
+ .map(izouRoute1 -> {
+ if (izouRoute1.startsWith("/")) {
+ return izouRoute1;
+ } else {
+ return "/" + izouRoute1;
+ }
+ })
+ .map(izouRoute1 -> url + izouRoute1)
+ )
+ .map(url -> {
+ if (url.endsWith("/")) {
+ return url.substring(0, url.length() - 1);
+ } else {
+ return url;
+ }
+ })
+ .orElse("locahost:80/users/1/izou/1/instance/");
+ String urlWithAddon;
+ Optional serverID = addOnInformation.getServerID();
+ if (serverID.isPresent()) {
+ urlWithAddon = base + "apps/" + serverID.get();
+ } else {
+ urlWithAddon = base + "apps/dev/" + addOnInformation.getName();
+ }
+ if (!route.startsWith("/")) {
+ urlWithAddon = urlWithAddon + "/";
+ }
+ return urlWithAddon + route;
+ }
+
+ /**
+ * constructs the response for a String
+ * @param message the message to print
+ * @param status the status of the message
+ * @return the response
+ */
+ default Response stringResponse(String message, int status) {
+ return new Response(status, new HashMap<>(), "text/plain", message);
+ }
+
+ /**
+ * returns the context
+ * @return the context
+ */
+ Context getContext();
+
+ /**
+ * checks if the response is null or not valid, throws an InternalServerErrorException if thats the case
+ * @param response the response to check
+ */
+ default void sanityCheck(Response response) {
+ if (response == null || !response.isValidResponse()) {
+ throw new InternalServerErrorException("App returned illegal response");
+ }
+ }
+
+ /**
+ * sends the file, or throws an {@link NotFoundException} if not existing
+ * @param request the request to answer
+ * @param file the file to send
+ * @return an response
+ */
+ default Response sendFile(Request request, File file) throws NotFoundException {
+ if (file.exists()) {
+ throw new NotFoundException(request.getUrl() + " not found");
+ }
+
+ String contentType = mimetypesFileTypeMap.getContentType(file);
+ FileInputStream fileInputStream;
+ try {
+ fileInputStream = new FileInputStream(file);
+ } catch (FileNotFoundException e) {
+ throw new NotFoundException(request.getUrl() + " not found");
+ }
+ return new Response(200, new HashMap<>(), contentType, file.length(), fileInputStream);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/InternalServerErrorException.java b/src/main/java/org/intellimate/izou/sdk/server/InternalServerErrorException.java
new file mode 100644
index 0000000..cbe6c1c
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/InternalServerErrorException.java
@@ -0,0 +1,38 @@
+package org.intellimate.izou.sdk.server;
+
+/**
+ * represents an InternalServerError (500)
+ * @author LeanderK
+ * @version 1.0
+ */
+public class InternalServerErrorException extends RuntimeException {
+ /**
+ * Constructs a new runtime exception with the specified detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ *
+ * @param message the detail message. The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public InternalServerErrorException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new runtime exception with the specified detail message and
+ * cause. Note that the detail message associated with
+ * {@code cause} is not automatically incorporated in
+ * this runtime exception's detail message.
+ *
+ * @param message the detail message (which is saved for later retrieval
+ * by the {@link #getMessage()} method).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A null value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ * @since 1.4
+ */
+ public InternalServerErrorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Method.java b/src/main/java/org/intellimate/izou/sdk/server/Method.java
new file mode 100644
index 0000000..bff540c
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Method.java
@@ -0,0 +1,12 @@
+package org.intellimate.izou.sdk.server;
+
+/**
+ * represents the http-methods
+ * @author LeanderK
+ * @version 1.0
+ */
+//TODO add missing
+@SuppressWarnings("WeakerAccess")
+public enum Method {
+ GET, POST, PUT, PATCH, DELETE, OPTIONS
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/NotFoundException.java b/src/main/java/org/intellimate/izou/sdk/server/NotFoundException.java
new file mode 100644
index 0000000..dd29d95
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/NotFoundException.java
@@ -0,0 +1,49 @@
+package org.intellimate.izou.sdk.server;
+
+/**
+ * represents a 404 NotFound
+ * @author LeanderK
+ * @version 1.0
+ */
+public class NotFoundException extends RuntimeException {
+ /**
+ * Constructs a new runtime exception with the specified detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ *
+ * @param message the detail message. The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public NotFoundException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new runtime exception with the specified detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ *
+ * @param request the request not found
+ */
+ public NotFoundException(Request request) {
+ super(request.getUrl() + " not found");
+ }
+
+ /**
+ * Constructs a new runtime exception with the specified detail message and
+ * cause.
Note that the detail message associated with
+ * {@code cause} is not automatically incorporated in
+ * this runtime exception's detail message.
+ *
+ * @param message the detail message (which is saved for later retrieval
+ * by the {@link #getMessage()} method).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A null value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ * @since 1.4
+ */
+ public NotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Request.java b/src/main/java/org/intellimate/izou/sdk/server/Request.java
new file mode 100644
index 0000000..453d41a
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Request.java
@@ -0,0 +1,211 @@
+package org.intellimate.izou.sdk.server;
+
+import org.intellimate.izou.identification.AddOnInformation;
+import org.intellimate.izou.sdk.Context;
+
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
+
+/**
+ * @author LeanderK
+ * @version 1.0
+ */
+public class Request implements org.intellimate.izou.server.Request {
+ private final org.intellimate.izou.server.Request request;
+ private final Matcher matcher;
+ private final String shortenedUrl;
+ private final AddOnInformation source;
+ private final String token;
+ private final Map> queryParams;
+
+ public Request(org.intellimate.izou.server.Request request, Matcher matcher, Context context) {
+ this(request, matcher, null, context);
+ }
+
+ private Request(org.intellimate.izou.server.Request request, Matcher matcher, String shortenedUrl, Context context) {
+ this.request = request;
+ this.matcher = matcher;
+ if (shortenedUrl == null) {
+ this.shortenedUrl = shortenUrl(request.getUrl());
+ } else {
+ this.shortenedUrl = shortenedUrl;
+ }
+ List sourceList = request.getParams().get("app");
+ if (sourceList == null || sourceList.isEmpty()) {
+ source = null;
+ } else {
+ String source = sourceList.get(0);
+ int id = -1;
+ try {
+ id = Integer.parseInt(source);
+ } catch (NumberFormatException e) {}
+ if (id != -1) {
+ this.source = context.getAddOns().getAddOnInformation(id)
+ .orElse(null);
+ } else {
+ this.source = context.getAddOns().getAddOnInformation(source)
+ .orElse(null);
+ }
+ }
+ List tokenList = request.getParams().get("token");
+ if (tokenList != null && !tokenList.isEmpty()) {
+ this.token = tokenList.get(0);
+ } else {
+ this.token = null;
+ }
+
+ queryParams = getQueryMap(request.getUrl());
+ }
+
+ public Request(org.intellimate.izou.server.Request request, Matcher matcher, String shortenedUrl,
+ AddOnInformation source, String token, Map> queryParams) {
+ this.request = request;
+ this.matcher = matcher;
+ this.shortenedUrl = shortenedUrl;
+ this.source = source;
+ this.token = token;
+ this.queryParams = queryParams;
+ }
+
+ private String shortenUrl(String fullUrl) {
+ String withoutQuery = fullUrl;
+ int index = fullUrl.indexOf("?");
+ if (index != -1) {
+ withoutQuery = fullUrl.substring(0, index);
+ }
+ String withOutTrailingSlash;
+ if (withoutQuery.endsWith("/")) {
+ withOutTrailingSlash = withoutQuery.substring(0, fullUrl.length());
+ } else {
+ withOutTrailingSlash = withoutQuery;
+ }
+ String shortString;
+ if (withOutTrailingSlash.startsWith("/apps/dev")) {
+ shortString = withOutTrailingSlash.replaceFirst("/apps/dev/\\w+", "");
+ } else {
+ shortString = withOutTrailingSlash.replaceFirst("/apps/\\d+", "");
+ }
+ if (shortString.isEmpty()) {
+ shortString = "/";
+ }
+ return shortString;
+ }
+
+ private Map> getQueryMap(String url) {
+ int index = url.indexOf("?");
+ if (index != -1 && index != (url.length() -1)) {
+ String queryParams = url.substring(index + 1, url.length());
+ String[] split = queryParams.split("&");
+
+ return Arrays.stream(split)
+ .map(argument -> argument.split("="))
+ .filter(arguments -> arguments.length <= 2)
+ .filter(arguments -> arguments.length >= 1)
+ .collect(Collectors.toMap(
+ (String[] arguments) -> {
+ try {
+ return URLDecoder.decode(arguments[0], "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new InternalServerErrorException("Unable to decode query parameter: "+arguments[0], e);
+ }
+ },
+ (String[] arguments) -> {
+ List arrayList = new ArrayList<>();
+ if (arguments.length == 1) {
+ return arrayList;
+ } else {
+ try {
+ arrayList.add(URLDecoder.decode(arguments[1], "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new InternalServerErrorException("Unable to decode query parameter: "+arguments[1], e);
+ }
+ return arrayList;
+ }
+ },
+ (arrayList, arrayList2) -> {
+ arrayList.addAll(arrayList2);
+ return arrayList;
+ }
+ ));
+
+ } else {
+ return new HashMap<>();
+ }
+ }
+
+ @Override
+ public String getUrl() {
+ return request.getUrl();
+ }
+
+ @Override
+ public Map> getParams() {
+ return request.getParams();
+ }
+
+ @Override
+ public String getMethod() {
+ return request.getMethod();
+ }
+
+ @Override
+ public String getContentType() {
+ return request.getContentType();
+ }
+
+ @Override
+ public int getContentLength() {
+ return request.getContentLength();
+ }
+
+ @Override
+ public InputStream getData() {
+ return request.getData();
+ }
+
+ /**
+ * returns the source of the request, if it came from an app
+ * @return the information about the Addon
+ */
+ public Optional getSource() {
+ return Optional.ofNullable(source);
+ }
+
+ /**
+ * returns the Access token of the app, only present if the app is calling itself!
+ * @return the token if the request comes from the app itself
+ */
+ public Optional getToken() {
+ return Optional.ofNullable(token);
+ }
+
+ /**
+ * returns the named group from the regex
+ * @param name the name of the group
+ * @return the resulting capture, or null
+ */
+ public String getGroup(String name) {
+ return matcher.group(name);
+ }
+
+ /**
+ * this string does not contain {@code /apps/id} or {@code /apps/dev/name} and also no slash at the end, also no query params
+ * @return the shortened url
+ */
+ public String getShortUrl() {
+ return shortenedUrl;
+ }
+
+ /**
+ * sets the matcher for the request
+ * @param matcher the matcher
+ * @return the request
+ */
+ public Request setMatcher(Matcher matcher) {
+ return new Request(request, matcher, shortenedUrl, source, token, queryParams);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Response.java b/src/main/java/org/intellimate/izou/sdk/server/Response.java
new file mode 100644
index 0000000..18589f3
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Response.java
@@ -0,0 +1,131 @@
+package org.intellimate.izou.sdk.server;
+
+import com.google.protobuf.Message;
+import com.google.protobuf.MessageOrBuilder;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * represents an response.
+ *
+ * this class is immutable!
+ *
+ * @author LeanderK
+ * @version 1.0
+ */
+public class Response implements org.intellimate.izou.server.Response {
+ private final int status;
+ private final Map> headers;
+ private final String contentType;
+ private final long dataSize;
+ private final InputStream stream;
+
+ public Response() {
+ this(-1, new HashMap<>(), null, -1, null);
+ }
+
+ public Response(int status, Map> headers, String contentType, String data) {
+ this(status, headers, contentType, data.getBytes(Charset.forName("UTF-8")));
+ }
+
+ public Response(int status, Map> headers, String contentType, byte[] data) {
+ this(status, headers, contentType, data.length, new ByteArrayInputStream(data));
+ }
+
+ public Response(int status, Map> headers, String contentType, long dataSize, InputStream stream) {
+ this.status = status;
+ this.headers = headers;
+ this.contentType = contentType;
+ this.dataSize = dataSize;
+ this.stream = stream;
+ }
+
+ @Override
+ public int getStatus() {
+ return status;
+ }
+
+ /**
+ * sets the status
+ * @param status the status to set
+ * @return a new Response with the applied status
+ */
+ @SuppressWarnings("unused")
+ public Response setStaus(int status) {
+ return new Response(status, headers, contentType, dataSize, stream);
+ }
+
+ @Override
+ public Map> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * sets the headers
+ * @param headers the headers to set
+ * @return a new Response with the applied headers
+ */
+ @SuppressWarnings("unused")
+ public Response setHeaders(Map> headers) {
+ return new Response(status, headers, contentType, dataSize, stream);
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * sets the content-type
+ * @param contentType the content-type to set
+ * @return a new Response with the applied content-type
+ */
+ @SuppressWarnings("unused")
+ public Response setContentType(String contentType) {
+ return new Response(status, headers, contentType, dataSize, stream);
+ }
+
+ @Override
+ public long getDataSize() {
+ return dataSize;
+ }
+
+ @Override
+ public InputStream getData() {
+ return stream;
+ }
+
+ /**
+ * sets the data
+ * @param data the data to set
+ * @return a new Response with the applied data
+ */
+ public Response setData(byte[] data) {
+ return new Response(status, headers, contentType, data.length, new ByteArrayInputStream(data));
+ }
+
+ /**
+ * sets the data
+ * @param dataSize the size of the data
+ * @param stream the stream
+ * @return a new Response with the applied data
+ */
+ @SuppressWarnings("unused")
+ public Response setData(int dataSize, InputStream stream) {
+ return new Response(status, headers, contentType, dataSize, stream);
+ }
+
+ /**
+ * performs basic validity checking for the response
+ * @return true if valid, false if not
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean isValidResponse() {
+ return status != -1 && contentType != null && !contentType.isEmpty() && dataSize != -1 && stream != null;
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Route.java b/src/main/java/org/intellimate/izou/sdk/server/Route.java
new file mode 100644
index 0000000..6fa6023
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Route.java
@@ -0,0 +1,244 @@
+package org.intellimate.izou.sdk.server;
+
+import org.intellimate.izou.sdk.Context;
+import org.intellimate.izou.sdk.util.AddOnModule;
+import org.intellimate.izou.sdk.util.FireEvent;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.aspectj.weaver.tools.cache.SimpleCacheFactory.path;
+
+/**
+ * a route in the router
+ * @author LeanderK
+ * @version 1.0
+ */
+//TODO automatically add things for Ajax
+@SuppressWarnings("WeakerAccess")
+public class Route extends AddOnModule implements HandlerHelper, FireEvent {
+ /**
+ * the regex route
+ */
+ private final String route;
+ private final Pattern pattern;
+ private final List handlers = new ArrayList<>();
+ private final Context context;
+ private final String addOnPackageName;
+ private final boolean internal;
+
+ public Route(Context context, String route, String addOnPackageName, boolean internal) {
+ super(context, addOnPackageName+"."+route);
+ this.context = context;
+ String realRoute = route;
+ if (route.equals("/")) {
+ realRoute = "";
+ }
+ this.route = realRoute;
+ this.pattern = Pattern.compile(route);
+ this.addOnPackageName = addOnPackageName;
+ this.internal = internal;
+ }
+
+ /**
+ * maybe handles a request
+ * @param request the request
+ * @return the request
+ */
+ Optional handle(Request request) {
+ if (internal && !request.getToken().isPresent()) {
+ return Optional.empty();
+ }
+ Matcher matcher = pattern.matcher(request.getShortUrl());
+ if (matcher.matches()) {
+ Request internalRequest = request.setMatcher(matcher);
+ return handlers.stream()
+ .map(handler -> handler.handle(internalRequest))
+ .filter(Optional::isPresent)
+ .findAny()
+ .orElseThrow(() -> new NotFoundException(request));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * returns the current context
+ * @return the current context
+ */
+ @SuppressWarnings("unused")
+ public Context getContext() {
+ return context;
+ }
+
+ /**
+ * returns the active route
+ * @return the route
+ */
+ @SuppressWarnings("unused")
+ public String getRoute() {
+ return route;
+ }
+
+ /**
+ * handles all the get-Requests on the route
+ * @param handleFunction the function to use for the get-requests
+ */
+ @SuppressWarnings("unused")
+ public void get(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.GET, false, handleFunction));
+ }
+
+ /**
+ * handles all the get-Requests on the route with the authentication of this app
+ * @param handleFunction the function to use for the get-requests
+ */
+ @SuppressWarnings("unused")
+ public void getInternal(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.GET, true, handleFunction));
+ }
+
+ /**
+ * handles all the put-Requests on the route
+ * @param handleFunction the function to use for the put-requests
+ */
+ @SuppressWarnings("unused")
+ public void put(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.PUT, false, handleFunction));
+ }
+
+ /**
+ * handles all the put-Requests on the route with the authentication of this app
+ * @param handleFunction the function to use for the put-requests
+ */
+ @SuppressWarnings("unused")
+ public void putInternal(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.PUT, true, handleFunction));
+ }
+
+ /**
+ * handles all the patch-Requests on the route
+ * @param handleFunction the function to use for the patch-requests
+ */
+ @SuppressWarnings("unused")
+ public void patch(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.PATCH, false, handleFunction));
+ }
+
+ /**
+ * handles all the patch-Requests on the route with the authentication of this app
+ * @param handleFunction the function to use for the patch-requests
+ */
+ @SuppressWarnings("unused")
+ public void patchInternal(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.PATCH, true, handleFunction));
+ }
+
+ /**
+ * handles all the post-Requests on the route
+ * @param handleFunction the function to use for the post-requests
+ */
+ @SuppressWarnings("unused")
+ public void post(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.POST, false, handleFunction));
+ }
+
+ /**
+ * handles all the post-Requests on the route with the authentication of this app
+ * @param handleFunction the function to use for the post-requests
+ */
+ @SuppressWarnings("unused")
+ public void postInternal(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.POST, true, handleFunction));
+ }
+
+ /**
+ * handles all the delete-Requests on the route
+ * @param handleFunction the function to use for the delete-requests
+ */
+ @SuppressWarnings("unused")
+ public void delete(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.DELETE, false, handleFunction));
+ }
+
+ /**
+ * handles all the delete-Requests on the route with the authentication of this app
+ * @param handleFunction the function to use for the delete-requests
+ */
+ @SuppressWarnings("unused")
+ public void deleteInternal(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.DELETE, true, handleFunction));
+ }
+
+ /**
+ * handles all the options-Requests on the route
+ * @param handleFunction the function to use for the options-requests
+ */
+ public void options(Function handleFunction) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.OPTIONS, true, handleFunction));
+ }
+
+ /**
+ * serves the files at path for the get-requests, this method allows querying subdirectories.
+ * @param path the path (must be a directory)
+ */
+ @SuppressWarnings("unused")
+ public void files(String path) {
+ files(path, true);
+ }
+
+ /**
+ * serves the files at path for the get-requests, this is public to the other addons
+ * @param path the path (must be a directory)
+ * @param subdirectory whether to allow querying subdirectories
+ */
+ @SuppressWarnings("unused")
+ public void files(String path, boolean subdirectory) {
+ handlers.add(new DefaultHandler(context, addOnPackageName, route, Method.GET, false, request -> {
+ String[] split = request.getShortUrl().split("\\\\");
+ if (!subdirectory && split.length != 1) {
+ throw new NotFoundException(request.getUrl() + " not found");
+ }
+ String subpath = Arrays.stream(split).collect(Collectors.joining(File.separator));
+ File file = new File(path + File.separator + subpath);
+ return sendFile(request, file);
+ }));
+ }
+
+ /**
+ * handles all the Requests on the route
+ * @param handleFunction the function to use for the requests
+ */
+ @SuppressWarnings("unused")
+ public void all(Function handleFunction) {
+ handlers.add(new Handler() {
+ @Override
+ public Optional handle(Request request) {
+ Response response = handleFunction.apply(request);
+ sanityCheck(response);
+ return Optional.of(response);
+ }
+
+ @Override
+ public Context getContext() {
+ return context;
+ }
+ });
+ }
+
+ /**
+ * adds an handler
+ * @param handler the handler to add
+ */
+ @SuppressWarnings("unused")
+ public void addHandler(Handler handler) {
+ handlers.add(handler);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/Router.java b/src/main/java/org/intellimate/izou/sdk/server/Router.java
new file mode 100644
index 0000000..bf457ac
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/Router.java
@@ -0,0 +1,168 @@
+package org.intellimate.izou.sdk.server;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import org.intellimate.izou.sdk.Context;
+import org.intellimate.server.proto.ErrorResponse;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+/**
+ * a simple router, please look at {@link org.intellimate.izou.sdk.server.properties.PropertiesRouter} for an example.
+ * @author LeanderK
+ * @version 1.0
+ */
+public abstract class Router implements HandlerHelper {
+ private final String addOnPackageName;
+ private List routes = new ArrayList<>();
+ private Map, BiFunction> exceptionHandlers = new HashMap<>();
+ private final Context context;
+ private static JsonFormat.Printer PRINTER = JsonFormat.printer().includingDefaultValueFields();
+
+ /**
+ * creates a new Router
+ * @param context the context to use
+ */
+ public Router(Context context) {
+ this.context = context;
+ this.addOnPackageName = getClass().getPackage().toString();
+
+ exception(InternalServerErrorException.class, (request, e) -> {
+ getContext().getLogger().error("an internal error occurred while handling " + request.getShortUrl(), e);
+ return ErrorResponse.newBuilder().setCode("an internal error occurred").setDetail(e.getMessage()).build();
+ }, 500);
+
+ exception(NotFoundException.class, (request, e) -> {
+ getContext().getLogger().debug("not found " + request.getShortUrl(), e);
+ return ErrorResponse.newBuilder().setCode("not found").setDetail(e.getMessage()).build();
+ }, 404);
+
+ exception(BadRequestException.class, (request, e) -> {
+ getContext().getLogger().error("bad request " + request.getShortUrl(), e);
+ return ErrorResponse.newBuilder().setCode("bad request").setDetail(e.getMessage()).build();
+ }, 400);
+ }
+
+ public Response handle(org.intellimate.izou.server.Request request) {
+ org.intellimate.izou.sdk.server.Request internalRequest = new org.intellimate.izou.sdk.server.Request(request, null, context);
+ Response response = routes.stream()
+ .map(route -> {
+ try {
+ return route.handle(internalRequest);
+ } catch (Exception e) {
+ return Optional.of(handleException(e, internalRequest));
+ }
+ })
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findAny()
+ .orElseGet(() -> {
+ ErrorResponse message = ErrorResponse.newBuilder()
+ .setCode("not found")
+ .setDetail("no matching route for: " + internalRequest.getShortUrl())
+ .build();
+ return constructMessageResponse(message, 404);
+ });
+ sanityCheck(response);
+ return response;
+ }
+
+ /**
+ * handles the thrown exceptions while generating content
+ * @param e the exception
+ * @return a response
+ */
+ private Response handleException(Exception e, Request request) {
+ Response response = exceptionHandlers.entrySet().stream()
+ .filter(entry -> entry.getKey().isAssignableFrom(e.getClass()))
+ .map(entry -> {
+ BiFunction value = entry.getValue();
+ return (Response) value.apply(request, e);
+ })
+ .findAny()
+ .orElseGet(() -> {
+ context.getLogger().error("en error occured while trying to server request for: " + request.getUrl(), e);
+ ErrorResponse error = ErrorResponse.newBuilder()
+ .setCode("an internal error occurred while handling " + request.getUrl())
+ .setDetail(e.getMessage())
+ .build();
+ return constructMessageResponse(error, 500);
+ });
+ if (response == null || !response.isValidResponse()) {
+ return stringResponse("error handling returned illegal response", 500);
+ } else {
+ return response;
+ }
+ }
+
+ /**
+ * returns the instance of Context
+ *
+ * @return the instance of Context
+ */
+ public Context getContext() {
+ return context;
+ }
+
+ /**
+ * adds a new route, accessible for all apps (this still depends on the handlers for the methods).
+ *
+ * Every route starts with a {@code /}! Example {@code /assets/new}.
+ * @param regex the regex-route to match
+ * @param consumer the consumer used to initialize the route
+ */
+ @SuppressWarnings({"WeakerAccess", "unused"})
+ public void route(String regex, Consumer consumer) {
+ Route route = new Route(context, regex, addOnPackageName, false);
+ consumer.accept(route);
+ routes.add(route);
+ }
+
+ /**
+ * adds a new route only accessible with the authentication of this app
+ *
+ * Every route starts with a {@code /}! Example {@code /assets/new}.
+ * @param regex the regex-route to match
+ * @param consumer the consumer used to initialize the route
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void routeInternal(String regex, Consumer consumer) {
+ Route route = new Route(context, regex, addOnPackageName, true);
+ consumer.accept(route);
+ routes.add(route);
+ }
+
+ /**
+ * registers an Exception to catch and handle
+ * @param handleFunction the function to call when an exception was triggered
+ */
+ public void exception(Class clazz, BiFunction handleFunction) {
+ exceptionHandlers.put(clazz, handleFunction);
+ }
+
+ /**
+ * registers an Exception to catch and handle with the standard error-response
+ * @param handleFunction the function to call when an exception was triggered
+ */
+ public void exception(Class clazz, BiFunction handleFunction, int status) {
+ exception(clazz, (request, t) -> {
+ ErrorResponse errorResponse = handleFunction.apply(request, t);
+ return constructMessageResponse(errorResponse, status);
+ });
+ }
+
+ private Response constructMessageResponse(Message message, int status) {
+ String response;
+ try {
+ response = PRINTER.print(message);
+ } catch (InvalidProtocolBufferException e) {
+ getContext().getLogger().debug("unable to print error-message", e);
+ return new Response(status, new HashMap<>(), "text/plain", "unable to print error-message, " + e.getMessage());
+ }
+ return new Response(status, new HashMap<>(), "application/json", response);
+ }
+
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/SDKRouter.java b/src/main/java/org/intellimate/izou/sdk/server/SDKRouter.java
new file mode 100644
index 0000000..af310fa
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/SDKRouter.java
@@ -0,0 +1,98 @@
+package org.intellimate.izou.sdk.server;
+
+import freemarker.template.*;
+import org.intellimate.izou.identification.AddOnInformation;
+import org.intellimate.izou.sdk.Context;
+import org.intellimate.izou.sdk.server.properties.PropertiesRouter;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.*;
+
+/**
+ * the router active in the sdk
+ * @author LeanderK
+ * @version 1.0
+ */
+public class SDKRouter extends Router {
+ private final Configuration cfg;
+
+ /**
+ * creates a new Router
+ *
+ * @param context the context to use
+ */
+ public SDKRouter(Context context) {
+ super(context);
+
+ cfg = new Configuration(new Version(2,3,24));
+ cfg.setClassForTemplateLoading(SDKRouter.class, "server/freemarker");
+ cfg.setDefaultEncoding("UTF-8");
+ cfg.setLocale(Locale.US);
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+
+ route("/template/properties", route -> {
+ route.get(this::getHtmlBaseFile);
+ });
+
+ route("/", route -> {
+ route.get(this::getResponse);
+ });
+
+ route("/index.html", route -> {
+ route.get(this::getResponse);
+ });
+ }
+
+ private Response getResponse(Request request) {
+ URL resource = getClass().getClassLoader().getResource("server/static/greetings.html");
+ try {
+ File file = new File(resource.toURI());
+ return sendFile(request, file);
+ } catch (URISyntaxException e) {
+ throw new InternalServerErrorException("Unable to open file", e);
+ }
+ }
+
+ private Response getHtmlBaseFile(Request request) {
+ AddOnInformation addOnInformation = request.getSource()
+ .orElseThrow(() -> new BadRequestException("this route is only intended for apps"));
+
+ List tokenList = request.getParams().get("token");
+ if (tokenList == null || tokenList.isEmpty()) {
+ throw new BadRequestException("query parameter token is missing");
+ }
+ String token = tokenList.get(0);
+ if (token == null || token.isEmpty()) {
+ throw new BadRequestException("query parameter token is missing");
+ }
+
+ String urlProperties = constructLinkToAddon(addOnInformation, "/properties");
+ String urlDescription = constructLinkToAddon(addOnInformation, "/description");
+
+ Map input = new HashMap();
+ input.put("app_name", addOnInformation.getArtifactID());
+ input.put("url_properties", urlProperties);
+ input.put("url_description", urlDescription);
+ input.put("token", token);
+
+ Template template;
+ try {
+ template = cfg.getTemplate("properties.ftl");
+ } catch (IOException e) {
+ throw new InternalServerErrorException("unable to load template", e);
+ }
+
+ StringWriter stringWriter = new StringWriter();
+ try {
+ template.process(input, stringWriter);
+ } catch (TemplateException | IOException e) {
+ throw new InternalServerErrorException("an error occured while templating", e);
+ }
+ String html = stringWriter.toString();
+ return new Response(200, new HashMap<>(), "text/html", html);
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/server/properties/PropertiesRouter.java b/src/main/java/org/intellimate/izou/sdk/server/properties/PropertiesRouter.java
new file mode 100644
index 0000000..b04226d
--- /dev/null
+++ b/src/main/java/org/intellimate/izou/sdk/server/properties/PropertiesRouter.java
@@ -0,0 +1,145 @@
+package org.intellimate.izou.sdk.server.properties;
+
+import com.google.common.io.ByteStreams;
+import org.intellimate.izou.identification.AddOnInformation;
+import org.intellimate.izou.sdk.Context;
+import org.intellimate.izou.sdk.server.*;
+
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Optional;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+
+/**
+ * the default router used for serving the default-properties file.
+ * @author LeanderK
+ * @version 1.0
+ */
+public class PropertiesRouter extends Router {
+ private Lock writeLock = new ReentrantLock();
+ private final Consumer validatorFunction;
+ @SuppressWarnings("WeakerAccess")
+ protected AddOnInformation addOnInformation;
+
+
+ /**
+ * creates a new Router
+ *
+ * @param context the context to use
+ * @param validatorFunction can be provided to check the syntax of the temporary submitted file,
+ * should return empty if valid, or an exception if not
+ * (the server will display the detail if it is an BadRequest).
+ *
+ */
+ @SuppressWarnings("WeakerAccess")
+ public PropertiesRouter(Context context, Consumer validatorFunction) {
+ super(context);
+ this.validatorFunction = validatorFunction;
+
+ addOnInformation = context.getAddOns().getAddOnInformation("org.intellimate.izou.sdk")
+ .orElse(null);
+
+ init();
+ }
+
+ /**
+ * creates a new Router
+ *
+ * @param context the context to use
+ */
+ public PropertiesRouter(Context context) {
+ this(context, file -> Optional.empty());
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected void init() {
+ routeInternal("/properties", route -> {
+ route.get(request -> {
+ File propertiesFile = getContext().getPropertiesAssistant().getPropertiesFile();
+ return sendFile(request, propertiesFile)
+ .setContentType("text/plain");
+ });
+
+ route.patch(this::patchConfigFile);
+ });
+
+ routeInternal("/", route -> {
+ route.get(this::redirect);
+ });
+
+ routeInternal("/index.html", route -> {
+ route.get(this::redirect);
+ });
+ }
+
+ /**
+ * creates the redirect to the
+ * @param request the request to handle
+ * @return the redirect
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected Response redirect(Request request) {
+ if (addOnInformation == null) {
+ throw new InternalServerErrorException("Unable to get AddOnInformation for SDK");
+ }
+ String redirect = constructLinkToAddon(addOnInformation, "/");
+ redirect = redirect + "?token=" + request.getToken()
+ .orElseThrow(() -> new BadRequestException("unable to extract authentication token"));
+ return createRedirect(redirect);
+ }
+
+ /**
+ * the logic for the patch-operation on the config-file
+ * @param request the request to work on
+ * @return a response
+ */
+ @SuppressWarnings("WeakerAccess")
+ protected Response patchConfigFile(Request request) {
+ if (!request.getContentType().equals("text/plain")) {
+ throw new BadRequestException("Content type must be: text/plain");
+ }
+ boolean locked = writeLock.tryLock();
+ if (!locked) {
+ throw new BadRequestException("Server is already processing another request");
+ }
+ FileOutputStream fileOutputStream = null;
+ File tempFile = null;
+ try {
+ File propertiesDir = getContext().getPropertiesAssistant().getPropertiesFile().getParentFile();
+ tempFile = new File(propertiesDir, "default.properties.tmp");
+ if (!tempFile.exists()) {
+ tempFile.createNewFile();
+ }
+ fileOutputStream = new FileOutputStream(tempFile);
+ long copied = ByteStreams.copy(request.getData(), fileOutputStream);
+ if (copied != request.getContentLength()) {
+ throw new BadRequestException("Actual size does not match advertised size");
+ }
+ validatorFunction.accept(tempFile);
+ File propertiesFile = getContext().getPropertiesAssistant().getPropertiesFile();
+ Files.copy(tempFile.toPath(), propertiesFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return sendFile(request, propertiesFile)
+ .setContentType("text/plain");
+ } catch (FileNotFoundException e) {
+ throw new InternalServerErrorException("unable to create temp-file", e);
+ } catch (IOException e) {
+ throw new InternalServerErrorException("an internal server-error occured while saving the properties file", e);
+ } finally {
+ writeLock.unlock();
+ if (tempFile != null) {
+ tempFile.delete();
+ }
+ if (fileOutputStream != null) {
+ try {
+ fileOutputStream.close();
+ } catch (IOException e) {
+ getContext().getLogger().error("unable to close OutputStream", e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/intellimate/izou/sdk/util/ResourceUser.java b/src/main/java/org/intellimate/izou/sdk/util/ResourceUser.java
index 0314d0c..54d9392 100644
--- a/src/main/java/org/intellimate/izou/sdk/util/ResourceUser.java
+++ b/src/main/java/org/intellimate/izou/sdk/util/ResourceUser.java
@@ -16,7 +16,7 @@
//TODO: Leander the return type optional is really unnecessary, i don't think the information resulting from it is
// useful. We should implement the tip and return CompletableFuture and log if there was an error obtaining the ID.
// The question is whether to archive this without breaking backwards compatibility (might turn really ugly and i like
-// the current method name)
+// the current method name) - Leander: i know, but backward-compatibility is really bad here (no overloading based on return type)
public interface ResourceUser extends ContextProvider, Identifiable {
/**
* generates the specified resource from the first matching ResourceBuilder (use the ID if you want to be sure).
diff --git a/src/main/resources/server/freemarker/properties.ftl b/src/main/resources/server/freemarker/properties.ftl
new file mode 100644
index 0000000..f48c738
--- /dev/null
+++ b/src/main/resources/server/freemarker/properties.ftl
@@ -0,0 +1,92 @@
+
+
+
+
+ ${app_name} Properties
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/server/static/greetings.html b/src/main/resources/server/static/greetings.html
new file mode 100644
index 0000000..bc78e82
--- /dev/null
+++ b/src/main/resources/server/static/greetings.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Izou SDK
+
+
+Hello, I am the Izou-SDK
+ I don't need to be configured, i am just working out of the box.
+ Have a good day!
+
+
\ No newline at end of file