) {
val serviceClass = convention.declarationFor(view.service).name
val annotation = ApiAnnotation(serviceClass, annotationClass)
- annotation.registerWith(context!!)
+ annotation.registerWith(context)
annotation.renderSources(sources)
}
diff --git a/mc-java-base/build.gradle.kts b/mc-java-base/build.gradle.kts
index e9536a6d0..a6218315e 100644
--- a/mc-java-base/build.gradle.kts
+++ b/mc-java-base/build.gradle.kts
@@ -24,9 +24,13 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+import io.spine.dependency.lib.KotlinPoet
+import io.spine.dependency.local.Base
import io.spine.dependency.local.Logging
+import io.spine.dependency.local.ModelCompiler
import io.spine.dependency.local.ProtoData
import io.spine.dependency.local.Spine
+import io.spine.dependency.local.TestLib
import io.spine.dependency.local.ToolBase
import io.spine.dependency.local.Validation
@@ -41,29 +45,30 @@ dependencies {
val apiDeps = arrayOf(
Logging.lib,
- Spine.modelCompiler,
+ ModelCompiler.lib,
ProtoData.java,
Validation.config,
- ToolBase.pluginBase
+ ToolBase.pluginBase,
+ KotlinPoet.lib,
)
apiDeps.forEach {
api(it) {
excludeSpineBase()
}
}
- api(Spine.base)
+ api(Base.lib)
arrayOf(
- Spine.base,
+ Base.lib,
gradleTestKit() /* for creating a Gradle project. */,
- Spine.testlib,
+ TestLib.lib,
ProtoData.testlib /* `PipelineSetup` API. */
).forEach {
// Expose using API level for the submodules.
testFixturesApi(it)
}
- testImplementation(Spine.testlib)
+ testImplementation(TestLib.lib)
testImplementation(gradleTestKit())
testImplementation(ToolBase.pluginTestlib)
}
diff --git a/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessor.java b/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessor.java
index 004d79df2..da2588458 100644
--- a/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessor.java
+++ b/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessor.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* 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
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -28,7 +28,6 @@
import com.google.common.base.Objects;
import com.google.errorprone.annotations.Immutable;
-import io.spine.tools.java.code.field.FieldName;
import java.io.Serializable;
@@ -70,18 +69,6 @@ public static Accessor prefixAndPostfix(String prefix, String suffix) {
return new Accessor(prefix, suffix);
}
- /**
- * Formats an accessor method name based on this template and the given field name.
- *
- * @param field
- * the name of the field to access
- * @return the method name
- */
- public String format(FieldName field) {
- var name = String.format(template(), field.capitalize());
- return name;
- }
-
private String template() {
return prefix + "%s" + postfix;
}
diff --git a/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessors.java b/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessors.java
deleted file mode 100644
index 85e1add46..000000000
--- a/mc-java-base/src/main/java/io/spine/tools/mc/java/field/Accessors.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2022, TeamDev. All rights reserved.
- *
- * 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
- *
- * Redistribution and use in source and/or binary forms, with or without
- * modification, must retain the above copyright notice and the following
- * disclaimer.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package io.spine.tools.mc.java.field;
-
-import com.google.common.collect.ImmutableSet;
-import io.spine.annotation.Internal;
-import io.spine.tools.java.code.field.FieldName;
-
-import java.util.Collection;
-
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-
-/**
- * Property accessor methods generated by the Protobuf compiler for a field.
- *
- * Each Protobuf field results in a number of accessor methods. The count and naming of
- * the methods depends on the field type.
- */
-@Internal
-public final class Accessors {
-
- private final FieldName propertyName;
- private final FieldType type;
-
- private Accessors(FieldName propertyName, FieldType type) {
- this.propertyName = propertyName;
- this.type = type;
- }
-
- /**
- * Creates an instance of {@code GeneratedAccessors} for the given field.
- *
- * @param name
- * the name of the field associated with the accessors
- * @param type
- * the type of the field associated with the accessors
- * @return new instance
- */
- public static Accessors forField(io.spine.code.proto.FieldName name, FieldType type) {
- var javaFieldName = FieldName.from(name);
- return new Accessors(javaFieldName, type);
- }
-
- /**
- * Obtains all the names of the accessor methods.
- *
- *
The accessor methods may have different parameters. Some of the obtained names may
- * reference several method overloads.
- */
- public ImmutableSet names() {
- var names = names(type.accessors());
- return names;
- }
-
- private ImmutableSet names(Collection accessors) {
- return accessors.stream()
- .map(accessor -> accessor.format(propertyName))
- .collect(toImmutableSet());
- }
-}
diff --git a/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/GeneratedAnnotation.kt b/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/GeneratedAnnotation.kt
index 5b88b26bf..ba3fa2ed8 100644
--- a/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/GeneratedAnnotation.kt
+++ b/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/GeneratedAnnotation.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,10 +29,12 @@ package io.spine.tools.mc.java
import com.intellij.psi.PsiAnnotation
import io.spine.annotation.Generated
import io.spine.tools.java.reference
-import io.spine.tools.mc.java.GeneratedAnnotation.create
+import io.spine.tools.mc.java.GeneratedAnnotation.forPsi
import io.spine.tools.mc.java.VersionHolder.version
import io.spine.tools.psi.java.Environment.elementFactory
import org.intellij.lang.annotations.Language
+import com.squareup.javapoet.AnnotationSpec as JAnnotationSpec
+import com.squareup.kotlinpoet.AnnotationSpec as KAnnotationSpec
/**
* Creates [PsiAnnotation] for marking code elements created by McJava.
@@ -43,21 +45,20 @@ import org.intellij.lang.annotations.Language
* one renderer to others.
*
* @see Generated
- * @see create
+ * @see forPsi
* @see VersionHolder
*/
public object GeneratedAnnotation {
+ private val defaultValue = "by Spine Model Compiler (version: ${version.value})"
+
/**
- * Creates a new [PsiAnnotation] with [javax.annotation.Generated] referencing the current
- * version of Spine Model Compiler.
+ * Creates a new [PsiAnnotation] with the [Generated] annotation.
*
* @param value The string to be put into the annotation `value` parameter.
* The default value refers to the current version of Spine Model Compiler.
*/
- public fun create(
- value: String = "by Spine Model Compiler (version: ${version.value})"
- ): PsiAnnotation {
+ public fun forPsi(value: String = defaultValue): PsiAnnotation {
val reference = Generated::class.java.reference
@Language("JAVA") @Suppress("EmptyClass", "DuplicateStringLiteralInspection")
val annotation = elementFactory.createAnnotationFromText(
@@ -67,4 +68,27 @@ public object GeneratedAnnotation {
)
return annotation
}
+
+ /**
+ * Creates a new [PsiAnnotation] with the [Generated] annotation.
+ *
+ * @param value The string to be put into the annotation `value` parameter.
+ * The default value refers to the current version of Spine Model Compiler.
+ */
+ public fun forJavaPoet(value: String = defaultValue): JAnnotationSpec =
+ JAnnotationSpec.builder(Generated::class.java)
+ .addMember("value", "\"%L\"", value)
+ .build()
+
+
+ /**
+ * Creates a new [PsiAnnotation] with the [Generated] annotation.
+ *
+ * @param value The string to be put into the annotation `value` parameter.
+ * The default value refers to the current version of Spine Model Compiler.
+ */
+ public fun forKotlinPoet(value: String = defaultValue): KAnnotationSpec =
+ KAnnotationSpec.builder(Generated::class)
+ .addMember("\"%L\"", value)
+ .build()
}
diff --git a/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/field/AddFieldClass.kt b/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/field/AddFieldClass.kt
index 0b1385683..29c089df0 100644
--- a/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/field/AddFieldClass.kt
+++ b/mc-java-base/src/main/kotlin/io/spine/tools/mc/java/field/AddFieldClass.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,7 +69,7 @@ public open class AddFieldClass(
public const val NAME: String = "Field"
}
- override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.create()
+ override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.forPsi()
@Language("JAVA") @Suppress("EmptyClass")
override fun classJavadoc(): String = """
diff --git a/mc-java-base/src/testFixtures/kotlin/io/spine/tools/mc/java/PluginTestSetup.kt b/mc-java-base/src/testFixtures/kotlin/io/spine/tools/mc/java/PluginTestSetup.kt
index 1b0ca4eab..87385d96b 100644
--- a/mc-java-base/src/testFixtures/kotlin/io/spine/tools/mc/java/PluginTestSetup.kt
+++ b/mc-java-base/src/testFixtures/kotlin/io/spine/tools/mc/java/PluginTestSetup.kt
@@ -26,6 +26,7 @@
package io.spine.tools.mc.java
+import com.google.protobuf.Descriptors.GenericDescriptor
import com.google.protobuf.Message
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
@@ -35,13 +36,13 @@ import io.spine.protodata.params.WorkingDirectory
import io.spine.protodata.plugin.Plugin
import io.spine.protodata.render.SourceFile
import io.spine.protodata.render.SourceFileSet
-import io.spine.protodata.util.Format
import io.spine.protodata.settings.SettingsDirectory
import io.spine.protodata.testing.PipelineSetup
import io.spine.protodata.testing.PipelineSetup.Companion.byResources
import io.spine.protodata.testing.pipelineParams
import io.spine.protodata.testing.withRequestFile
import io.spine.protodata.testing.withSettingsDir
+import io.spine.protodata.util.Format
import io.spine.tools.code.Java
import io.spine.tools.code.SourceSetName
import io.spine.tools.mc.java.gradle.settings.CodegenSettings
@@ -101,7 +102,11 @@ abstract class PluginTestSetup(
* [settings] will be written to the [WorkingDirectory.settingsDirectory] before
* creation of a [Pipeline][io.spine.protodata.backend.Pipeline].
*/
- fun setup(projectDir: Path, settings: S): PipelineSetup {
+ fun setup(
+ projectDir: Path,
+ settings: S,
+ excludedDescriptors: List = listOf()
+ ): PipelineSetup {
val workingDir = projectDir.resolve("build").resolve(Directories.PROTODATA_WORKING_DIR)
val workingDirectory = WorkingDirectory(workingDir)
val requestFile = workingDirectory.requestDirectory.file(SourceSetName("testFixtures"))
@@ -119,6 +124,9 @@ abstract class PluginTestSetup(
JavaCodeStyleFormatterPlugin()
),
outputRoot = outputDir,
+ descriptorFilter = {
+ excludedDescriptors.find { d -> d.fullName == it.fullName } == null
+ }
) {
writeSettings(it, settings)
}
@@ -134,11 +142,14 @@ abstract class PluginTestSetup(
*
* @see createSettings
*/
- fun runPipeline(projectDir: Path) {
+ fun runPipeline(
+ projectDir: Path,
+ excludedDescriptors: List = listOf()
+ ) {
// Clear the cache of previously parsed files to avoid repeated code generation.
SourceFile.clearCache()
val settings = createSettings(projectDir)
- val setup = setup(projectDir, settings)
+ val setup = setup(projectDir, settings, excludedDescriptors)
val pipeline = setup.createPipeline()
pipeline()
this.sourceFileSet = pipeline.sources[0]
diff --git a/mc-java-checks/build.gradle.kts b/mc-java-checks/build.gradle.kts
index 954a7469b..d7c21264f 100644
--- a/mc-java-checks/build.gradle.kts
+++ b/mc-java-checks/build.gradle.kts
@@ -26,7 +26,9 @@
import io.spine.dependency.build.ErrorProne
import io.spine.dependency.lib.AutoService
-import io.spine.dependency.local.Spine
+import io.spine.dependency.local.Base
+import io.spine.dependency.local.ModelCompiler
+import io.spine.dependency.local.TestLib
import io.spine.dependency.local.ToolBase
dependencies {
@@ -38,13 +40,13 @@ dependencies {
ErrorProne.annotations.forEach { api(it) }
implementation(ErrorProne.GradlePlugin.lib)
- implementation(Spine.base)
+ implementation(Base.lib)
implementation(ToolBase.pluginBase)
- implementation(Spine.modelCompiler)
+ implementation(ModelCompiler.lib)
testImplementation(ErrorProne.testHelpers)
testImplementation(gradleKotlinDsl())
- testImplementation(Spine.testlib)
+ testImplementation(TestLib.lib)
}
/**
diff --git a/mc-java-comparable-tests/src/test/kotlin/io/spine/tools/mc/java/comparable/AddComparatorSpec.kt b/mc-java-comparable-tests/src/test/kotlin/io/spine/tools/mc/java/comparable/AddComparatorSpec.kt
index 65b5d4ace..49e6052f2 100644
--- a/mc-java-comparable-tests/src/test/kotlin/io/spine/tools/mc/java/comparable/AddComparatorSpec.kt
+++ b/mc-java-comparable-tests/src/test/kotlin/io/spine/tools/mc/java/comparable/AddComparatorSpec.kt
@@ -41,7 +41,10 @@ import io.spine.tools.mc.java.comparable.given.Account
import io.spine.tools.mc.java.comparable.given.BytesProhibited
import io.spine.tools.mc.java.comparable.given.Citizen
import io.spine.tools.mc.java.comparable.given.Debtor
+import io.spine.tools.mc.java.comparable.given.InvalidNested
+import io.spine.tools.mc.java.comparable.given.Invalid
import io.spine.tools.mc.java.comparable.given.MapsProhibited
+import io.spine.tools.mc.java.comparable.given.Name
import io.spine.tools.mc.java.comparable.given.NestedBytesProhibited
import io.spine.tools.mc.java.comparable.given.NestedMapsProhibited
import io.spine.tools.mc.java.comparable.given.NestedNonComparableProhibited
@@ -77,7 +80,16 @@ internal class AddComparatorSpec {
@JvmStatic
fun setup(@TempDir projectDir: Path) {
withLoggingMutedIn(AddComparator::class.java.packageName) {
- runPipeline(projectDir)
+ runPipeline(
+ projectDir,
+ // Exclude files and message types that cause errors.
+ // We'll test negative cases separately.
+ listOf(
+ InvalidNested.getDescriptor(),
+ Invalid.getDescriptor(),
+ Name.getDescriptor()
+ )
+ )
}
}
}
diff --git a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddComparator.kt b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddComparator.kt
index 6d39f088e..44de63537 100644
--- a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddComparator.kt
+++ b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddComparator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,12 +32,16 @@ import io.spine.base.copy
import io.spine.base.fieldPath
import io.spine.compare.ComparatorRegistry
import io.spine.option.CompareByOption
+import io.spine.protodata.Compilation
import io.spine.protodata.ast.Cardinality.CARDINALITY_SINGLE
import io.spine.protodata.ast.MessageType
+import io.spine.protodata.ast.Option
import io.spine.protodata.ast.PrimitiveType.PT_UNKNOWN
import io.spine.protodata.ast.PrimitiveType.TYPE_BYTES
import io.spine.protodata.ast.cardinality
import io.spine.protodata.ast.find
+import io.spine.protodata.ast.option
+import io.spine.protodata.ast.unpack
import io.spine.protodata.context.CodegenContext
import io.spine.protodata.java.ClassName
import io.spine.protodata.java.MethodCall
@@ -49,7 +53,6 @@ import io.spine.tools.code.Java
import io.spine.tools.mc.java.GeneratedAnnotation
import io.spine.tools.mc.java.base.joined
import io.spine.tools.mc.java.base.resolve
-import io.spine.tools.mc.java.comparable.ComparableMessage
import io.spine.tools.mc.java.comparable.WellKnownComparables.isWellKnownComparable
import io.spine.tools.psi.addFirst
@@ -67,42 +70,50 @@ public class AddComparator(
context: CodegenContext
) : DirectMessageAction(type, file, Empty.getDefaultInstance(), context) {
+ /** The declaration of the [CompareByOption] option in the [type]. */
+ private val option: Option by lazy {
+ type.option()
+ }
+
override fun doRender() {
- val option = compareByOption(type)
- val comparisonFields = option.fieldList.map(::toComparisonField)
+ val compareBy = option.unpack()
+ val comparisonFields = compareBy.fieldList.map(::toComparisonField)
require(comparisonFields.isNotEmpty()) {
"The `(compare_by)` option should have at least one field specified."
}
- val comparator = ComparatorBuilder(cls, option.descending)
+ val comparator = ComparatorBuilder(cls, compareBy.descending)
comparisonFields.forEach { comparator.comparingBy(it) }
val javaField = comparator.build().toPsi()
- .apply { addFirst(GeneratedAnnotation.create()) }
+ .apply { addFirst(GeneratedAnnotation.forPsi()) }
cls.addAfter(javaField, cls.lBrace)
}
- /**
- * Queries the [CompareByOption] option for the given message [type].
- */
- private fun compareByOption(type: MessageType) = select(ComparableMessage::class.java)
- .findById(type)!!
- .option
-
/**
* Maps the field [path] to an appropriate instance of [ComparisonField],
* depending on the field type.
*/
+ @Suppress("SwallowedException") // We transform "unknown field" into compilation error.
private fun toComparisonField(path: String): ComparisonField {
- val fieldPath = fieldPath { fieldName.addAll(path.split(".")) }
- val field = typeSystem.resolve(fieldPath, type)
+ val fieldPath = path.toFieldPath()
+ val field = try {
+ typeSystem.resolve(fieldPath, type)
+ } catch (e: IllegalStateException) {
+ Compilation.error(type.file, option.span) {
+ "Unable to find a field with the path `$path` in the type `${type.qualifiedName}`."
+ }
+ }
+
val fieldType = field.type
- check(field.type.cardinality == CARDINALITY_SINGLE) {
- "Repeated fields or maps can't participate in comparison. " +
- "The invalid field: `$field`, its type: `$fieldType`. " +
- "Please, make sure the type of the passed field is compatible with " +
- "the `(compare_by)` option."
+ if (field.type.cardinality != CARDINALITY_SINGLE) {
+ Compilation.error(type.file, field.span) {
+ "Repeated fields or maps can't participate in comparison. " +
+ "The invalid field: `$field`, its type: `$fieldType`. " +
+ "Please, make sure the type of the passed field is compatible with " +
+ "the `(compare_by)` option."
+ }
}
return when {
@@ -227,3 +238,13 @@ public class AddComparator(
private val MessageType.hasCompareByOption: Boolean
get() = optionList.find() != null
+
+/**
+ * Transforms this potentially dot-delimited string into [FieldPath].
+ *
+ * If there are no dots in this string the returned [FieldPath] contains
+ * only this string.
+ */
+private fun String.toFieldPath() = fieldPath {
+ fieldName.addAll(this@toFieldPath.split("."))
+}
diff --git a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddCompareTo.kt b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddCompareTo.kt
index 0701eb42b..54ba9ad6a 100644
--- a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddCompareTo.kt
+++ b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/AddCompareTo.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -65,7 +65,7 @@ public class AddCompareTo(
)
method.run {
addFirst(OverrideAnnotation.create())
- addFirst(GeneratedAnnotation.create())
+ addFirst(GeneratedAnnotation.forPsi())
}
cls.addLast(method)
}
diff --git a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/ComparatorBuilder.kt b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/ComparatorBuilder.kt
index 81b1421f9..a177725e7 100644
--- a/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/ComparatorBuilder.kt
+++ b/mc-java-comparable/src/main/kotlin/io/spine/tools/mc/java/comparable/action/ComparatorBuilder.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@ import com.intellij.psi.PsiClass
import io.spine.base.FieldPath
import io.spine.protodata.java.ClassName
import io.spine.protodata.java.Expression
-import io.spine.protodata.java.InitField
+import io.spine.protodata.java.FieldDeclaration
import io.spine.protodata.java.ParameterizedClassName
import io.spine.protodata.java.call
import io.spine.string.camelCase
@@ -67,7 +67,7 @@ internal class ComparatorBuilder(cls: PsiClass, private val reversed: Boolean =
/**
* Builds a private static `comparator` Java field.
*/
- fun build(): InitField> {
+ fun build(): FieldDeclaration> {
val comparator = ClassName(Comparator::class)
var comparisons = comparator.call>("comparing", fields.first())
for (i in 1 until fields.size) {
@@ -76,7 +76,7 @@ internal class ComparatorBuilder(cls: PsiClass, private val reversed: Boolean =
if (reversed) {
comparisons = comparisons.chain("reversed")
}
- return InitField(
+ return FieldDeclaration(
modifiers = "private static final",
type = ParameterizedClassName(comparator, message),
name = "comparator",
diff --git a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/EntityStateRenderer.kt b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/EntityStateRenderer.kt
index 84d391e2e..aa3aa6456 100644
--- a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/EntityStateRenderer.kt
+++ b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/EntityStateRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ public class EntityStateRenderer :
override fun doRender(type: MessageType, file: SourceFile) {
execute {
- RenderActions(type, file, settings.actions, context!!).apply()
+ RenderActions(type, file, settings.actions, context).apply()
}
}
}
diff --git a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/column/AddColumnClass.kt b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/column/AddColumnClass.kt
index 9c2314061..611623a45 100644
--- a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/column/AddColumnClass.kt
+++ b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/column/AddColumnClass.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@ public class AddColumnClass(type: MessageType, file: SourceFile, context:
private val columns: List = type.columns
- override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.create()
+ override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.forPsi()
@Language("JAVA") @Suppress("EmptyClass")
override fun classJavadoc(): String = """
diff --git a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QueryMethod.kt b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QueryMethod.kt
index 1486478c7..1fe0dff14 100644
--- a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QueryMethod.kt
+++ b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QueryMethod.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -77,7 +77,7 @@ internal class QueryMethod(private val file: SourceFile) : WithLogging {
""".trimIndent(), entityStateClass
)
newMethod.run {
- val annotation = GeneratedAnnotation.create()
+ val annotation = GeneratedAnnotation.forPsi()
addFirst(annotation)
addFirst(javadoc)
}
diff --git a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QuerySupportClass.kt b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QuerySupportClass.kt
index e58d4145d..a56f48356 100644
--- a/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QuerySupportClass.kt
+++ b/mc-java-entity/src/main/kotlin/io/spine/tools/mc/java/entity/query/QuerySupportClass.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,7 +51,7 @@ internal abstract class QuerySupportClass(
context: CodegenContext
) : CreateNestedClass(type, file, className, context) {
- override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.create()
+ override fun createAnnotation(): PsiAnnotation = GeneratedAnnotation.forPsi()
/**
* The class of the entity state, same as [messageClass].
diff --git a/mc-java-marker-tests/src/test/kotlin/io/spne/mc/java/marker/EveryIsOptionRendererSpec.kt b/mc-java-marker-tests/src/test/kotlin/io/spne/mc/java/marker/EveryIsOptionRendererSpec.kt
index 337631107..0b9bbdd1e 100644
--- a/mc-java-marker-tests/src/test/kotlin/io/spne/mc/java/marker/EveryIsOptionRendererSpec.kt
+++ b/mc-java-marker-tests/src/test/kotlin/io/spne/mc/java/marker/EveryIsOptionRendererSpec.kt
@@ -76,7 +76,7 @@ internal class EveryIsOptionRendererSpec {
@Test
fun `annotated as generated by Spine Model Compiler`() {
- val annotation = GeneratedAnnotation.create().text
+ val annotation = GeneratedAnnotation.forPsi().text
code shouldContain annotation
}
diff --git a/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/EveryIsOptionRenderer.kt b/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/EveryIsOptionRenderer.kt
index ed4c0cecd..5452939d5 100644
--- a/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/EveryIsOptionRenderer.kt
+++ b/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/EveryIsOptionRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,7 +87,7 @@ internal class EveryIsOptionRenderer : MarkerRenderer() {
private fun annotate(file: SourceFile) {
val psiFile = file.psi() as PsiJavaFile
- val annotation = GeneratedAnnotation.create()
+ val annotation = GeneratedAnnotation.forPsi()
psiFile.topLevelClass.addFirst(annotation)
val updatedCode = psiFile.text
file.overwrite(updatedCode)
diff --git a/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/MarkerRenderer.kt b/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/MarkerRenderer.kt
index 93368d01b..68487049a 100644
--- a/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/MarkerRenderer.kt
+++ b/mc-java-marker/src/main/kotlin/io/spine/tools/mc/java/marker/MarkerRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ internal abstract class MarkerRenderer> : BaseRenderer() {
*/
protected fun MessageType.implementInterface(superInterface: SuperInterface) {
val file = sources.javaFileOf(this)
- val action = ImplementInterface(this, file, superInterface, context!!)
+ val action = ImplementInterface(this, file, superInterface, context)
action.render()
}
}
diff --git a/mc-java-message-group/src/main/kotlin/io/spine/tools/mc/java/mgroup/GroupedMessageRenderer.kt b/mc-java-message-group/src/main/kotlin/io/spine/tools/mc/java/mgroup/GroupedMessageRenderer.kt
index 9ba1e1240..858b52640 100644
--- a/mc-java-message-group/src/main/kotlin/io/spine/tools/mc/java/mgroup/GroupedMessageRenderer.kt
+++ b/mc-java-message-group/src/main/kotlin/io/spine/tools/mc/java/mgroup/GroupedMessageRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ internal class GroupedMessageRenderer : JavaRenderer(), MessageGroupPluginCompon
private fun GroupedMessage.doRender(sourceFile: SourceFile) {
groupList.forEach {
- RenderActions(type, sourceFile, it.actions, context!!).apply()
+ RenderActions(type, sourceFile, it.actions, context).apply()
}
}
diff --git a/mc-java-routing-tests/build.gradle.kts b/mc-java-routing-tests/build.gradle.kts
new file mode 100644
index 000000000..6542dbac3
--- /dev/null
+++ b/mc-java-routing-tests/build.gradle.kts
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.lib.AutoService
+import io.spine.dependency.lib.AutoServiceKsp
+import io.spine.dependency.local.CoreJava
+
+plugins {
+ kotlin("jvm")
+ ksp
+ `java-test-fixtures`
+ id("io.spine.mc-java")
+}
+
+dependencies {
+ ksp(AutoServiceKsp.processor)
+ compileOnlyApi(AutoService.annotations)
+
+ testImplementation(kotlin("stdlib"))
+ testImplementation(CoreJava.testUtilServer)
+
+ kspTest(project(":mc-java-routing"))
+ kspTestFixtures(project(":mc-java-routing"))
+ testFixturesImplementation(CoreJava.server)
+
+ testFixturesImplementation(project(":mc-java-routing"))?.because(
+ "We need this dependency temporarily, until the interfaces defined" +
+ " in the package `io.spine.server.route` are moved to CoreJava."
+ )
+}
+
+kotlin {
+ sourceSets.main {
+ kotlin.srcDir("build/generated/ksp/main/kotlin")
+ }
+ sourceSets.test {
+ kotlin.srcDir("build/generated/ksp/test/kotlin")
+ }
+ sourceSets.testFixtures {
+ kotlin.srcDir("build/generated/ksp/testFixtures/kotlin")
+ }
+}
+
+// Avoid Gradle warning on disabled execution optimization because of the absence of
+// explicit or implicit dependencies.
+afterEvaluate {
+ val kspTestFixturesKotlin by tasks.getting
+ val launchTestFixturesProtoData by tasks.getting
+ kspTestFixturesKotlin.dependsOn(launchTestFixturesProtoData)
+}
diff --git a/mc-java-routing-tests/src/test/kotlin/io/spine/tools/mc/java/routing/tests/EventRoutingSetupITest.kt b/mc-java-routing-tests/src/test/kotlin/io/spine/tools/mc/java/routing/tests/EventRoutingSetupITest.kt
new file mode 100644
index 000000000..a874a6d67
--- /dev/null
+++ b/mc-java-routing-tests/src/test/kotlin/io/spine/tools/mc/java/routing/tests/EventRoutingSetupITest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.tests
+
+import io.kotest.matchers.collections.shouldContain
+import io.kotest.matchers.shouldBe
+import io.spine.given.home.DeviceId
+import io.spine.given.home.Room
+import io.spine.given.home.RoomId
+import io.spine.given.home.RoomProjection
+import io.spine.given.home.events.deviceMoved
+import io.spine.given.home.events.roomAdded
+import io.spine.given.home.homeAutomation
+import io.spine.testing.server.blackbox.BlackBox
+import org.junit.jupiter.api.Test
+import io.spine.testing.server.blackbox.assertEntity
+
+internal class EventRoutingSetupITest {
+
+ @Test
+ fun `loads event routing setup as a service`() {
+ BlackBox.from(homeAutomation()).use { context ->
+ val r1 = RoomId.generate()
+ val lamp = DeviceId.generate()
+ val n1 = "Living Room"
+
+ context.receivesEvent(
+ roomAdded {
+ room = r1
+ name = n1
+ }
+ )
+ context.receivesEvent(
+ deviceMoved {
+ device = lamp
+ room = r1
+ }
+ )
+
+ val room = context.assertEntity(r1).actual()?.state() as Room
+
+ room.run {
+ name shouldBe n1
+ deviceList shouldContain lamp
+ }
+ }
+ }
+}
diff --git a/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/DeviceEvent.java b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/DeviceEvent.java
new file mode 100644
index 000000000..230eebb65
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/DeviceEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.given.home.events;
+
+import io.spine.annotation.GeneratedMixin;
+import io.spine.base.EventMessage;
+
+import io.spine.given.home.DeviceId;
+
+@GeneratedMixin
+public interface DeviceEvent extends EventMessage{
+ DeviceId getDevice();
+}
diff --git a/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomDeviceEvent.java b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomDeviceEvent.java
new file mode 100644
index 000000000..7643be197
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomDeviceEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.given.home.events;
+
+import io.spine.annotation.GeneratedMixin;
+
+@GeneratedMixin
+public interface RoomDeviceEvent extends RoomEvent, DeviceEvent {
+}
diff --git a/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomEvent.java b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomEvent.java
new file mode 100644
index 000000000..a699766f5
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/RoomEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.given.home.events;
+
+import io.spine.annotation.GeneratedMixin;
+import io.spine.base.EventMessage;
+
+import io.spine.given.home.RoomId;
+
+@GeneratedMixin
+public interface RoomEvent extends EventMessage {
+ RoomId getRoom();
+}
diff --git a/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/package-info.java b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/package-info.java
new file mode 100644
index 000000000..3d2b39e4e
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/java/io/spine/given/home/events/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@CheckReturnValue
+@ParametersAreNonnullByDefault
+package io.spine.given.home.events;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/mc-java-routing-tests/src/testFixtures/kotlin/io/spine/given/home/HomeAutomationContext.kt b/mc-java-routing-tests/src/testFixtures/kotlin/io/spine/given/home/HomeAutomationContext.kt
new file mode 100644
index 000000000..9f79dffdb
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/kotlin/io/spine/given/home/HomeAutomationContext.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.given.home
+
+import io.spine.core.Subscribe
+import io.spine.given.home.events.DeviceMoved
+import io.spine.given.home.events.RoomAdded
+import io.spine.given.home.events.RoomEvent
+import io.spine.given.home.events.RoomRenamed
+import io.spine.protobuf.isDefault
+import io.spine.server.BoundedContext
+import io.spine.server.entity.alter
+import io.spine.server.projection.Projection
+import io.spine.server.projection.ProjectionRepository
+import io.spine.server.route.EventRouting
+import io.spine.server.route.setup.EventRoutingSetup
+import io.spine.server.route.Route
+import io.spine.server.route.setup.StateRoutingSetup
+import io.spine.server.route.StateUpdateRouting
+
+fun homeAutomation(): BoundedContext = BoundedContext.singleTenant("HomeAutomation")
+ .add(RoomProjectionRepository())
+ .build()
+
+public class RoomProjection : Projection() {
+
+ @Subscribe
+ internal fun on(e: RoomAdded) = alter {
+ name = e.name
+ }
+
+ @Subscribe
+ internal fun on(e: RoomRenamed) = alter {
+ name = e.name
+ }
+
+ @Subscribe
+ internal fun on(e: DeviceMoved) = alter {
+ if (id == e.prevRoom) {
+ val toRemove = deviceBuilderList.find { b -> b.uuid == e.device.uuid }
+ if (toRemove != null) {
+ deviceBuilderList.remove(toRemove)
+ }
+ }
+ if (id == e.room) {
+ addDevice(e.device)
+ }
+ }
+
+ companion object {
+
+ @Route
+ @JvmStatic
+ fun route(e: RoomEvent): RoomId = e.room
+
+ @Route
+ @JvmStatic
+ fun routeMoved(e: DeviceMoved): Set =
+ if (e.prevRoom.isDefault()) setOf(e.room) else setOf(e.prevRoom, e.room)
+ }
+}
+
+internal class RoomProjectionRepository : ProjectionRepository() {
+
+ override fun setupEventRouting(routing: EventRouting) {
+ super.setupEventRouting(routing)
+
+ // Remove routs added via reflective class analysis.
+ routing.run {
+ remove()
+ remove()
+ }
+
+ EventRoutingSetup.apply(entityClass(), routing)
+ }
+
+ override fun setupStateRouting(routing: StateUpdateRouting) {
+ super.setupStateRouting(routing)
+ StateRoutingSetup.apply(entityClass(), routing)
+ }
+}
diff --git a/mc-java-routing-tests/src/testFixtures/proto/given/home/commands.proto b/mc-java-routing-tests/src/testFixtures/proto/given/home/commands.proto
new file mode 100644
index 000000000..5c77e41c3
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/proto/given/home/commands.proto
@@ -0,0 +1,59 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.home;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.home.commands";
+option java_outer_classname = "CommandsProto";
+option java_multiple_files = true;
+
+import "given/home/values.proto";
+
+message AddDevice {
+ DeviceId device = 1;
+ string name = 2 [(required) = true];
+}
+
+message AddRoom {
+ RoomId room = 1;
+ string name = 2 [(required) = true];
+}
+
+message MoveDevice {
+ DeviceId device = 1 [(required) = true];
+ RoomId from = 2;
+ RoomId to = 3 [(required) = true];
+}
+
+message SetState {
+ DeviceId device = 1;
+ State state = 2 [(required) = true];
+}
diff --git a/mc-java-routing-tests/src/testFixtures/proto/given/home/entities.proto b/mc-java-routing-tests/src/testFixtures/proto/given/home/entities.proto
new file mode 100644
index 000000000..05974e1ba
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/proto/given/home/entities.proto
@@ -0,0 +1,59 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.home;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.home";
+option java_outer_classname = "EntitiesProto";
+option java_multiple_files = true;
+
+import "given/home/values.proto";
+
+message Device {
+ option (entity).kind = AGGREGATE;
+ DeviceId id = 1;
+ string name = 2 [(required) = true];
+ State state = 3;
+ RoomId room = 4;
+}
+
+message Room {
+ option (entity).kind = PROJECTION;
+ RoomId id = 1;
+ string name = 2 [(required) = true];
+ repeated DeviceId device = 3;
+}
+
+message Home {
+ option (entity).kind = PROJECTION;
+ string name = 1;
+ repeated Room room = 2;
+}
diff --git a/mc-java-routing-tests/src/testFixtures/proto/given/home/events.proto b/mc-java-routing-tests/src/testFixtures/proto/given/home/events.proto
new file mode 100644
index 000000000..51004dcd0
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/proto/given/home/events.proto
@@ -0,0 +1,79 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.home;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.home.events";
+option java_outer_classname = "EventsProto";
+option java_multiple_files = true;
+
+import "given/home/values.proto";
+
+message DeviceAdded {
+ option (is).java_type = "DeviceEvent";
+ DeviceId device = 1 [(required) = true];
+ string name = 2 [(required) = true];
+}
+
+message DeviceRenamed {
+ option (is).java_type = "DeviceEvent";
+ DeviceId device = 1 [(required) = true];
+ string prev_name = 2 [(required) = true];
+ string name = 3 [(required) = true];
+}
+
+message RoomAdded {
+ option (is).java_type = "RoomEvent";
+ RoomId room = 1;
+ string name = 2 [(required) = true];
+}
+
+message RoomRenamed {
+ option (is).java_type = "RoomEvent";
+ RoomId room = 1;
+ string prev_name = 2 [(required) = true];
+ string name = 3 [(required) = true];
+}
+
+message DeviceMoved {
+ option (is).java_type = "RoomDeviceEvent";
+ DeviceId device = 1 [(required) = true];
+
+ RoomId prev_room = 3;
+ // This field is named `room` to satisfy the getter in the interface.
+ RoomId room = 4 [(required) = true];
+}
+
+message StateChanged {
+ option (is).java_type = "DeviceEvent";
+ DeviceId device = 1 [(required) = true];
+ State current = 2 [(required) = true];
+}
diff --git a/mc-java-routing-tests/src/testFixtures/proto/given/home/values.proto b/mc-java-routing-tests/src/testFixtures/proto/given/home/values.proto
new file mode 100644
index 000000000..b40707ee0
--- /dev/null
+++ b/mc-java-routing-tests/src/testFixtures/proto/given/home/values.proto
@@ -0,0 +1,57 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.home;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.home";
+option java_outer_classname = "ValuesProto";
+option java_multiple_files = true;
+
+message DeviceId {
+ string uuid = 1 [(required) = true];
+}
+
+message RoomId {
+ string uuid = 1 [(required) = true];
+}
+
+enum DeviceKind {
+ DK_UNDEFINED = 0;
+ LIGHT_BULB = 1;
+ THERMOSTAT = 2;
+ TV = 3;
+}
+
+enum State {
+ STATUS_UNKNOWN = 0;
+ OFF = 1;
+ ON = 2;
+}
diff --git a/mc-java-routing/build.gradle.kts b/mc-java-routing/build.gradle.kts
new file mode 100644
index 000000000..b432ac88f
--- /dev/null
+++ b/mc-java-routing/build.gradle.kts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.dependency.build.Ksp
+import io.spine.dependency.lib.AutoService
+import io.spine.dependency.lib.AutoServiceKsp
+import io.spine.dependency.lib.Kotlin
+import io.spine.dependency.lib.KotlinPoet
+import io.spine.dependency.local.CoreJava
+import io.spine.dependency.local.Logging
+import io.spine.dependency.test.Kotest
+import io.spine.dependency.test.KotlinCompileTesting
+
+plugins {
+ kotlin("jvm")
+ ksp
+ id("io.spine.mc-java")
+}
+
+dependencies {
+ ksp(AutoServiceKsp.processor)
+ compileOnlyApi(AutoService.annotations)
+ implementation(kotlin("stdlib"))
+ implementation(Ksp.symbolProcessingApi)
+ implementation(KotlinPoet.ksp)
+ implementation(CoreJava.server)
+ implementation(project(":mc-java-base"))
+
+ testImplementation(AutoService.annotations)
+ testImplementation(Kotest.assertions)
+ testImplementation(KotlinCompileTesting.libKsp)
+ testImplementation(Logging.testLib)
+}
+
+configurations
+ // https://detekt.dev/docs/gettingstarted/gradle/#dependencies
+ .matching { it.name != "detekt" }
+ .all {
+ resolutionStrategy {
+ force(
+ Ksp.symbolProcessingApi,
+ Ksp.symbolProcessing,
+ Kotlin.Compiler.embeddable,
+ )
+ }
+}
+
+// Avoid the missing file error for generated code when running tests out of IDE.
+afterEvaluate {
+ val kspTestKotlin by tasks.getting
+ val launchTestProtoData by tasks.getting
+ kspTestKotlin.dependsOn(launchTestProtoData)
+}
diff --git a/mc-java-routing/src/main/kotlin/KSFunctionDeclarationExts.kt b/mc-java-routing/src/main/kotlin/KSFunctionDeclarationExts.kt
new file mode 100644
index 000000000..61ace9641
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/KSFunctionDeclarationExts.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.Origin.JAVA
+import com.google.devtools.ksp.symbol.Origin.JAVA_LIB
+
+/**
+ * Selects either diagnostic message depending on
+ * the [origin][KSFunctionDeclaration.origin] of the declaration.
+ *
+ * For origins [JAVA] and [JAVA_LIB] the value of the [java] parameter is returned.
+ * Otherwise, the [kotlin] string is returned.
+ */
+internal fun KSFunctionDeclaration.msg(kotlin: String, java: String): String =
+ if (origin == JAVA || origin == JAVA_LIB) {
+ java
+ } else {
+ kotlin
+ }
+
+/**
+ * Obtains the text for referencing this function in a diagnostic message.
+ */
+internal val KSFunctionDeclaration.funRef: String
+ get() {
+ val shortRef = "`${simpleName.getShortName()}()`"
+ return msg("function $shortRef", "method $shortRef")
+ }
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteSignature.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteSignature.kt
new file mode 100644
index 000000000..0096930db
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteSignature.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import io.spine.base.CommandMessage
+import io.spine.core.CommandContext
+
+internal class CommandRouteSignature(
+ environment: Environment
+) : RouteSignature(
+ CommandMessage::class.java,
+ CommandContext::class.java,
+ environment
+) {
+ override fun matchDeclaringClass(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): Boolean = environment.run {
+ val isAggregate = aggregateClass.isAssignableFrom(declaringClass.type)
+ val isProcessManager = processManagerClass.isAssignableFrom(declaringClass.type)
+ val match = isAggregate || isProcessManager
+ if (!match) {
+ val parent = declaringClass.superClass()
+ logger.error(
+ "A command routing function can be declared in a class derived" +
+ " from ${processManagerClass.ref} or ${aggregateClass.ref}." +
+ " Encountered: ${parent.qualifiedRef}.",
+ fn)
+ }
+ return match
+ }
+
+ override fun create(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+ ): CommandRouteFun = CommandRouteFun(fn, declaringClass, parameters, returnType)
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteVisitor.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteVisitor.kt
new file mode 100644
index 000000000..03c5f2020
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommandRouteVisitor.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.squareup.kotlinpoet.ksp.toClassName
+
+internal class CommandRouteVisitor(
+ functions: List,
+ environment: Environment
+) : RouteVisitor(
+ environment.commandRoutingSetup,
+ functions,
+ environment
+) {
+ override val classNameSuffix: String = "CommandRouting"
+
+ override fun addRoute(fn: CommandRouteFun) {
+ val params = if (fn.acceptsContext) "c, ctx" else "c"
+ routingRunBlock.add(
+ "%L<%T> { %L -> %T.%L(%L) }\n",
+ ROUTE_FUN_NAME,
+ fn.messageClass,
+ params,
+ entityClass.type.toClassName(),
+ fn.decl.simpleName.asString(),
+ params
+ )
+ }
+
+ companion object {
+ fun process(qualified: List, environment: Environment) {
+ runVisitors(qualified) { functions ->
+ CommandRouteVisitor(functions, environment)
+ }
+ }
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommonChecks.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommonChecks.kt
new file mode 100644
index 000000000..4630def92
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/CommonChecks.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.symbol.FunctionKind
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.Origin
+import com.google.devtools.ksp.symbol.Origin.JAVA
+import com.google.devtools.ksp.symbol.Origin.KOTLIN
+import funRef
+import io.spine.server.entity.Entity
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.jvmStaticRef
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.routeRef
+import msg
+
+/**
+ * Runs general usage checks for this function declaration.
+ *
+ * The function runs all the checks, assuming that compilation does not terminate after
+ * an error is reported via [KSPLogger.error].
+ *
+ * @param environment The environment for resolving types and reporting errors or warnings.
+ * @return The number of detected errors, or zero if no errors were found.
+ */
+internal fun KSFunctionDeclaration.commonChecks(environment: Environment): Int {
+ val logger = environment.logger
+ val declaredInAClass = declaredInAClass(logger)
+ val isStatic = isStatic(logger)
+ val acceptsOneOrTwoParameters = acceptsOneOrTwoParameters(logger)
+ return (declaredInAClass
+ + isStatic
+ + acceptsOneOrTwoParameters)
+}
+
+private fun Boolean.toErrorCount(): Int = if (this) 0 else 1
+
+private fun KSFunctionDeclaration.isStatic(logger: KSPLogger): Int {
+ val isStatic = when (origin) {
+ JAVA -> functionKind == FunctionKind.STATIC
+ KOTLIN -> parentDeclaration is KSClassDeclaration &&
+ (parentDeclaration as KSClassDeclaration).isCompanionObject
+ else -> false
+ }
+ if (!isStatic) {
+ logger.error(msg(
+ "The $funRef annotated with $routeRef must be a member of a companion object.",
+ "The $funRef annotated with $routeRef must be `static`."
+ ),
+ this
+ )
+ }
+ return isStatic.toErrorCount()
+}
+
+private fun KSFunctionDeclaration.declaredInAClass(logger: KSPLogger): Int {
+ val inClass = parentDeclaration is KSClassDeclaration
+ if (!inClass) {
+ // This case is Kotlin-only because in Java a function would belong to a class.
+ logger.error(
+ "The $funRef annotated with $routeRef must be" +
+ " a member of a companion object of an entity class.",
+ this
+ )
+ }
+ return inClass.toErrorCount()
+}
+
+private fun KSFunctionDeclaration.acceptsOneOrTwoParameters(logger: KSPLogger): Int {
+ val wrongNumber = parameters.isEmpty() || parameters.size > 2
+ if (wrongNumber) {
+ logger.error(
+ "The $funRef annotated with $routeRef must accept one or two parameters. " +
+ "Encountered: ${parameters.size}.",
+ this
+ )
+ }
+ return (!wrongNumber).toErrorCount()
+}
+
+internal fun KSFunctionDeclaration.declaringClass(environment: Environment): EntityClass? {
+ val parent = parentDeclaration!!.qualifiedName!!
+ var declaringClass = environment.resolver.getClassDeclarationByName(parent)!!
+ if (declaringClass.isCompanionObject) {
+ // In Kotlin routing functions are declared in a companion object.
+ // We need the enclosing entity class.
+ declaringClass = declaringClass.parentDeclaration!! as KSClassDeclaration
+ }
+ if (!environment.entityInterface.isAssignableFrom(declaringClass.asStarProjectedType())) {
+ environment.logger.error(
+ "The declaring class of the $funRef annotated with $routeRef" +
+ " must implement the `${Entity::class.java.canonicalName}` interface.",
+ this
+ )
+ return null
+ }
+ return EntityClass(declaringClass, environment.entityInterface)
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EntityClass.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EntityClass.kt
new file mode 100644
index 000000000..7c41e6ea9
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EntityClass.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.ClassKind.CLASS
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeArgument
+import com.google.devtools.ksp.symbol.KSTypeReference
+
+internal class EntityClass(
+ val decl: KSClassDeclaration,
+ entityInterface: KSType
+) {
+ fun accept(visitor: RouteVisitor<*>, data: Unit) {
+ decl.accept(visitor, data)
+ }
+
+ val type: KSType by lazy { decl.asStarProjectedType() }
+
+ val idClassTypeArgument: KSTypeArgument by lazy {
+ val asEntity = decl.superTypes.find {
+ entityInterface.isAssignableFrom(it.resolve())
+ }
+ check(asEntity != null) {
+ "The class `${decl.qualifiedName!!.asString()}`" +
+ " must implement ${entityInterface.declaration.qualified()}`."
+ }
+ asEntity.element!!.typeArguments.first()
+ }
+
+ private val idClassReference: KSTypeReference by lazy {
+ idClassTypeArgument.type!!
+ }
+
+ val idClass: KSType by lazy {
+ idClassReference.resolve()
+ }
+
+ fun superClass(): KSType {
+ val found = decl.superTypes.find {
+ val superType = it.resolve().declaration
+ (superType is KSClassDeclaration) && (superType.classKind == CLASS)
+ }
+ return found!!.resolve()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EntityClass) return false
+ return decl == other.decl
+ }
+
+ override fun hashCode(): Int {
+ return decl.hashCode()
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Environment.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Environment.kt
new file mode 100644
index 000000000..5fbbc4043
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Environment.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.symbol.KSType
+import io.spine.server.aggregate.Aggregate
+import io.spine.server.entity.Entity
+import io.spine.server.procman.ProcessManager
+import io.spine.server.projection.Projection
+import io.spine.server.route.CommandRouting
+import io.spine.server.route.setup.CommandRoutingSetup
+import io.spine.server.route.EventRouting
+import io.spine.server.route.setup.EventRoutingSetup
+import io.spine.server.route.MessageRouting
+import io.spine.server.route.setup.StateRoutingSetup
+import io.spine.server.route.StateUpdateRouting
+import kotlin.reflect.KClass
+
+/**
+ * Provides instances required for resolving types or reporting errors or warnings.
+ */
+internal class Environment(
+ val resolver: Resolver,
+ val logger: KSPLogger,
+ val codeGenerator: CodeGenerator
+) {
+ val entityInterface by lazy { Entity::class.toType(resolver) }
+ val aggregateClass by lazy { Aggregate::class.toType(resolver) }
+ val projectionClass by lazy { Projection::class.toType(resolver) }
+ val processManagerClass by lazy { ProcessManager::class.toType(resolver) }
+ val setClass by lazy { Set::class.toType(resolver) }
+
+ val commandRoutingSetup = SetupType(CommandRoutingSetup::class, CommandRouting::class)
+ val eventRoutingSetup = SetupType(EventRoutingSetup::class, EventRouting::class)
+ val stateRoutingSetup = SetupType(StateRoutingSetup::class, StateUpdateRouting::class)
+
+ inner class SetupType(
+ val cls: KClass,
+ val routingClass: KClass>
+ ) {
+ val type: KSType by lazy { cls.toType(resolver) }
+ }
+}
+
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteSignature.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteSignature.kt
new file mode 100644
index 000000000..55a4eaa38
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteSignature.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import io.spine.base.EventMessage
+import io.spine.core.EventContext
+
+internal class EventRouteSignature(
+ environment: Environment
+) : RouteSignature(
+ EventMessage::class.java,
+ EventContext::class.java,
+ environment
+) {
+ override fun matchDeclaringClass(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): Boolean = environment.run {
+ val isAggregate = aggregateClass.isAssignableFrom(declaringClass.type)
+ val isProjection = projectionClass.isAssignableFrom(declaringClass.type)
+ val isProcessManager = processManagerClass.isAssignableFrom(declaringClass.type)
+ val match = isAggregate || isProjection || isProcessManager
+ if (!match) {
+ val parent = declaringClass.superClass()
+ logger.error(
+ "An event routing function can be declared in a class derived" +
+ " from ${processManagerClass.ref} or ${aggregateClass.ref} or" +
+ " ${projectionClass.ref}." +
+ " Encountered: ${parent.qualifiedRef}.",
+ fn)
+ }
+ return match
+ }
+
+ @Suppress("ReturnCount") // Prefer a sooner exit to reduce nesting.
+ override fun matchReturnType(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): KSType? = environment.run {
+ val unicast = super.matchReturnType(fn, declaringClass)
+ if (unicast != null) {
+ return unicast
+ }
+ // Return type is not the entity ID.
+ val returnType = fn.returnType?.resolve()!!
+ if (!setClass.isAssignableFrom(returnType)) {
+ logger.error(
+ "A multicast routing function for events must return" +
+ " a ${setClass.ref}` of entity identifiers." +
+ " Encountered: ${returnType.qualifiedRef}.",
+ fn
+ )
+ return null
+ }
+ // The returned type is a `Set`. Let's check the generic argument.
+ val firstArg = returnType.arguments.firstOrNull()
+ if (firstArg == null) {
+ logger.error(
+ "A multicast routing function for events must return" +
+ " a `Set` whose generic argument is an entity identifier." +
+ " Encountered: no argument.",
+ fn
+ )
+ return null
+ }
+ val argumentClass = firstArg.type!!.resolve()
+ if (!declaringClass.idClass.isAssignableFrom(argumentClass)) {
+ logger.error(
+ "A multicast routing function for events must return" +
+ " a `Set` whose generic argument is an entity identifier." +
+ " Expected: ${declaringClass.idClass.ref}." +
+ " Encountered: ${argumentClass.qualifiedRef}.",
+ fn
+ )
+ return null
+ }
+ return returnType
+ }
+
+ override fun create(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+ ): EventRouteFun = EventRouteFun(fn, declaringClass, parameters, returnType)
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteVisitor.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteVisitor.kt
new file mode 100644
index 000000000..1e06b13dc
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/EventRouteVisitor.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.squareup.kotlinpoet.ksp.toClassName
+
+internal class EventRouteVisitor(
+ functions: List,
+ environment: Environment
+) : RouteVisitor(
+ environment.eventRoutingSetup,
+ functions,
+ environment
+) {
+ override val classNameSuffix: String = "EventRouting"
+
+ /**
+ * Adds the entry in the routing setup function inside the [routingRunBlock].
+ *
+ * For a multicast route it would be something like:
+ * ```kotlin
+ * route { e, c -> MyEntity.myRouteFun(e, c) }
+ * ```
+ * For an unicast route it would be something like:
+ * ```kotlin
+ * unicast { e, c -> MyEntity.myRoutFun(e, c) }
+ * ```
+ * If a route function does not accept context, the lambdas would have only the `e` parameter.
+ */
+ override fun addRoute(fn: EventRouteFun) {
+ val params = if (fn.acceptsContext) "e, c" else "e"
+ val entryFn = if (fn.isUnicast) "unicast" else ROUTE_FUN_NAME
+
+ routingRunBlock.add(
+ "%L<%T> { %L -> %T.%L(%L) }\n",
+ entryFn,
+ fn.messageClass,
+ params,
+ entityClass.type.toClassName(),
+ fn.decl.simpleName.asString(),
+ params
+ )
+ }
+
+ companion object {
+
+ /**
+ * Processes the given route functions using [EventRouteVisitor].
+ */
+ internal fun process(qualified: List, environment: Environment) {
+ runVisitors(qualified) { functions ->
+ EventRouteVisitor(functions, environment)
+ }
+ }
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Exts.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Exts.kt
new file mode 100644
index 000000000..c28bff4ab
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Exts.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.symbol.KSType
+import kotlin.reflect.KClass
+
+/**
+ * Converts this class into [KSType] using the given resolver.
+ */
+internal fun KClass<*>.toType(resolver: Resolver): KSType {
+ val name = resolver.getKSNameFromString(qualifiedName!!)
+ val classDecl = resolver.getClassDeclarationByName(name)
+ // This is a reminder to add corresponding JAR to `KotlinCompilation` in tests.
+ check(classDecl != null) {
+ "Unable to find the declaration of `$qualifiedName!!`." +
+ " Make sure the class is in the compilation classpath."
+ }
+ val type = classDecl.asStarProjectedType()
+ return type
+}
+
+/**
+ * Converts this class into [KSType] using the given resolver.
+ */
+internal fun Class<*>.toType(resolver: Resolver): KSType = kotlin.toType(resolver)
+
+/**
+ * Transform this string into a plural form if the count is greater than one.
+ *
+ * @param pluralForm Optional parameter to be used for count > 1. If not specified `"s"` will
+ * be appended to this string in the returned result.
+ * @return this string if count == 1, [pluralForm], if specified, "${this}s" otherwise.
+ */
+internal fun String.pluralize(count: Int, pluralForm: String? = null): String {
+ return if (count == 1) this else pluralForm ?: "${this}s"
+}
+
+/**
+ * Tells if this type has the same qualified name as the given one.
+ */
+internal fun KSType.isSame(other: KSType): Boolean =
+ declaration.qualifiedName?.asString() == other.declaration.qualifiedName?.asString()
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSDeclarationExts.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSDeclarationExts.kt
new file mode 100644
index 000000000..30af5da6e
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSDeclarationExts.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSDeclaration
+
+/**
+ * Obtains the qualified name of this declaration or `null`
+ * if the declaration does not have a qualified name.
+ */
+internal fun KSDeclaration.qualified(): String? = qualifiedName?.asString()
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSTypeExts.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSTypeExts.kt
new file mode 100644
index 000000000..df4a81490
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/KSTypeExts.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.ClassKind.INTERFACE
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSType
+
+/**
+ * Tells if this type represents an interface.
+ */
+internal val KSType.isInterface: Boolean
+ get() = (declaration is KSClassDeclaration)
+ && (declaration as KSClassDeclaration).classKind == INTERFACE
+
+/**
+ * Obtains a simple name of the type surrounded with back ticks.
+ */
+internal val KSType.ref: String
+ get() = "`${declaration.simpleName.asString()}`"
+
+/**
+ * Obtains a qualified name of the type surrouned with back ticks.
+ */
+internal val KSType.qualifiedRef: String
+ get() = "`${declaration.qualifiedName?.asString()}`"
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Qualifier.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Qualifier.kt
new file mode 100644
index 000000000..e283bedb4
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/Qualifier.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.routeRef
+
+/**
+ * The helper class which transforms the incoming sequence with [functions] into
+ * a list containing [CommandRouteFun] or [EventRouteFun].
+ *
+ * If a function is not recognized to be one of these types,
+ * the compilation terminates with an error.
+ */
+internal class Qualifier(
+ private val functions: Sequence,
+ private val environment: Environment
+) {
+ private var errorCount = 0
+ private val commandRoutes = CommandRouteSignature(environment)
+ private val eventRoutes = EventRouteSignature(environment)
+ private val stateRoutes = StateUpdateRouteSignature(environment)
+
+ fun run(): List {
+ val result = mutableListOf()
+ functions.forEach { fn ->
+ val commonChecksErrors = fn.commonChecks(environment)
+ if (commonChecksErrors != 0) {
+ errorCount += commonChecksErrors
+ return@forEach
+ }
+ val declaringClass = fn.declaringClass(environment)
+ if (declaringClass == null) {
+ errorCount += 1
+ return@forEach
+ }
+ val qualified = qualify(fn, declaringClass)
+ if (qualified != null) {
+ result.add(qualified)
+ } else {
+ environment.logger.error(
+ "Unqualified function encountered: `${fn.qualifiedName?.asString()}`."
+ )
+ errorCount += 1
+ }
+ }
+ if (errorCount > 0) {
+ error("${"Error".pluralize(errorCount)} using $routeRef.")
+ }
+ return result
+ }
+
+ @Suppress("ReturnCount")
+ private fun qualify(fn: KSFunctionDeclaration, declaringClass: EntityClass): RouteFun? {
+ commandRoutes.match(fn, declaringClass)?.let {
+ return it
+ } ?: eventRoutes.match(fn, declaringClass)?.let {
+ return it
+ } ?: stateRoutes.match(fn, declaringClass)?.let {
+ return it
+ }
+ return null
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteFun.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteFun.kt
new file mode 100644
index 000000000..71e0a49bb
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteFun.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.ksp.toClassName
+
+internal sealed class RouteFun(
+ val decl: KSFunctionDeclaration,
+ val declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+) {
+ val messageParameter: KSType = parameters.first
+ val messageClass: ClassName = messageParameter.toClassName()
+ val acceptsContext: Boolean = parameters.second != null
+ val isUnicast: Boolean = returnType.declaration.typeParameters.isEmpty()
+}
+
+internal class CommandRouteFun(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+) : RouteFun(fn, declaringClass, parameters, returnType)
+
+internal class EventRouteFun(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+) : RouteFun(fn, declaringClass, parameters, returnType)
+
+internal class StateUpdateRouteFun(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+) : RouteFun(fn, declaringClass, parameters, returnType)
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessor.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessor.kt
new file mode 100644
index 000000000..aa12781f6
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessor.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.validate
+import io.spine.server.route.Route
+
+/**
+ * Gathers all functions annotated with [Route] and initiates their processing
+ * by [RouteVisitor]s.
+ *
+ * @see RouteVisitor.process
+ */
+internal class RouteProcessor(
+ private val codeGenerator: CodeGenerator,
+ private val logger: KSPLogger
+) : SymbolProcessor {
+
+ override fun process(resolver: Resolver): List {
+ val allAnnotated = resolver.getSymbolsWithAnnotation(Route::class.qualifiedName!!)
+ val allValid = allAnnotated.filter { it.validate() }
+ .map { it as KSFunctionDeclaration }
+
+ val environment = Environment(resolver, logger, codeGenerator)
+ RouteVisitor.process(allValid, environment)
+
+ val unprocessed = allAnnotated.filterNot { it.validate() }.toList()
+ return unprocessed
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessorProvider.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessorProvider.kt
new file mode 100644
index 000000000..c47fba0dd
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteProcessorProvider.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+
+/**
+ * Creates a symbol processor for the [Route][io.spine.server.route.Route] annotation.
+ *
+ * @see RouteProcessor
+ */
+@AutoService(SymbolProcessorProvider::class)
+public class RouteProcessorProvider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
+ RouteProcessor(environment.codeGenerator, environment.logger)
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteSignature.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteSignature.kt
new file mode 100644
index 000000000..6b36f969a
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteSignature.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper
+import funRef
+import io.spine.core.SignalContext
+import io.spine.server.route.Route
+import io.spine.string.simply
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.qualify
+import io.spine.type.KnownMessage
+
+/**
+ * The base class for classes checking the contract of the functions with the [Route] annotation.
+ *
+ * The [match] function checks the signature of the function and creates an instance of
+ * a class derived from [RouteFun], if the contract is satisfied.
+ *
+ * The [qualify] method of the companion object walks through the annotated functions
+ * detected by the [RouteProcessor] matching them to corresponding signature kind and thus
+ * producing proper [RouteFun] instances.
+ *
+ * @property messageClass The class of the messages accepted as the first parameter of
+ * a routing function.
+ * @property contextClass The class of message contexts accepted as an optional second parameter of
+ * a routing function.
+ * @property environment The environment for resolving types and reporting errors and warnings.
+ */
+internal sealed class RouteSignature(
+ protected val messageClass: Class,
+ protected val contextClass: Class,
+ protected val environment: Environment
+) {
+ private val messageType by lazy { messageClass.toType(environment.resolver) }
+ private val contextType by lazy { contextClass.toType(environment.resolver) }
+
+ protected abstract fun matchDeclaringClass(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): Boolean
+
+ @OverridingMethodsMustInvokeSuper
+ protected open fun matchReturnType(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): KSType? {
+ val idClass = declaringClass.idClass
+ val returnType = fn.returnType!!.resolve()
+ if (idClass.isAssignableFrom(returnType)) {
+ return returnType
+ }
+ return null
+ }
+
+ /**
+ * Creates a [RouteFun] of the type [F] for the given function and resolved types.
+ */
+ protected abstract fun create(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+ ): F
+
+ @Suppress("ReturnCount")
+ fun match(fn: KSFunctionDeclaration, declaringClass: EntityClass): F? {
+ val params = matchParameters(fn)
+ ?: return null
+ if (!matchDeclaringClass(fn, declaringClass)) {
+ return null
+ }
+ val returnType = matchReturnType(fn, declaringClass)
+ ?: return null
+ return create(fn, declaringClass, params, returnType)
+ }
+
+ /**
+ * Verifies that the given function accepts one or two parameters with
+ * the types matching [messageClass] and [contextClass].
+ *
+ * The first parameter must be of [messageClass] or implement the interface specified
+ * by this property.
+ *
+ * The second parameter, if any, must be of the [contextClass] type.
+ */
+ @OverridingMethodsMustInvokeSuper
+ @Suppress("ReturnCount")
+ protected open fun matchParameters(fn: KSFunctionDeclaration): Pair? {
+ checkParamSize(fn)
+
+ val firstParamType = fn.parameters[0].type.resolve()
+ if (!messageType.isAssignableFrom(firstParamType)) {
+ // Even if the parameter does not match, it could be another kind of
+ // routing function, so we simply return `false`.
+ return null
+ }
+ var secondParamType: KSType? = null
+ if (fn.parameters.size == 2) {
+ secondParamType = fn.parameters[1].type.resolve()
+ val match = contextType.isSame(secondParamType)
+ if (!match) {
+ // Here, knowing that the first parameter type is correct, we can complain
+ // about the type of the second parameter.
+ val actualSecondParamName = secondParamType.declaration.simpleName.getShortName()
+ environment.logger.error(
+ "The second parameter of the ${fn.funRef} annotated with $routeRef" +
+ " must be `${contextClass.simpleName}`." +
+ " Encountered: `$actualSecondParamName`.",
+ fn
+ )
+ return null
+ }
+ }
+ return Pair(firstParamType, secondParamType)
+ }
+
+ /**
+ * A safety net to accept functions with the proper number of parameters.
+ *
+ * We formally check for this to be true in [KSFunctionDeclaration.acceptsOneOrTwoParameters].
+ */
+ private fun checkParamSize(fn: KSFunctionDeclaration) =
+ require(fn.parameters.size == 1 || fn.parameters.size == 2)
+
+ companion object {
+
+ val routeRef by lazy { "`@${simply()}`" }
+ val jvmStaticRef by lazy { "`@${simply()}`" }
+
+ fun qualify(
+ functions: Sequence,
+ environment: Environment
+ ): List {
+ val qualifier = Qualifier(functions, environment)
+ return qualifier.run()
+ }
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteVisitor.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteVisitor.kt
new file mode 100644
index 000000000..da4e5a3af
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/RouteVisitor.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSFile
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSTypeArgument
+import com.google.devtools.ksp.symbol.KSVisitorVoid
+import com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper
+import com.squareup.kotlinpoet.AnnotationSpec
+import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.STAR
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.WildcardTypeName
+import com.squareup.kotlinpoet.asClassName
+import com.squareup.kotlinpoet.ksp.toClassName
+import com.squareup.kotlinpoet.ksp.toTypeName
+import com.squareup.kotlinpoet.ksp.writeTo
+import io.spine.server.entity.Entity
+import io.spine.string.Indent
+import io.spine.tools.mc.java.GeneratedAnnotation
+import io.spine.tools.mc.java.routing.proessor.Environment.SetupType
+
+internal sealed class RouteVisitor(
+ protected val setup: SetupType,
+ private val functions: List,
+ protected val environment: Environment,
+) : KSVisitorVoid() {
+
+ protected abstract val classNameSuffix: String
+
+ private lateinit var packageName: String
+ private lateinit var originalFile: KSFile
+
+ protected lateinit var routingClass: TypeSpec.Builder
+ protected lateinit var setupFun: FunSpec.Builder
+ protected lateinit var routingRunBlock: CodeBlock.Builder
+
+ val entityClass: EntityClass by lazy {
+ val fn = functions.first()
+ fn.declaringClass
+ }
+
+ private val idClassTypeArgument: KSTypeArgument by lazy {
+ entityClass.idClassTypeArgument
+ }
+
+ override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
+ originalFile = classDeclaration.containingFile!!
+ packageName = originalFile.packageName.asString()
+ val className = classDeclaration.simpleName.asString() + classNameSuffix
+ createClass(className)
+ handleRouteFunctions()
+ }
+
+ @OverridingMethodsMustInvokeSuper
+ protected open fun createClass(className: String) {
+ val generated = GeneratedAnnotation.forKotlinPoet()
+ val autoService = AnnotationSpec.builder(AutoService::class)
+ .addMember("%T::class", setup.cls)
+ .build()
+
+ routingClass = TypeSpec.classBuilder(className)
+ .addKdoc(classKDoc())
+ // Must be `public` because created reflectively via `AutoService`
+ .addModifiers(KModifier.PUBLIC)
+ .addAnnotation(generated)
+ .addAnnotation(autoService)
+
+ val superInterface = setup.type
+ .replace(listOf(idClassTypeArgument))
+ .toTypeName()
+ routingClass.addSuperinterface(superInterface)
+ addEntityClassFunction()
+ }
+
+ private fun classKDoc(): CodeBlock = CodeBlock.builder()
+ .add("Configures [%T] of the repository managing [%T] instances.\n\n",
+ setup.routingClass.asClassName(),
+ entityClass.type.toClassName())
+ .add("@see %T.apply()\n", setup.cls.asClassName())
+ .build()
+
+ /**
+ * Adds the method that overrides [io.spine.server.route.RoutingSetup.entityClass].
+ */
+ private fun addEntityClassFunction() {
+ val entityType = Entity::class.asClassName().parameterizedBy(
+ idClassTypeArgument.type!!.toTypeName(),
+ STAR
+ )
+ val classOfEntityInterface = Class::class.asClassName().parameterizedBy(
+ WildcardTypeName.producerOf(entityType)
+ )
+
+ val funSpec = FunSpec.builder("entityClass")
+ .addModifiers(KModifier.OVERRIDE)
+ .returns(classOfEntityInterface)
+ .addCode("return %T::class.java\n", entityClass.type.toClassName())
+ .build()
+
+ routingClass.addFunction(funSpec)
+ }
+
+ private fun handleRouteFunctions() {
+ openSetupFunction()
+ functions.forEach { addRoute(it) }
+ closeSetupFunction()
+ }
+
+ private fun openSetupFunction() {
+ val paramName = "routing"
+ val paramType = setup.routingClass.asClassName()
+ .parameterizedBy(idClassTypeArgument.type!!.toTypeName())
+ val param = ParameterSpec.builder(paramName, paramType)
+ setupFun = FunSpec.builder("setup")
+ .addModifiers(KModifier.OVERRIDE)
+ .addParameter(param.build())
+
+ routingRunBlock = CodeBlock.builder()
+ .beginControlFlow("%N.run", paramName)
+ }
+
+ protected abstract fun addRoute(fn: F)
+
+ private fun closeSetupFunction() {
+ routingRunBlock.endControlFlow()
+ setupFun.addCode(routingRunBlock.build())
+ routingClass.addFunction(setupFun.build())
+ }
+
+ fun writeFile() {
+ val cls = routingClass.build()
+ val code = FileSpec.builder(packageName, cls.name!!)
+ .indent(Indent.defaultJavaIndent.value)
+ .addType(cls)
+ .build()
+ val deps = Dependencies(true, originalFile)
+ code.writeTo(environment.codeGenerator, deps)
+ }
+
+ companion object {
+
+ /**
+ * The name of the inline extension functions for classes extending
+ * [MessageRouting][io.spine.server.route.MessageRouting] which are used in
+ * the generated code of [RoutingSetup][io.spine.server.route.RoutingSetup] classes.
+ */
+ internal const val ROUTE_FUN_NAME: String = "route"
+
+ internal fun process(
+ allValid: Sequence,
+ environment: Environment
+ ) {
+ val qualified = RouteSignature.qualify(allValid, environment)
+ CommandRouteVisitor.process(qualified, environment)
+ EventRouteVisitor.process(qualified, environment)
+ StateUpdateRouteVisitor.process(qualified, environment)
+ }
+
+ internal inline fun , reified F : RouteFun> runVisitors(
+ qualified: List,
+ createVisitor: (List) -> V
+ ) {
+ val routing = qualified.filterIsInstance()
+ val grouped = routing.groupByClasses()
+ grouped.forEach { (declaringClass, functions) ->
+ val v = createVisitor(functions)
+ declaringClass.accept(v, Unit)
+ v.writeFile()
+ }
+ }
+ }
+}
+
+/**
+ * Groups this list of route functions by the classes in which they are declared.
+ *
+ * The grouped functions are then sorted by the [first parameter type][RouteFun.messageParameter],
+ * putting classes first, and then less abstract interfaces, and so on down to more abstract ones.
+ *
+ * This order will be used when adding routes for each [entity class][RouteFun.declaringClass].
+ *
+ * @see RouteFunComparator
+ */
+private fun List.groupByClasses(): Map> =
+ groupBy { it.declaringClass }
+ .mapValues { (_, list) ->
+ RouteFunComparator.sort(list)
+ }
+
+/**
+ * Compares two [RouteFun] instances by their [messageParameter][RouteFun.messageParameter]
+ * properties, putting more abstract type further in the sorting order.
+ */
+private class RouteFunComparator : Comparator {
+
+ @Suppress("ReturnCount")
+ override fun compare(o1: RouteFun, o2: RouteFun): Int {
+ val m1 = o1.messageParameter
+ val m2 = o2.messageParameter
+
+ if (m1 == m2) {
+ return 0
+ }
+ // An interface should come after a class in the sorting.
+ if (m1.isInterface && !m2.isInterface) {
+ return 1
+ }
+ if (!m1.isInterface && m2.isInterface) {
+ return -1
+ }
+ // Both are either classes or interfaces.
+ // The one that is more abstract goes further in sorting.
+ if (m1.isAssignableFrom(m2)) {
+ return 1
+ }
+ if (m2.isAssignableFrom(m1)) {
+ return -1
+ }
+ val n1 = m1.declaration.qualifiedName?.asString()
+ val n2 = m2.declaration.qualifiedName?.asString()
+ return compareValues(n1, n2)
+ }
+
+ companion object {
+
+ fun sort(list: List): List {
+ val comparator = RouteFunComparator()
+ return list.sortedWith(comparator)
+ }
+ }
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteSignature.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteSignature.kt
new file mode 100644
index 000000000..f333d2aa0
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteSignature.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSType
+import io.spine.base.EntityState
+import io.spine.core.EventContext
+
+internal class StateUpdateRouteSignature(
+ environment: Environment
+) : RouteSignature(
+ EntityState::class.java,
+ EventContext::class.java,
+ environment
+) {
+ override fun matchDeclaringClass(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass
+ ): Boolean = environment.run {
+ val isProjection = projectionClass.isAssignableFrom(declaringClass.type)
+ val isProcessManager = processManagerClass.isAssignableFrom(declaringClass.type)
+ return isProjection || isProcessManager
+ }
+
+ override fun create(
+ fn: KSFunctionDeclaration,
+ declaringClass: EntityClass,
+ parameters: Pair,
+ returnType: KSType
+ ): StateUpdateRouteFun = StateUpdateRouteFun(fn, declaringClass, parameters, returnType)
+}
diff --git a/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteVisitor.kt b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteVisitor.kt
new file mode 100644
index 000000000..78100869a
--- /dev/null
+++ b/mc-java-routing/src/main/kotlin/io/spine/tools/mc/java/routing/proessor/StateUpdateRouteVisitor.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing.proessor
+
+internal class StateUpdateRouteVisitor(
+ functions: List,
+ environment: Environment
+) : RouteVisitor(
+ environment.stateRoutingSetup,
+ functions,
+ environment
+) {
+
+ override val classNameSuffix: String = "StateUpdateRouting"
+
+ override fun addRoute(fn: StateUpdateRouteFun) {
+ //TODO:2025-01-22:alexander.yevsyukov: Implement.
+ }
+
+ companion object {
+ fun process(qualified: List, environment: Environment) {
+ runVisitors(qualified) { functions ->
+ StateUpdateRouteVisitor(functions, environment)
+ }
+ }
+ }
+}
diff --git a/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteErrorSpec.kt b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteErrorSpec.kt
new file mode 100644
index 000000000..7ccdb67b0
--- /dev/null
+++ b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteErrorSpec.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress(
+ "ClassNameDiffersFromFileName" /* false positive in IDEA */,
+ "MissingPackageInfo" /* don't need them for these tests. */
+)
+
+package io.spine.tools.mc.java.routing
+
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.spine.core.CommandContext
+import io.spine.core.EventContext
+import io.spine.server.aggregate.Aggregate
+import io.spine.server.entity.Entity
+import io.spine.server.procman.ProcessManager
+import io.spine.server.projection.Projection
+import io.spine.string.simply
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.routeRef
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+/**
+ * This test suite covers handling errors associated with misuse of
+ * the [Route][io.spine.server.route.Route] annotation in the Java code.
+ *
+ * For tests of the positive scenarios please see [JavaRouteSpec].
+ *
+ * @see KotlinRouteErrorSpec
+ */
+@ExperimentalCompilerApi
+@DisplayName("`RouteProcessor` should detect Java code errors")
+internal class JavaRouteErrorSpec : RouteCompilationTest() {
+
+ /*
+ * Error: non-static method.
+ */
+ private val nonStatic = javaFile("NonStatic", """
+
+ package io.spine.given.devices;
+
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class NonStatic extends Projection {
+ @Route
+ DeviceId route(StatusReported event) {
+ return event.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when a non-static method is annotated`() {
+ compilation.apply {
+ sources = listOf(nonStatic)
+ }
+ val result = compilation.compileSilently()
+
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "The method `route()`"
+ it shouldContain routeRef
+ it shouldContain "must be `static`."
+ }
+ }
+
+ /*
+ * Error: no parameters.
+ */
+ private val noParameters = javaFile("NoParameters", """
+
+ package io.spine.given.devices;
+
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class NoParameters extends Projection {
+ @Route
+ static DeviceId route() {
+ return DeviceId.generate();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when no parameters are specified`() {
+ compilation.apply {
+ sources = listOf(noParameters)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "The method `route()`"
+ it shouldContain routeRef
+ it shouldContain "one or two parameters. Encountered: 0."
+ }
+ }
+
+ /*
+ * Error: too many parameters.
+ */
+ private val tooManyParameters = javaFile("TooManyParameters", """
+
+ package io.spine.given.devices;
+
+ import io.spine.core.EventContext;
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class TooManyParameters extends Projection {
+ @Route
+ static DeviceId route(StatusReported event, EventContext context, Object other) {
+ return event.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when too many parameters are specified`() {
+ compilation.apply {
+ sources = listOf(tooManyParameters)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "The method `route()`"
+ it shouldContain routeRef
+ it shouldContain "one or two parameters. Encountered: 3."
+ }
+ }
+
+ /**
+ * Error: the second parameter is [io.spine.core.CommandContext] instead of [EventContext].
+ */
+ private val wrongSecondParameter = javaFile("WrongSecondParameter", """
+
+ package io.spine.given.devices;
+
+ import io.spine.core.CommandContext;
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class WrongSecondParameter extends Projection {
+ @Route
+ static DeviceId route(StatusReported event, CommandContext context) {
+ return event.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when the second parameters is of incorrect type`() {
+ compilation.apply {
+ sources = listOf(wrongSecondParameter)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "The second parameter of the method `route()`"
+ it shouldContain routeRef
+ it shouldContain "must be `${simply()}`."
+ it shouldContain "Encountered: `${simply()}`."
+ }
+ }
+
+ /**
+ * Error: a routing function declared in a non-entity class.
+ */
+ private val nonEntityClass = javaFile("NonEntityClass", """
+
+ package io.spine.given.devices;
+
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.server.event.AbstractEventReactor;
+ import io.spine.server.route.Route;
+
+ /**
+ * This inheritance does not make any sense.
+ *
+ * We just want to have a class which extends another class explicitly.
+ */
+ class NonEntityClass extends AbstractEventReactor {
+ @Route
+ static DeviceId route(StatusReported event) {
+ return event.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when a function declared in a non-entity class`() {
+ compilation.apply {
+ sources = listOf(nonEntityClass)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "The declaring class of the method `route()`"
+ it shouldContain routeRef
+ it shouldContain " must implement the `${Entity::class.java.canonicalName}` interface."
+ }
+ }
+
+ /**
+ * Error: Command route method declared in a projection class.
+ */
+ private val wrongSignalRouted = javaFile("WrongSignalRouted", """
+
+ package io.spine.given.devices;
+
+ import io.spine.core.CommandContext;
+ import io.spine.given.devices.commands.RegisterDevice;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class WrongSignalRouted extends Projection {
+
+ @Route
+ static DeviceId route(RegisterDevice command, CommandContext context) {
+ // Commands are automatically routed by the first message field.
+ // We add this method with meaningless routing just to cause the error.
+ return command.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when a command routing declared in a non-applicable class`() {
+ compilation.apply {
+ sources = listOf(wrongSignalRouted)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "A command routing function can be declared in a class derived"
+ it shouldContain simply>()
+ it shouldContain simply>()
+ it shouldContain routeRef
+ it shouldContain " Encountered: `${Projection::class.java.canonicalName}`."
+ }
+ }
+}
+
diff --git a/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteSpec.kt b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteSpec.kt
new file mode 100644
index 000000000..29790ead6
--- /dev/null
+++ b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/JavaRouteSpec.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress(
+ "ClassNameDiffersFromFileName" /* false positive in IDEA */,
+ "MissingPackageInfo" /* don't need them for these tests. */
+)
+
+package io.spine.tools.mc.java.routing
+
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
+import io.kotest.matchers.shouldBe
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+/**
+ * This test suite checks positive scenarios of the compilation of
+ * the [Route][io.spine.server.route.Route] annotation in the Java code.
+ *
+ * For error scenarios, please see [JavaRouteErrorSpec].
+ *
+ * @see JavaRouteErrorSpec
+ */
+@ExperimentalCompilerApi
+@DisplayName("`RouteProcessor` should detect Java code errors")
+internal class JavaRouteSpec : RouteCompilationTest() {
+
+ private val twoRoutes = javaFile("TwoRoutes", """
+
+ package io.spine.given.devices;
+
+ import io.spine.given.devices.events.StatusReported;
+ import io.spine.given.devices.events.DeviceRegistered;
+ import io.spine.server.projection.Projection;
+ import io.spine.server.route.Route;
+
+ class TwoRoutes extends Projection {
+
+ @Route
+ static DeviceId route(StatusReported event) {
+ return event.getDevice();
+ }
+
+ @Route
+ static DeviceId route(DeviceRegistered event) {
+ return event.getDevice();
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `handle two routes`() {
+ compilation.apply {
+ sources = listOf(twoRoutes)
+ }
+ val result = compilation.compile()
+
+ result.exitCode shouldBe ExitCode.OK
+ }
+}
diff --git a/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/KotlinRouteErrorSpec.kt b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/KotlinRouteErrorSpec.kt
new file mode 100644
index 000000000..470b9b49d
--- /dev/null
+++ b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/KotlinRouteErrorSpec.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@file:Suppress(
+ "ClassNameDiffersFromFileName" /* false positive in IDEA */,
+ "MissingPackageInfo" /* don't need them for these tests. */
+)
+
+package io.spine.tools.mc.java.routing
+
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
+import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.spine.tools.mc.java.routing.proessor.RouteSignature.Companion.routeRef
+import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+@ExperimentalCompilerApi
+@DisplayName("`RouteProcessor` should detect Kotlin code errors")
+internal class KotlinRouteErrorSpec : RouteCompilationTest() {
+
+ /**
+ * Error: The function must be a static method of a class.
+ */
+ private val fileLevelFunction = kotlinFile("FileLevelFunction", """
+
+ package io.spine.given.devices
+
+ import io.spine.base.EventMessage
+ import io.spine.server.route.Route
+
+ @Route
+ private fun route(e: EventMessage): String = "Hello"
+ """.trimIndent())
+
+ @Test
+ fun `when a function is defined on a file level`() {
+ compilation.apply {
+ sources = listOf(fileLevelFunction)
+ }
+
+ val result = compilation.compileSilently()
+
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "`route()`" // The name of the function in error.
+ it shouldContain routeRef
+ it shouldContain "a member of a companion object of an entity class."
+ }
+ }
+
+ /**
+ * Error: The method must belong to a companion object.
+ */
+ private val notCompanionMember = kotlinFile("NotCompanionMember", """
+ package io.spine.given.devices
+
+ import io.spine.given.devices.events.StatusReported
+ import io.spine.server.projection.Projection
+ import io.spine.server.route.Route
+
+ class NotCompanionMember : Projection() {
+ @Route
+ fun route(e: StatusReported): DeviceId {
+ return event.getDevice()
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `when a function is not a member of a companion object`() {
+ compilation.apply {
+ sources = listOf(notCompanionMember)
+ }
+
+ val result = compilation.compileSilently()
+
+ result.exitCode shouldBe COMPILATION_ERROR
+ result.messages.let {
+ it shouldContain "`route()`" // The name of the function in error.
+ it shouldContain routeRef
+ it shouldContain "a member of a companion object."
+ }
+ }
+
+ /**
+ * Correct routing method.
+ */
+ private val companionMember = kotlinFile("CompanionMember", """
+ package io.spine.given.devices
+
+ import io.spine.given.devices.events.StatusReported
+ import io.spine.server.projection.Projection
+ import io.spine.server.route.Route
+
+ class CompanionMember : Projection() {
+
+ companion object {
+ @Route
+ @JvmStatic
+ fun route(e: StatusReported): DeviceId {
+ return event.getDevice()
+ }
+ }
+ }
+ """.trimIndent())
+
+ @Test
+ fun `accept a function defined in a companion object`() {
+ compilation.apply {
+ sources = listOf(companionMember)
+ }
+ val result = compilation.compileSilently()
+ result.exitCode shouldBe ExitCode.OK
+ }
+}
diff --git a/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/RouteCompilationTest.kt b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/RouteCompilationTest.kt
new file mode 100644
index 000000000..70244b8e4
--- /dev/null
+++ b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/RouteCompilationTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing
+
+import com.google.auto.service.AutoService
+import com.google.protobuf.MessageOrBuilder
+import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.symbolProcessorProviders
+import io.spine.base.CommandMessage
+import io.spine.core.EventContext
+import io.spine.given.devices.Device
+import io.spine.logging.testing.ConsoleTap
+import io.spine.server.route.Route
+import io.spine.tools.mc.java.routing.proessor.RouteProcessorProvider
+import io.spine.validate.ValidatingBuilder
+import kotlin.collections.plus
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.BeforeEach
+
+/**
+ * Abstract base for tests checking handling compilation of the [Route] annotation.
+ *
+ * The tests use types from the Protobuf code generated for the `given.devices` proto package.
+ */
+sealed class RouteCompilationTest {
+
+ companion object {
+
+ /**
+ * Suppress excessive console output produced by [KotlinCompilation.compile].
+ *
+ * @see Related issue
+ * @see KotlinCompilation.compileSilently
+ */
+ @BeforeAll
+ @JvmStatic
+ fun redirectStreams() {
+ ConsoleTap.install()
+ }
+ }
+ protected lateinit var compilation: KotlinCompilation
+
+ @BeforeEach
+ fun prepareCompilation() {
+ compilation = KotlinCompilation()
+
+ val dependencyJars = setOf(
+ AutoService::class.java,
+ MessageOrBuilder::class.java, // Protobuf
+ CommandMessage::class.java, // Base
+ ValidatingBuilder::class.java, // Validation runtime
+ EventContext::class.java, // CoreJava.core
+ Route::class.java, // CoreJava.server
+ RouteProcessorProvider::class.java, // RouteProcessor
+ Device::class.java // Compiled protos
+ ).map { it.classpathFile() }
+
+ compilation.apply {
+ javaPackagePrefix = "io.spine.routing.given"
+ symbolProcessorProviders = listOf(RouteProcessorProvider())
+ classpaths = classpaths + dependencyJars
+ }
+ }
+}
diff --git a/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/TestExts.kt b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/TestExts.kt
new file mode 100644
index 000000000..4f5510576
--- /dev/null
+++ b/mc-java-routing/src/test/kotlin/io/spine/tools/mc/java/routing/TestExts.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.tools.mc.java.routing
+
+import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.SourceFile
+import com.tschuchort.compiletesting.SourceFile.Companion.java
+import com.tschuchort.compiletesting.SourceFile.Companion.kotlin
+import io.spine.logging.testing.tapConsole
+import java.io.File
+import org.intellij.lang.annotations.Language
+
+/**
+ * Obtains the path to the classpath element which contains the receiver class.
+ */
+internal fun Class<*>.classpathFile(): File = File(protectionDomain.codeSource.location.path)
+
+/**
+ * The package directory is `io/spine/given/devices/` which matches the options of
+ * proto types defined under `test/proto/given/devices/`.
+ */
+@Suppress("TopLevelPropertyNaming")
+private const val packageDir = "io/spine/given/devices"
+
+/**
+ * Creates an instance of [SourceFile] with the Java file containing the class
+ * with the specified name and contents.
+ */
+internal fun javaFile(
+ simpleClassName: String,
+ @Language("java") contents: String
+): SourceFile = java(
+ name = "$packageDir/${simpleClassName}.java",
+ contents = contents,
+ trimIndent = true
+)
+
+/**
+ * Creates an instance of [SourceFile] with the Kotlin file containing the class
+ * with the specified name and contents.
+ */
+internal fun kotlinFile(
+ simpleClassName: String,
+ @Language("kotlin") contents: String
+): SourceFile = kotlin(
+ name = "$packageDir/${simpleClassName}.kt",
+ contents = contents,
+ trimIndent = true
+)
+
+/**
+ * Performs compilation with redirected console output.
+ *
+ * @see io.spine.logging.testing.ConsoleTap.install
+ * @see tapConsole
+ */
+internal fun KotlinCompilation.compileSilently(): KotlinCompilation.Result {
+ var result: KotlinCompilation.Result? = null
+ tapConsole {
+ result = compile()
+ }
+ return result!!
+}
diff --git a/mc-java-routing/src/test/proto/given/devices/commands.proto b/mc-java-routing/src/test/proto/given/devices/commands.proto
new file mode 100644
index 000000000..c1c154a81
--- /dev/null
+++ b/mc-java-routing/src/test/proto/given/devices/commands.proto
@@ -0,0 +1,47 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.devices;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.devices.commands";
+option java_outer_classname = "CommandsProto";
+option java_multiple_files = true;
+
+import "given/devices/values.proto";
+
+message RegisterDevice {
+ DeviceId device = 1;
+ FirmwareVersion firmware = 2 [(required) = true];
+}
+
+message ReportStatus {
+ DeviceId device = 1;
+}
diff --git a/mc-java-routing/src/test/proto/given/devices/entities.proto b/mc-java-routing/src/test/proto/given/devices/entities.proto
new file mode 100644
index 000000000..d8d8d6eba
--- /dev/null
+++ b/mc-java-routing/src/test/proto/given/devices/entities.proto
@@ -0,0 +1,50 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.devices;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.devices";
+option java_outer_classname = "EntitiesProto";
+option java_multiple_files = true;
+
+import "given/devices/values.proto";
+
+message Device {
+ option (entity).kind = AGGREGATE;
+ DeviceId id = 1;
+ FirmwareVersion firmware = 2 [(required) = true];
+}
+
+message DeviceStatus {
+ option (entity).kind = PROJECTION;
+ DeviceId id = 1;
+ repeated string status = 2 [(required) = true];
+}
diff --git a/mc-java-routing/src/test/proto/given/devices/events.proto b/mc-java-routing/src/test/proto/given/devices/events.proto
new file mode 100644
index 000000000..732ceaf1f
--- /dev/null
+++ b/mc-java-routing/src/test/proto/given/devices/events.proto
@@ -0,0 +1,48 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.devices;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.devices.events";
+option java_outer_classname = "EventsProto";
+option java_multiple_files = true;
+
+import "given/devices/values.proto";
+
+message DeviceRegistered {
+ DeviceId device = 1 [(required) = true];
+ FirmwareVersion firmware = 2 [(required) = true];
+}
+
+message StatusReported {
+ DeviceId device = 1[(required) = true];
+ string status = 2 [(required) = true];
+}
diff --git a/mc-java-routing/src/test/proto/given/devices/values.proto b/mc-java-routing/src/test/proto/given/devices/values.proto
new file mode 100644
index 000000000..bf35baaa2
--- /dev/null
+++ b/mc-java-routing/src/test/proto/given/devices/values.proto
@@ -0,0 +1,44 @@
+/*
+* Copyright 2025, TeamDev. All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* https://www.apache.org/licenses/LICENSE-2.0
+*
+* Redistribution and use in source and/or binary forms, with or without
+* modification, must retain the above copyright notice and the following
+* disclaimer.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+syntax = "proto3";
+
+package given.devices;
+
+import "spine/options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.given.devices";
+option java_outer_classname = "ValuesProto";
+option java_multiple_files = true;
+
+message DeviceId {
+ string uuid = 1 [(required) = true];
+}
+
+message FirmwareVersion {
+ string value = 1 [(required) = true];
+}
diff --git a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/Renderers.kt b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/Renderers.kt
index d9019a49c..7b3637ae5 100644
--- a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/Renderers.kt
+++ b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/Renderers.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,7 @@ internal abstract class SignalRenderer :
@OverridingMethodsMustInvokeSuper
override fun doRender(type: MessageType, file: SourceFile) {
execute {
- RenderActions(type, file, typeSettings.actions, context!!).apply()
+ RenderActions(type, file, typeSettings.actions, context).apply()
}
}
}
diff --git a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableCode.kt b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableCode.kt
index 87f0546e9..156c486d5 100644
--- a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableCode.kt
+++ b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableCode.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,10 +35,9 @@ import io.spine.protodata.ast.MessageType
import io.spine.protodata.java.MessageOrEnumConvention
import io.spine.protodata.type.TypeSystem
import io.spine.tools.java.classSpec
-import io.spine.tools.java.code.GeneratedBy
-import io.spine.tools.java.code.field.FieldName
import io.spine.tools.java.constructorSpec
import io.spine.tools.java.methodSpec
+import io.spine.tools.mc.java.GeneratedAnnotation
import io.spine.tools.mc.java.signal.rejection.Javadoc.forConstructorOfThrowable
import io.spine.tools.mc.java.signal.rejection.Javadoc.forThrowableOf
import javax.lang.model.element.Modifier.FINAL
@@ -81,7 +80,7 @@ internal class RThrowableCode(
fun toPoet(): TypeSpec = classSpec(simpleClassName) {
addJavadoc(forThrowableOf(rejection))
- addAnnotation(GeneratedBy.spineModelCompiler())
+ addAnnotation(GeneratedAnnotation.forJavaPoet())
addModifiers(PUBLIC)
superclass(RejectionThrowable::class.java)
addField(serialVersionUID())
@@ -126,7 +125,7 @@ private val messageThrown = NoArgMethod("messageThrown")
private fun serialVersionUID(): FieldSpec {
return FieldSpec.builder(
Long::class.javaPrimitiveType,
- FieldName.serialVersionUID().value(), PRIVATE, STATIC, FINAL)
+ "serialVersionUID", PRIVATE, STATIC, FINAL)
.initializer("0L")
.build()
}
diff --git a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableRenderer.kt b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableRenderer.kt
index f4a5cc4b4..b7ed8d22a 100644
--- a/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableRenderer.kt
+++ b/mc-java-signal/src/main/kotlin/io/spine/tools/mc/java/signal/rejection/RThrowableRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -105,7 +105,7 @@ internal class RThrowableRenderer: JavaRenderer(), WithLogging {
}
private fun generateRejection(protoFile: ProtobufSourceFile, rejection: MessageType) {
- val rtCode = RThrowableCode(protoFile.javaPackage(), rejection, typeSystem!!)
+ val rtCode = RThrowableCode(protoFile.javaPackage(), rejection, typeSystem)
val file = rejection.throwableJavaFile(protoFile)
rtCode.writeToFile(file)
diff --git a/mc-java-uuid-tests/src/test/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethodsSpec.kt b/mc-java-uuid-tests/src/test/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethodsSpec.kt
index a04c496a9..71d00cef5 100644
--- a/mc-java-uuid-tests/src/test/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethodsSpec.kt
+++ b/mc-java-uuid-tests/src/test/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethodsSpec.kt
@@ -39,7 +39,7 @@ import org.junit.jupiter.api.io.TempDir
@DisplayName("`AddFactoryMethods` should")
internal class AddFactoryMethodsSpec {
- private val annotationText = GeneratedAnnotation.create().text
+ private val annotationText = GeneratedAnnotation.forPsi().text
companion object : UuidPluginTestSetup(
AddFactoryMethods::class.java,
diff --git a/mc-java-uuid/src/main/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethods.kt b/mc-java-uuid/src/main/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethods.kt
index db14599c4..ee0dc6061 100644
--- a/mc-java-uuid/src/main/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethods.kt
+++ b/mc-java-uuid/src/main/kotlin/io/spine/tools/mc/java/uuid/AddFactoryMethods.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -96,7 +96,7 @@ private class MethodGenerate(private val cls: PsiClass) {
""".trimIndent(), cls
)
method.run {
- val annotation = GeneratedAnnotation.create()
+ val annotation = GeneratedAnnotation.forPsi()
addFirst(annotation)
addFirst(javadoc)
}
@@ -138,7 +138,7 @@ private class MethodOf(private val cls: PsiClass) {
""".trimIndent(), cls
)
method.run {
- val annotation = GeneratedAnnotation.create()
+ val annotation = GeneratedAnnotation.forPsi()
addFirst(annotation)
addFirst(javadoc)
}
diff --git a/pom.xml b/pom.xml
index cf8e940f7..d37e83246 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,4 +1,30 @@
+
+
4.0.0
io.spine.tools
mc-java
-2.0.0-SNAPSHOT.262
+2.0.0-SNAPSHOT.263
2015
@@ -23,6 +49,12 @@ all modules and does not describe the project structure per-subproject.
+
+ com.google.devtools.ksp
+ symbol-processing-api
+ 1.8.22-1.0.11
+ compile
+
com.google.guava
guava
@@ -35,16 +67,28 @@ all modules and does not describe the project structure per-subproject.
0.9.4
compile
+
+ com.squareup
+ kotlinpoet
+ 2.0.0
+ compile
+
+
+ com.squareup
+ kotlinpoet-ksp
+ 2.0.0
+ compile
+
io.spine
protodata
- 0.90.1
+ 0.92.3
compile
io.spine
spine-base
- 2.0.0-SNAPSHOT.234
+ 2.0.0-SNAPSHOT.241
compile
@@ -62,19 +106,19 @@ all modules and does not describe the project structure per-subproject.
io.spine
spine-server
- 2.0.0-SNAPSHOT.191
+ 2.0.0-SNAPSHOT.203
compile
io.spine.protodata
protodata-java
- 0.90.1
+ 0.92.3
compile
io.spine.protodata
protodata-params
- 0.90.1
+ 0.92.3
compile
@@ -86,25 +130,25 @@ all modules and does not describe the project structure per-subproject.
io.spine.tools
spine-plugin-base
- 2.0.0-SNAPSHOT.240
+ 2.0.0-SNAPSHOT.244
compile
io.spine.tools
spine-psi-java
- 2.0.0-SNAPSHOT.240
+ 2.0.0-SNAPSHOT.244
compile
io.spine.validation
spine-validation-configuration
- 2.0.0-SNAPSHOT.182
+ 2.0.0-SNAPSHOT.192
compile
io.spine.validation
spine-validation-java-runtime
- 2.0.0-SNAPSHOT.182
+ 2.0.0-SNAPSHOT.192
compile
@@ -113,12 +157,24 @@ all modules and does not describe the project structure per-subproject.
3.1.0
compile
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 1.9.24
+ compile
+
org.jetbrains.kotlin
kotlin-stdlib-jdk8
- 1.9.23
+ 1.9.24
compile
+
+ com.github.tschuchortdev
+ kotlin-compile-testing-ksp
+ 1.5.0
+ test
+
com.google.errorprone
error_prone_test_helpers
@@ -149,16 +205,28 @@ all modules and does not describe the project structure per-subproject.
1.1.5
test
+
+ io.kotest
+ kotest-assertions-core
+ 5.8.0
+ test
+
io.spine.protodata
protodata-testlib
- 0.90.1
+ 0.92.3
+ test
+
+
+ io.spine.tools
+ spine-logging-testlib
+ 2.0.0-SNAPSHOT.242
test
io.spine.tools
spine-plugin-testlib
- 2.0.0-SNAPSHOT.240
+ 2.0.0-SNAPSHOT.244
test
@@ -167,10 +235,16 @@ all modules and does not describe the project structure per-subproject.
2.0.0-SNAPSHOT.185
test
+
+ io.spine.tools
+ spine-testutil-server
+ 2.0.0-SNAPSHOT.203
+ test
+
io.spine.tools
spine-tool-base
- 2.0.0-SNAPSHOT.240
+ 2.0.0-SNAPSHOT.244
test
@@ -227,6 +301,16 @@ all modules and does not describe the project structure per-subproject.
3.0.2
provided
+
+ com.google.devtools.ksp
+ symbol-processing
+ 1.8.22-1.0.11
+
+
+ com.google.devtools.ksp
+ symbol-processing-cmdline
+ 1.8.22-1.0.11
+
com.google.errorprone
error_prone_annotations
@@ -259,6 +343,11 @@ all modules and does not describe the project structure per-subproject.
checkstyle
10.12.1
+
+ dev.zacsweers.autoservice
+ auto-service-ksp
+ 1.2.0
+
io.gitlab.arturbosch.detekt
detekt-cli
@@ -272,33 +361,33 @@ all modules and does not describe the project structure per-subproject.
io.spine.protodata
protodata-fat-cli
- 0.90.0
+ 0.91.4
io.spine.protodata
protodata-protoc
- 0.90.0
+ 0.91.4
io.spine.tools
prototap-protoc-plugin
- 0.8.7
+ 0.9.1
io.spine.tools
spine-mc-java-checks
- 2.0.0-SNAPSHOT.261
+ 2.0.0-SNAPSHOT.262
provided
io.spine.tools
spine-mc-java-plugins
- 2.0.0-SNAPSHOT.261
+ 2.0.0-SNAPSHOT.262
io.spine.validation
spine-validation-java-bundle
- 2.0.0-SNAPSHOT.182
+ 2.0.0-SNAPSHOT.192
net.sourceforge.pmd
@@ -383,4 +472,4 @@ all modules and does not describe the project structure per-subproject.
-
\ No newline at end of file
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 621996cf2..47e2ae528 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -41,6 +41,8 @@ include(
"mc-java-marker-tests",
"mc-java-message-group",
"mc-java-message-group-tests",
+ "mc-java-routing",
+ "mc-java-routing-tests",
"mc-java-uuid",
"mc-java-uuid-tests",
"mc-java-plugin-bundle"
diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts
index d356bdae6..d018ee5bf 100644
--- a/tests/build.gradle.kts
+++ b/tests/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -76,6 +76,9 @@ buildscript {
all {
resolutionStrategy {
force(
+ io.spine.dependency.lib.KotlinX.Coroutines.bom,
+ io.spine.dependency.lib.KotlinX.Coroutines.core,
+ io.spine.dependency.lib.KotlinX.Coroutines.jdk8,
grpc.api,
"io.spine:protodata:${protoData.version}",
spine.reflect,
diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts
index 26f0d3f5a..51d6594dd 100644
--- a/tests/settings.gradle.kts
+++ b/tests/settings.gradle.kts
@@ -1,11 +1,11 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* 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
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
@@ -33,7 +33,7 @@ include(
"model-compiler",
"rejection",
"mc-java-comparable",
- "validation-smoke"
+// "validation-smoke"
)
/*
diff --git a/tests/validation-smoke/build.gradle.kts b/tests/validation-smoke/build.gradle.kts
index 4ae532c72..1b5dcc90c 100644
--- a/tests/validation-smoke/build.gradle.kts
+++ b/tests/validation-smoke/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright 2024, TeamDev. All rights reserved.
+ * Copyright 2025, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,3 +28,5 @@ plugins {
java
id("io.spine.mc-java")
}
+
+protoDataRemoteDebug(enabled = false)
diff --git a/version.gradle.kts b/version.gradle.kts
index 8b98cb6df..163e62e26 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -31,5 +31,5 @@
*
* For versions of Spine-based dependencies please see [io.spine.internal.dependency.spine].
*/
-val mcJavaVersion by extra("2.0.0-SNAPSHOT.262")
+val mcJavaVersion by extra("2.0.0-SNAPSHOT.263")
val versionToPublish by extra(mcJavaVersion)