commit 12e0eb37a855d83edfe84fd6a258720aee798f59 Author: Jaroslav Držík Date: Sun Jan 5 07:50:23 2025 +0100 First commit diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 0000000..e216ec9 --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + + { + "name": "test [run] (1)", + "type": "gradle", + "workingDir": "$PROJECT_DIR$", + "tasks": ["run"], + "args": [""], + "initScripts": { + "flmapper": "ext.mapPath = { path -> path }" + } + } + ] +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..969c9ae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + # Enable version updates for Gradle + - package-ecosystem: "gradle" + # Look for `build.gradle` in the `root` directory + directory: "/" + # Check for updates once daily + schedule: + interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5b333c3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '21' ] + + steps: + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + files: build/reports/kover/report.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f763b0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +.DS_Store +.idea/shelf +/android.tests.dependencies +/confluence/target +/dependencies +/dist +/local +/gh-pages +/ideaSDK +/clionSDK +/android-studio/sdk +out/ +/tmp +workspace.xml +*.versionsBackup +/idea/testData/debugger/tinyApp/classes* +/jps-plugin/testData/kannotator +/ultimate/dependencies +/ultimate/ideaSDK +/ultimate/out +/ultimate/tmp +/js/js.translator/testData/out/ +/js/js.translator/testData/out-min/ +.gradle/ +build/ +!**/src/**/build +!**/test/**/build +*.iml +!**/testData/**/*.iml +*.iml +/local.properties +/.idea +.DS_Store +/build +/captures diff --git a/.kotlin/errors/errors-1732378159970.log b/.kotlin/errors/errors-1732378159970.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1732378159970.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/.kotlin/errors/errors-1732477966480.log b/.kotlin/errors/errors-1732477966480.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1732477966480.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/.kotlin/errors/errors-1734368771992.log b/.kotlin/errors/errors-1734368771992.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1734368771992.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/.kotlin/errors/errors-1735820034213.log b/.kotlin/errors/errors-1735820034213.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1735820034213.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dec458e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM gradle:8-jdk21 AS build + +USER gradle +WORKDIR /app + +COPY build.gradle settings.gradle ./ +COPY src/ ./src + +RUN gradle installDist --no-daemon + +FROM eclipse-temurin:21-jre-jammy + +EXPOSE 8080 + +WORKDIR /app + +COPY --from=build /app/build/install/kotlin-ktor-exposed-starter . + +ENTRYPOINT ["./bin/kotlin-ktor-exposed-starter"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..354f842 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +[![Kotlin](https://img.shields.io/badge/kotlin-2.0.21-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Ktor](https://img.shields.io/badge/ktor-3.0.0-blue.svg)](https://github.com/ktorio/ktor) +[![Build](https://github.com/raharrison/kotlin-ktor-exposed-starter/workflows/Build/badge.svg)](https://github.com/raharrison/kotlin-ktor-exposed-starter/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/raharrison/kotlin-ktor-exposed-starter/branch/master/graph/badge.svg?token=v2k9oObm0C)](https://codecov.io/gh/raharrison/kotlin-ktor-exposed-starter) + +## Starter project to create a simple RESTful web service in Kotlin + +**Updated for Kotlin 2.0.21 and Ktor 3.0.0** + +Companion article: + +## Getting Started + +1. Clone the repo. +2. In the root directory execute `./gradlew run` +3. By default, the server will start on port `8080`. See below [Routes](#routes) section for more information. + +### Libraries used: + + - [Ktor](https://github.com/ktorio/ktor) - Kotlin async web framework + - [Netty](https://github.com/netty/netty) - Async web server + - [Kotlin Serialization](https://github.com/Kotlin/kotlinx.serialization) - JSON serialization/deserialization + - [Exposed](https://github.com/JetBrains/Exposed) - Kotlin SQL framework + - [H2](https://github.com/h2database/h2database) - Embeddable database + - [HikariCP](https://github.com/brettwooldridge/HikariCP) - High performance JDBC connection pooling + - [Flyway](https://flywaydb.org/) - Database migrations + - [JUnit 5](https://junit.org/junit5/), [AssertJ](http://joel-costigliola.github.io/assertj/) + and [Rest Assured](http://rest-assured.io/) for testing + - [Kover](https://github.com/Kotlin/kotlinx-kover) for code coverage, publishing + to [Codecov](https://about.codecov.io/) through GitHub Actions + +The starter project creates a new in-memory H2 database with one table for `Widget` instances. + +As ktor is async and based on coroutines, standard blocking JDBC may cause performance issues when used +directly on the main thread pool (as threads must be reused for other requests). Therefore, another dedicated thread +pool is created for all database queries, alongside connection pooling with HikariCP. + +### Routes: + +`GET /widgets` --> get all widgets in the database + +`GET /widgets/{id}` --> get one widget instance by id (integer) + +`POST /widgets` --> add a new widget to the database by providing a JSON object (converted to a NewWidget instance). e.g - + +```json +{ + "name": "new widget", + "quantity": 64 +} +``` + +returns + +```json +{ + "id": 3, + "name": "new widget", + "quantity": 64, + "dateUpdated": 1519926898 +} +``` + +`PUT /widgets` --> update an existing widgets name or quantity. Pass in the id in the JSON request to determine which record to update + +`DELETE /widgets/{id}` --> delete the widget with the specified id + +### Notifications (WebSocket) + +All updates (creates, updates and deletes) to `Widget` instances are served as notifications through a WebSocket endpoint: + +`WS /updates` --> returns `Notification` instances containing the change type, id and entity (if applicable) e.g: + +```json +{ + "type": "CREATE", + "id": 12, + "entity": { + "id": 12, + "name": "widget1", + "quantity": 5, + "dateUpdated": 1533583858169 + } +} +``` + +The websocket listener will also log out any text messages send by the client. Refer to [this blog post](https://ryanharrison.co.uk/2018/08/19/testing-websockets.html) for some useful tools to test the websocket behaviour. + +### Testing + +The sample Widget service and corresponding endpoints are also tested with 100% coverage. Upon startup of the main JUnit suite (via the `test` source folder), the server is started ready for testing and is torn down after all tests are run. + +- Unit testing of services with AssertJ - DAO and business logic is tested by initialising an in-memory H2 database with + Exposed, using the same schema as the main app. With this approach database queries are fully tested without any + mocking. +- Integration testing of endpoints using a fully running server with Rest Assured - routing tests/status codes/response + structure. This utilises the fact that Ktor is a small microframework that can be easily spun up and down as part of + the test suite. You could also use the special test engine that [Ktor provides](https://ktor.io/docs/testing.html), + however my preference is to always start a full version of the server so that HTTP behaviour can be tested without + relying on special internal mechanisms. +- Code coverage and reporting performed automatically by Kover as part of the Gradle build diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1cbe532 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,59 @@ +val ktorVersion = "3.0.0" +val exposedVersion = "0.55.0" +val h2Version = "2.3.232" +val hikariCpVersion = "5.1.0" +val flywayVersion = "10.20.1" +val logbackVersion = "1.5.12" +val assertjVersion = "3.26.3" +val restAssuredVersion = "5.5.0" +val junitVersion = "5.11.3" +val psqlVersion = "42.7.4" + +plugins { + kotlin("jvm") version "2.0.21" + kotlin("plugin.serialization") version "2.0.21" + id("org.jetbrains.kotlinx.kover") version "0.8.2" + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.ktor:ktor-server-core:$ktorVersion") + implementation("io.ktor:ktor-serialization:$ktorVersion") + implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.ktor:ktor-server-call-logging:$ktorVersion") + implementation("io.ktor:ktor-server-default-headers:$ktorVersion") + implementation("io.ktor:ktor-server-websockets:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.20") + implementation("com.github.seratch:kotliquery:1.9.0") + implementation("com.h2database:h2:$h2Version") + implementation("org.postgresql:postgresql:$psqlVersion") + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-json:$exposedVersion") + implementation("com.zaxxer:HikariCP:$hikariCpVersion") + implementation("org.flywaydb:flyway-core:$flywayVersion") + implementation("ch.qos.logback:logback-classic:$logbackVersion") + implementation("de.m3y.kformat:kformat:0.11") + + testImplementation("org.assertj:assertj-core:$assertjVersion") + testImplementation("io.rest-assured:rest-assured:$restAssuredVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("io.ktor:ktor-client-cio:$ktorVersion") +} + +application { + mainClass.set("MainKt") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 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..19cfad9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..3a8c0d1 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright � 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions �$var�, �${var}�, �${var:-default}�, �${var+SET}�, +# �${var#prefix}�, �${var%suffix}�, and �$( cmd )�; +# * compound commands having a testable exit status, especially �case�; +# * various built-in commands including �command�, �set�, and �ulimit�. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx1024m" "-Xms1024m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2c65c8d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kotlin-ktor-exposed-starter' diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt new file mode 100644 index 0000000..35852db --- /dev/null +++ b/src/main/kotlin/Main.kt @@ -0,0 +1,40 @@ +import io.ktor.serialization.kotlinx.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.calllogging.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.defaultheaders.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import service.DatabaseFactory +//import service.WidgetService +import util.JsonMapper +import web.index +//import web.widget + +fun Application.module() { + install(DefaultHeaders) + install(CallLogging) + install(WebSockets) { + contentConverter = KotlinxWebsocketSerializationConverter(JsonMapper.defaultMapper) + } + + install(ContentNegotiation) { + json(JsonMapper.defaultMapper) + } + + DatabaseFactory.connectAndMigrate() + + //val widgetService = WidgetService() + + routing { + index() + // widget(widgetService) + } + +} + +fun main(args: Array) { + EngineMain.main(args) +} \ No newline at end of file diff --git a/src/main/kotlin/dao/DictTypeDao.kt b/src/main/kotlin/dao/DictTypeDao.kt new file mode 100644 index 0000000..24d85a0 --- /dev/null +++ b/src/main/kotlin/dao/DictTypeDao.kt @@ -0,0 +1,12 @@ +package dao + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.DictTypes + +class DictTypeDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(DictTypes) + + var shortName by DictTypes.shortName + var fullName by DictTypes.fullName +} \ No newline at end of file diff --git a/src/main/kotlin/dao/DictionaryDao.kt b/src/main/kotlin/dao/DictionaryDao.kt new file mode 100644 index 0000000..8fc2b4d --- /dev/null +++ b/src/main/kotlin/dao/DictionaryDao.kt @@ -0,0 +1,12 @@ +package dao + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.Dictionaries + +class DictionaryDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Dictionaries) + + var name by Dictionaries.name + var fullName by Dictionaries.fullName +} \ No newline at end of file diff --git a/src/main/kotlin/dao/LanguageDao.kt b/src/main/kotlin/dao/LanguageDao.kt new file mode 100644 index 0000000..ff3dca0 --- /dev/null +++ b/src/main/kotlin/dao/LanguageDao.kt @@ -0,0 +1,12 @@ +package dao + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.Languages + +class LanguageDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Languages) + + var name by Languages.name + var shortName by Languages.shortName +} \ No newline at end of file diff --git a/src/main/kotlin/dao/PronunciationDao.kt b/src/main/kotlin/dao/PronunciationDao.kt new file mode 100644 index 0000000..580b4a2 --- /dev/null +++ b/src/main/kotlin/dao/PronunciationDao.kt @@ -0,0 +1,13 @@ +package dao + +import models.Translation +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.Pronunciations + +class PronunciationDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Pronunciations) + + var ipa by Pronunciations.ipa + var filename by Pronunciations.filename +} \ No newline at end of file diff --git a/src/main/kotlin/dao/PronunciationTypeDao.kt b/src/main/kotlin/dao/PronunciationTypeDao.kt new file mode 100644 index 0000000..03e9a13 --- /dev/null +++ b/src/main/kotlin/dao/PronunciationTypeDao.kt @@ -0,0 +1,12 @@ +package dao + +import models.Translation +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.PronunciationTypes + +class PronunciationTypeDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(PronunciationTypes) + + var name by PronunciationTypes.name +} \ No newline at end of file diff --git a/src/main/kotlin/dao/SuffixDao.kt b/src/main/kotlin/dao/SuffixDao.kt new file mode 100644 index 0000000..8f90e29 --- /dev/null +++ b/src/main/kotlin/dao/SuffixDao.kt @@ -0,0 +1,11 @@ +package dao + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.Suffixes + +class SuffixDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Suffixes) + + var text by Suffixes.text +} \ No newline at end of file diff --git a/src/main/kotlin/dao/TranslationDao.kt b/src/main/kotlin/dao/TranslationDao.kt new file mode 100644 index 0000000..681ebf5 --- /dev/null +++ b/src/main/kotlin/dao/TranslationDao.kt @@ -0,0 +1,14 @@ +package dao + +import models.Translation +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.dao.id.EntityID +import tables.Translations + +class TranslationDao(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Translations) + + var langName1 by Translations.langName1 + var langName2 by Translations.langName2 + var direction by Translations.direction +} \ No newline at end of file diff --git a/src/main/kotlin/db/migration/V1__test_connection.kt b/src/main/kotlin/db/migration/V1__test_connection.kt new file mode 100644 index 0000000..7c2c66b --- /dev/null +++ b/src/main/kotlin/db/migration/V1__test_connection.kt @@ -0,0 +1,193 @@ +package db.migration + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.transactions.transaction +import kotlin.collections.* +import kotliquery.* +import de.m3y.kformat.* +import de.m3y.kformat.Table.Hints +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.Json + +val format = Json { encodeDefaults = true } + +data class Dictionary( + val id: Int, + val lang1Id: Int, + val lang2Id: Int, + val name: String, + val fullName: String) + +data class DictType( + val id: Int, + val shortName: String, + val fullName: String +) +data class Language( + val id: Int, + val name: String, + val shortName: String +) +data class Translation( + val id: Int, + val dictionaryId: Int, + val lang1Id: Int, + val lang2Id: Int, + val lang1Name: String, + val lang2Name: String, + val direction: Int +) + +data class Pronunciation( + val id: Int, + val typeId: Int, + val ipa: String?, + val filename: String? +) + +data class PronunciationType( + val id: Int, + val name: String? +) +data class TermsPronunciation( + val termId: Int, + val pronunciationId: Int, +) + +data class Suffix( + val id: Int, + val text: String +) + +data class Term( + val id: Int, + val dictionaryId: Int, + val string1: String, + val string2: String, + val suffix1Id: Int?, + val suffix2Id: Int?, + val typeId: Int?, + val member: String?, + val order2: Int?, + val flags: String? +) +val toDictionary: (Row) -> Dictionary = { row -> + Dictionary( + row.int("id"), + row.int("lang1_id"), + row.int("lang2_id"), + row.string("name"), + row.string("full_name") + ) +} + +val toDictType: (Row) -> DictType = { row -> + DictType( + row.int("id"), + row.string("short_name"), + row.string("full_name") + ) +} +val toLanguage: (Row) -> Language = { row -> + Language( + row.int("id"), + row.string("name"), + row.string("short_name") + ) +} +val toTranslation: (Row) -> Translation = { row -> + Translation( + row.int("id"), + row.int("dictionary_id"), + row.int("lang1_id"), + row.int("lang2_id"), + row.string("lang_name1"), + row.string("lang_name2"), + row.int("direction"), + ) +} + +val toPronunciation: (Row) -> Pronunciation = { row -> + Pronunciation( + row.int("id"), + row.int("type_id"), + row.stringOrNull("ipa"), + row.stringOrNull("filename") + ) +} + +val toTermsPronunciation: (Row) -> TermsPronunciation = { row -> + TermsPronunciation( + row.int("term_id"), + row.int("pronunciation_id"), + ) +} + +val toSuffix: (Row) -> Suffix = { row -> + Suffix( + row.int("id"), + row.string("text") + ) +} +val toTerm: (Row) -> Term = { row -> + Term( + row.int("id"), + row.int("dictionary_id"), + row.string("string1"), + row.string("string2"), + row.intOrNull("suffix1_id"), + row.intOrNull("suffix2_id"), + row.intOrNull("type_id"), + row.stringOrNull("member"), + row.intOrNull("order2"), + row.stringOrNull("flags") + ) +} + +object SharedData { + var hashDict: Map = mapOf() + var hashDictType: Map = mapOf() + var hashLang: Map = mapOf() + var hashTransMap: Map = mapOf() + var hashPron: Map = mapOf() + var hashSuffix: Map = mapOf() + var hashTermPron : Map = mapOf() + var listTerm : List = listOf() + var getListofTerms : (Int) -> List = { + listOf() + } +} + +class V1__test_connection: BaseJavaMigration() { + override fun migrate(context: Context?) { + val session = sessionOf("jdbc:postgresql://nuc.lan:5432/dict", "dict_user", "PW4dbdict") + val allNameQuery = queryOf("select * from dictionary").map(toDictionary).asList + val allDict = session.run(allNameQuery) + SharedData.hashDict = allDict.associateBy { it.id } + val allDictTypeQuery = queryOf("select * from dict_type").map(toDictType).asList + val allDictType = session.run(allDictTypeQuery) + SharedData.hashDictType = allDictType.associateBy { it.id } + val allTranslationQuery = queryOf("select * from translation").map(toTranslation).asList + val allTranslation = session.run(allTranslationQuery) + SharedData.hashTransMap = allTranslation.associateBy { it.id } + val allLangQuery = queryOf("select * from language").map(toLanguage).asList + val allLanguages = session.run(allLangQuery) + SharedData.hashLang = allLanguages.associateBy { it.id } + val allSuffixQuery = queryOf("select * from suffix").map(toSuffix).asList + val allSuffix = session.run(allSuffixQuery) + SharedData.hashSuffix = allSuffix.associateBy { it.id } + val allPronunciationQuery = queryOf("select * from pronunciation").map(toPronunciation).asList + val allPronunciation = session.run(allPronunciationQuery) + SharedData.hashPron = allPronunciation.associateBy { it.id } + val allTermsPronunciationQuery = queryOf("select * from terms_pronunciations").map(toTermsPronunciation).asList + val allTermsPronunciation = session.run(allTermsPronunciationQuery) + SharedData.hashTermPron = allTermsPronunciation.associateBy { it.termId } + SharedData.getListofTerms = { + session.run(queryOf("select * from term WHERE dictionary_id=${it}").map(toTerm).asList) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/db/migration/V2__crete_settingsdb.kt b/src/main/kotlin/db/migration/V2__crete_settingsdb.kt new file mode 100644 index 0000000..ded30aa --- /dev/null +++ b/src/main/kotlin/db/migration/V2__crete_settingsdb.kt @@ -0,0 +1,53 @@ +package db.migration + +import dao.DictionaryDao +import kotlinx.serialization.decodeFromString +import tables.* +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.upsert + +class V2__create_settingsdb: BaseJavaMigration() { + override fun migrate(context: Context?) { + transaction { + SchemaUtils.create(Dictionaries, Languages, Translations, Settings) + + for ((di, lang) in SharedData.hashLang) { + Languages.insert { + it[id] = lang.id + it[name] = lang.name + it[shortName] = lang.shortName + } + } + + for ((di, dict) in SharedData.hashDict) { + Dictionaries.insert { + it[id] = dict.id + it[lang1] = dict.lang1Id + it[lang2] = dict.lang2Id + it[name] = dict.name + it[fullName] = dict.fullName + } + } + + + + for ((di, trans) in SharedData.hashTransMap) { + Translations.insert { + it[id] = trans.id + it[lang1Id] = trans.lang1Id + it[lang2Id] = trans.lang2Id + it[dictionaryId] = trans.dictionaryId + it[langName1] = trans.lang1Name + it[langName2] = trans.lang2Name + it[direction] = trans.direction + } + } + + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/db/migration/V3__create_dictionaries.kt b/src/main/kotlin/db/migration/V3__create_dictionaries.kt new file mode 100644 index 0000000..23bb4f3 --- /dev/null +++ b/src/main/kotlin/db/migration/V3__create_dictionaries.kt @@ -0,0 +1,117 @@ +package db.migration + +import dao.DictionaryDao +import kotlinx.serialization.decodeFromString +import tables.* +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.upsert +import kotlin.system.exitProcess + +class V3__create_dictionaries: BaseJavaMigration() { + override fun migrate(context: Context?) { + println(SharedData.hashDict) + //exitProcess(0) + for ((db_id,dict) in SharedData.hashDict) { + + val db_name = "jdbc:h2:file:/Users/jaro/data/${SharedData.hashLang[dict.lang1Id]?.shortName}-${SharedData.hashLang[dict.lang2Id]?.shortName}" + println(db_name) + val db = Database.connect(db_name) + transaction (db) { + SchemaUtils.create(Dictionaries, Languages, Translations, Pronunciations, PronunciationTypes, DictTypes, Suffixes, Terms) + + for ((di, lang) in SharedData.hashLang) { + Languages.insert { + it[id] = lang.id + it[name] = lang.name + it[shortName] = lang.shortName + } + } + + for ((di, d) in SharedData.hashDict) { + Dictionaries.insert { + it[id] = d.id + it[lang1] = d.lang1Id + it[lang2] = d.lang2Id + it[name] = d.name + it[fullName] = d.fullName + } + } + + for ((di, trans) in SharedData.hashTransMap) { + Translations.insert { + it[id] = trans.id + it[lang1Id] = trans.lang1Id + it[lang2Id] = trans.lang2Id + it[dictionaryId] = trans.dictionaryId + it[langName1] = trans.lang1Name + it[langName2] = trans.lang2Name + it[direction] = trans.direction + } + } + + PronunciationTypes.insert { + it[id] = 1 + it[name] = "English" + } + + PronunciationTypes.insert { + it[id] = 2 + it[name] = "American english" + } + + PronunciationTypes.insert { + it[id] = 3 + it[name] = "Business english" + } + + for ((di, pron) in SharedData.hashPron) { + Pronunciations.insert { + it[id] = pron.id + it[typeId] = pron.typeId + it[ipa] = pron.ipa + it[filename] = pron.filename + } + } + + for ((di, dt) in SharedData.hashDictType) { + DictTypes.insert { + it[id] = dt.id + it[shortName] = dt.shortName + it[fullName] = dt.fullName + } + } + + for ((di, ss) in SharedData.hashSuffix) { + Suffixes.insert { + it[id] = ss.id + it[text] = ss.text + } + } + + var dictTerms = SharedData.getListofTerms(db_id) + for (t in dictTerms) { + Terms.insert { + it[id] = t.id + it[string1] = t.string1 + it[string2] = t.string2 + it[suffix1] = t.suffix1Id + it[suffix2] = t.suffix2Id + it[type] = t.typeId + it[member] = t.member + it[order2] = t.order2 + it[flags] = format.decodeFromString(t.flags?.toString() ?:"[]") + } + } + dictTerms = listOf() + + } + + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/models/DictType.kt b/src/main/kotlin/models/DictType.kt new file mode 100644 index 0000000..e0e29ff --- /dev/null +++ b/src/main/kotlin/models/DictType.kt @@ -0,0 +1,14 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + + + +@Serializable +data class DictType( + val id: Int, + val shortName: String, + val fullName: String + ) + diff --git a/src/main/kotlin/models/Dictionary.kt b/src/main/kotlin/models/Dictionary.kt new file mode 100644 index 0000000..58cccb9 --- /dev/null +++ b/src/main/kotlin/models/Dictionary.kt @@ -0,0 +1,17 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + + + +@Serializable +data class Dictionary( + val id: Int, + val lang1Id: Int, + val lang2Id: Int, + val name: String?, + val fullName: String?, + val translations: List = emptyList() +) + diff --git a/src/main/kotlin/models/Language.kt b/src/main/kotlin/models/Language.kt new file mode 100644 index 0000000..72dd624 --- /dev/null +++ b/src/main/kotlin/models/Language.kt @@ -0,0 +1,15 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + + + +@Serializable +data class Language( + val id: Int, + val name: String, + val shortName: String +) + + diff --git a/src/main/kotlin/models/Pronunciation.kt b/src/main/kotlin/models/Pronunciation.kt new file mode 100644 index 0000000..684c57f --- /dev/null +++ b/src/main/kotlin/models/Pronunciation.kt @@ -0,0 +1,12 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + +@Serializable +data class Pronunciation( + val id: Int, + val typeId: Int, + val ipa: String?, + val filename: String? +) \ No newline at end of file diff --git a/src/main/kotlin/models/PronunciationType.kt b/src/main/kotlin/models/PronunciationType.kt new file mode 100644 index 0000000..3c601f4 --- /dev/null +++ b/src/main/kotlin/models/PronunciationType.kt @@ -0,0 +1,10 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + +@Serializable +data class PronunciationType( + val id: Int, + val name: String? +) \ No newline at end of file diff --git a/src/main/kotlin/models/Setting.kt b/src/main/kotlin/models/Setting.kt new file mode 100644 index 0000000..429db90 --- /dev/null +++ b/src/main/kotlin/models/Setting.kt @@ -0,0 +1,12 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + +@Serializable +data class Settings( + val id: Int, + val dictionary: Int?, + val lastSearch: String?, +) + diff --git a/src/main/kotlin/models/Suffix.kt b/src/main/kotlin/models/Suffix.kt new file mode 100644 index 0000000..8a89529 --- /dev/null +++ b/src/main/kotlin/models/Suffix.kt @@ -0,0 +1,11 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + + +@Serializable +data class Suffix( + val id: Int, + val text: String +) diff --git a/src/main/kotlin/models/Term.kt b/src/main/kotlin/models/Term.kt new file mode 100644 index 0000000..5232de8 --- /dev/null +++ b/src/main/kotlin/models/Term.kt @@ -0,0 +1,20 @@ +package models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import org.jetbrains.exposed.sql.Table + + +@Serializable +data class Term( + val id: Int, + val dictionaryId: Int, + val string1: String, + val string2: String, + val typeId: Int, + val suffix1Id: Int, + val suffix2Id: Int, + val member: String?, + val order2: Int, + val flags: JsonObject? +) diff --git a/src/main/kotlin/models/Translation.kt b/src/main/kotlin/models/Translation.kt new file mode 100644 index 0000000..2015013 --- /dev/null +++ b/src/main/kotlin/models/Translation.kt @@ -0,0 +1,17 @@ +package models + +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Table + + + +@Serializable +data class Translation( + val id: Int, + val dictionaryId: Int, + val lang1Id: Int, + val lang2Id: Int, + val lang1Name: String?, + val lang2Name: String?, + val direction: Int +) diff --git a/src/main/kotlin/service/DatabaseFactory.kt b/src/main/kotlin/service/DatabaseFactory.kt new file mode 100644 index 0000000..c3ccfc1 --- /dev/null +++ b/src/main/kotlin/service/DatabaseFactory.kt @@ -0,0 +1,54 @@ +package service + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.flywaydb.core.Flyway +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.LoggerFactory +import javax.sql.DataSource + +object DatabaseFactory { + + private val log = LoggerFactory.getLogger(this::class.java) + + fun connectAndMigrate() { + log.info("Initialising database") + val pool = hikari() + Database.connect(pool) + runFlyway(pool) + } + + private fun hikari(): HikariDataSource { + val config = HikariConfig().apply { + driverClassName = "org.h2.Driver" + jdbcUrl = "jdbc:h2:file:/Users/jaro/data/dict_settings" + maximumPoolSize = 3 + isAutoCommit = false + transactionIsolation = "TRANSACTION_REPEATABLE_READ" + validate() + } + return HikariDataSource(config) + } + + private fun runFlyway(datasource: DataSource) { + val flyway = Flyway.configure().dataSource(datasource).load() + try { + flyway.info() + flyway.migrate() + } catch (e: Exception) { + log.error("Exception running flyway migration", e) + throw e + } + log.info("Flyway migration has finished") + } + + suspend fun dbExec( + block: () -> T + ): T = withContext(Dispatchers.IO) { + transaction { block() } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/tables/DictType.kt b/src/main/kotlin/tables/DictType.kt new file mode 100644 index 0000000..0112ffb --- /dev/null +++ b/src/main/kotlin/tables/DictType.kt @@ -0,0 +1,8 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object DictTypes : IntIdTable() { + val shortName = varchar("short_name", 255) + val fullName = varchar("full_name", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Dictionary.kt b/src/main/kotlin/tables/Dictionary.kt new file mode 100644 index 0000000..89d3019 --- /dev/null +++ b/src/main/kotlin/tables/Dictionary.kt @@ -0,0 +1,10 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Dictionaries : IntIdTable() { + val lang1 = reference("lang1_id", Languages) + val lang2 = reference("lang2_id", Languages) + val name = varchar("name", 255) + val fullName = varchar("full_name", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Language.kt b/src/main/kotlin/tables/Language.kt new file mode 100644 index 0000000..bca9bc3 --- /dev/null +++ b/src/main/kotlin/tables/Language.kt @@ -0,0 +1,8 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Languages : IntIdTable() { + val name = varchar("name", 255) + val shortName = varchar("short_name", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Pronunciation.kt b/src/main/kotlin/tables/Pronunciation.kt new file mode 100644 index 0000000..345f423 --- /dev/null +++ b/src/main/kotlin/tables/Pronunciation.kt @@ -0,0 +1,10 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Pronunciations : IntIdTable() { + val typeId = reference("type_id",PronunciationTypes) + + val ipa = varchar("ipa", 255).nullable() + val filename = varchar("filename", 255).nullable() +} \ No newline at end of file diff --git a/src/main/kotlin/tables/PronunciationType.kt b/src/main/kotlin/tables/PronunciationType.kt new file mode 100644 index 0000000..4727799 --- /dev/null +++ b/src/main/kotlin/tables/PronunciationType.kt @@ -0,0 +1,7 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object PronunciationTypes : IntIdTable() { + val name = varchar("name", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Setting.kt b/src/main/kotlin/tables/Setting.kt new file mode 100644 index 0000000..854b6c6 --- /dev/null +++ b/src/main/kotlin/tables/Setting.kt @@ -0,0 +1,8 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Settings : IntIdTable() { + val dictionary = reference("dictionary_id",Dictionaries) + val lastSearch = varchar("last_search", 255).nullable() +} diff --git a/src/main/kotlin/tables/Suffix.kt b/src/main/kotlin/tables/Suffix.kt new file mode 100644 index 0000000..5dea41a --- /dev/null +++ b/src/main/kotlin/tables/Suffix.kt @@ -0,0 +1,7 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Suffixes : IntIdTable() { + val text = varchar("text", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Term.kt b/src/main/kotlin/tables/Term.kt new file mode 100644 index 0000000..2546db0 --- /dev/null +++ b/src/main/kotlin/tables/Term.kt @@ -0,0 +1,17 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.json.json +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import tables.Suffixes +object Terms : IntIdTable() { + val string1 = varchar("string1", 255) + val string2 = varchar("string2", 255) + val suffix1 = reference("suffix1_id",Suffixes).nullable() + val suffix2 = reference("suffix2_id",Suffixes).nullable() + val type = reference("type_id",DictTypes).nullable() + val member = varchar("member", 255).nullable() + val order2 = integer("order2").nullable() + val flags = json("flags", Json.Default).nullable() +} \ No newline at end of file diff --git a/src/main/kotlin/tables/Translation.kt b/src/main/kotlin/tables/Translation.kt new file mode 100644 index 0000000..923957e --- /dev/null +++ b/src/main/kotlin/tables/Translation.kt @@ -0,0 +1,13 @@ +package tables + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Translations : IntIdTable() { + val dictionaryId = reference("dictionary_id",Dictionaries) + val lang1Id = reference("lang1_id",Languages) + val lang2Id = reference("lang2_id",Languages) + + val langName1 = varchar("lang_name1", 255) + val langName2 = varchar("lang_name2", 255) + val direction = integer("direction") +} \ No newline at end of file diff --git a/src/main/kotlin/util/JsonMapper.kt b/src/main/kotlin/util/JsonMapper.kt new file mode 100644 index 0000000..21b29a8 --- /dev/null +++ b/src/main/kotlin/util/JsonMapper.kt @@ -0,0 +1,11 @@ +package util + +import kotlinx.serialization.json.Json + +object JsonMapper { + + val defaultMapper = Json { + prettyPrint = true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/web/IndexResource.kt b/src/main/kotlin/web/IndexResource.kt new file mode 100644 index 0000000..31191ba --- /dev/null +++ b/src/main/kotlin/web/IndexResource.kt @@ -0,0 +1,14 @@ +package web + +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.index() { + + val indexPage = javaClass.getResource("/index.html").readText() + + get("/") { + call.respondText(indexPage, ContentType.Text.Html) + } +} diff --git a/src/main/kotlin/web/WidgetResource.kt.old b/src/main/kotlin/web/WidgetResource.kt.old new file mode 100644 index 0000000..bd7e258 --- /dev/null +++ b/src/main/kotlin/web/WidgetResource.kt.old @@ -0,0 +1,64 @@ +package web + +import io.ktor.http.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import model.NewWidget +import service.WidgetService + +fun Route.widget(widgetService: WidgetService) { + + route("/widgets") { + + get { + call.respond(widgetService.getAllWidgets()) + } + + get("/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalStateException("Must provide id") + val widget = widgetService.getWidget(id) + if (widget == null) call.respond(HttpStatusCode.NotFound) + else call.respond(widget) + } + + post { + val widget = call.receive() + call.respond(HttpStatusCode.Created, widgetService.addWidget(widget)) + } + + put { + val widget = call.receive() + val updated = widgetService.updateWidget(widget) + if (updated == null) call.respond(HttpStatusCode.NotFound) + else call.respond(HttpStatusCode.OK, updated) + } + + delete("/{id}") { + val id = call.parameters["id"]?.toInt() ?: throw IllegalStateException("Must provide id") + val removed = widgetService.deleteWidget(id) + if (removed) call.respond(HttpStatusCode.OK) + else call.respond(HttpStatusCode.NotFound) + } + + } + + webSocket("/updates") { + try { + widgetService.addChangeListener(this.hashCode()) { + sendSerialized(it) + } + for (frame in incoming) { + if (frame.frameType == FrameType.CLOSE) { + break + } else if (frame is Frame.Text) { + call.application.environment.log.info("Received websocket message: {}", frame.readText()) + } + } + } finally { + widgetService.removeChangeListener(this.hashCode()) + } + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..4ae3bc0 --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,10 @@ +ktor { + deployment { + port = 8080 + watch = [ build ] + } + + application { + modules = [ MainKt.module ] + } +} \ No newline at end of file diff --git a/src/main/resources/index.html b/src/main/resources/index.html new file mode 100644 index 0000000..e390543 --- /dev/null +++ b/src/main/resources/index.html @@ -0,0 +1,53 @@ + + + + + + Kotlin/Ktor/Exposed Starter - It's Working! + + + + +

It's Working!

+

This starter project creates a new in-memory H2 database with one table for Widget instances. A simple RESTful interface is provided + to perform CRUD operations on Widgets alongside a websocket to be notified in real-time of any changes.

+

Routes:

+

GET /widgets --> get all widgets in the database

+

GET /widgets/{id} --> get one widget instance by id (integer)

+

POST /widgets --> add a new widget to the database by providing a JSON object (converted to a NewWidget instance). + e.g -

+
{
+    "name": "new widget",
+    "quantity": 64
+}
+
+

returns

+
{
+    "id": 4,
+    "name": "new widget",
+    "quantity": 64,
+    "dateCreated": 1519926898
+}
+
+

PUT /widgets --> update an existing widgets name or quantity. Pass in the id in the JSON request to determine which record to update +

+

DELETE /widgets/{id} --> delete the widget with the specified id

+

Notifications (WebSocket)

+

All updates (creates, updates and deletes) to Widget instances are served as notifications through a WebSocket endpoint:

+

WS /updates --> returns Notification instances containing the change type, id and entity (if applicable) e.g:

+
{
+    "type": "CREATE",
+    "id": 12,
+    "entity": {
+      "id": 12,
+      "name": "widget1",
+      "quantity": 5,
+      "dateUpdated": 1533583858169
+    }
+}
+
+ + + + + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..11622e6 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +