diff --git a/README.md b/README.md index 8ac2e05..3cc7278 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,19 @@ If you want to build the extension locally, you need to check it out from GiHub Please execute the following command line if you are interested in producing KDoc and Source archives: - ./mvnw clean install -Pjavadoc-and-sources + ./mvnw clean install -Pdocs-and-sources +### Collecting code coverage data + +If you are interested in test code coverage, please run the following command: + + ./mvnw clean install -Pcoverage + +### Building example project + +The project includes an example module demonstrating usage of the extension. If you want to skip the example +build, please run the following command line: + + ./mvnw clean install -DskipExamples --- diff --git a/kotlin-example/pom.xml b/kotlin-example/pom.xml new file mode 100644 index 0000000..9e12aaa --- /dev/null +++ b/kotlin-example/pom.xml @@ -0,0 +1,55 @@ + + + + + 4.0.0 + + Axon Framework - Kotlin Extension Example + Module for the Kotlin Extension Example of Axon Framework + + + org.axonframework.extensions.kotlin + axon-kotlin-parent + 0.3.0-SNAPSHOT + + + axon-kotlin-example + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-validation + + + org.axonframework.extensions.kotlin + axon-kotlin-springboot-starter + + + org.axonframework + axon-spring-boot-starter + + + io.github.microutils + kotlin-logging-jvm + + + diff --git a/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/AxonKotlinExampleApplication.kt b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/AxonKotlinExampleApplication.kt new file mode 100644 index 0000000..3f1fdbd --- /dev/null +++ b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/AxonKotlinExampleApplication.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.example + +import mu.KLogging +import org.axonframework.config.Configurer +import org.axonframework.eventhandling.EventBus +import org.axonframework.eventhandling.EventMessage +import org.axonframework.eventhandling.interceptors.EventLoggingInterceptor +import org.axonframework.eventhandling.tokenstore.TokenStore +import org.axonframework.eventhandling.tokenstore.inmemory.InMemoryTokenStore +import org.axonframework.eventsourcing.eventstore.EmbeddedEventStore +import org.axonframework.eventsourcing.eventstore.EventStore +import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine +import org.axonframework.extension.kotlin.spring.EnableAggregateWithImmutableIdentifierScan +import org.axonframework.messaging.interceptors.LoggingInterceptor +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Bean + +/** + * Starting point. + * @param args CLI parameters. + */ +fun main(args: Array) { + SpringApplication.run(AxonKotlinExampleApplication::class.java, *args) +} + +/** + * Main example application class. + */ +@SpringBootApplication +@EnableAggregateWithImmutableIdentifierScan +class AxonKotlinExampleApplication { + + companion object : KLogging() + + /** + * Configures to use in-memory embedded event store. + */ + @Bean + fun eventStore(): EventStore = EmbeddedEventStore.builder().storageEngine(InMemoryEventStorageEngine()).build() + + /** + * Configure logging interceptor. + */ + @Autowired + fun configureEventHandlingInterceptors(eventBus: EventBus) { + eventBus.registerDispatchInterceptor(EventLoggingInterceptor()) + } + + /** + * Configures to use in-memory token store. + */ + @Bean + fun tokenStore(): TokenStore = InMemoryTokenStore() + +} + + diff --git a/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Commands.kt b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Commands.kt new file mode 100644 index 0000000..cea8263 --- /dev/null +++ b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Commands.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.example.api + +import org.axonframework.extension.kotlin.example.core.BankAccountIdentifier +import org.axonframework.modelling.command.TargetAggregateIdentifier +import javax.validation.constraints.Min + +/** + * Create account. + */ +data class CreateBankAccountCommand( + @TargetAggregateIdentifier + val bankAccountId: String, + @Min(value = 0, message = "Overdraft limit must not be less than zero") + val overdraftLimit: Long +) + +/** + * Create advanced account. + */ +data class CreateAdvancedBankAccountCommand( + @TargetAggregateIdentifier + val bankAccountId: BankAccountIdentifier, + @Min(value = 0, message = "Overdraft limit must not be less than zero") + val overdraftLimit: Long +) + + +/** + * Deposit money. + */ +data class DepositMoneyCommand( + @TargetAggregateIdentifier + val bankAccountId: String, + val amountOfMoney: Long +) + +/** + * Withdraw money. + */ +data class WithdrawMoneyCommand( + @TargetAggregateIdentifier + val bankAccountId: String, + val amountOfMoney: Long +) + +/** + * Return money if transfer is not possible. + */ +data class ReturnMoneyOfFailedBankTransferCommand( + @TargetAggregateIdentifier + val bankAccountId: String, + val amount: Long +) diff --git a/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Events.kt b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Events.kt new file mode 100644 index 0000000..25a3252 --- /dev/null +++ b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Events.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.example.api + +import org.axonframework.extension.kotlin.example.core.BankAccountIdentifier + +/** + * Account created. + */ +data class BankAccountCreatedEvent( + val id: String, + val overdraftLimit: Long +) + +/** + * Advanced account created. + */ +data class AdvancedBankAccountCreatedEvent( + val id: BankAccountIdentifier, + val overdraftLimit: Long +) + +/** + * Collecting event for increasing amount. + */ +sealed class MoneyAddedEvent( + open val bankAccountId: String, + open val amount: Long +) + +/** + * Money deposited. + */ +data class MoneyDepositedEvent(override val bankAccountId: String, override val amount: Long) : MoneyAddedEvent(bankAccountId, amount) + +/** + * Money returned. + */ +data class MoneyOfFailedBankTransferReturnedEvent(override val bankAccountId: String, override val amount: Long) : MoneyAddedEvent(bankAccountId, amount) + +/** + * Money received via transfer. + */ +data class DestinationBankAccountCreditedEvent(override val bankAccountId: String, override val amount: Long, val bankTransferId: String) : MoneyAddedEvent(bankAccountId, amount) + +/** + * Collecting event for decreasing amount. + */ +sealed class MoneySubtractedEvent( + open val bankAccountId: String, + open val amount: Long +) + +/** + * Money withdrawn. + */ +data class MoneyWithdrawnEvent(override val bankAccountId: String, override val amount: Long) : MoneySubtractedEvent(bankAccountId, amount) + +/** + * Money transferred. + */ +data class SourceBankAccountDebitedEvent(override val bankAccountId: String, override val amount: Long, val bankTransferId: String) : MoneySubtractedEvent(bankAccountId, amount) + +/** + * Money transfer rejected. + */ +data class SourceBankAccountDebitRejectedEvent(val bankTransferId: String) diff --git a/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccount.kt b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccount.kt new file mode 100644 index 0000000..0975c97 --- /dev/null +++ b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccount.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.example.core + +import mu.KLogging +import org.axonframework.commandhandling.CommandHandler +import org.axonframework.commandhandling.gateway.CommandGateway +import org.axonframework.eventsourcing.EventSourcingHandler +import org.axonframework.extension.kotlin.example.AxonKotlinExampleApplication +import org.axonframework.extension.kotlin.example.api.* +import org.axonframework.extension.kotlin.spring.AggregateWithImmutableIdentifier +import org.axonframework.extensions.kotlin.send +import org.axonframework.modelling.command.AggregateCreationPolicy +import org.axonframework.modelling.command.AggregateIdentifier +import org.axonframework.modelling.command.AggregateLifecycle.apply +import org.axonframework.modelling.command.CreationPolicy +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.stereotype.Service +import java.util.* + +/** + * Bank configuration. + */ +@Configuration +class BankConfiguration(private val bankAccountService: BankAccountService) { + + /** + * Application runner of bank ops. + */ + @Bean + fun accountOperationsRunner() = ApplicationRunner { + bankAccountService.accountOperations() + } +} + +/** + * Bank service. + */ +@Service +class BankAccountService(private val commandGateway: CommandGateway) { + + companion object : KLogging() + + /** + * Bank ops. + */ + fun accountOperations() { + val accountId = UUID.randomUUID().toString() + + logger.info { "\nPerforming basic operations on account $accountId" } + + commandGateway.send( + command = CreateBankAccountCommand(accountId, 100), + onSuccess = { _, result: Any?, _ -> + AxonKotlinExampleApplication.logger.info { "Successfully created account with id: $result" } + commandGateway.send( + command = DepositMoneyCommand(accountId, 20), + onSuccess = { c, _: Any?, _ -> logger.info { "Successfully deposited ${c.payload.amountOfMoney}" } }, + onError = { c, e, _ -> logger.error(e) { "Error depositing money on ${c.payload.bankAccountId}" } } + ) + }, + onError = { c, e, _ -> logger.error(e) { "Error creating account ${c.payload.bankAccountId}" } } + ) + + } +} + +/** + * Bank account aggregate as data class. + */ +@AggregateWithImmutableIdentifier +data class BankAccount( + @AggregateIdentifier + private val id: UUID +) { + + private var overdraftLimit: Long = 0 + private var balanceInCents: Long = 0 + + /** + * Creates account. + */ + @CommandHandler + @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) + fun create(command: CreateBankAccountCommand): String { + apply(BankAccountCreatedEvent(command.bankAccountId, command.overdraftLimit)) + return command.bankAccountId + } + + /** + * Deposits money to account. + */ + @CommandHandler + fun deposit(command: DepositMoneyCommand) { + apply(MoneyDepositedEvent(id.toString(), command.amountOfMoney)) + } + + /** + * Withdraw money from account. + */ + @CommandHandler + fun withdraw(command: WithdrawMoneyCommand) { + if (command.amountOfMoney <= balanceInCents + overdraftLimit) { + apply(MoneyWithdrawnEvent(id.toString(), command.amountOfMoney)) + } + } + + /** + * Return money from account. + */ + @CommandHandler + fun returnMoney(command: ReturnMoneyOfFailedBankTransferCommand) { + apply(MoneyOfFailedBankTransferReturnedEvent(id.toString(), command.amount)) + } + + /** + * Handler to initialize bank accounts attributes. + */ + @EventSourcingHandler + fun on(event: BankAccountCreatedEvent) { + overdraftLimit = event.overdraftLimit + balanceInCents = 0 + } + + /** + * Handler adjusting balance. + */ + @EventSourcingHandler + fun on(event: MoneyAddedEvent) { + balanceInCents += event.amount + } + + /** + * Handler adjusting balance. + */ + @EventSourcingHandler + fun on(event: MoneySubtractedEvent) { + balanceInCents -= event.amount + } + +} \ No newline at end of file diff --git a/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccountAdvanced.kt b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccountAdvanced.kt new file mode 100644 index 0000000..0b790f0 --- /dev/null +++ b/kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccountAdvanced.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.example.core + +import mu.KLogging +import org.axonframework.commandhandling.CommandHandler +import org.axonframework.commandhandling.gateway.CommandGateway +import org.axonframework.eventsourcing.EventSourcingHandler +import org.axonframework.extension.kotlin.example.api.AdvancedBankAccountCreatedEvent +import org.axonframework.extension.kotlin.example.api.CreateAdvancedBankAccountCommand +import org.axonframework.extension.kotlin.spring.AggregateWithImmutableIdentifier +import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter +import org.axonframework.extensions.kotlin.send +import org.axonframework.modelling.command.AggregateCreationPolicy +import org.axonframework.modelling.command.AggregateIdentifier +import org.axonframework.modelling.command.AggregateLifecycle.apply +import org.axonframework.modelling.command.CreationPolicy +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.stereotype.Service +import java.util.* + +/** + * Advanced bank config. + */ +@Configuration +class AdvancedBankConfiguration(private val advancedBankAccountService: AdvancedBankAccountService) { + + /** + * Application run starting bank ops. + */ + @Bean + fun advancedAccountOperationsRunner() = ApplicationRunner { + advancedBankAccountService.accountOperations() + } + + /** + * Bank identifier converter. + */ + @Bean + fun bankIdentifierConverter() = object : AggregateIdentifierConverter { + override fun apply(aggregateIdentifier: String) = BankAccountIdentifier(aggregateIdentifier.subSequence(3, aggregateIdentifier.length - 3).toString()) + } + + /** + * Long converter (not used), should remain to demonstrate correct converter selection. + */ + @Bean + fun longConverter() = object : AggregateIdentifierConverter { + override fun apply(aggregateIdentifier: String) = aggregateIdentifier.toLong() + } + +} + +/** + * Advanced bank service. + */ +@Service +class AdvancedBankAccountService(private val commandGateway: CommandGateway) { + + companion object : KLogging() + + /** + * Runs account ops. + */ + fun accountOperations() { + + val accountIdAdvanced = BankAccountIdentifier(UUID.randomUUID().toString()) + logger.info { "\nPerforming advanced operations on account $accountIdAdvanced" } + + commandGateway.send( + command = CreateAdvancedBankAccountCommand(accountIdAdvanced, 100), + onSuccess = { _, result: Any?, _ -> + logger.info { "Successfully created account with id: $result" } + }, + onError = { c, e, _ -> logger.error(e) { "Error creating account ${c.payload.bankAccountId}" } } + ) + } +} + + +/** + * Value type for bank account identifier. + */ +data class BankAccountIdentifier(val id: String) { + override fun toString(): String = "<<<$id>>>" +} + +/** + * Aggregate using a complex type as identifier. + */ +@AggregateWithImmutableIdentifier +data class BankAccountAdvanced( + @AggregateIdentifier + private val id: BankAccountIdentifier +) { + + private var overdraftLimit: Long = 0 + private var balanceInCents: Long = 0 + + /** + * Create command handler. + */ + @CommandHandler + @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) + fun create(command: CreateAdvancedBankAccountCommand): BankAccountIdentifier { + apply(AdvancedBankAccountCreatedEvent(command.bankAccountId, command.overdraftLimit)) + return command.bankAccountId + } + + + /** + * Handler to initialize bank accounts attributes. + */ + @EventSourcingHandler + fun on(event: AdvancedBankAccountCreatedEvent) { + overdraftLimit = event.overdraftLimit + balanceInCents = 0 + } +} + diff --git a/kotlin-example/src/main/resources/application.yaml b/kotlin-example/src/main/resources/application.yaml new file mode 100644 index 0000000..6da84a4 --- /dev/null +++ b/kotlin-example/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +axon: + axonserver: + enabled: false \ No newline at end of file diff --git a/kotlin-springboot-autoconfigure/pom.xml b/kotlin-springboot-autoconfigure/pom.xml new file mode 100644 index 0000000..6cbb34e --- /dev/null +++ b/kotlin-springboot-autoconfigure/pom.xml @@ -0,0 +1,74 @@ + + + + + 4.0.0 + + Axon Framework - Kotlin Extension SpringBoot AutoConfigure + Module for the Kotlin SpringBoot AutoConfigure of Axon Framework + + + org.axonframework.extensions.kotlin + axon-kotlin-parent + 0.3.0-SNAPSHOT + + + axon-kotlin-springboot-autoconfigure + + + + false + + + + + + org.axonframework.extensions.kotlin + axon-kotlin + ${project.version} + + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.axonframework + axon-modelling + + + org.axonframework + axon-configuration + + + io.github.microutils + kotlin-logging-jvm + + + diff --git a/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregateWithImmutableIdentifier.kt b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregateWithImmutableIdentifier.kt new file mode 100644 index 0000000..11543a5 --- /dev/null +++ b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregateWithImmutableIdentifier.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.spring + +import org.axonframework.modelling.command.AggregateRoot + +/** + * Marker for an aggregate with immutable identifier. + * + * @since 0.2.0 + * @author Simon Zambrovski + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +@AggregateRoot +annotation class AggregateWithImmutableIdentifier \ No newline at end of file diff --git a/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierConfiguration.kt b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierConfiguration.kt new file mode 100644 index 0000000..1580813 --- /dev/null +++ b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierConfiguration.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.spring + +import mu.KLogging +import org.axonframework.config.AggregateConfigurer.defaultConfiguration +import org.axonframework.config.Configurer +import org.axonframework.eventsourcing.EventSourcingRepository +import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter +import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingIdentifier +import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingStringIdentifier +import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingUUIDIdentifier +import org.axonframework.extensions.kotlin.aggregate.EventSourcingImmutableIdentifierAggregateRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.util.* + +/** + * Configuration to activate the aggregate with immutable identifier detection and registration of the corresponding factories and repositories. + * @see EnableAggregateWithImmutableIdentifierScan for activation. + * + * @author Simon Zambrovski + * @since 0.2.0 + */ +@Configuration +class AggregatesWithImmutableIdentifierConfiguration( + private val context: ApplicationContext +) { + + companion object : KLogging() + + /** + * Initializes the settings. + * @return settings. + */ + @Bean + @ConditionalOnMissingBean + fun initialize(): AggregatesWithImmutableIdentifierSettings { + val beans = context.getBeansWithAnnotation(EnableAggregateWithImmutableIdentifierScan::class.java) + require(beans.isNotEmpty()) { + "EnableAggregateWithImmutableIdentifierScan should be activated exactly once." + } + require(beans.size == 1) { + "EnableAggregateWithImmutableIdentifierScan should be activated exactly once, but was found on ${beans.size} beans:\n" + beans.map { it.key }.joinToString() + } + val basePackage = EnableAggregateWithImmutableIdentifierScan.getBasePackage(beans.entries.first().value) + return AggregatesWithImmutableIdentifierSettings(basePackage = basePackage + ?: throw IllegalStateException("Required setting basePackage could not be initialized, consider to provide your own AggregatesWithImmutableIdentifierSettings.") + ) + } + + @Autowired + fun configureAggregates( + configurer: Configurer, + settings: AggregatesWithImmutableIdentifierSettings, + @Autowired(required = false) identifierConverters: List>? + ) { + val converters = identifierConverters ?: emptyList() // fallback to empty list if none are defined + + logger.info { "Discovered ${converters.size} converters for aggregate identifiers." } + logger.info { "Scanning ${settings.basePackage} for aggregates" } + + AggregateWithImmutableIdentifier::class + .findAnnotatedAggregateClasses(settings.basePackage) + .map { aggregateClazz -> + + val aggregateFactory = when (val idFieldClazz = aggregateClazz.extractAggregateIdentifierClass()) { + String::class -> usingStringIdentifier(aggregateClazz) + UUID::class -> usingUUIDIdentifier(aggregateClazz) + else -> usingIdentifier(aggregateClazz, idFieldClazz, converters.findIdentifierConverter(idFieldClazz)) + }.also { + logger.debug { "Registering aggregate factory $it" } + } + + configurer.configureAggregate( + defaultConfiguration(aggregateClazz.java) + .configureRepository { config -> + EventSourcingImmutableIdentifierAggregateRepository( + builder = EventSourcingRepository + .builder(aggregateClazz.java) + .eventStore(config.eventStore()) + .aggregateFactory(aggregateFactory) + ) + } + ) + } + } + +} + diff --git a/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierSettings.kt b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierSettings.kt new file mode 100644 index 0000000..8e8b4b0 --- /dev/null +++ b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierSettings.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.spring + +/** + * Settings class to pass values into configuration of aggregate scan. + * @param basePackage base package of scan. + * + * @author Simon Zambrovski + * @since 0.2.0 + */ +data class AggregatesWithImmutableIdentifierSettings( + val basePackage: String +) \ No newline at end of file diff --git a/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/EnableAggregateWithImmutableIdentifierScan.kt b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/EnableAggregateWithImmutableIdentifierScan.kt new file mode 100644 index 0000000..93c7201 --- /dev/null +++ b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/EnableAggregateWithImmutableIdentifierScan.kt @@ -0,0 +1,40 @@ +package org.axonframework.extension.kotlin.spring + +import org.springframework.context.annotation.Import +import org.springframework.core.annotation.AnnotationUtils + +/** + * Annotation to enable aggregate scan. + * @param basePackage specifies the package to scan for aggregates, if not specified, defaults to base package of the annotated class. + * + * @author Simon Zambrovski + * @since 0.2.0 + */ +@MustBeDocumented +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Import(AggregatesWithImmutableIdentifierConfiguration::class) +annotation class EnableAggregateWithImmutableIdentifierScan( + val basePackage: String = NULL_VALUE +) { + companion object { + + /** + * Null value to allow package scan on bean. + */ + const val NULL_VALUE = "" + + /** + * Reads base package from annotation or, if not provided from the annotated class. + * @param bean annotated bean + * @return base package or null if not defined and can't be read. + */ + fun getBasePackage(bean: Any): String? { + val annotation = AnnotationUtils.findAnnotation(bean::class.java, EnableAggregateWithImmutableIdentifierScan::class.java) ?: return null + return if (annotation.basePackage == NULL_VALUE) { + bean::class.java.`package`.name + } else { + annotation.basePackage + } + } + } +} \ No newline at end of file diff --git a/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/ReflectionExtensions.kt b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/ReflectionExtensions.kt new file mode 100644 index 0000000..98e3188 --- /dev/null +++ b/kotlin-springboot-autoconfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/ReflectionExtensions.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extension.kotlin.spring + +import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider +import org.springframework.core.type.filter.AnnotationTypeFilter +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.jvm.jvmErasure + +/** + * Scans classpath for annotated aggregate classes. + * @param scanPackage package to scan. + * @return list of annotated classes. + */ +internal fun KClass.findAnnotatedAggregateClasses(scanPackage: String): List> { + val provider = ClassPathScanningCandidateComponentProvider(false) + provider.addIncludeFilter(AnnotationTypeFilter(this.java)) + return provider.findCandidateComponents(scanPackage).map { + @Suppress("UNCHECKED_CAST") + Class.forName(it.beanClassName).kotlin as KClass + } +} + +/** + * Extracts the class of aggregate identifier from an aggregate. + * @return class of aggregate identifier. + * @throws IllegalArgumentException if the required constructor is not found. + */ +internal fun KClass<*>.extractAggregateIdentifierClass(): KClass { + + /** + * Holder for constructor and its parameters. + * @param constructor constructor to holf info for. + */ + data class ConstructorParameterInfo(val constructor: KFunction) { + private val valueProperties by lazy { constructor.parameters.filter { it.kind == KParameter.Kind.VALUE } } // collect only "val" properties + + /** + * Check if the provided constructor has only one value parameter. + */ + fun isConstructorWithOneValue() = valueProperties.size == 1 + + /** + * Retrieves the class of value parameter. + * @return class of value. + */ + fun getParameterClass(): KClass<*> = valueProperties[0].type.jvmErasure + } + + val constructors = this.constructors.map { ConstructorParameterInfo(it) }.filter { it.isConstructorWithOneValue() } // exactly one parameter in primary constructor + require(constructors.size == 1) { "Expected exactly one constructor with aggregate identifier parameter, but found ${constructors.size}." } + @Suppress("UNCHECKED_CAST") + return constructors[0].getParameterClass() as KClass +} + + +/** + * Extension function to find a matching converter for provided identifier class. + * @param idClazz class of identifier to look for. + * @return a matching converter. + * @throws IllegalArgumentException if converter can not be identified (none or more than one are defined). + */ +internal fun List>.findIdentifierConverter(idClazz: KClass): AggregateIdentifierConverter { + val converters = this.filter { + idClazz == it.getConverterIdentifierClass() + }.map { + @Suppress("UNCHECKED_CAST") + it as AggregateIdentifierConverter + } + require(converters.isNotEmpty()) { + "Could not find an AggregateIdentifierConverter for ${idClazz.qualifiedName}. Consider to register a bean implementing AggregateIdentifierConverter<${idClazz.qualifiedName}>" + } + require(converters.size == 1) { + "Found more than one AggregateIdentifierConverter for ${idClazz.qualifiedName}. This is currently not supported." + } + return converters.first() +} + +/** + * Returns the concrete class of ID. + * @return class of aggregate identifier or null if it can't be resolved. + */ +internal fun AggregateIdentifierConverter<*>.getConverterIdentifierClass() = this::class.supertypes.first { superTypes -> superTypes.classifier == AggregateIdentifierConverter::class }.arguments[0].type?.jvmErasure + +/** + * Converts a string to the same string with first lower letter. + */ +fun String?.toFirstLower() = this?.substring(0, 1)?.toLowerCase() + this?.substring(1, this?.length - 1) \ No newline at end of file diff --git a/kotlin-springboot-starter/pom.xml b/kotlin-springboot-starter/pom.xml new file mode 100644 index 0000000..7232220 --- /dev/null +++ b/kotlin-springboot-starter/pom.xml @@ -0,0 +1,43 @@ + + + + + 4.0.0 + + Axon Framework - Kotlin Extension SpringBoot Starter + Module for the Kotlin Extension SpringBoot Starter of Axon Framework + + + org.axonframework.extensions.kotlin + axon-kotlin-parent + 0.3.0-SNAPSHOT + + + axon-kotlin-springboot-starter + + + + org.axonframework.extensions.kotlin + axon-kotlin-springboot-autoconfigure + ${project.version} + + + org.springframework.boot + spring-boot-starter + + + diff --git a/kotlin/pom.xml b/kotlin/pom.xml index e310c5c..fe153f5 100644 --- a/kotlin/pom.xml +++ b/kotlin/pom.xml @@ -49,5 +49,4 @@ - diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/Utils.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/Utils.kt new file mode 100644 index 0000000..fd202c6 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/Utils.kt @@ -0,0 +1,15 @@ +package org.axonframework.extensions.kotlin + +/** + * Tries to execute the given function or reports an error on failure. + * @param errorMessage message to report on error. + * @param function: function to invoke + */ +@Throws(IllegalArgumentException::class) +internal fun invokeReporting(errorMessage: String, function: () -> T): T { + return try { + function.invoke() + } catch (e: Exception) { + throw IllegalArgumentException(errorMessage, e) + } +} diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateConfigurerExtensions.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateConfigurerExtensions.kt new file mode 100644 index 0000000..46660b2 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateConfigurerExtensions.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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 + * + * http://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. + */ + +package org.axonframework.extensions.kotlin.aggregate + +import org.axonframework.common.jpa.EntityManagerProvider +import org.axonframework.config.AggregateConfigurer +import org.axonframework.config.AggregateConfigurer.jpaMappedConfiguration + +/** + * Creates default aggregate configurer with a usage of a reified type information. + * @param [A] type of aggregate. + */ +inline fun defaultConfiguration(): AggregateConfigurer = AggregateConfigurer.defaultConfiguration(A::class.java) + +/** + * Creates JPA-mapped aggregate configurer with a usage of a reified type information. + * @param [A] type of aggregate. + */ +inline fun jpaMappedConfiguration(): AggregateConfigurer = jpaMappedConfiguration(A::class.java) + +/** + * Creates JPA-mapped aggregate configurer with a usage of a reified type information. + * @param entityManagerProvider entity manager provider. + * @param [A] type of aggregate. + */ +inline fun jpaMappedConfiguration(entityManagerProvider: EntityManagerProvider): AggregateConfigurer = jpaMappedConfiguration(A::class.java, entityManagerProvider) \ No newline at end of file diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateIdentifierConverter.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateIdentifierConverter.kt new file mode 100644 index 0000000..e5257b3 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateIdentifierConverter.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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 + * + * http://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. + */ + +package org.axonframework.extensions.kotlin.aggregate + +import java.util.* +import java.util.function.Function + +/** + * Defines a converter from a string to custom identifier type. + * @param [ID] type of aggregate identifier. + * + * @author Simon Zambrovski + * @since 0.2.0 + */ +interface AggregateIdentifierConverter : Function { + + /** + * Default string converter. + */ + object DefaultString : AggregateIdentifierConverter { + override fun apply(it: String): String = it + override fun toString(): String = this::class.qualifiedName!! + } + + /** + * Default UUID converter. + */ + object DefaultUUID : AggregateIdentifierConverter { + override fun apply(it: String): UUID = UUID.fromString(it) + override fun toString(): String = this::class.qualifiedName!! + } +} \ No newline at end of file diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingImmutableIdentifierAggregateRepository.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingImmutableIdentifierAggregateRepository.kt new file mode 100644 index 0000000..f041416 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingImmutableIdentifierAggregateRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extensions.kotlin.aggregate + +import org.axonframework.eventsourcing.EventSourcedAggregate +import org.axonframework.eventsourcing.EventSourcingRepository +import org.axonframework.messaging.unitofwork.CurrentUnitOfWork +import org.axonframework.modelling.command.Aggregate +import org.axonframework.modelling.command.LockAwareAggregate +import java.util.concurrent.Callable + +/** + * Event souring repository which uses a aggregate factory to create new aggregate passing null as first event. + * @param builder repository builder with configuration. + * + * @since 0.2.0 + * @author Simon Zambrovski + */ +class EventSourcingImmutableIdentifierAggregateRepository( + builder: Builder +) : EventSourcingRepository(builder) { + + override fun loadOrCreate(aggregateIdentifier: String, factoryMethod: Callable): Aggregate { + val factory = super.getAggregateFactory() + val uow = CurrentUnitOfWork.get() + val aggregates: MutableMap>> = managedAggregates(uow) + val aggregate = aggregates.computeIfAbsent(aggregateIdentifier) { aggregateId: String -> + try { + return@computeIfAbsent doLoadOrCreate(aggregateId) { + // call the factory and instead of newInstance on the aggregate class + factory.createAggregateRoot(aggregateId, null) + } + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw RuntimeException(e) + } + } + uow.onRollback { aggregates.remove(aggregateIdentifier) } + prepareForCommit(aggregate) + return aggregate + } +} \ No newline at end of file diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/ImmutableIdentifierAggregateFactory.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/ImmutableIdentifierAggregateFactory.kt new file mode 100644 index 0000000..293b95c --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/aggregate/ImmutableIdentifierAggregateFactory.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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 + * + * http://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. + */ + +package org.axonframework.extensions.kotlin.aggregate + +import org.axonframework.eventhandling.DomainEventMessage +import org.axonframework.eventsourcing.AggregateFactory +import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter.DefaultString +import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter.DefaultUUID +import org.axonframework.extensions.kotlin.invokeReporting +import java.util.* +import kotlin.reflect.KClass + +/** + * Factory to create aggregates [A] with immutable aggregate identifier of type [ID]. + * @constructor creates aggregate factory. + * @param clazz aggregate class. + * @param idClazz aggregate identifier class. + * @param aggregateFactoryMethod factory method to create instances, defaults to default constructor of the provided [clazz]. + * @param idExtractor function to convert aggregate identifier from string to [ID]. + * @param [A] aggregate type. + * @param [ID] aggregate identifier type. + * + * @since 0.2.0 + * @author Simon Zambrovski + */ +data class ImmutableIdentifierAggregateFactory( + val clazz: KClass, + val idClazz: KClass, + val aggregateFactoryMethod: AggregateFactoryMethod = extractConstructorFactory(clazz, idClazz), + val idExtractor: AggregateIdentifierConverter +) : AggregateFactory { + + companion object { + + /** + * Reified factory method for aggregate factory using string as aggregate identifier. + * @return instance of ImmutableIdentifierAggregateFactory + */ + inline fun usingStringIdentifier() = usingIdentifier(String::class) { it } + + /** + * Factory method for aggregate factory using string as aggregate identifier. + * @return instance of ImmutableIdentifierAggregateFactory + */ + fun usingStringIdentifier(clazz: KClass) = usingIdentifier(aggregateClazz = clazz, idClazz = String::class, idExtractor = DefaultString) + + /** + * Reified factory method for aggregate factory using UUID as aggregate identifier. + * @return instance of ImmutableIdentifierAggregateFactory + */ + inline fun usingUUIDIdentifier() = usingIdentifier(idClazz = UUID::class, idExtractor = DefaultUUID::apply) + + /** + * Factory method for aggregate factory using UUID as aggregate identifier. + * @return instance of ImmutableIdentifierAggregateFactory + */ + fun usingUUIDIdentifier(clazz: KClass) = usingIdentifier(aggregateClazz = clazz, idClazz = UUID::class, idExtractor = DefaultUUID) + + /** + * Reified factory method for aggregate factory using specified identifier type and converter function. + * @param idClazz identifier class. + * @param idExtractor extractor function for identifier from string. + * @return instance of ImmutableIdentifierAggregateFactory + */ + inline fun usingIdentifier(idClazz: KClass, noinline idExtractor: (String) -> ID) = + ImmutableIdentifierAggregateFactory(clazz = A::class, idClazz = idClazz, idExtractor = object : AggregateIdentifierConverter { + override fun apply(it: String): ID = idExtractor(it) + }) + + /** + * Factory method for aggregate factory using specified identifier type and converter. + * @param idClazz identifier class. + * @param idExtractor extractor for identifier from string. + * @return instance of ImmutableIdentifierAggregateFactory + */ + fun usingIdentifier(aggregateClazz: KClass, idClazz: KClass, idExtractor: AggregateIdentifierConverter) = + ImmutableIdentifierAggregateFactory(clazz = aggregateClazz, idClazz = idClazz, idExtractor = idExtractor) + + /** + * Tries to extract constructor from given class. Used as a default factory method for the aggregate. + * @param clazz aggregate class. + * @param idClazz id class. + * @return factory method to create new instances of aggregate. + */ + fun extractConstructorFactory(clazz: KClass, idClazz: KClass): AggregateFactoryMethod = { + val constructor = invokeReporting( + "The aggregate [${clazz.java.name}] doesn't provide a constructor for the identifier type [${idClazz.java.name}]." + ) { clazz.java.getConstructor(idClazz.java) } + constructor.newInstance(it) + } + } + + + @Throws(IllegalArgumentException::class) + override fun createAggregateRoot(aggregateIdentifier: String, message: DomainEventMessage<*>?): A { + + val id: ID = invokeReporting( + "The identifier [$aggregateIdentifier] could not be converted to the type [${idClazz.java.name}], required for the ID of aggregate [${clazz.java.name}]." + ) { idExtractor.apply(aggregateIdentifier) } + + return aggregateFactoryMethod.invoke(id) + } + + override fun getAggregateType(): Class = clazz.java + +} + +/** + * Type alias for function creating the aggregate from id. + */ +typealias AggregateFactoryMethod = (ID) -> A diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/QueryUpdateEmitterExtensionsTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/QueryUpdateEmitterExtensionsTest.kt index 6e98ad0..73ac53c 100644 --- a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/QueryUpdateEmitterExtensionsTest.kt +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/QueryUpdateEmitterExtensionsTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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 + * + * http://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. + */ package org.axonframework.extensions.kotlin import io.mockk.every diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateWithImmutableIdentifierFactoryTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateWithImmutableIdentifierFactoryTest.kt new file mode 100644 index 0000000..f18871f --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateWithImmutableIdentifierFactoryTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extensions.kotlin.aggregate + +import org.axonframework.extensions.kotlin.TestLongAggregate +import org.axonframework.extensions.kotlin.TestStringAggregate +import org.axonframework.extensions.kotlin.TestUUIDAggregate +import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingIdentifier +import java.util.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +/** + * Test for the aggregate factory. + * + * @author Simon Zambrovski + */ +internal class AggregateWithImmutableIdentifierFactoryTest { + + @Test + fun `should create string aggregate`() { + val aggregateId = UUID.randomUUID().toString() + val factory = ImmutableIdentifierAggregateFactory.usingStringIdentifier() + val aggregate = factory.createAggregateRoot(aggregateId, null) + + assertEquals(aggregateId, aggregate.aggregateId) + } + + @Test + fun `should create uuid aggregate`() { + val aggregateId = UUID.randomUUID() + val factory: ImmutableIdentifierAggregateFactory = usingIdentifier(UUID::class) { UUID.fromString(it) } + val aggregate = factory.createAggregateRoot(aggregateId.toString(), null) + + assertEquals(aggregateId, aggregate.aggregateId) + } + + @Test + fun `should fail create aggregate with wrong constructor type`() { + val aggregateId = UUID.randomUUID() + // pretending the TestLongAggregate to have UUID as identifier. + val factory: ImmutableIdentifierAggregateFactory = usingIdentifier(UUID::class) { UUID.fromString(it) } + + val exception = assertFailsWith { + factory.createAggregateRoot(aggregateId.toString(), null) + } + + assertEquals(exception.message, + "The aggregate [${factory.aggregateType.name}] doesn't provide a constructor for the identifier type [${UUID::class.java.name}].") + } + + @Test + fun `should fail create aggregate error in extractor`() { + val aggregateId = UUID.randomUUID() + // the extractor is broken. + val factory: ImmutableIdentifierAggregateFactory = usingIdentifier(UUID::class) { throw java.lang.IllegalArgumentException("") } + + val exception = assertFailsWith { + factory.createAggregateRoot(aggregateId.toString(), null) + } + + assertEquals(exception.message, + "The identifier [$aggregateId] could not be converted to the type [${UUID::class.java.name}], required for the ID of aggregate [${factory.aggregateType.name}].") + } + +} \ No newline at end of file diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingAggregateWithImmutableIdentifierRepositoryTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingAggregateWithImmutableIdentifierRepositoryTest.kt new file mode 100644 index 0000000..2dfed78 --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/aggregate/EventSourcingAggregateWithImmutableIdentifierRepositoryTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020. Axon Framework + * + * 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 + * + * http://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. + */ +package org.axonframework.extensions.kotlin.aggregate + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.axonframework.commandhandling.GenericCommandMessage +import org.axonframework.eventsourcing.AggregateFactory +import org.axonframework.eventsourcing.EventSourcingRepository +import org.axonframework.eventsourcing.eventstore.DomainEventStream +import org.axonframework.eventsourcing.eventstore.EventStore +import org.axonframework.extensions.kotlin.TestStringAggregate +import org.axonframework.messaging.unitofwork.DefaultUnitOfWork +import org.axonframework.messaging.unitofwork.UnitOfWork +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.* +import java.util.concurrent.Callable +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.fail + +/** + * Test for the aggregate repository using the factory. + * + * @author Simon Zambrovski + */ +internal class EventSourcingAggregateWithImmutableIdentifierRepositoryTest { + + private val aggregateIdentifier = UUID.randomUUID().toString() + private val eventStore = mockk() + private val aggregateFactory = mockk>() + private lateinit var uow: DefaultUnitOfWork<*> + + @BeforeTest + fun `init components`() { + uow = DefaultUnitOfWork.startAndGet(GenericCommandMessage("some payload")) + + every { aggregateFactory.aggregateType }.returns(TestStringAggregate::class.java) + every { aggregateFactory.createAggregateRoot(aggregateIdentifier, null) }.returns(TestStringAggregate(aggregateIdentifier)) + + // no events + every { eventStore.readEvents(aggregateIdentifier) }.returns(DomainEventStream.empty()) + } + + @AfterTest + fun `check uow`() { + assertEquals(UnitOfWork.Phase.STARTED, uow.phase()) + } + + @Test + fun `should ask factory to create the aggregate`() { + + + val repo = EventSourcingImmutableIdentifierAggregateRepository( + EventSourcingRepository + .builder(TestStringAggregate::class.java) + .eventStore(eventStore) + .aggregateFactory(aggregateFactory) + ) + + val factoryMethod = Callable { + fail("The factory method should not be called.") + } + val aggregate = repo.loadOrCreate(aggregateIdentifier = aggregateIdentifier, factoryMethod = factoryMethod) + + assertEquals(aggregateIdentifier, aggregate.identifierAsString()) + verify { + aggregateFactory.createAggregateRoot(aggregateIdentifier, null) + } + + } +} \ No newline at end of file diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/testObjects.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/testObjects.kt index 99c8961..1c596bd 100644 --- a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/testObjects.kt +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/testObjects.kt @@ -1,6 +1,23 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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 + * + * http://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. + */ package org.axonframework.extensions.kotlin +import org.axonframework.modelling.command.AggregateIdentifier import org.axonframework.modelling.command.TargetAggregateIdentifier +import java.util.* /** * Simple Query class to be used in tests. @@ -11,3 +28,18 @@ internal data class ExampleQuery(val value: Number) * Simple Command class to be used in tests. */ internal data class ExampleCommand(@TargetAggregateIdentifier val id: String) + +/** + * Immutable aggregate with String identifier. + */ +internal data class TestStringAggregate(@AggregateIdentifier val aggregateId: String) + +/** + * Immutable aggregate with UUID identifier. + */ +internal data class TestUUIDAggregate(@AggregateIdentifier val aggregateId: UUID) + +/** + * Immutable aggregate with Long identifier. + */ +internal data class TestLongAggregate(@AggregateIdentifier val aggregateId: Long) \ No newline at end of file diff --git a/pom.xml b/pom.xml index a891693..d8e9b1d 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,8 @@ kotlin kotlin-test + kotlin-springboot-autoconfigure + kotlin-springboot-starter @@ -42,6 +44,8 @@ 2.13.3 1.5.31 + 2.5.5 + + + ${project.groupId} + axon-kotlin + ${project.version} + + + + ${project.groupId} + axon-kotlin-springboot-autoconfigure + ${project.version} + + + + ${project.groupId} + axon-kotlin-springboot-starter + ${project.version} + + + org.jetbrains.kotlin kotlin-bom @@ -60,17 +85,36 @@ import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + org.axonframework axon-configuration ${axon.version} + + org.axonframework + axon-modelling + ${axon.version} + org.axonframework axon-test ${axon.version} - + + org.axonframework + axon-spring-boot-starter + ${axon.version} + com.fasterxml.jackson.module jackson-module-kotlin @@ -135,6 +179,11 @@ junit-jupiter test + + org.junit.jupiter + junit-jupiter-params + test + org.jetbrains.kotlin kotlin-test-junit5 @@ -150,7 +199,6 @@ slf4j-simple test - @@ -207,12 +255,14 @@ org.jacoco jacoco-maven-plugin - 0.8.7 prepare-agent + + surefireArgLine + report @@ -247,6 +297,19 @@ + + + example + + + !skipExamples + + + + kotlin-example + + + @@ -349,6 +412,7 @@ **/*Test_*.java **/*Tests_*.java + ${surefireArgLine} ${slf4j.version} ${log4j.version} @@ -432,6 +496,7 @@ no-arg + spring all-open @@ -480,6 +545,17 @@ + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.jetbrains.kotlin + kotlin-maven-plugin + +