Compare commits

..

No commits in common. "main" and "3" have entirely different histories.
main ... 3

33 changed files with 31 additions and 780 deletions

View File

@ -1,41 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DocViewer KORGE" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="GRADLE_USER_HOME" value="$USER_HOME$/.gradle" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":apps:documentViewerKorge:runJvmAutoreload" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
</ENTRIES>
</extension>
</EXTENSION>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@ -1,25 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DocumentViewerKt" type="JetRunConfigurationType" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="de.itkl.documentViewer.DocumentViewerKt" />
<module name="docthor.apps.documentViewer.main" />
<shortenClasspath name="NONE" />
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
</ENTRIES>
</extension>
<extension name="software.aws.toolkits.jetbrains.core.execution.JavaAwsConnectionExtension">
<option name="credential" />
<option name="region" />
<option name="useCurrentConnection" value="false" />
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@ -164,6 +164,28 @@ fun <T> ZoomedImage(
fun loadImageBitmap(file: File): ImageBitmap = fun loadImageBitmap(file: File): ImageBitmap =
file.inputStream().buffered().use(::loadImageBitmap) file.inputStream().buffered().use(::loadImageBitmap)
data class PointConverter(
val docWidth: Int,
val docHeight: Int,
val canvasWidth: Float,
val canvasHeight: Float
) {
fun convertX(x: Int): Float {
val xf = x.toFloat()
val relXf = docWidth / xf
val scaledXf = canvasWidth * relXf
// println("X: $scaledXf")
return scaledXf
}
fun convertY(y: Int): Float {
val yf = y.toFloat()
val relYf = docHeight / yf
val scaledYf = canvasHeight * relYf
// println("Y: $scaledYf")
return scaledYf
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun shapes(zoomableState: ZoomableState) { fun shapes(zoomableState: ZoomableState) {
@ -193,6 +215,13 @@ fun canvas(zoomableState: ZoomableState, first: OcrPage) {
// } // }
) )
{ {
val converter = PointConverter(
docWidth = 2481,
docHeight = 3507,
canvasWidth = this.size.width,
canvasHeight = this.size.height
)
first.words.forEach { word -> first.words.forEach { word ->
val rect = word.rectangle val rect = word.rectangle
drawRect( drawRect(

View File

@ -1,15 +0,0 @@
import korlibs.korge.gradle.korge
plugins {
id("com.soywiz.korge") version "5.3.0"
}
dependencies {
jvmMainImplementation("ch.qos.logback:logback-classic:1.4.14")
jvmMainImplementation(project(":libraries:docthor-core"))
}
korge {
targetJvm()
serializationJson()
}

View File

@ -1,2 +0,0 @@
dependencies:
- https://github.com/korlibs/korge-box2d/tree/v0.1.2/korge-box2d##b1737ab3985c0bd3e2f002346ff2ac43ca1ebf48

View File

@ -1,108 +0,0 @@
import de.itkl.docthor.core.DocumentViewer
import de.itkl.textprocessing.Document
import korlibs.event.Key
import korlibs.image.bitmap.context2d
import korlibs.korge.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.image.color.*
import korlibs.image.format.*
import korlibs.io.file.std.*
import korlibs.korge.input.*
import korlibs.korge.tween.get
import korlibs.korge.tween.tween
import korlibs.korge.ui.tooltip
import korlibs.korge.ui.uiCheckBox
import korlibs.logger.AnsiEscape
import korlibs.math.geom.*
import korlibs.math.geom.shape.toShape2D
import korlibs.math.geom.shape.toShape2d
import korlibs.math.geom.vector.VectorPath
import korlibs.math.interpolation.Easing
import java.nio.file.Paths
import kotlin.time.Duration.Companion.seconds
suspend fun main() {
val document = DocumentViewer().loadTestDocument(Paths.get("assets/xs-reg"), "00001.jpg")
Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) {
val sceneContainer = sceneContainer()
sceneContainer.changeTo { ViewDocument(document) }
}
}
class ViewDocument(private val document: Document) : Scene() {
override suspend fun SContainer.sceneMain() {
var scaleFactor = 0.5
var offset = Point(0.0,0.0)
val moveFactor = 50.0
var rotationFactor = 0.0
scale(scaleFactor)
suspend fun zoom(amount: Double) {
scaleFactor += amount
tween(
this@sceneMain::scale[scaleFactor],
time = 0.1.seconds,
easing = Easing.EASE_IN_OUT
)
}
suspend fun moveX(amount: Double) {
offset = offset.copy(x = offset.x + amount)
tween(
this@sceneMain::x[offset.x],
time = 0.1.seconds,
easing = Easing.EASE_IN_OUT)
}
suspend fun moveY(amount: Double) {
offset = offset.copy(y = offset.y + amount)
tween(
this@sceneMain::y[offset.y],
time = 0.1.seconds,
easing = Easing.EASE_IN_OUT)
}
suspend fun rotateBy(amount: Double){
rotationFactor += amount
tween(
this@sceneMain::rotation[rotationFactor.degrees],
time = 0.1.seconds,
easing = Easing.EASE_IN_OUT
)
}
keys {
down { key ->
when (key.key) {
Key.EQUAL -> zoom(0.05)
Key.MINUS -> zoom(-0.05)
Key.UP -> moveY(moveFactor)
Key.DOWN -> moveY(-moveFactor)
Key.RIGHT -> moveX(moveFactor)
Key.LEFT -> moveX(-moveFactor)
Key.E -> rotateBy(10.0)
Key.R -> rotateBy(-10.0)
else -> {}
}
}
}
val imageFile = localCurrentDirVfs["assets/xs-reg/00001.jpg"].readBitmap()
image(imageFile)
uiCheckBox { text = "foo" }
document.retrieveOcrPages().first().words.forEach { word ->
solidRect(
width = word.rectangle.width,
height = word.rectangle.height,
color = Colors.AQUAMARINE.withA(128)
) {
x = word.rectangle.x
y = word.rectangle.y
}.onClick {
println(word.text)
}
}
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,3 +0,0 @@
dependencies {
implementation(project(":libraries:httpClient"))
}

View File

@ -1,68 +0,0 @@
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.int
import de.itkl.httpClient.clients.TaskWaitStatus
import de.itkl.httpClient.clients.XsClient
import de.itkl.httpClient.httpClientModule
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import java.util.Random
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.startKoin
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.time.Duration.Companion.minutes
class Cli : CliktCommand() {
override fun run() {}
inner class StressAnalyse : CliktCommand(name = "stress-analyse") {
val inputDirectory: File by option(help="Input directory path").convert { File(it) }.required()
val tasks: Int by option(help="Number of tasks").int().required()
override fun run() {
}
}
init {
subcommands(StressAnalyse())
}
}
class XsCli : KoinComponent {
private val xsClient: XsClient by inject()
suspend fun run(tasks: Int, inputDirectory: Path) = coroutineScope {
val files = inputDirectory.toFile()
.listFiles()!!
.toList()
.filter { it.isFile }.filter { it.extension in listOf("pdf", "ttf", "jpg", "jpeg") }
.filter { !it.name.startsWith(".") }
val random = Random()
val (success, error) = (0..<tasks)
.map {
val file = files[random.nextInt(files.size)]
async {
val taskRef = xsClient.analyse(file.toPath())
xsClient.waitFor(taskRef)
}
}
.awaitAll()
.partition { it.status == TaskWaitStatus.SUCCESS }
println("Summary: ${success.size + error.size}: ${success.size}/${error.size}")
}
}
suspend fun main(args: Array<String>) {
startKoin {
modules(httpClientModule)
}
XsCli().run(tasks = 100, inputDirectory = Paths.get("assets/xs-reg"))
}

View File

@ -1,15 +0,0 @@
import korlibs.korge.gradle.korge
plugins {
id("com.soywiz.korge") version "5.3.0"
}
dependencies {
jvmMainImplementation("ch.qos.logback:logback-classic:1.4.14")
jvmMainImplementation(project(":libraries:docthor-core"))
}
korge {
targetJvm()
serializationJson()
}

View File

@ -3,8 +3,5 @@ project(":libraries").subprojects {
} }
project(":apps").subprojects { project(":apps").subprojects {
if(name != "documentViewerKorge" && name != "xsViewer") {
apply(plugin = "docthor.kotlin-application-conventions") apply(plugin = "docthor.kotlin-application-conventions")
} }
}

View File

@ -1,6 +1,4 @@
import gradle.kotlin.dsl.accessors._6ebf0b5d05ec1eff67605a516c5db18b.implementation
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.testImplementation
import org.gradle.api.plugins.jvm.JvmTestSuite import org.gradle.api.plugins.jvm.JvmTestSuite
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@ -15,15 +13,13 @@ repositories {
dependencies { dependencies {
val koin_version = "3.5.3" val koin_version = "3.5.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
api("io.insert-koin:koin-core:$koin_version") implementation("io.insert-koin:koin-core:$koin_version")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
api("io.github.oshai:kotlin-logging-jvm:5.1.0") api("io.github.oshai:kotlin-logging-jvm:5.1.0")
testImplementation("io.insert-koin:koin-test:$koin_version") testImplementation("io.insert-koin:koin-test:$koin_version")
testImplementation("ch.qos.logback:logback-classic:1.4.14")
testImplementation("com.willowtreeapps.assertk:assertk:0.28.0")
} }
java { java {

View File

@ -8,9 +8,6 @@ batik = "1.17"
versionchecker = "0.50.0" versionchecker = "0.50.0"
mavenpublish = "0.25.3" mavenpublish = "0.25.3"
[plugins]
korge = { id = "com.soywiz.korge", version = "5.3.0" }
[libraries] [libraries]
compose-desktop = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" } compose-desktop = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

View File

@ -1,15 +0,0 @@
dependencies {
fun addProjects(vararg names: String) {
names.forEach {
api(project(":libraries:$it"))
}
}
addProjects(
"assetmanager",
"core-api",
"textprocessing",
"httpClient",
"tui",
)
}

View File

@ -1,36 +0,0 @@
package de.itkl.docthor.core
import de.itkl.assetmanager.assetManagerModule
import de.itkl.core_api.coreApiModule
import de.itkl.httpClient.clients.MsOcr
import de.itkl.httpClient.httpClientModule
import de.itkl.textprocessing.CorpusFactory
import de.itkl.textprocessing.Document
import de.itkl.textprocessing.textProcessingModule
import de.itkl.tui.tuiModule
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.startKoin
import java.nio.file.Path
class DocumentViewer : KoinComponent {
init {
startKoin {
modules(
coreApiModule,
textProcessingModule,
tuiModule,
assetManagerModule,
httpClientModule
)
}
}
suspend fun loadTestDocument(corpusPath: Path, documentName: String): Document {
val corpus = CorpusFactory().load(corpusPath.toAbsolutePath().toString())
val document = corpus.document(documentName)
val ocrExtractor: MsOcr by inject()
document.process(ocrExtractor)
return document
}
}

View File

@ -12,7 +12,4 @@ dependencies {
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("com.akuleshov7:ktoml-core:0.5.1")
implementation("com.akuleshov7:ktoml-file:0.5.1")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
} }

View File

@ -1,4 +0,0 @@
package de.itkl.httpClient
class BearerTokenCache {
}

View File

@ -1,22 +0,0 @@
package de.itkl.httpClient
import de.itkl.httpClient.implementation.SmartCloudAuthStrategy
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.plugins.api.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.*
import org.koin.core.component.KoinComponent
private val Log = KotlinLogging.logger { }
val smartCloudAuthPlugin = createClientPlugin("SmartCloudAuth") {
val smartCloudAuthStrategy = SmartCloudAuthStrategy()
on(Send) { request ->
smartCloudAuthStrategy.login(client, request)?.let { token ->
request.headers {
set(HttpHeaders.Authorization, token.toBearer())
}
}
proceed(request)
}
}

View File

@ -1,12 +0,0 @@
package de.itkl.httpClient
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
class TrustAllX509TrustManager : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate?> = arrayOfNulls(0)
override fun checkClientTrusted(certs: Array<X509Certificate?>?, authType: String?) {}
override fun checkServerTrusted(certs: Array<X509Certificate?>?, authType: String?) {}
}

View File

@ -1,11 +0,0 @@
package de.itkl.httpClient.auth
import io.ktor.client.*
import io.ktor.client.request.*
interface AuthStrategy {
suspend fun login(
httpClient: HttpClient,
request: HttpRequestBuilder
): AuthenticationToken?
}

View File

@ -1,23 +0,0 @@
package de.itkl.httpClient.auth
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
sealed class AuthenticationToken {
abstract val expires: Expires
abstract fun toBearer(): String
}
@Serializable
data class BearerToken(val token: String, val validUntil: Instant) : AuthenticationToken() {
override val expires: Expires
get() = Expires.ExpiresAt(validUntil)
override fun toBearer(): String {
return "Bearer $token"
}
}
sealed class Expires {
data object Never : Expires()
data class ExpiresAt(val instant: Instant) : Expires()
}

View File

@ -1,17 +0,0 @@
package de.itkl.httpClient.auth
import io.ktor.http.*
import org.koin.core.component.KoinComponent
typealias UrlMatcher = (Url) -> Boolean
class Authenticator : KoinComponent {
fun addStrategy(strategy: AuthStrategy, urlMatcher: UrlMatcher) {}
fun requiresAuthentication(url: Url): Boolean {
TODO()
}
suspend fun authenticate(url: Url, username: String): AuthenticationToken? {
TODO()
}
}

View File

@ -1,8 +0,0 @@
package de.itkl.httpClient.auth
import kotlinx.serialization.Serializable
sealed class Credentials {
@Serializable
data class LoginAndPassword(val username: String, val password: String) : Credentials()
}

View File

@ -1,5 +0,0 @@
package de.itkl.httpClient.auth
interface CredentialsProvider {
suspend fun lookupByUsername(username: String): Credentials?
}

View File

@ -1,9 +0,0 @@
package de.itkl.httpClient.clients
import io.ktor.client.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class RestClient : KoinComponent {
private val httpClient by inject<HttpClient>()
}

View File

@ -1,134 +0,0 @@
package de.itkl.httpClient.clients
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.client.utils.EmptyContent.contentType
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.util.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import io.ktor.utils.io.streams.*
import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.awt.SystemColor.info
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import kotlin.io.path.exists
import kotlin.io.path.isRegularFile
import kotlin.io.path.name
private val Log = KotlinLogging.logger { }
class XsClient : KoinComponent {
private val httpClient by inject<HttpClient>()
suspend fun waitFor(task: XsTask): WaitForResponse {
Log.info { "Wait for competition: $task" }
val response = httpClient.get {
url("http://localhost:8080/api/v1/analyse/tasks/wait/${task.xsTaskId.taskId}")
user = "xs.dev.klara"
}
val result = response.body<WaitForResponse>()
Log.info { "Waiting done for task $task: ${response.status}: $result" }
return result
}
suspend fun analyseResult(task: XsTask) {
val response = httpClient.get {
url("http://localhost:8080/api/v1/analyse-async-result/${task.xsTaskId.taskId}")
user = "xs.dev.klara"
}
check(response.status.isSuccess()) {
"HTTP Error"
}
val text = response.bodyAsText()
println(text)
}
suspend fun analyse(image: Path): TaskReference {
Log.info { "Starting analysis for image at path: $image" }
check(image.isRegularFile()) {
"The provided path $image is not a file"
}
Log.info { "Submitting form with binary data to http://localhost:8080/api/v1/analyse-async" }
val response = httpClient.submitFormWithBinaryData(
url = "http://localhost:8080/api/v1/analyse-async",
formData {
append(
"image",
image.toChannelProvider(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.defaultForFile(image))
append(HttpHeaders.ContentDisposition, """filename="${image.name}"""")
}
)
}
) {
user = "xs.dev.klara"
}
Log.info { "Received the response from the server." }
if(response.status.isSuccess()) {
Log.info { "Successful response with status: ${response.status}" }
return response.body()
} else {
val responseText = response.bodyAsText()
Log.warn { "Error creating analyse task: ${response.status}: $responseText" }
error("Could not create analyse task: $responseText")
}
}
}
var HttpRequestBuilder.user: String?
get() = this.attributes[AttributeKey(("username"))]
set(value) = value?.let { this.attributes.put(AttributeKey("username"), it) } ?: this.attributes.remove(
AttributeKey("username")
)
interface XsTask {
val xsTaskId: XsTaskId
}
@Serializable
data class TaskReference(
override val xsTaskId: XsTaskId
) : XsTask {
override fun toString(): String {
return "Task($xsTaskId)"
}
}
@Serializable
data class XsTaskId(val tenantId: String, val taskId: String) {
override fun toString(): String {
return "$tenantId/$taskId"
}
}
@Serializable
data class WaitForResponse(
val xsTaskId: XsTaskId,
val status: TaskWaitStatus,
)
@Serializable
enum class TaskWaitStatus {
ERROR,
SUCCESS
}
fun Path.toChannelProvider(): ChannelProvider {
val file = toFile()
return ChannelProvider(file.length()) { file.readChannel() }
}

View File

@ -1,41 +1,14 @@
package de.itkl.httpClient package de.itkl.httpClient
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.* import io.ktor.serialization.kotlinx.json.*
import java.net.InetSocketAddress
import java.net.Socket
import kotlin.time.Duration.Companion.minutes
private val Log = KotlinLogging.logger { }
fun createHttpClient(): HttpClient { fun createHttpClient(): HttpClient {
return HttpClient(CIO) { return HttpClient(CIO) {
install(ContentNegotiation) { install(ContentNegotiation) {
json() json()
} }
install(smartCloudAuthPlugin)
install(HttpTimeout) {
requestTimeoutMillis = 30.minutes.inWholeMilliseconds
}
engine {
val isPortOpen = try {
Socket().use { it.connect(InetSocketAddress("localhost", 9999), 200) }
true
} catch (ex: Exception) {
false
}
if (isPortOpen) {
proxy = ProxyBuilder.http(Url("http://localhost:9999"))
}
https {
trustManager = TrustAllX509TrustManager()
}
}
} }
} }

View File

@ -1,20 +1,10 @@
package de.itkl.httpClient package de.itkl.httpClient
import de.itkl.httpClient.auth.CredentialsProvider
import de.itkl.httpClient.clients.MsOcr import de.itkl.httpClient.clients.MsOcr
import de.itkl.httpClient.clients.XsClient
import de.itkl.httpClient.implementation.StaticCredentialsProvider
import io.ktor.client.* import io.ktor.client.*
import org.koin.dsl.module import org.koin.dsl.module
import java.nio.file.Paths
val httpClientModule = module { val httpClientModule = module {
single<HttpClient> { createHttpClient() } single<HttpClient> { createHttpClient() }
single<MsOcr> { MsOcr() } single<MsOcr> { MsOcr() }
single<CredentialsProvider> {
val homeDirectory = System.getProperty("user.home")
val credentialsFilePath = Paths.get(homeDirectory, ".auth.toml")
StaticCredentialsProvider.load(credentialsFilePath)
}
single<XsClient> { XsClient() }
} }

View File

@ -1,72 +0,0 @@
package de.itkl.httpClient.implementation
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Cache
import de.itkl.httpClient.auth.*
import de.itkl.httpClient.clients.user
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private val Log = KotlinLogging.logger { }
class SmartCloudAuthStrategy : AuthStrategy, KoinComponent {
private val credentialsProvider: CredentialsProvider by inject()
private val loginMutex = Mutex()
// Cache Helpers
private val tokenCache: Cache<String, AuthenticationToken> = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build()
override suspend fun login(httpClient: HttpClient, request: HttpRequestBuilder): AuthenticationToken? {
Log.debug { "Attempting login..." }
val user = request.attributes.getOrNull(AttributeKey<String>("username")) ?: run {
Log.info { "No username is specified for this request" }
return null
}
tokenCache.getIfPresent(user)?.let {
Log.info { "Returning cached token for user: $user" }
return it
}
return loginMutex.withLock {
tokenCache.getIfPresent(user)?.let {
Log.info { "Returning cached token for user: $user" }
return it
}
val credentials: Credentials = credentialsProvider.lookupByUsername(user) ?: error("No credentials found for user: $user")
val loginAndPassword: Credentials.LoginAndPassword = credentials as? Credentials.LoginAndPassword ?: error("Only username and password is supported by smartcloud auth login")
Log.debug { "User: $user, using smartcloud auth login" }
val response = httpClient.post {
url("https://api.internal.insiders.cloud/1/rest/accounts/authentication/requesttoken")
contentType(ContentType.Application.Json)
setBody(loginAndPassword)
}
check(response.status.isSuccess()) {
"could not login into smart cloud: ${response.bodyAsText()}"
}
val token = response.body<BearerToken>()
Log.info { "Login successful. Valid until: ${token.validUntil}" }
// Cache the token after successful login
tokenCache.put(user, token)
token
}
}
}

View File

@ -1,49 +0,0 @@
package de.itkl.httpClient.implementation
import com.akuleshov7.ktoml.Toml
import de.itkl.httpClient.auth.Credentials
import de.itkl.httpClient.auth.CredentialsProvider
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import java.nio.file.Files
import java.nio.file.Path
private val Log = KotlinLogging.logger { }
class StaticCredentialsProvider(private val credentials: Map<String, Credentials.LoginAndPassword>) : CredentialsProvider {
companion object {
fun load(path: Path): StaticCredentialsProvider {
Log.info { "Loading credentials from path: $path" }
val content = Files.readString(path)
val credentials = Toml.decodeFromString<CredentialsTable>(content).toCredentialsMap()
Log.info { "Credentials loaded successfully" }
return StaticCredentialsProvider(credentials)
}
}
override suspend fun lookupByUsername(username: String): Credentials? {
Log.info { "Looking up credentials by username: $username" }
val credentialsEntry = credentials[username]
if (credentialsEntry != null) {
Log.info { "Credentials found for username: $username" }
return credentialsEntry
}
Log.info { "No credentials found for username: $username" }
return null
}
}
@Serializable
data class CredentialsTable(
val credentials: Map<String, Map<String, String>>
) {
fun toCredentialsMap(): Map<String, Credentials.LoginAndPassword> {
return credentials.mapValues {
val login = it.value["Login"] ?: throw IllegalArgumentException("Missing login")
val password = it.value["Password"] ?: throw IllegalArgumentException("Missing password")
Credentials.LoginAndPassword(login, password)
}
}
}

View File

@ -1,25 +0,0 @@
package de.itkl.httpClient.implementation
import assertk.assertThat
import assertk.assertions.isDataClassEqualTo
import assertk.assertions.isNotNull
import de.itkl.httpClient.auth.Credentials
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import java.nio.file.Paths
class StaticCredentialsProviderTest {
@Test
fun `can load credentials file`() = runBlocking {
val target = StaticCredentialsProvider.load(Paths.get("../../assets/credentials.toml"))
val credentials = target.lookupByUsername("test user")
assertThat(credentials)
.isNotNull()
.isDataClassEqualTo(Credentials.LoginAndPassword("TestiTest", "Secret"))
Unit
}
}

View File

@ -1,11 +1,3 @@
pluginManagement {
repositories {
mavenCentral();
google();
gradlePluginPortal() }
}
plugins { plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0" id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0"
} }