diff --git a/.gitignore b/.gitignore index 51c8315..0ab51c9 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ local.properties .texlipse +/ci/local/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..996e44b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: java +jdk: +- oraclejdk8 +before_cache: +- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ +deploy: + provider: script + script: ci/deploy.sh + on: + branch: master +branches: + except: + - gh-pages +dd: +- openssl aes-256-cbc -K $encrypted_c0073d2c294f_key -iv $encrypted_c0073d2c294f_iv + -in ci/gradle.properties.enc -out gradle.properties -d +- openssl aes-256-cbc -K $encrypted_a724ed19665a_key -iv $encrypted_a724ed19665a_iv + -in ci/secring.gpg.enc -out ci/secring.gpg -d diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4b391f6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,136 @@ +import java.util.stream.Collectors + +group 'org.intellimate.izou' +version '0.19.0' + +apply plugin: 'java' +apply plugin: 'application' +apply plugin: 'com.bmuschko.nexus' +apply plugin: 'distribution' +apply plugin: 'io.codearte.nexus-staging' + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' + classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" + } +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +mainClassName = 'org.intellimate.izou.Main' + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + compile 'org.intellimate.izou:izou:1.16.5' + compile 'commons-lang:commons-lang:2.+' + compile 'org.freemarker:freemarker:2.3.24-incubating' + testCompile 'org.mockito:mockito-core:1.+' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + + + +def String projectName = 'IzouSDK'; +def String pluginClass = 'org.intellimate.izou.sdk.ZipFileManagerImpl'; +def String provider = 'intellimate'; +def List dependencies = []; +def String serverId = '1'; +def String repoURL = 'https://github.com/intellimate/IzouSDK'; + +jar { + baseName = projectName + manifest { + attributes 'Plugin-Class' : pluginClass, + 'Plugin-Id' : group + "." + project.name, + 'Plugin-Version' : project.version, + 'Plugin-Provider' : provider, + 'Plugin-Dependencies' : dependencies.stream().collect(Collectors.joining(",")), + 'Server-ID': serverId, + 'SDK-Version' : project.version, + 'Artifact-ID' : project.name + } +} + +modifyPom { + project { + name projectName + description 'the sdk used to program for izou' + url 'https://github.com/intellimate/IzouSDK' + + scm { + url 'https://github.com/intellimate/IzouSDK' + connection 'scm:git:git@github.com:intellimate/IzouSDK.git' + developerConnection 'scm:git:git@github.com:intellimate/IzouSDK.git' + } + + licenses { + license { + name 'TThe Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } +} + +nexus { + sign = true +} + +task release() { + description = 'Release artifacts to nexus' + dependsOn 'uploadArchives', 'closeAndPromoteRepository' + doLast { + println "Released $version" + } +} + +task plugin(type: Zip) { + baseName = project.name + version = project.version + def allExcluded = []; + doFirst { + def excluded = ["org.intellimate.izou"]; + excluded.addAll(dependencies); + def recursiveAdd + recursiveAdd = { dep -> + allExcluded.add("${dep.module.id.name}-${dep.module.id.version}.jar".toString()) + dep.children.each { childResolvedDep -> + if(dep in childResolvedDep.getParents() && childResolvedDep.getConfiguration() == 'compile'){ + recursiveAdd(childResolvedDep); + } + } + } + + configurations.compile.resolvedConfiguration.firstLevelModuleDependencies.each { dep -> + if (excluded.contains(dep.module.id.group)) { + recursiveAdd(dep) + } + } + } + from (configurations.compile) { + into ('lib/') + exclude { allExcluded.contains(it.file.name) } + exclude(allExcluded) + } + from (sourceSets.main.output.classesDir) { + into ('classes/') + } + from (sourceSets.main.resources) { + into ('classes/') + } + from (new File(project.buildDir, 'tmp/jar/')) { + into ('classes/META-INF/') + } +} + +plugin.dependsOn jar \ No newline at end of file diff --git a/build.properties b/build.properties new file mode 100644 index 0000000..e69de29 diff --git a/ci/deploy.sh b/ci/deploy.sh new file mode 100755 index 0000000..d9b72d7 --- /dev/null +++ b/ci/deploy.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +if [ "$TRAVIS_PULL_REQUEST" != "false" ] ; then + echo "Skipping deploy" + exit 0 +fi + +./../gradlew -p .. release \ No newline at end of file diff --git a/ci/gradle.properties.enc b/ci/gradle.properties.enc new file mode 100644 index 0000000..5813c07 --- /dev/null +++ b/ci/gradle.properties.enc @@ -0,0 +1,2 @@ + ‚<ÈX5ëdèãz,öpÇü'€ ÞZ2&ˆ²yª!ÍžÎh¨ñÃad«Ç‹ßªŠÈ­ZêÐZõÏA½5~n—‰X-çkºwüav(YÖGòul”DJ“ šž±ËDÓ¨Àäú~k +'*îÇ>^µÁ Š:°FLü瀠²D× ˆˆžƒÜ¥Ïtóü)˜4î­<¥Ó#›wN„5 \ No newline at end of file diff --git a/ci/secring.gpg.enc b/ci/secring.gpg.enc new file mode 100644 index 0000000..2e8a92e Binary files /dev/null and b/ci/secring.gpg.enc differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ca78035 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..31d2a74 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jun 05 18:02:54 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..832fdb6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml index e69932f..05fac7e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.intellimate.izou sdk - 0.18.2 + 0.19.0 IzouSDK the sdk used to program for izou https://github.com/intellimate/IzouSDK @@ -60,13 +60,18 @@ org.intellimate.izou izou - 1.15.6 + 1.16.5 commons-lang commons-lang 2.6 + + org.freemarker + freemarker + 2.3.24-incubating + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0f2b828 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'sdk' \ No newline at end of file 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..58dd3f9 --- /dev/null +++ b/src/main/java/org/intellimate/izou/sdk/server/HandlerHelper.java @@ -0,0 +1,167 @@ +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.net.URL; +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.getArtifactID(); + } + 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, URL file) throws NotFoundException { + String path = file.getFile(); + if (path == null) { + throw new InternalServerErrorException("requested file for url: " + request.getUrl() + " not found"); + } + return sendFile(request, new File(path)); + } + + /** + * 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 InternalServerErrorException("requested file for url: " + 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..935930e --- /dev/null +++ b/src/main/java/org/intellimate/izou/sdk/server/Route.java @@ -0,0 +1,240 @@ +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; + this.route = route; + 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..79cfc95 --- /dev/null +++ b/src/main/java/org/intellimate/izou/sdk/server/SDKRouter.java @@ -0,0 +1,93 @@ +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"); + return sendFile(request, resource); + } + + 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 + + + + + + +

+

${description}

+
+
+
+
+ + +
+ + \ 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