commit 9fd976a2982f0f275c32e456fc62875732a0d7d9 Author: Torma Kristóf Date: Wed May 20 22:25:28 2020 +0200 initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..cb4a360 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,68 @@ +kind: pipeline +type: docker +name: default + +steps: +- name: restore-cache-with-filesystem + image: meltwater/drone-cache + settings: + backend: "filesystem" + restore: true + cache_key: "{{ .Repo.Name }}" + archive_format: "gzip" + filesystem_cache_root: "/tmp/cache" + mount: + - 'build' + - '.gradle' + volumes: + - name: cache + path: /tmp/cache + +- name: build_application + image: openjdk:11-jdk + commands: + - ./gradlew build -x test + +- name: rebuild-cache-with-filesystem + image: meltwater/drone-cache:dev + pull: true + settings: + backend: "filesystem" + rebuild: true + cache_key: "{{ .Repo.Name }}" + archive_format: "gzip" + filesystem_cache_root: "/tmp/cache" + mount: + - 'build' + - '.gradle' + volumes: + - name: cache + path: /tmp/cache + +- name: kaniko + image: banzaicloud/drone-kaniko + settings: + registry: registry.kmlabz.com + repo: tormakris/${DRONE_REPO_NAME} + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + tags: + - latest + - ${DRONE_BUILD_NUMBER} + +- name: send telegram notification + image: appleboy/drone-telegram + settings: + token: + from_secret: TELEGRAM_TOKEN + to: + from_secret: TELEGRAM_TO_ID + when: + status: [ failure ] + +volumes: +- name: cache + host: + path: "/tmp/cache" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9affc3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.gradle +/.idea +/out +/build +*.iml +*.ipr +*.iws +*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..185c6ea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM openjdk:11-jre + +ENV APPLICATION_USER ktor +RUN useradd $APPLICATION_USER + +RUN mkdir /app +RUN chown -R $APPLICATION_USER /app + +USER $APPLICATION_USER + +COPY ./build/libs/input-service.jar /app/input-service.jar +WORKDIR /app + +CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "input-service.jar"] \ No newline at end of file diff --git a/api.yaml b/api.yaml new file mode 100644 index 0000000..9405601 --- /dev/null +++ b/api.yaml @@ -0,0 +1,151 @@ +swagger: "2.0" +info: + description: "This is the input interface of the Birbnetes system." + version: "1.1.0" + title: "Input Service" + contact: + email: "tormakristof@tormakristof.eu" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +host: "dev.k8s.tcloud.enginner" +basePath: "/api/input/v1" +tags: + - name: "input" + description: "Input Service interaction" +schemes: + - "https" + - "http" +paths: + /sample: + get: + summary: Get all stored input queries + operationId: getall + tags: + - input + responses: + "200": + description: Array of input objects + schema: + $ref: '#/definitions/InputResponse' + "404": + description: No object matching filter + schema: + $ref: '#/definitions/ApiResponse' + + post: + tags: + - "input" + summary: "uploads a sample into the system" + description: "" + operationId: "uploadFile" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - name: "description" + in: "formData" + description: "JSON" + required: true + type: "string" + format: "date" + - name: "file" + in: "formData" + description: "Wave file to upload" + required: true + type: "file" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/ApiResponse" + 415: + description: "Media type error" + schema: + $ref: "#/definitions/ApiResponse" + 400: + description: "JSON parse error" + schema: + $ref: "#/definitions/ApiResponse" + 417: + description: "JSON invalid schema" + schema: + $ref: "#/definitions/ApiResponse" + 469: + description: "No file found" + schema: + $ref: "#/definitions/ApiResponse" + 470: + description: "Description missing" + schema: + $ref: "#/definitions/ApiResponse" + + /sample/{tagID}: + get: + summary: Get input object by ID + operationId: getInput + tags: + - input + parameters: + - name: tagID + in: path + description: ID of input object file + required: true + type: string + format: uuid + responses: + "200": + description: input object + schema: + $ref: '#/definitions/InputSingeResponse' + "404": + description: Tag not found + schema: + $ref: '#/definitions/ApiResponse' + + +definitions: + InputSingeResponse: + type: "object" + properties: + status: + type: "string" + message: + $ref: '#/definitions/InputObject' + required: + - status + - message + + InputResponse: + type: "array" + items: + $ref: "#/definitions/InputObject" + + + InputObject: + type: "object" + properties: + tag: + type: "string" + format: "uuid" + date: + type: "string" + format: "date" + device_id: + type: "integer" + required: + - tag + - date + - device_id + + ApiResponse: + type: "object" + properties: + status: + type: "string" + message: + type: "string" + required: + - status + - message \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..02b0d77 --- /dev/null +++ b/build.gradle @@ -0,0 +1,76 @@ +buildscript { + repositories { + jcenter() + maven { url "https://plugins.gradle.org/m2/" } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.github.jengelman.gradle.plugins:shadow:5.2.0" + classpath "org.koin:koin-gradle-plugin:$koin_version" + } +} + +tasks.withType(JavaCompile) { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +apply plugin: 'kotlin' +apply plugin: "com.github.johnrengelman.shadow" +apply plugin: 'application' +apply plugin: 'koin' + +group 'com.kmalbz' +version '0.0.1' +mainClassName = "io.ktor.server.netty.EngineMain" + +sourceSets { + main.kotlin.srcDirs = main.java.srcDirs = ['src'] + test.kotlin.srcDirs = test.java.srcDirs = ['test'] + main.resources.srcDirs = ['resources'] + test.resources.srcDirs = ['testresources'] +} + +repositories { + mavenLocal() + jcenter() + maven { url 'https://kotlin.bintray.com/ktor' } +} + +dependencies { + compile 'org.postgresql:postgresql:42.2.2' + compile 'org.jetbrains.exposed:exposed-core:0.23.1' + compile 'org.jetbrains.exposed:exposed-dao:0.23.1' + compile 'org.jetbrains.exposed:exposed-jdbc:0.23.1' + compile 'org.jetbrains.exposed:exposed-java-time:0.23.1' + compile 'com.rabbitmq:amqp-client:2.7.1' + compile 'com.zaxxer:HikariCP:2.7.8' + compile 'com.viartemev:the-white-rabbit:0.0.5' + implementation "org.koin:koin-ktor:$koin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "io.ktor:ktor-server-netty:$ktor_version" + implementation "ch.qos.logback:logback-classic:$logback_version" + implementation "io.ktor:ktor-server-core:$ktor_version" + implementation "io.ktor:ktor-gson:$ktor_version" + implementation "io.ktor:ktor-server-host-common:$ktor_version" + implementation "io.ktor:ktor-client-core:$ktor_version" + implementation "io.ktor:ktor-client-core-jvm:$ktor_version" + implementation "io.ktor:ktor-client-apache:$ktor_version" + implementation "io.ktor:ktor-auth:$ktor_version" + testImplementation "io.ktor:ktor-server-tests:$ktor_version" +} + +kotlin.experimental.coroutines = 'enable' + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "11" + } +} + +shadowJar { + baseName = 'input-service' + classifier = null + version = null +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..75f8942 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3' + +services: + + input-service-postgres: + image: "postgres:12" + restart: "always" + volumes: + - "ktor-data:/var/lib/postgresql/data" + ports: + - "127.0.0.1:54321:5432" + environment: + POSTGRES_USER: "input-service" + POSTGRES_PASSWORD: "input-service" + POSTGRES_DB: "input-service" + + input-service: + image: "registry.kmlabz.com/tormakris/input-service" + restart: "always" + ports: + - "127.0.0.1:8080:8080" + environment: + DB_USER: "input-service" + DB_PASSWORD: "input-service" + POSTGRES_DB: "input-service" + DB_URL: "jdbc:postgresql://input-service-postgres:5432/input-service" + MQ_HOST: rabbitmq + MQ_USERNAME: rabbitmq + MQ_PASSWORD: rabbitmq + depends_on: + - rabbitmq + - output-service-postgres + + rabbitmq: + image: "rabbitmq:3-management" + hostname: "rabbitmq" + environment: + RABBITMQ_ERLANG_COOKIE: "SWQOKODSQALRPCLNMEQG" + RABBITMQ_DEFAULT_USER: "rabbitmq" + RABBITMQ_DEFAULT_PASS: "rabbitmq" + RABBITMQ_DEFAULT_VHOST: "/" + ports: + - "127.0.0.1:15672:15672" + - "127.0.0.1:5672:5672" + +volumes: + ktor-data: \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a6f0c66 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +ktor_version=1.3.2 +kotlin.code.style=official +kotlin_version=1.3.72 +logback_version=1.2.1 +koin_version=2.1.5 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..28861d2 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..6b47e69 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 04 18:02:20 CEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## 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 + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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 + +: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=%* + +: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/k8s/configmap.yml b/k8s/configmap.yml new file mode 100644 index 0000000..418aa85 --- /dev/null +++ b/k8s/configmap.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: input-service-config + labels: + app: input-service + namespace: birbnetes +data: + PORT: "8080" + DB_URL: "jdbc:postgresql://input-postgres:5432/input-service" + DB_USER: input-service + DB_PASSWORD: input-service-supersecret + MQ_HOST: birb-rabbitmq + MQ_USERNAME: user + MQ_PASSWORD: 1wZVQnP5vy \ No newline at end of file diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000..4bb7c9d --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: input-service + namespace: birbnetes + labels: + app: input-service +spec: + replicas: 1 + selector: + matchLabels: + app: input-service + strategy: + type: Recreate + template: + metadata: + labels: + app: input-service + spec: + containers: + - image: registry.kmlabz.com/tormakris/input-service + name: input-service + envFrom: + - configMapRef: + name: input-service-config + ports: + - containerPort: 8080 + imagePullSecrets: + - name: regcred \ No newline at end of file diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000..7f70eb5 --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: input-service + namespace: birbnetes + labels: + app: input-service +spec: + ports: + - name: input-service + port: 80 + targetPort: 8080 + protocol: TCP + selector: + app: input-service + type: ClusterIP \ No newline at end of file diff --git a/resources/application.conf b/resources/application.conf new file mode 100644 index 0000000..8a59613 --- /dev/null +++ b/resources/application.conf @@ -0,0 +1,21 @@ +ktor { + deployment { + port = 8080 + port = ${?PORT} + } + application { + modules = [ com.kmalbz.ApplicationKt.module ] + } + db { + jdbc = ${DB_URL} + user = ${DB_USER} + password = ${DB_PASSWORD} + } + mq{ + host = ${MQ_HOST} + username = ${MQ_USERNAME} + password = ${MQ_PASSWORD} + exchange = "input" + exchange = ${?MQ_EXCHANGE} + } +} \ No newline at end of file diff --git a/resources/logback.xml b/resources/logback.xml new file mode 100644 index 0000000..bdbb64e --- /dev/null +++ b/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1da238b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "input-sevice" diff --git a/src/Application.kt b/src/Application.kt new file mode 100644 index 0000000..9c1e403 --- /dev/null +++ b/src/Application.kt @@ -0,0 +1,71 @@ +package com.kmalbz + +import com.kmalbz.api.route.InputServiceServer +import io.ktor.application.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.http.* +import io.ktor.gson.* +import io.ktor.features.* +import org.apache.http.HttpException +import com.kmalbz.database.DatabaseFactory +import com.kmalbz.database.dao.InputObjects +import io.ktor.util.KtorExperimentalAPI +import com.rabbitmq.client.* +import com.typesafe.config.ConfigFactory +import io.ktor.config.HoconApplicationConfig +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction +import org.koin.ktor.ext.Koin + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +@KtorExperimentalAPI +@Suppress("unused") // Referenced in application.conf +fun Application.module() { + install(ContentNegotiation) { + gson { + } + } + + install(Koin) { + printLogger() + modules(com.kmalbz.di.injectionModule) + } + + DatabaseFactory.init() + transaction{ + SchemaUtils.create(InputObjects) + } + + val appConfig = HoconApplicationConfig(ConfigFactory.load()) + val factory = ConnectionFactory() + factory.host = appConfig.property("ktor.mq.host").getString() + factory.username = appConfig.property("ktor.mq.username").getString() + factory.password = appConfig.property("ktor.mq.password").getString() + + val connection = factory.newConnection() + val channel = connection.createChannel() + + val rabbitExchangeName = appConfig.property("ktor.mq.exchange").getString() + + channel.exchangeDeclare(rabbitExchangeName, BuiltinExchangeType.FANOUT) + val queueName = channel.queueDeclare().queue + channel.queueBind(queueName, rabbitExchangeName, "") + + routing { + install(StatusPages) { + exception { + call.respond(HttpStatusCode.BadRequest) + } + + exception { + call.respond(HttpStatusCode.NotAcceptable) + } + } + + InputServiceServer().apply { + registerOutput() + } + } +} diff --git a/src/api/model/ApiObject.kt b/src/api/model/ApiObject.kt new file mode 100644 index 0000000..6fda350 --- /dev/null +++ b/src/api/model/ApiObject.kt @@ -0,0 +1,8 @@ +package com.kmalbz.api.model + +import com.google.gson.annotations.SerializedName + +data class ApiObject( + @SerializedName("tag") val tag: String, + @SerializedName("probability") val probability: Double +) \ No newline at end of file diff --git a/src/api/route/InputServiceServer.kt b/src/api/route/InputServiceServer.kt new file mode 100644 index 0000000..429487e --- /dev/null +++ b/src/api/route/InputServiceServer.kt @@ -0,0 +1,71 @@ +package com.kmalbz.api.route + +import com.kmalbz.database.service.IInputObjectService +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import io.ktor.routing.Routing +import io.ktor.routing.get +import org.koin.ktor.ext.inject +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +/** + * Output Service - RDB + * + * This is the output interface of the Birbnetes system. + */ +class InputServiceServer { + /** + * output + */ + fun Routing.registerOutput() { + val resultObjectService by inject() + get("/output"){ + call.respond(resultObjectService.getAllResultObjects()) + } + + get("/output/filter/negative") { + val resultList = resultObjectService.getResultObjecLessthanProbability(0.5) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultList) + } + + get("/output/filter/positive") { + val resultList = resultObjectService.getResultObjecGreaterthanProbability(0.5) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultList) + } + + get("/output/filter/undecided") { + val resultList = resultObjectService.getResultObjecEqualsProbability(0.5) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultList) + } + + get("/output/after/{dateAfter}") { + val dateAfter = call.parameters["dateAfter"] ?: error(HttpStatusCode.NotAcceptable) + val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE + val localDateAfter : LocalDate = LocalDate.parse(dateAfter,dateTimeFormatter) + val resultList = resultObjectService.getResultObjectafterDate(localDateAfter) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultList) + } + + get("/output/before/{dateBefore}") { + val dateAfter = call.parameters["dateBefore"] ?: error(HttpStatusCode.NotAcceptable) + val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE + val localDateBefore : LocalDate = LocalDate.parse(dateAfter,dateTimeFormatter) + val resultList = resultObjectService.getResultObjectbeforeDate(localDateBefore) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultList) + } + + get("/output/{tagID}") { + val tagID = call.parameters["tagID"] ?: error(HttpStatusCode.NotAcceptable) + val resultObject = resultObjectService.getResultObjectbyTag(tagID) ?: call.respond(HttpStatusCode.NotFound) + + call.respond(resultObject) + } + } +} diff --git a/src/database/DatabaseFactory.kt b/src/database/DatabaseFactory.kt new file mode 100644 index 0000000..c2467e5 --- /dev/null +++ b/src/database/DatabaseFactory.kt @@ -0,0 +1,46 @@ +package com.kmalbz.database + +import com.typesafe.config.ConfigFactory +import com.zaxxer.hikari.* +import io.ktor.config.HoconApplicationConfig +import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +object DatabaseFactory { + + @KtorExperimentalAPI + private val appConfig = HoconApplicationConfig(ConfigFactory.load()) + @KtorExperimentalAPI + private val dbUrl = appConfig.property("ktor.db.jdbc").getString() + @KtorExperimentalAPI + private val dbUser = appConfig.property("ktor.db.user").getString() + @KtorExperimentalAPI + private val dbPassword = appConfig.property("ktor.db.password").getString() + + @KtorExperimentalAPI + fun init() { + Database.connect(hikari()) + } + + @KtorExperimentalAPI + private fun hikari(): HikariDataSource { + val config = HikariConfig() + config.driverClassName = "org.postgresql.Driver" + config.jdbcUrl = dbUrl + config.username = dbUser + config.password = dbPassword + config.maximumPoolSize = 3 + config.isAutoCommit = false + config.transactionIsolation = "TRANSACTION_REPEATABLE_READ" + config.validate() + return HikariDataSource(config) + } + + suspend fun dbQuery(block: () -> T): T = + withContext(Dispatchers.IO) { + transaction { block() } + } +} \ No newline at end of file diff --git a/src/database/dao/InputObjects.kt b/src/database/dao/InputObjects.kt new file mode 100644 index 0000000..71dad31 --- /dev/null +++ b/src/database/dao/InputObjects.kt @@ -0,0 +1,13 @@ +package com.kmalbz.database.dao + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.`java-time`.date +import java.time.LocalDate + +object InputObjects : IntIdTable() { + val tag: Column = varchar("tag",32) + val date: Column = date("date").default(LocalDate.now()) + val probability: Column = double("probability") + override val primaryKey = PrimaryKey(id, name = "PK_ResultObject_Id") +} \ No newline at end of file diff --git a/src/database/model/InputObject.kt b/src/database/model/InputObject.kt new file mode 100644 index 0000000..9b9ff36 --- /dev/null +++ b/src/database/model/InputObject.kt @@ -0,0 +1,13 @@ +package com.kmalbz.database.model + +import com.kmalbz.database.dao.InputObjects +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID + +class InputObject(id: EntityID): IntEntity(id) { + companion object : IntEntityClass(InputObjects) + var tag by InputObjects.tag + var date by InputObjects.date + var probability by InputObjects.probability +} \ No newline at end of file diff --git a/src/database/service/IInputObjectService.kt b/src/database/service/IInputObjectService.kt new file mode 100644 index 0000000..c43d8c1 --- /dev/null +++ b/src/database/service/IInputObjectService.kt @@ -0,0 +1,16 @@ +package com.kmalbz.database.service + +import com.kmalbz.api.model.ApiObject +import java.time.LocalDate + +interface IInputObjectService{ + fun addOne(apiObject: ApiObject) + suspend fun getAllResultObjects(): List + suspend fun getResultObjectbyTag(tag: String): ApiObject? + suspend fun getResultObjectbyDate(date: LocalDate): List? + suspend fun getResultObjectbeforeDate(date: LocalDate): List? + suspend fun getResultObjectafterDate(date: LocalDate): List? + suspend fun getResultObjecGreaterthanProbability(probability: Double): List? + suspend fun getResultObjecLessthanProbability(probability: Double): List? + suspend fun getResultObjecEqualsProbability(probability: Double): List? +} \ No newline at end of file diff --git a/src/database/service/InputObjectService.kt b/src/database/service/InputObjectService.kt new file mode 100644 index 0000000..5719c9a --- /dev/null +++ b/src/database/service/InputObjectService.kt @@ -0,0 +1,77 @@ +package com.kmalbz.database.service + +import com.kmalbz.database.DatabaseFactory.dbQuery +import com.kmalbz.database.model.InputObject +import com.kmalbz.database.dao.InputObjects +import com.kmalbz.api.model.ApiObject +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.LocalDate + + +class InputObjectService : IInputObjectService { + + override fun addOne(apiObject: ApiObject) { + transaction { + InputObject.new { + tag = apiObject.tag + probability = apiObject.probability + } + } + } + + override suspend fun getAllResultObjects(): List = dbQuery { + InputObjects.selectAll().map { toResultObject(it) } + } + + override suspend fun getResultObjectbyTag(tag: String): ApiObject? = dbQuery { + InputObjects.select { + (InputObjects.tag eq tag) + }.mapNotNull { toResultObject(it) } + .singleOrNull() + } + + override suspend fun getResultObjectbyDate(date: LocalDate): List? = dbQuery { + InputObjects.select { + (InputObjects.date eq date) + }.mapNotNull { toResultObject(it) } + } + + override suspend fun getResultObjectbeforeDate(date: LocalDate): List? = dbQuery { + InputObjects.select { + (InputObjects.date less date) + }.mapNotNull { toResultObject(it) } + } + + override suspend fun getResultObjectafterDate(date: LocalDate): List? = dbQuery { + InputObjects.select { + (InputObjects.date greater date) + }.mapNotNull { toResultObject(it) } + } + + override suspend fun getResultObjecGreaterthanProbability(probability: Double): List? = dbQuery { + InputObjects.select { + (InputObjects.probability greater probability) + }.mapNotNull { toResultObject(it) } + } + + override suspend fun getResultObjecLessthanProbability(probability: Double): List? = dbQuery { + InputObjects.select { + (InputObjects.probability less probability) + }.mapNotNull { toResultObject(it) } + } + + override suspend fun getResultObjecEqualsProbability(probability: Double): List? = dbQuery { + InputObjects.select { + (InputObjects.probability eq probability) + }.mapNotNull { toResultObject(it) } + } + + private fun toResultObject(row: ResultRow): ApiObject = + ApiObject( + tag = row[InputObjects.tag], + probability = row[InputObjects.probability] + ) +} \ No newline at end of file diff --git a/src/di/InjectionModule.kt b/src/di/InjectionModule.kt new file mode 100644 index 0000000..2465f41 --- /dev/null +++ b/src/di/InjectionModule.kt @@ -0,0 +1,10 @@ +package com.kmalbz.di + +import com.kmalbz.database.service.IInputObjectService +import com.kmalbz.database.service.InputObjectService +import org.koin.dsl.module +import org.koin.experimental.builder.singleBy + +val injectionModule = module(createdAtStart = true) { + singleBy() +}