From 22d7437cf88606338adb1cf26607711e7534ee70 Mon Sep 17 00:00:00 2001 From: Idan Elhalwani Date: Sat, 6 Jul 2024 14:04:49 +0300 Subject: [PATCH] feat: add support for specifying sub interface in component map signature --- .circleci/config.yml | 59 ---------- .github/workflows/publish.yml | 52 +++++++++ .github/workflows/release.yml | 33 ++++++ .github/workflows/test.yml | 44 ++++++++ .releaserc.json | 7 ++ build.gradle.kts | 105 +++++++++--------- .../componentmap/ComponentMapPostProcessor.kt | 61 +++------- .../dev/krud/spring/componentmap/util.kt | 35 +++++- .../spring/componentmap/ComponentMapTest.kt | 15 +++ .../dev/krud/spring/componentmap/helpers.kt | 27 +++++ 10 files changed, 279 insertions(+), 159 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .releaserc.json diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 967d946..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,59 +0,0 @@ -version: 2.1 -orbs: - ktlint: idanelh/ktlint@1.0.1 -executors: - jdk8: - docker: - - image: cimg/openjdk:8.0 -commands: - run_gradle: - parameters: - gradlew_binary: - type: string - default: ./gradlew - tasks: - description: Gradle tasks to run - type: string - default: NONE - steps: - - run: - name: Run Gradle - command: | - << parameters.gradlew_binary >> \ - --info \ - --stacktrace \ - --console=plain \ - --no-daemon \ - << parameters.tasks >> -jobs: - test: - executor: jdk8 - steps: - - checkout - - run_gradle: - tasks: clean test - - run: - name: Save test results - command: | - mkdir -p ~/test-results/junit/ - mkdir -p ~/test-results/jacoco - find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/junit/ \; - find . -type f -regex ".*/build/reports/.*xml" -exec cp {} ~/test-results/jacoco/ \; - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov -t ${CODECOV_TOKEN} - when: always - - store_test_results: - path: ~/test-results - - store_artifacts: - path: ~/test-results/junit - - store_artifacts: - path: ~/test-results/jacoco - -workflows: - version: 2 - build: - jobs: - - test - - ktlint/lint: - working-directory: spring-componentmap \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..152bcd1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,52 @@ +name: Publish + +on: + push: + branches: + - master + tags: + - v* + pull_request: + branches: + - master +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + - name: Install Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + OSSRH_GPG_SECRET_KEY_BASE64: ${{ secrets.OSSRH_GPG_SECRET_KEY_BASE64 }} + OSSRH_GPG_SECRET_KEY_PASSWORD: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + export RELEASE_VERSION=${{ github.event.number }}-PR-SNAPSHOT + echo "Deploying version $RELEASE_VERSION" + ./gradlew -Prelease jar publishToSonatype + else + if [ "${{ github.ref_type }}" = "tag" ]; then + export RELEASE_VERSION=${{ github.ref_name }} + export RELEASE_VERSION=${RELEASE_VERSION:1} + else + export RELEASE_VERSION=${{ github.sha }}-SNAPSHOT + fi + echo "Deploying version $RELEASE_VERSION" + ./gradlew -Prelease jar publishToSonatype closeAndReleaseSonatypeStagingRepository + fi + \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8122204 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release +on: + push: + branches: + - master + - beta + +permissions: + contents: read # for checkout + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.CLONE_TOKEN }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.CLONE_TOKEN }} + run: npx semantic-release \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4c70771 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Test + +on: + push: + branches: + - master + tags: + - v* + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + - name: Install Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew test --info + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: success() || failure() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..6c4fb71 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github" + ] +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index ce629e6..113e8af 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.dokka.gradle.DokkaTask +import java.util.* plugins { `java-library` @@ -6,48 +7,57 @@ plugins { signing jacoco id("org.jetbrains.dokka") version "1.6.0" + id("io.github.gradle-nexus.publish-plugin") version "2.0.0" } if (hasProperty("release")) { - subprojects { - apply(plugin = "java-library") - apply(plugin = "signing") - apply(plugin = "maven-publish") - apply(plugin = "org.jetbrains.dokka") - group = "dev.krud" - version = extra["spring-componentmap.version"] ?: error("spring-componentmap.version is not set") - java.sourceCompatibility = JavaVersion.VERSION_1_8 - val isSnapshot = version.toString().endsWith("-SNAPSHOT") - val repoUri = if (isSnapshot) { - "https://s01.oss.sonatype.org/content/repositories/snapshots/" - } else { - "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val ossrhUsername = System.getenv("OSSRH_USERNAME") + val ossrhPassword = System.getenv("OSSRH_PASSWORD") + val releaseVersion = System.getenv("RELEASE_VERSION") + group = "dev.krud" + version = releaseVersion + nexusPublishing { + this@nexusPublishing.repositories { + sonatype { + username.set(ossrhUsername) + password.set(ossrhPassword) + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } } + } +} - if (!isSnapshot) { - java { - withJavadocJar() - withSourcesJar() - } +allprojects { + repositories { + mavenLocal() + mavenCentral() + } + apply(plugin = "java-library") + apply(plugin = "maven-publish") + apply(plugin = "signing") + + if (hasProperty("release")) { + val releaseVersion = System.getenv("RELEASE_VERSION") + val signingKeyBase64 = System.getenv("OSSRH_GPG_SECRET_KEY_BASE64") + val signingPassword = System.getenv("OSSRH_GPG_SECRET_KEY_PASSWORD") + group = "dev.krud" + version = releaseVersion + val isSnapshot = version.toString().endsWith("-SNAPSHOT") + java { + withJavadocJar() + withSourcesJar() } publishing { publications.create("maven") { from(components["java"]) - repositories { - maven { - name = "OSSRH" - url = uri(repoUri) - credentials { - username = System.getenv("OSSRH_USERNAME") ?: extra["ossrh.username"]?.toString() - password = System.getenv("OSSRH_PASSWORD") ?: extra["ossrh.password"]?.toString() - } - } - } + artifactId = project.name + pom { - name.set(this@subprojects.name) + name.set(project.name) description.set("A Kotlin library for intelligent object mapping.") - url.set("https://github.com/krud-dev/spring-componentmap") + url.set("https://github.com/krud-dev/shapeshift/shapeshift") licenses { license { name.set("MIT License") @@ -73,31 +83,22 @@ if (hasProperty("release")) { } } - if (!isSnapshot) { - val javadocTask = tasks.named("javadoc").get() - - tasks.withType { - javadocTask.dependsOn(this) - outputDirectory.set(javadocTask.destinationDir) - } + val javadocTask = tasks.named("javadoc").get() - signing { - sign(publishing.publications["maven"]) - } + tasks.withType { + javadocTask.dependsOn(this) + outputDirectory.set(javadocTask.destinationDir) + } + signing { + val signingKey = signingKeyBase64?.let { decodeBase64(it) } + useInMemoryPgpKeys( + signingKey, signingPassword + ) + sign(publishing.publications["maven"]) } } } -subprojects { - apply(plugin = "jacoco") - tasks.withType { - finalizedBy(tasks.withType()) - } - tasks.withType { - dependsOn(tasks.test) - reports { - xml.required.set(true) - html.required.set(false) - } - } +fun decodeBase64(base64: String): String { + return String(Base64.getDecoder().decode(base64)) } \ No newline at end of file diff --git a/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/ComponentMapPostProcessor.kt b/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/ComponentMapPostProcessor.kt index 527a99d..c020cb2 100644 --- a/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/ComponentMapPostProcessor.kt +++ b/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/ComponentMapPostProcessor.kt @@ -1,12 +1,9 @@ package dev.krud.spring.componentmap -import org.apache.commons.lang3.ClassUtils -import org.springframework.aop.TargetClassAware import org.springframework.aop.framework.Advised import org.springframework.beans.factory.config.BeanPostProcessor import org.springframework.core.annotation.AnnotationUtils import org.springframework.util.ReflectionUtils -import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap class ComponentMapPostProcessor : BeanPostProcessor { @@ -14,25 +11,17 @@ class ComponentMapPostProcessor : BeanPostProcessor { private val componentMaps: MutableMap>> = ConcurrentHashMap() override fun postProcessAfterInitialization(bean: Any, beanName: String): Any { - try { - registerComponentMapKeyIfExists(bean) - fillComponentMapIfExists(bean) - } catch (e: Exception) { - } + registerComponentMapKey(bean) + fillComponentMapsIfExist(bean) return bean } - private fun fillComponentMapIfExists(bean: Any) { - var handler: Any? = bean - if (handler is TargetClassAware) { - try { - handler = (handler as Advised).targetSource.target - } catch (e: Exception) { - } + private fun fillComponentMapsIfExist(bean: Any) { + var handler: Any = bean + if (handler is Advised) { + handler = handler.targetSource.target ?: error("Target is null") } - handler ?: error("Handler is null") - val fields = getFields(handler.javaClass) for (field in fields) { @@ -80,46 +69,24 @@ class ComponentMapPostProcessor : BeanPostProcessor { return componentMaps[identifier]!! } - private fun registerComponentMapKeyIfExists(bean: Any) { + private fun registerComponentMapKey(bean: Any) { val methods = getMethods(bean.javaClass) for (method in methods) { val annotation = AnnotationUtils.findAnnotation(method, ComponentMapKey::class.java) if (annotation != null) { - try { - val key = method.invoke(bean) - val keyClass = key::class.java - val valueClass = getMethodDeclarer(method) - val map = getOrCreateComponentMap(keyClass, valueClass) + val key = method.invoke(bean) + val keyClass = key::class.java + val valueClasses = getMethodDeclarer(method) + valueClasses.forEach { + val map = getOrCreateComponentMap(keyClass, it) val list = map.computeIfAbsent(key) { mutableListOf() } list += bean - } catch (e: Exception) { + } } } } } -private data class ComponentMapIdentifier(val keyClazz: Class<*>, val valueClazz: Class<*>) - -fun getMethodDeclarer(method: Method): Class<*> { - var declaringClass: Class<*> = method.declaringClass - val methodName: String = method.name - val parameterTypes: Array> = method.parameterTypes - for (interfaceType in ClassUtils.getAllInterfaces(declaringClass)) try { - return interfaceType.getMethod(methodName, *parameterTypes).declaringClass - } catch (ex: NoSuchMethodException) { - } - while (true) { - declaringClass = declaringClass.superclass - if (declaringClass == null) break - try { - val newMethod = declaringClass.getMethod(methodName, *parameterTypes) - return getMethodDeclarer(newMethod) - } catch (ex: NoSuchMethodException) { - break - } - } - - error("Could not find method declarer for ${method.declaringClass.canonicalName}::${method.name}") -} \ No newline at end of file +private data class ComponentMapIdentifier(val keyClazz: Class<*>, val valueClazz: Class<*>) \ No newline at end of file diff --git a/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/util.kt b/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/util.kt index 9fff82b..9771d44 100644 --- a/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/util.kt +++ b/spring-componentmap/src/main/kotlin/dev/krud/spring/componentmap/util.kt @@ -1,10 +1,10 @@ package dev.krud.spring.componentmap +import org.apache.commons.lang3.ClassUtils import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.ParameterizedType import java.lang.reflect.WildcardType -import java.util.* internal fun Field.getGenericClass(index: Int): Class<*>? { return try { @@ -64,4 +64,37 @@ fun getMethods(type: Class<*>): List { classToGetMethods = classToGetMethods.superclass } return methods +} + +fun getMethodDeclarer(method: Method): List> { + var declaringClass: Class<*> = method.declaringClass + val methodName: String = method.name + val parameterTypes: Array> = method.parameterTypes + val allInterfaces = ClassUtils.getAllInterfaces(declaringClass).toMutableList() + val possibleDeclarers = mutableListOf>() + var methodFound = false + for (interfaceType in allInterfaces) { + try { + interfaceType.getMethod(methodName, *parameterTypes) + methodFound = true + } catch (ex: NoSuchMethodException) { + if (methodFound) { + break + } + } + possibleDeclarers.add(interfaceType) + } + while (true) { + declaringClass = declaringClass.superclass ?: break + try { + declaringClass.getMethod(methodName, *parameterTypes) + methodFound = true + } catch (ex: NoSuchMethodException) { + if (methodFound) { + break + } + } + possibleDeclarers.add(declaringClass) + } + return possibleDeclarers } \ No newline at end of file diff --git a/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/ComponentMapTest.kt b/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/ComponentMapTest.kt index 92487a8..4f7cba7 100644 --- a/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/ComponentMapTest.kt +++ b/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/ComponentMapTest.kt @@ -17,6 +17,12 @@ class ComponentMapTest { @Autowired private lateinit var testImpl2: TestImpl2 + @Autowired + private lateinit var subImpl: SubImpl + + @Autowired + private lateinit var subImpl2: SubImpl2 + @Autowired private lateinit var testImpl2Duplicate: TestImpl2Duplicate @@ -44,4 +50,13 @@ class ComponentMapTest { ) assertEquals(expected, testMapUser.multiMap) } + + @Test + fun `subMap should populate`() { + val expected = mapOf( + subImpl.type to subImpl, + subImpl2.type to subImpl2 + ) + assertEquals(expected, testMapUser.subMap) + } } \ No newline at end of file diff --git a/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/helpers.kt b/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/helpers.kt index 7c70571..8c648a7 100644 --- a/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/helpers.kt +++ b/spring-componentmap/src/test/kotlin/dev/krud/spring/componentmap/helpers.kt @@ -30,6 +30,9 @@ class TestMapUser { @ComponentMap lateinit var multiMap: Map> + + @ComponentMap + lateinit var subMap: Map } @Configuration @@ -46,6 +49,30 @@ class ComponentMapTestConfig { @Order(3) fun testImpl2Duplicate() = TestImpl2Duplicate() + @Bean + fun subImpl() = SubImpl() + + @Bean + fun subImpl2() = SubImpl2() + @Bean fun testMapUser() = TestMapUser() +} + +interface BaseInterface { + @get:ComponentMapKey + val type: String +} + +interface SubInterface : BaseInterface { +} + +class SubImpl : SubInterface { + override val type: String + get() = "SubImpl" +} + +class SubImpl2 : SubInterface { + override val type: String + get() = "SubImpl2" } \ No newline at end of file