diff --git a/.editorconfig b/.editorconfig index 45d4fa6..72ef65f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,4 +2,5 @@ ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false ktlint_code_style = intellij_idea -ktlint_standard_no-unused-imports = enabled \ No newline at end of file +ktlint_standard_no-unused-imports = enabled +ktlint_standard_blank-line-between-when-conditions = disabled diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 44c64ea..11a705f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -15,7 +15,7 @@ jobs: - name: setup java uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: "25" distribution: graalvm cache: "gradle" - name: Publish diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 829c6bf..409715a 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -15,7 +15,7 @@ jobs: - name: setup java uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: "25" distribution: graalvm cache: "gradle" - name: Lint @@ -27,7 +27,7 @@ jobs: - name: setup java uses: graalvm/setup-graalvm@v1 with: - java-version: "21" + java-version: "25" distribution: graalvm cache: "gradle" - name: Test diff --git a/build.gradle.kts b/build.gradle.kts index fea8f1b..07cb6d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "com.valensas" -java.sourceCompatibility = JavaVersion.VERSION_21 +java.sourceCompatibility = JavaVersion.VERSION_25 repositories { mavenCentral() @@ -24,7 +24,7 @@ dependencies { compileOnly("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.data:spring-data-commons") implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("com.valensas:exception:2.5.0") + implementation("com.valensas:exception:3.0.0") compileOnly("io.micrometer:micrometer-core") api("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.11.0") testImplementation("org.springframework.boot:spring-boot-starter-webflux") @@ -35,7 +35,7 @@ dependencies { tasks.withType { compilerOptions { freeCompilerArgs.addAll("-Xjsr305=strict") - jvmTarget = JvmTarget.JVM_21 + jvmTarget = JvmTarget.JVM_25 } } @@ -43,7 +43,7 @@ extra["kotlin.version"] = "2.4.0" dependencyManagement { imports { - mavenBom("org.springframework.boot:spring-boot-dependencies:3.5.15") + mavenBom("org.springframework.boot:spring-boot-dependencies:4.1.0") } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5dd3c01..eb84db6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index b740cf1..b9bb139 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 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. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # 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" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 7101f8e..aa5f10b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -21,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -49,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -63,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :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% equ 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! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/src/main/kotlin/com/valensas/util/Monitoring.kt b/src/main/kotlin/com/valensas/util/Monitoring.kt index 836f323..1a8c217 100644 --- a/src/main/kotlin/com/valensas/util/Monitoring.kt +++ b/src/main/kotlin/com/valensas/util/Monitoring.kt @@ -26,11 +26,11 @@ fun measure( if (exception == null) { mapOf("exception" to "none") } else { - mapOf("exception" to exception!!.javaClass.canonicalName) + mapOf("exception" to exception.javaClass.canonicalName) } meterRegistry.timer(name, (tags + extraTags).map { Tag.of(it.key, it.value) }).record(nanoDuration, TimeUnit.NANOSECONDS) - if (exception != null) throw exception!! + if (exception != null) throw exception } fun runScheduledTask( diff --git a/src/main/kotlin/com/valensas/util/autoconfigure/JacksonConfiguration.kt b/src/main/kotlin/com/valensas/util/autoconfigure/JacksonConfiguration.kt index 45ab16b..350ca82 100644 --- a/src/main/kotlin/com/valensas/util/autoconfigure/JacksonConfiguration.kt +++ b/src/main/kotlin/com/valensas/util/autoconfigure/JacksonConfiguration.kt @@ -1,31 +1,35 @@ package com.valensas.util.autoconfigure -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.SerializationFeature import com.valensas.util.serializer.BigDecimalMoneyDeserializer import com.valensas.util.serializer.InstantSerializer import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Primary -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import tools.jackson.databind.DeserializationFeature +import tools.jackson.databind.cfg.DateTimeFeature +import tools.jackson.databind.cfg.EnumFeature +import tools.jackson.databind.module.SimpleModule import java.math.BigDecimal import java.time.Instant @Configuration class JacksonConfiguration { @Bean - @Primary - fun jackson2ObjectMapperBuilder( + fun valensasJsonMapperBuilderCustomizer( @Value("\${valensas.server.bigdecimal.scale:8}") scale: Int - ): Jackson2ObjectMapperBuilder = Jackson2ObjectMapperBuilder() - .deserializerByType(BigDecimal::class.java, BigDecimalMoneyDeserializer(scale)) - .serializerByType(Instant::class.java, InstantSerializer()) - .createXmlMapper(false) - .featuresToDisable( - SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, - SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, - SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS - ).featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + ): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder -> + val module = SimpleModule() + .addDeserializer(BigDecimal::class.java, BigDecimalMoneyDeserializer(scale)) + .addSerializer(Instant::class.java, InstantSerializer()) + builder + .addModule(module) + .disable( + DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, + DateTimeFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, + DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS + ).enable(EnumFeature.WRITE_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + } } diff --git a/src/main/kotlin/com/valensas/util/extension/CollectionExtension.kt b/src/main/kotlin/com/valensas/util/extension/CollectionExtension.kt index 3fb00aa..69ad1d6 100644 --- a/src/main/kotlin/com/valensas/util/extension/CollectionExtension.kt +++ b/src/main/kotlin/com/valensas/util/extension/CollectionExtension.kt @@ -9,12 +9,12 @@ import kotlinx.coroutines.reactor.mono import reactor.core.publisher.Flux import reactor.core.publisher.Mono -suspend fun Collection.parallelForEach(mapper: suspend (T) -> Unit) { +suspend fun Collection.parallelForEach(mapper: suspend (T) -> Unit) { if (this.isEmpty()) return Flux.fromIterable(this).parallelForEach(mapper) } -suspend fun Collection.parallelMap(mapper: suspend (T) -> U): List { +suspend fun Collection.parallelMap(mapper: suspend (T) -> U): List { if (this.isEmpty()) return emptyList() return Flux .fromIterable(this) @@ -23,6 +23,6 @@ suspend fun Collection.parallelMap(mapper: suspend (T) -> U): List .awaitSingle() } -suspend fun Flux.parallelForEach(function: suspend (T) -> Unit) { +suspend fun Flux.parallelForEach(function: suspend (T) -> Unit) { this.flatMap { mono { function(it) } }.switchIfEmpty(Mono.just(Unit)).awaitLast() } diff --git a/src/main/kotlin/com/valensas/util/pagination/Pageable.kt b/src/main/kotlin/com/valensas/util/pagination/Pageable.kt index aecec2d..0041af3 100644 --- a/src/main/kotlin/com/valensas/util/pagination/Pageable.kt +++ b/src/main/kotlin/com/valensas/util/pagination/Pageable.kt @@ -9,6 +9,7 @@ import org.springframework.util.StringUtils import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.util.UriBuilder import java.io.Serializable +import java.util.Optional class Pageable( @RequestParam(required = false) var page: Int?, @@ -65,6 +66,6 @@ private fun parseParameterIntoSort(sort: String?): Sort { } fun UriBuilder.pageable(pageable: Pageable) = this - .queryParam("page", pageable.page) - .queryParam("sort", pageable.sort) - .queryParam("size", pageable.size) + .queryParamIfPresent("page", Optional.ofNullable(pageable.page)) + .queryParamIfPresent("sort", Optional.ofNullable(pageable.sort)) + .queryParamIfPresent("size", Optional.ofNullable(pageable.size)) diff --git a/src/main/kotlin/com/valensas/util/serializer/BigDecimalMoneyDeserializer.kt b/src/main/kotlin/com/valensas/util/serializer/BigDecimalMoneyDeserializer.kt index 7ff1876..5909457 100644 --- a/src/main/kotlin/com/valensas/util/serializer/BigDecimalMoneyDeserializer.kt +++ b/src/main/kotlin/com/valensas/util/serializer/BigDecimalMoneyDeserializer.kt @@ -1,17 +1,14 @@ package com.valensas.util.serializer -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonProcessingException -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import java.io.IOException +import tools.jackson.core.JsonParser +import tools.jackson.databind.DeserializationContext +import tools.jackson.databind.ValueDeserializer import java.math.BigDecimal import java.math.RoundingMode class BigDecimalMoneyDeserializer( private val scale: Int -) : JsonDeserializer() { - @Throws(IOException::class, JsonProcessingException::class) +) : ValueDeserializer() { override fun deserialize( jp: JsonParser, ctx: DeserializationContext diff --git a/src/main/kotlin/com/valensas/util/serializer/InstantSerializer.kt b/src/main/kotlin/com/valensas/util/serializer/InstantSerializer.kt index 1b4810c..f34f607 100644 --- a/src/main/kotlin/com/valensas/util/serializer/InstantSerializer.kt +++ b/src/main/kotlin/com/valensas/util/serializer/InstantSerializer.kt @@ -1,20 +1,20 @@ package com.valensas.util.serializer -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider +import tools.jackson.core.JsonGenerator +import tools.jackson.databind.SerializationContext +import tools.jackson.databind.ValueSerializer import java.time.Instant import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatterBuilder -class InstantSerializer : JsonSerializer() { +class InstantSerializer : ValueSerializer() { private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatterBuilder().appendInstant(3).toFormatter() override fun serialize( - value: Instant?, - gen: JsonGenerator?, - serializers: SerializerProvider? + value: Instant, + gen: JsonGenerator, + ctxt: SerializationContext ) { - gen?.writeString(dateTimeFormatter.format(value)) + gen.writeString(dateTimeFormatter.format(value)) } } diff --git a/src/test/kotlin/com/valensas/util/MapperTest.kt b/src/test/kotlin/com/valensas/util/MapperTest.kt index d797a15..f1e03b7 100644 --- a/src/test/kotlin/com/valensas/util/MapperTest.kt +++ b/src/test/kotlin/com/valensas/util/MapperTest.kt @@ -1,7 +1,5 @@ package com.valensas.util -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.valensas.util.autoconfigure.JacksonConfiguration import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -9,15 +7,19 @@ import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.mockito.junit.jupiter.MockitoExtension +import tools.jackson.databind.exc.MismatchedInputException +import tools.jackson.databind.json.JsonMapper import java.math.BigDecimal @ExtendWith(MockitoExtension::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MapperTest { - private val mapper = - JacksonConfiguration() - .jackson2ObjectMapperBuilder(8) - .build() + private val mapper: JsonMapper = + JsonMapper + .builder() + .findAndAddModules() + .also { JacksonConfiguration().valensasJsonMapperBuilderCustomizer(8).customize(it) } + .build() @Test fun `serialize and deserialize bigdecimal`() {