diff --git a/skills/kotlin-tooling-java-to-kotlin/SKILL.md b/skills/kotlin-tooling-java-to-kotlin/SKILL.md new file mode 100644 index 0000000..e856280 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/SKILL.md @@ -0,0 +1,138 @@ +--- +name: kotlin-tooling-java-to-kotlin +description: > + Use when converting Java source files to idiomatic Kotlin, when user mentions + "java to kotlin", "j2k", "convert java", "migrate java to kotlin", or when + working with .java files that need to become .kt files. Handles framework-aware + conversion for Spring, Lombok, Hibernate, Jackson, Micronaut, Quarkus, Dagger/Hilt, + RxJava, JUnit, Guice, Retrofit, and Mockito. +license: Apache-2.0 +metadata: + author: JetBrains + version: "1.0.0" +--- + +# Java to Kotlin Conversion + +Convert Java source files to idiomatic Kotlin using a disciplined 4-step conversion +methodology with 5 invariants checked at each step. Supports framework-aware conversion +that handles annotation site targets, library idioms, and API preservation. + +## Workflow + +```dot +digraph j2k_workflow { + rankdir=TB; + "User specifies files" -> "Step 0: Scan & Detect"; + "Step 0: Scan & Detect" -> "Load framework guides"; + "Load framework guides" -> "Step 1: Convert"; + "Step 1: Convert" -> "Step 2: Write .kt"; + "Step 2: Write .kt" -> "Step 3: Git rename"; + "Step 3: Git rename" -> "Step 4: Verify"; + "Step 4: Verify" -> "Next file?" [label="pass"]; + "Step 4: Verify" -> "Fix issues" [label="fail"]; + "Fix issues" -> "Step 1: Convert"; + "Next file?" -> "Step 0: Scan & Detect" [label="batch: yes"]; + "Next file?" -> "Done" [label="no more files"]; +} +``` + +## Step 0: Scan & Detect Frameworks + +Before converting, scan the Java file's import statements to detect which frameworks +are in use. Load ONLY the matching framework reference files to keep context focused. + +### Framework Detection Table + +| Import prefix | Framework guide | +|---|---| +| `org.springframework.*` | [SPRING.md](references/frameworks/SPRING.md) | +| `lombok.*` | [LOMBOK.md](references/frameworks/LOMBOK.md) | +| `javax.persistence.*`, `jakarta.persistence.*`, `org.hibernate.*` | [HIBERNATE.md](references/frameworks/HIBERNATE.md) | +| `com.fasterxml.jackson.*` | [JACKSON.md](references/frameworks/JACKSON.md) | +| `io.micronaut.*` | [MICRONAUT.md](references/frameworks/MICRONAUT.md) | +| `io.quarkus.*`, `javax.enterprise.*`, `jakarta.enterprise.*` | [QUARKUS.md](references/frameworks/QUARKUS.md) | +| `dagger.*`, `dagger.hilt.*` | [DAGGER-HILT.md](references/frameworks/DAGGER-HILT.md) | +| `io.reactivex.*`, `rx.*` | [RXJAVA.md](references/frameworks/RXJAVA.md) | +| `org.junit.*`, `org.testng.*` | [JUNIT.md](references/frameworks/JUNIT.md) | +| `com.google.inject.*` | [GUICE.md](references/frameworks/GUICE.md) | +| `retrofit2.*`, `okhttp3.*` | [RETROFIT.md](references/frameworks/RETROFIT.md) | +| `org.mockito.*` | [MOCKITO.md](references/frameworks/MOCKITO.md) | + +If `javax.inject.*` is detected, check for Dagger/Hilt vs Guice by looking for other +imports from those frameworks. If ambiguous, load both guides. + +## Step 1: Convert + +Apply the conversion methodology from [CONVERSION-METHODOLOGY.md](references/CONVERSION-METHODOLOGY.md). + +This is a 4-step chain-of-thought process: +1. **Faithful 1:1 translation** — exact semantics preserved +2. **Nullability & mutability audit** — val/var, nullable types +3. **Collection type conversion** — Java mutable → Kotlin types +4. **Idiomatic transformations** — properties, string templates, lambdas + +Five invariants are checked after each step. If any invariant is violated, revert +to the previous step and redo. + +Apply any loaded framework-specific guidance during step 4 (idiomatic transformations). + +## Step 2: Write Output + +Write the converted Kotlin code to a `.kt` file with the same name as the original +Java file, in the same directory. + +## Step 3: Preserve Git History + +To preserve `git blame` history, use a two-phase approach: + +```bash +# Phase 1: Rename (creates rename tracking) +git mv src/main/java/com/example/Foo.java src/main/kotlin/com/example/Foo.kt +git commit -m "Rename Foo.java to Foo.kt" + +# Phase 2: Replace content (tracked as modification, not new file) +# Write the converted Kotlin content to Foo.kt +git commit -m "Convert Foo from Java to Kotlin" +``` + +If the project keeps Java and Kotlin in the same source root (e.g., `src/main/java/`), +rename in place: + +```bash +git mv src/main/java/com/example/Foo.java src/main/java/com/example/Foo.kt +``` + +If the project does not use Git, simply write the `.kt` file and delete the `.java` file. + +## Step 4: Verify + +After conversion, verify using [checklist.md](assets/checklist.md): +- Attempt to compile the converted file +- Run existing tests +- Check annotation site targets +- Confirm no behavioral changes + +## Batch Conversion + +When converting multiple files (a directory or package): + +1. **List all `.java` files** in the target scope +2. **Sort by dependency order** — convert leaf dependencies first (files that don't + import other files in the conversion set), then work up to files that depend on them +3. **Convert one file at a time** — apply the full workflow (steps 0-4) for each +4. **Track progress** — report which files are done, which remain +5. **Handle cross-references** — after converting a file, update imports in other Java + files if needed (e.g., if a class moved packages) + +For large batches, consider converting in packages (bottom-up from leaf packages). + +## Common Pitfalls + +See [KNOWN-ISSUES.md](references/KNOWN-ISSUES.md) for: +- Kotlin keyword conflicts (`when`, `in`, `is`, `object`) +- SAM conversion ambiguity +- Platform types from Java interop +- `@JvmStatic` / `@JvmField` / `@JvmOverloads` usage +- Checked exceptions and `@Throws` +- Wildcard generics → Kotlin variance diff --git a/skills/kotlin-tooling-java-to-kotlin/assets/checklist.md b/skills/kotlin-tooling-java-to-kotlin/assets/checklist.md new file mode 100644 index 0000000..6b629b3 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/assets/checklist.md @@ -0,0 +1,60 @@ +# Post-Conversion Verification Checklist + +Use this checklist after converting each Java file to Kotlin. + +## Compilation & Tests +- [ ] The `.kt` file compiles without errors +- [ ] All existing tests still pass +- [ ] No new compiler warnings introduced + +## Semantic Correctness +- [ ] No new side-effects or behavioural changes +- [ ] All public API signatures preserved (method names, parameter types, return types) +- [ ] Exception behaviour unchanged (same exceptions thrown in same conditions) + +## Annotations +- [ ] All annotations preserved from the original Java code +- [ ] Annotation site targets correct (`@field:`, `@get:`, `@set:`, `@param:`) +- [ ] No annotations accidentally dropped during conversion + +## Imports & Package +- [ ] Package declaration matches original +- [ ] All imports carried forward (except Java types that shadow Kotlin builtins) +- [ ] No new imports added unnecessarily + +## Documentation +- [ ] All Javadoc converted to KDoc format +- [ ] `{@code ...}` → backtick code in KDoc +- [ ] `{@link ...}` → `[...]` KDoc links +- [ ] `

` paragraph tags → blank lines +- [ ] `@param`, `@return`, `@throws` tags preserved +- [ ] Class-level and method-level documentation preserved + +## Nullability & Mutability +- [ ] Non-null types used only where provably non-null +- [ ] Nullable types (`?`) used for all Java types that could be null +- [ ] `val` used for all immutable variables/properties +- [ ] `var` used only for mutable variables/properties + +## Collections +- [ ] `MutableList`/`MutableSet`/`MutableMap` for Java's mutable collections +- [ ] `List`/`Set`/`Map` only where Java used immutable wrappers + +## Kotlin Idioms +- [ ] Getters/setters replaced with Kotlin properties where appropriate +- [ ] String concatenation replaced with string templates where clearer +- [ ] Elvis operator used where appropriate +- [ ] `when` expression used instead of `switch` +- [ ] Smart casts used after `is` checks (no explicit casts) + +## Framework-Specific (check applicable items) +- [ ] **Spring**: Classes that need proxying are `open`; `@Bean` methods are `open` +- [ ] **Lombok**: All Lombok annotations removed; replaced with Kotlin equivalents +- [ ] **Hibernate/JPA**: Entities are `open` (not data classes); no-arg constructor provided +- [ ] **Jackson**: `@field:` and `@get:` annotation site targets correct +- [ ] **RxJava**: Reactive types correctly mapped to Coroutines/Flow +- [ ] **Mockito**: `when` keyword escaped or replaced with MockK equivalent + +## Git History +- [ ] File renamed via `git mv` (not delete + create) +- [ ] Rename commit separate from content change commit diff --git a/skills/kotlin-tooling-java-to-kotlin/references/CONVERSION-METHODOLOGY.md b/skills/kotlin-tooling-java-to-kotlin/references/CONVERSION-METHODOLOGY.md new file mode 100644 index 0000000..908fb23 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/CONVERSION-METHODOLOGY.md @@ -0,0 +1,352 @@ +# Conversion Methodology + +You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is +to convert provided Java code into **idiomatic Kotlin**, preserving behaviour while +improving readability, safety and maintainability. + +## The 4-Step Precognition Process + +Before emitting any code, run through the provided Java input and perform these 4 steps +of thinking. After each step, output the code as you have it after that step's +transformation has been applied. + +### Step 1: Faithful 1:1 Translation + +Convert the Java code 1 to 1 into Kotlin, prioritising faithfulness to the original +Java semantics, to replicate the Java code's functionality and logic exactly. + +**Rules:** +- Java classes that are implicitly open MUST be converted as Kotlin classes that are + explicitly `open`, using the `open` keyword. +- To convert Java constructors that inject into fields, use the Kotlin primary + constructor. Any further logic within the Java constructor can be replicated with the + Kotlin secondary constructor. + +### Step 2: Nullability & Mutability + +Check that mutability and nullability are correctly expressed in your Kotlin conversion. +Only express types as non-null where you are sure that it can never be null, inferred +from the original Java. Use `val` instead of `var` where you see variables that are +never modified. + +**Rules:** +- If you see a logical assertion that a value is not null (e.g., `Objects.requireNonNull`), + this shows that the author has considered that the value can never be null. Use a + non-null type in this case, and remove the logical assertion. +- In all other cases, preserve the fact that types can be null in Java by using the + Kotlin nullable version of that type. + +### Step 3: Collection Type Conversion + +Convert datatypes like collections from their Java variants to the Kotlin variants. + +**Rules:** +- For Java collections like `List` that are mutable by default, always use the Kotlin + `MutableList`, unless you see explicitly that the Java code uses an immutable wrapper + (e.g., `Collections.unmodifiableList()`) — in this case, use the Kotlin `List` (and + so on for other collections like `Set`, `Map` etc.) + +### Step 4: Idiomatic Transformations + +Introduce syntactic transformations to make the output truly idiomatic. + +**Rules:** +- Where getters and setters are defined as methods in Java, use the Kotlin syntax to + replace these methods with a more idiomatic version. +- Lambdas should be used where they can simplify code complexity while replicating the + exact behaviour of the previous code. + +## The 5 Invariants + +In each stage of your chain of thought, the following invariants must hold. + +**Invariant 1:** No new side-effects or behaviour. + +**Invariant 2:** Preserve all annotations and targets exactly. +- Annotations must target the backing field in Kotlin where they targeted the field in + Java. Use annotation site targets: `@field:`, `@get:`, `@set:`, `@param:`. + +**Invariant 3:** Preserve the package declaration and all imports. +- Carry forwards every single import, adding no new imports. Only remove imports where + they would shadow Kotlin names (e.g., `java.util.List` shadows Kotlin's `List`). + +**Invariant 4:** Preserve all Javadoc comments. +- In step 1, convert any Javadoc comments to KDoc comments. +- In all other steps, carry forwards the KDoc comments, preserving information where + structure changes. + +**Invariant 5:** Ensure the output result is in Kotlin. +- The emitted code must be syntactically valid Kotlin. + +**After each step**, go through each of these invariants, listing the ones that no +longer hold. If any exist, revert to the previous step and recalculate from there. + +--- + +## Example 1: Utility Class with Nullability + +### Java Input + +```java +package com.acme.util; + +import java.time.LocalDate; +import java.util.Objects; + +/** +* Utility for printing a greeting that includes the current date. +*/ +public class DateGreeter { + /** + * Prints a greeting for the given name and the current date. + * + * @param name an optional name; if {@code null}, the greeting uses {@code "Guest"} + */ + public static void greet(String name) { + String who = (name != null) ? name : "Guest"; + System.out.println("Hello, " + who + " - today is " + LocalDate.now()); + } +} +``` + +### Conversion Walkthrough + +**Step 1** — Faithful translation. The DateGreeter class is implicitly open, the +`greet` method is static → companion object. Javadoc converted to KDoc. + +```kotlin +package com.acme.util + +import java.time.LocalDate +import java.util.Objects + +/** +* Utility for printing a greeting that includes the current date. +*/ +open class DateGreeter { + companion object { + /** + * Prints a greeting for the given [name] and the current date. + * + * @param name an optional name; if `null`, the greeting uses `"Guest"` + */ + fun greet(name: String?) { + var who = if (name != null) name else "Guest" + println("Hello, " + who + " - today is " + LocalDate.now()) + } + } +} +``` + +Invariants check: All OK. + +**Step 2** — The `String?` is correct since the author checks for null. The `who` +variable is only read, so change `var` → `val`. + +```kotlin +// Same as above but with: +val who = if (name != null) name else "Guest" +``` + +Invariants check: All OK. + +**Step 3** — No collections in this code. No changes. + +Invariants check: All OK. + +**Step 4** — The `greet` function is not tied to any state of DateGreeter, so move it +to a top-level function. Use string templates and Elvis operator. + +```kotlin +package com.acme.util + +import java.time.LocalDate +import java.util.Objects + +/** +* Prints a greeting for the given [name] and the current date. +* +* @param name an optional name; if `null`, the greeting uses `"Guest"` +*/ +fun greet(name: String?) { + println("Hello, ${name ?: "Guest"} - today is ${LocalDate.now()}") +} +``` + +Invariants check: All OK. + +--- + +## Example 2: Domain Model with Annotations + +### Java Input + +```java +package com.acme.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import java.util.Objects; + +/** +* Domain model for a user with a required identifier and an optional nickname. +*

+* The {@code id} is serialized as {@code "id"} and is required. +* The {@code nickname} may be absent. +*/ +public class User { + /** + * Stable, non-null identifier serialized as {@code "id"}. + */ + @JsonProperty("id") + private final String id; + + /** + * Optional nickname for display purposes. + */ + @Nullable + private String nickname; + + /** + * Creates a user with the given non-null identifier. + * + * @param id required identifier for the user + * @throws NullPointerException if {@code id} is null + */ + public User(String id) { + this.id = Objects.requireNonNull(id, "id"); + } + + /** + * Returns the identifier serialized as {@code "id"}. + * + * @return the user id + */ + @JsonProperty("id") + public String getId() { + return id; + } + + /** + * Returns the optional nickname. + * + * @return the nickname or {@code null} if absent + */ + @Nullable + public String getNickname() { + return nickname; + } + + /** + * Sets the optional nickname. + * + * @param nickname the nickname or {@code null} to clear it + */ + public void setNickname(@Nullable String nickname) { + this.nickname = nickname; + } +} +``` + +### Conversion Walkthrough + +**Step 1** — Faithful translation. Class is implicitly open → `open class`. +`@JsonProperty("id")` on the field → `@field:JsonProperty("id")`. +`@JsonProperty("id")` on the getter → `@get:JsonProperty("id")` when converted to +property later. Keep explicit getters/setters at this step for faithfulness. + +```kotlin +package com.acme.model + +import com.fasterxml.jackson.annotation.JsonProperty +import javax.annotation.Nullable +import java.util.Objects + +/** +* Domain model for a user with a required identifier and an optional nickname. +* +* The `id` is serialized as `"id"` and is required. +* The `nickname` may be absent. +*/ +open class User { + + /** + * Stable, non-null identifier serialized as `"id"`. + */ + @field:JsonProperty("id") + private val id: String + + /** + * Optional nickname for display purposes. + */ + @field:Nullable + private var nickname: String? = null + + /** + * Creates a user with the given non-null identifier. + * + * @param id required identifier + * @throws NullPointerException if `id` is `null` + */ + constructor(id: String) { + this.id = Objects.requireNonNull(id, "id") + } + + @get:JsonProperty("id") + fun getId(): String { return id } + + @Nullable + fun getNickname(): String? { return nickname } + + fun setNickname(@Nullable nickname: String?) { this.nickname = nickname } +} +``` + +Invariants check: All OK. + +**Step 2** — `id` is non-null by design (`Objects.requireNonNull` enforces it). +`nickname` is nullable (`@Nullable`). No val/var changes needed beyond what's already +done. Code unchanged. + +Invariants check: All OK. + +**Step 3** — No collections. No changes. + +Invariants check: All OK. + +**Step 4** — Idiomatic Kotlin: +1. Primary constructor with `id` as a `val` property. Apply both `@field:JsonProperty` + and `@get:JsonProperty` to match both Java annotation targets. +2. Convert `nickname` getter/setter → Kotlin property with `@field:Nullable` and + `@get:Nullable`. +3. Drop `Objects.requireNonNull` — Kotlin's type system enforces non-null. +4. Preserve all imports even if now unused (invariant 3). + +```kotlin +package com.acme.model + +import com.fasterxml.jackson.annotation.JsonProperty +import javax.annotation.Nullable +import java.util.Objects + +/** +* Domain model for a user with a required identifier and an optional nickname. +* +* The `id` is serialized as `"id"` and is required. +* The `nickname` may be absent. +* +* @property id stable, non-null identifier serialized as `"id"` +* @property nickname optional nickname for display purposes; may be `null` if not set +*/ +open class User( + @field:JsonProperty("id") + @get:JsonProperty("id") + val id: String +) { + @field:Nullable + @get:Nullable + var nickname: String? = null +} +``` + +Invariants check: All OK. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/KNOWN-ISSUES.md b/skills/kotlin-tooling-java-to-kotlin/references/KNOWN-ISSUES.md new file mode 100644 index 0000000..9de639d --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/KNOWN-ISSUES.md @@ -0,0 +1,358 @@ +# Known Issues and Common Pitfalls + +A reference of common issues encountered during Java-to-Kotlin conversion, with solutions. + +### Kotlin Keyword Conflicts + +Java identifiers that are reserved keywords in Kotlin will cause compilation errors after conversion. + +**Affected keywords:** `when`, `in`, `is`, `object`, `fun`, `val`, `var`, `typealias`, `as` + +**Solution:** Backtick-escape them in Kotlin: + +```java +// Java +public void when(String event) { ... } +public boolean in(List items) { ... } +``` + +```kotlin +// Kotlin — backtick-escaped +fun `when`(event: String) { ... } +fun `in`(items: List): Boolean { ... } +``` + +When the API is internal (not exposed to other modules), prefer renaming the identifier to a non-keyword alternative instead of using backticks. For example, rename `when` to `onEvent` or `in` to `contains`. + +### SAM Conversion Ambiguity + +When a Java method has overloads that each accept a different SAM (Single Abstract Method) interface, Kotlin's trailing lambda syntax becomes ambiguous. The compiler cannot determine which SAM interface the lambda should implement. + +```java +// Java — overloaded method accepting different SAM types +public class TaskExecutor { + void submit(Runnable task) { ... } + void submit(Callable task) { ... } +} +``` + +```kotlin +// Kotlin — WRONG: ambiguous, won't compile +executor.submit { doWork() } + +// Kotlin — CORRECT: explicit SAM constructor +executor.submit(Runnable { doWork() }) +executor.submit(Callable { computeResult() }) +``` + +Use explicit SAM constructor calls whenever there are overloaded methods accepting different functional interfaces. + +### Platform Types + +Java types without nullability annotations (`@Nullable`, `@NotNull`, `@NonNull`) become "platform types" (`T!`) in Kotlin. Platform types bypass Kotlin's null-safety system — they are neither nullable nor non-null, and null checks are deferred to runtime. + +```java +// Java — no nullability annotations +public String getName() { return name; } +public List getItems() { return items; } +``` + +```kotlin +// Kotlin — BAD: platform types left in converted code +val name = obj.name // inferred as String! — unsafe +val items = obj.items // inferred as List! — unsafe + +// Kotlin — GOOD: explicit nullability based on code analysis +val name: String = obj.name // if provably non-null +val name: String? = obj.name // if could be null +val items: List = obj.items // if neither list nor elements are null +``` + +Always add explicit type declarations to eliminate platform types. Analyze the Java source code, documentation, and call sites to determine the correct nullability. + +### @JvmStatic / @JvmField / @JvmOverloads + +When converted Kotlin code is still called from Java, use JVM interop annotations to maintain a clean Java API: + +**`@JvmStatic`** — Makes companion object functions accessible as static methods from Java: + +```kotlin +class Config { + companion object { + @JvmStatic + fun getInstance(): Config = ... + } +} +``` + +```java +// Java callers can use: Config.getInstance() +// Without @JvmStatic they would need: Config.Companion.getInstance() +``` + +**`@JvmField`** — Exposes a property as a direct field rather than through getter/setter: + +```kotlin +class Constants { + companion object { + @JvmField + val DEFAULT_TIMEOUT = 30_000L + } +} +``` + +```java +// Java callers can use: Constants.DEFAULT_TIMEOUT +// Without @JvmField they would need: Constants.Companion.getDEFAULT_TIMEOUT() +``` + +**`@JvmOverloads`** — Generates Java overloads for functions with default parameters: + +```kotlin +@JvmOverloads +fun connect(host: String, port: Int = 443, secure: Boolean = true) { ... } +``` + +```java +// Java sees three overloads: +// connect(String host) +// connect(String host, int port) +// connect(String host, int port, boolean secure) +``` + +### Checked Exceptions + +Kotlin does not have checked exceptions. When Kotlin code is called from Java, the Java compiler will not know about thrown exceptions unless annotated with `@Throws`: + +```kotlin +// Without @Throws, Java callers cannot catch IOException in a catch block +// (the Java compiler will say "exception is never thrown in the corresponding try block") + +@Throws(IOException::class) +fun readFile(path: String): String { + return File(path).readText() +} +``` + +Add `@Throws` to every Kotlin function that throws checked exceptions and is called from Java code. + +### Wildcard Generics + +Java wildcard types map to Kotlin's variance annotations: + +| Java | Kotlin | Description | +|------|--------|-------------| +| `? extends T` | `out T` | Covariance (producer) | +| `? super T` | `in T` | Contravariance (consumer) | +| Raw type `List` | `List` | Add explicit type parameter | + +```java +// Java +public void process(List numbers) { ... } +public void addAll(List target) { ... } +public void legacy(List items) { ... } // raw type +``` + +```kotlin +// Kotlin +fun process(numbers: List) { ... } +fun addAll(target: MutableList) { ... } +fun legacy(items: List) { ... } // explicit type parameter +``` + +For raw types, analyze the code to determine the most specific type parameter rather than defaulting to `Any?`. + +### Static Members + +Java's `static` keyword has no direct equivalent in Kotlin. Use the following mappings: + +**Static methods** — Use companion object functions, or top-level functions if they don't need class state: + +```java +// Java +public class StringUtils { + public static String capitalize(String s) { ... } +} +``` + +```kotlin +// Kotlin — top-level function (preferred when no class state needed) +fun capitalize(s: String): String { ... } + +// Kotlin — companion object (when logically tied to the class) +class StringUtils { + companion object { + fun capitalize(s: String): String { ... } + } +} +``` + +**Static constants** — Use `const val` for compile-time constants (primitives and String), `val` for object constants: + +```kotlin +class HttpStatus { + companion object { + const val OK = 200 // primitive — const val + const val NOT_FOUND_MESSAGE = "Not Found" // String — const val + val DEFAULT_HEADERS = mapOf("Accept" to "application/json") // object — val + } +} +``` + +**Static initializers** — Use companion object `init {}` block or top-level code: + +```kotlin +class Registry { + companion object { + private val handlers = mutableMapOf() + init { + handlers["default"] = DefaultHandler() + } + } +} +``` + +### Synchronized Blocks + +Java's `synchronized` constructs map to Kotlin as follows: + +**Synchronized blocks** — Use Kotlin's `synchronized()` function: + +```java +// Java +synchronized (lock) { + sharedState.update(); +} +``` + +```kotlin +// Kotlin +synchronized(lock) { + sharedState.update() +} +``` + +**Synchronized methods** — Use the `@Synchronized` annotation: + +```java +// Java +public synchronized void update() { ... } +``` + +```kotlin +// Kotlin +@Synchronized +fun update() { ... } +``` + +### Anonymous Inner Classes + +**Single Abstract Method (SAM) interfaces** — Convert to lambda syntax: + +```java +// Java +executor.submit(new Runnable() { + @Override + public void run() { + doWork(); + } +}); +``` + +```kotlin +// Kotlin +executor.submit(Runnable { doWork() }) +``` + +**Multiple methods or abstract classes** — Use `object` expression: + +```java +// Java +view.addListener(new ViewListener() { + @Override + public void onOpen() { ... } + @Override + public void onClose() { ... } +}); +``` + +```kotlin +// Kotlin +view.addListener(object : ViewListener { + override fun onOpen() { ... } + override fun onClose() { ... } +}) +``` + +### Array Handling + +Java arrays map to Kotlin types as follows: + +| Java | Kotlin | Notes | +|------|--------|-------| +| `String[]` | `Array` | Reference type arrays | +| `int[]` | `IntArray` | Primitive array (not `Array`) | +| `long[]` | `LongArray` | Primitive array | +| `double[]` | `DoubleArray` | Primitive array | +| `boolean[]` | `BooleanArray` | Primitive array | +| `Object[]` | `Array` | | +| `new int[10]` | `IntArray(10)` | Array creation | +| `new String[10]` | `arrayOfNulls(10)` | Nullable element array | +| `String... args` | `vararg args: String` | Varargs parameter | + +Using `Array` instead of `IntArray` causes boxing overhead — always use the specialized primitive array types. + +### Ternary Operator + +Kotlin has no ternary operator. Use `if`/`else` as an expression: + +```java +// Java +String label = (count > 0) ? "Items: " + count : "Empty"; +``` + +```kotlin +// Kotlin +val label = if (count > 0) "Items: $count" else "Empty" +``` + +### instanceof + +Java's `instanceof` maps to Kotlin's `is` keyword. Kotlin supports smart casting, so an explicit cast after an `is` check is unnecessary: + +```java +// Java +if (shape instanceof Circle) { + Circle circle = (Circle) shape; + double area = circle.getArea(); +} +``` + +```kotlin +// Kotlin — smart cast, no explicit cast needed +if (shape is Circle) { + val area = shape.area // shape is automatically cast to Circle +} +``` + +### try-with-resources + +Java's try-with-resources maps to Kotlin's `.use {}` extension function: + +```java +// Java +try (BufferedReader reader = new BufferedReader(new FileReader(path))) { + String line = reader.readLine(); + process(line); +} +``` + +```kotlin +// Kotlin +BufferedReader(FileReader(path)).use { reader -> + val line = reader.readLine() + process(line) +} +``` + +The `.use {}` function works on any `Closeable` or `AutoCloseable` instance and guarantees the resource is closed even if an exception is thrown. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/DAGGER-HILT.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/DAGGER-HILT.md new file mode 100644 index 0000000..fe3636b --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/DAGGER-HILT.md @@ -0,0 +1,160 @@ +# Dagger / Hilt Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `dagger.*` or +`dagger.hilt.*`. This covers Dagger 2, Hilt for Android, and Hilt Jetpack integrations. + +## Key Rules + +### 1. @Inject constructor syntax + +Kotlin places `@Inject` before the `constructor` keyword in the primary constructor: + +```kotlin +class Foo @Inject constructor(private val bar: Bar) +``` + +### 2. @Module classes with @Provides methods + +Keep `@Provides` methods `open`, or use `object` for modules that contain only +`@JvmStatic` provides methods (companion object pattern): + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build() +} +``` + +### 3. @Binds abstract methods + +`@Binds` methods work in abstract classes exactly as in Java. Convert the abstract +class directly — no special Kotlin considerations. + +### 4. Hilt Android annotations + +`@HiltAndroidApp`, `@AndroidEntryPoint`, `@HiltViewModel` — preserve these exactly +on Application, Activity, Fragment, and ViewModel classes. + +### 5. Scoping annotations + +`@Singleton`, `@ActivityScoped`, `@ViewModelScoped`, `@FragmentScoped` — preserve +exactly. No annotation site target is needed. + +### 6. @AssistedInject / @AssistedFactory + +`@AssistedInject` replaces `@Inject` on the constructor. `@Assisted` parameters +appear alongside regular injected parameters in the primary constructor: + +```kotlin +class PlayerViewModel @AssistedInject constructor( + @Assisted private val playerId: String, + private val repository: PlayerRepository +) : ViewModel() +``` + +### 7. @Component / @Subcomponent interfaces + +Convert directly to Kotlin interfaces. Dagger's annotation processing works +identically with Kotlin interfaces via kapt or KSP. + +--- + +## Examples + +### Example 1: Hilt ViewModel with @Inject Constructor and a @Module + +**Java:** + +```java +package com.acme.feature; + +import androidx.lifecycle.ViewModel; +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.android.lifecycle.HiltViewModel; +import dagger.hilt.components.SingletonComponent; +import javax.inject.Inject; +import javax.inject.Singleton; + +@HiltViewModel +public class UserProfileViewModel extends ViewModel { + + private final UserRepository userRepository; + private final AnalyticsTracker analyticsTracker; + + @Inject + public UserProfileViewModel(UserRepository userRepository, AnalyticsTracker analyticsTracker) { + this.userRepository = userRepository; + this.analyticsTracker = analyticsTracker; + } + + public LiveData getUser(String userId) { + analyticsTracker.trackProfileView(userId); + return userRepository.getUser(userId); + } +} + +@Module +@InstallIn(SingletonComponent.class) +public class AnalyticsModule { + + @Provides + @Singleton + public AnalyticsTracker provideAnalyticsTracker(Application app) { + return new AnalyticsTracker(app); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.feature + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject +import javax.inject.Singleton + +@HiltViewModel +class UserProfileViewModel @Inject constructor( + private val userRepository: UserRepository, + private val analyticsTracker: AnalyticsTracker +) : ViewModel() { + + fun getUser(userId: String): LiveData { + analyticsTracker.trackProfileView(userId) + return userRepository.getUser(userId) + } +} + +@Module +@InstallIn(SingletonComponent::class) +object AnalyticsModule { + + @Provides + @Singleton + fun provideAnalyticsTracker(app: Application): AnalyticsTracker { + return AnalyticsTracker(app) + } +} +``` + +Key changes: +- `@Inject` moves before the `constructor` keyword in the primary constructor. +- Constructor parameters become `private val` in the primary constructor. +- The module class becomes an `object` since it contains only static-like provides methods. +- `SingletonComponent.class` becomes `SingletonComponent::class` (Kotlin class reference). +- Java getter method `getUser` becomes a regular function `getUser` (no `get` prefix + convention change needed here since it takes a parameter). diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/GUICE.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/GUICE.md new file mode 100644 index 0000000..11a795d --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/GUICE.md @@ -0,0 +1,165 @@ +# Guice Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `com.google.inject.*`. +This covers Google Guice core, Guice multibindings, and Guice servlet. + +## Key Rules + +### 1. @Inject constructor syntax + +Kotlin places `@Inject` before the `constructor` keyword in the primary constructor: + +```kotlin +class Foo @Inject constructor(private val bar: Bar) +``` + +### 2. @Provides methods in Modules + +Keep `@Provides` methods as regular functions. Guice modules extend `AbstractModule`, +so override `configure()` as usual. + +### 3. Module.configure() override + +Override `configure()` in Kotlin. Use Guice's binding DSL with Kotlin class references: + +```kotlin +bind(Foo::class.java).to(FooImpl::class.java) +``` + +### 4. @Named qualifier — annotation site targets + +In Kotlin, `@Named` on constructor parameters needs a site target to reach the +parameter (not the field or property). Use `@param:Named` for constructor injection: + +```kotlin +class Foo @Inject constructor( + @param:Named("primary") private val dataSource: DataSource +) +``` + +When used on function parameters (e.g., in `@Provides` methods), no site target +is needed. + +### 5. @Singleton scope + +Preserve `@Singleton` exactly. It can be placed on the class declaration or in +module bindings via `.in(Singleton::class.java)`. + +### 6. Provider + +`Provider` can stay as-is for lazy or scoped injection. Where the only purpose +is deferred initialization, Kotlin's `lazy` delegation can be used as an alternative +outside of Guice-managed contexts. + +--- + +## Examples + +### Example 1: Guice Module with Bindings and an Injected Class + +**Java:** + +```java +package com.acme.config; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +public class AppModule extends AbstractModule { + + @Override + protected void configure() { + bind(CacheService.class).to(RedisCacheService.class); + bind(NotificationService.class).to(EmailNotificationService.class).in(Singleton.class); + } + + @Provides + @Singleton + public HttpClient provideHttpClient(@Named("baseUrl") String baseUrl) { + return new HttpClient(baseUrl); + } +} +``` + +```java +package com.acme.service; + +import com.google.inject.Inject; +import com.google.inject.name.Named; + +public class OrderService { + + private final CacheService cacheService; + private final HttpClient httpClient; + private final String region; + + @Inject + public OrderService(CacheService cacheService, HttpClient httpClient, @Named("region") String region) { + this.cacheService = cacheService; + this.httpClient = httpClient; + this.region = region; + } + + public Order findById(Long id) { + return cacheService.getOrFetch(id, () -> httpClient.get("/orders/" + id, Order.class)); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.config + +import com.google.inject.AbstractModule +import com.google.inject.Provides +import com.google.inject.Singleton +import com.google.inject.name.Named + +class AppModule : AbstractModule() { + + override fun configure() { + bind(CacheService::class.java).to(RedisCacheService::class.java) + bind(NotificationService::class.java).to(EmailNotificationService::class.java).`in`(Singleton::class.java) + } + + @Provides + @Singleton + fun provideHttpClient(@Named("baseUrl") baseUrl: String): HttpClient { + return HttpClient(baseUrl) + } +} +``` + +```kotlin +package com.acme.service + +import com.google.inject.Inject +import com.google.inject.name.Named + +class OrderService @Inject constructor( + private val cacheService: CacheService, + private val httpClient: HttpClient, + @param:Named("region") private val region: String +) { + + fun findById(id: Long): Order? { + return cacheService.getOrFetch(id) { httpClient.get("/orders/$id", Order::class.java) } + } +} +``` + +Key changes: +- `@Inject` moves before the `constructor` keyword in the primary constructor. +- Constructor parameters become `private val` in the primary constructor. +- `@Named("region")` uses `@param:Named` site target so the annotation reaches the + constructor parameter rather than the Kotlin property. +- `.in(Singleton.class)` becomes `` .`in`(Singleton::class.java) `` — `in` is a + reserved keyword in Kotlin and must be escaped with backticks. +- The lambda in `getOrFetch` uses Kotlin's trailing lambda syntax instead of an + anonymous inner class. +- String concatenation `"/orders/" + id` becomes a string template `"/orders/$id"`. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/HIBERNATE.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/HIBERNATE.md new file mode 100644 index 0000000..014228d --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/HIBERNATE.md @@ -0,0 +1,227 @@ +# Hibernate / JPA Conversion Guide + +## When This Applies + +Detected when imports match any of: +- `javax.persistence.*` +- `jakarta.persistence.*` +- `org.hibernate.*` + +## Critical Rules + +1. **Do NOT use data classes for JPA entities.** Data classes generate `equals`/`hashCode` + based on all properties, which breaks Hibernate's identity semantics and proxy creation. + +2. **Keep entity classes `open`.** Hibernate creates proxies via subclassing. Kotlin classes + are `final` by default, so you must use `open` explicitly (or use the `allopen` compiler + plugin with JPA annotation support). + +3. **Provide a no-argument constructor** if Hibernate requires one for proxy creation. Use a + secondary constructor or default values for all primary constructor parameters. + +4. **Annotation site targets matter:** + - `@Id`, `@Column`, `@GeneratedValue` on fields → use `@field:Id`, `@field:Column`, etc. + in Kotlin, OR place annotations on constructor parameters with `@field:` site target. + - `@ManyToOne`, `@OneToMany`, `@JoinColumn` → same `@field:` targeting. + +5. **Lazy loading considerations:** `@ManyToOne(fetch = FetchType.LAZY)` requires the entity + class to be open for proxy creation. `@OneToMany` with lazy collections work with Kotlin's + `MutableList`. + +6. **`@Embeddable` classes**: Can be data classes (they don't need proxies). + +7. **`@MappedSuperclass`**: Must be `open abstract class` in Kotlin. + +## Examples + +### Example 1: JPA Entity with @Id, @Column, and Relationships + +**Java:** +```java +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "username", nullable = false, unique = true) + private String username; + + @Column(name = "email") + private String email; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "department_id") + private Department department; + + protected User() {} + + public User(String username, String email, Department department) { + this.username = username; + this.email = email; + this.department = department; + } + + public Long getId() { return id; } + public String getUsername() { return username; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public Department getDepartment() { return department; } + public void setDepartment(Department department) { this.department = department; } +} +``` + +**Kotlin:** +```kotlin +@Entity +@Table(name = "users") +open class User( + + @field:Column(name = "username", nullable = false, unique = true) + open val username: String, + + @field:Column(name = "email") + open var email: String? = null, + + @field:ManyToOne(fetch = FetchType.LAZY) + @field:JoinColumn(name = "department_id") + open var department: Department? = null + +) { + @field:Id + @field:GeneratedValue(strategy = GenerationType.IDENTITY) + open var id: Long? = null + protected set + + protected constructor() : this(username = "") +} +``` + +### Example 2: @Embeddable Value Object + +**Java:** +```java +@Embeddable +public class Address { + + @Column(name = "street") + private String street; + + @Column(name = "city") + private String city; + + @Column(name = "zip_code") + private String zipCode; + + protected Address() {} + + public Address(String street, String city, String zipCode) { + this.street = street; + this.city = city; + this.zipCode = zipCode; + } + + public String getStreet() { return street; } + public String getCity() { return city; } + public String getZipCode() { return zipCode; } +} +``` + +**Kotlin:** +```kotlin +@Embeddable +data class Address( + + @field:Column(name = "street") + val street: String = "", + + @field:Column(name = "city") + val city: String = "", + + @field:Column(name = "zip_code") + val zipCode: String = "" +) +``` + +`@Embeddable` classes can safely be data classes because Hibernate does not proxy them. +Default values satisfy the no-arg constructor requirement. + +### Example 3: Entity with @ManyToOne and @OneToMany + +**Java:** +```java +@Entity +@Table(name = "departments") +public class Department { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name", nullable = false) + private String name; + + @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true) + private List users = new ArrayList<>(); + + protected Department() {} + + public Department(String name) { + this.name = name; + } + + public Long getId() { return id; } + public String getName() { return name; } + public List getUsers() { return users; } + + public void addUser(User user) { + users.add(user); + user.setDepartment(this); + } + + public void removeUser(User user) { + users.remove(user); + user.setDepartment(null); + } +} +``` + +**Kotlin:** +```kotlin +@Entity +@Table(name = "departments") +open class Department( + + @field:Column(name = "name", nullable = false) + open val name: String = "" + +) { + @field:Id + @field:GeneratedValue(strategy = GenerationType.IDENTITY) + open var id: Long? = null + protected set + + @field:OneToMany(mappedBy = "department", cascade = [CascadeType.ALL], orphanRemoval = true) + open val users: MutableList = mutableListOf() + + protected constructor() : this(name = "") + + fun addUser(user: User) { + users.add(user) + user.department = this + } + + fun removeUser(user: User) { + users.remove(user) + user.department = null + } +} +``` + +Key points in this example: +- `cascade` array syntax uses Kotlin's `[CascadeType.ALL]` instead of Java's `{CascadeType.ALL}`. +- The collection is typed as `MutableList` to allow Hibernate to manage the relationship. +- The class and its properties are `open` so Hibernate can create proxies. +- The no-arg constructor delegates to the primary constructor with default values. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JACKSON.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JACKSON.md new file mode 100644 index 0000000..aa7b8f1 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JACKSON.md @@ -0,0 +1,248 @@ +# Jackson Conversion Guide + +## When This Applies + +Detected when imports match `com.fasterxml.jackson.*`. + +## Key Rules + +1. **Annotation site targets**: + - `@JsonProperty` on a Java field → `@field:JsonProperty` in Kotlin. + - `@JsonProperty` on a Java getter → `@get:JsonProperty` in Kotlin. + - When converting to Kotlin properties, apply BOTH `@field:` and `@get:` targets to + match Java's dual annotation on field + getter. + +2. **@JsonCreator**: Java's `@JsonCreator` static factory or constructor → Kotlin primary + constructor. The `@JsonCreator` annotation is often unnecessary on Kotlin's primary + constructor if using the Jackson Kotlin module, but preserve it for safety. + +3. **@JsonIgnore**: Preserve exactly. Use `@get:JsonIgnore` or `@field:JsonIgnore` + depending on original target. + +4. **@JsonDeserialize / @JsonSerialize**: Preserve exactly with correct site targets. + +5. **@JsonInclude**: Preserve on class or property level. + +6. **@JsonFormat**: Preserve with `@field:JsonFormat` site target. + +7. **Jackson Kotlin Module**: Note that projects using Jackson with Kotlin should add + `jackson-module-kotlin` for proper Kotlin support (data classes, default values, + nullable types). This is NOT something to add during conversion — just note it if + missing. + +8. **Builder pattern with @JsonPOJOBuilder**: Replace with primary constructor + + `@JsonCreator` if converting to data class. Otherwise preserve. + +--- + +## Example 1: DTO with Various Jackson Annotations + +### Java Input + +```java +package com.acme.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat; + +/** + * Data transfer object for an order summary. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class OrderSummaryDto { + + @JsonProperty("order_id") + private final String orderId; + + @JsonProperty("total_amount") + private final double totalAmount; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private final String createdDate; + + @JsonIgnore + private String internalNote; + + public OrderSummaryDto(String orderId, double totalAmount, String createdDate) { + this.orderId = orderId; + this.totalAmount = totalAmount; + this.createdDate = createdDate; + } + + @JsonProperty("order_id") + public String getOrderId() { + return orderId; + } + + @JsonProperty("total_amount") + public double getTotalAmount() { + return totalAmount; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + public String getCreatedDate() { + return createdDate; + } + + @JsonIgnore + public String getInternalNote() { + return internalNote; + } + + public void setInternalNote(String internalNote) { + this.internalNote = internalNote; + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.dto + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonFormat + +/** + * Data transfer object for an order summary. + * + * @property orderId unique identifier for the order, serialized as `"order_id"` + * @property totalAmount total monetary amount, serialized as `"total_amount"` + * @property createdDate date the order was created, formatted as `yyyy-MM-dd` + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +open class OrderSummaryDto( + @field:JsonProperty("order_id") + @get:JsonProperty("order_id") + val orderId: String?, + + @field:JsonProperty("total_amount") + @get:JsonProperty("total_amount") + val totalAmount: Double, + + @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + @get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + val createdDate: String? +) { + @field:JsonIgnore + @get:JsonIgnore + var internalNote: String? = null +} +``` + +**Key points:** +- `@JsonInclude` stays at class level — no site target needed. +- `@JsonProperty` gets both `@field:` and `@get:` to match the Java field + getter + annotations. +- `@JsonFormat` also gets both `@field:` and `@get:` since Java had it on both. +- `@JsonIgnore` gets both `@field:` and `@get:` to suppress serialization fully. + +--- + +## Example 2: Class with @JsonCreator Factory Method + +### Java Input + +```java +package com.acme.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Immutable configuration entry deserialized from JSON. + */ +public class ConfigEntry { + + private final String key; + private final String value; + private final boolean enabled; + + @JsonCreator + public static ConfigEntry create( + @JsonProperty("key") String key, + @JsonProperty("value") String value, + @JsonProperty("enabled") boolean enabled) { + return new ConfigEntry(key, value, enabled); + } + + private ConfigEntry(String key, String value, boolean enabled) { + this.key = key; + this.value = value; + this.enabled = enabled; + } + + @JsonProperty("key") + public String getKey() { + return key; + } + + @JsonProperty("value") + public String getValue() { + return value; + } + + @JsonProperty("enabled") + public boolean isEnabled() { + return enabled; + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.model + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Immutable configuration entry deserialized from JSON. + * + * @property key the configuration key + * @property value the configuration value + * @property enabled whether this entry is active + */ +data class ConfigEntry @JsonCreator constructor( + @field:JsonProperty("key") + @get:JsonProperty("key") + val key: String?, + + @field:JsonProperty("value") + @get:JsonProperty("value") + val value: String?, + + @field:JsonProperty("enabled") + @get:JsonProperty("enabled") + val enabled: Boolean +) { + companion object { + /** + * Factory method preserved for documentation; the primary constructor + * with [JsonCreator] handles deserialization directly. + */ + @JsonCreator + @JvmStatic + fun create( + @JsonProperty("key") key: String?, + @JsonProperty("value") value: String?, + @JsonProperty("enabled") enabled: Boolean + ): ConfigEntry = ConfigEntry(key, value, enabled) + } +} +``` + +**Key points:** +- The Java `@JsonCreator` static factory is converted to a Kotlin primary constructor + with `@JsonCreator`. The companion object factory is preserved for backward + compatibility but the primary constructor handles deserialization. +- The class becomes a `data class` since it is immutable and value-oriented. +- `@JsonCreator` is kept on the primary constructor for safety, ensuring Jackson can + deserialize even without the Jackson Kotlin module. +- String parameters remain nullable (`String?`) since Java strings are nullable by + default and there is no `@NonNull` or `Objects.requireNonNull` evidence. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JUNIT.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JUNIT.md new file mode 100644 index 0000000..fb4a286 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/JUNIT.md @@ -0,0 +1,193 @@ +# JUnit / TestNG Conversion Guide + +## When This Applies + +Detected when imports match `org.junit.*` or `org.testng.*`. + +## Key Rules + +### 1. JUnit 4 to Kotlin (with JUnit 5) + +| JUnit 4 | Kotlin (JUnit 5 / kotlin.test) | +|---|---| +| `@Test` | `@Test` (from `kotlin.test` or `org.junit.jupiter.api`) | +| `@Before` | `@BeforeEach` (JUnit 5) or `@BeforeTest` (kotlin.test) | +| `@After` | `@AfterEach` (JUnit 5) or `@AfterTest` (kotlin.test) | +| `@BeforeClass` | `@BeforeAll` in companion object with `@JvmStatic` | +| `@AfterClass` | `@AfterAll` in companion object with `@JvmStatic` | +| `@RunWith` | `@ExtendWith` (JUnit 5) | +| `@Ignore` | `@Disabled` (JUnit 5) | +| `@Rule` / `@ClassRule` | `@ExtendWith` or `@RegisterExtension` | +| `Assert.assertEquals(expected, actual)` | `assertEquals(expected, actual)` (kotlin.test) | +| `Assert.assertTrue(condition)` | `assertTrue(condition)` (kotlin.test) | +| `@Test(expected = X.class)` | `assertFailsWith { }` (kotlin.test) or `assertThrows { }` (JUnit 5) | + +### 2. JUnit 5 stays mostly the same + +JUnit 5 annotations (`@Test`, `@BeforeEach`, `@AfterEach`, etc.) remain unchanged. +Focus on Kotlin idioms in the test body: + +- `assertThrows { code }` — uses reified generics, no `.class` needed. +- Test classes and methods do not need to be `public` — Kotlin's default visibility + is public, which satisfies JUnit's requirements. +- Test methods do not need `open` unless using a framework that subclasses the test + (e.g., certain Spring test configurations). + +### 3. TestNG to Kotlin + +| TestNG | Kotlin (JUnit 5) | +|---|---| +| `@Test` | `@Test` | +| `@BeforeMethod` | `@BeforeEach` | +| `@AfterMethod` | `@AfterEach` | +| `@BeforeClass` | `@BeforeAll` with `@JvmStatic` in companion object | +| `@AfterClass` | `@AfterAll` with `@JvmStatic` in companion object | +| `@DataProvider` | `@ParameterizedTest` + `@MethodSource` | + +### 4. Assertion style + +Prefer `kotlin.test` assertions (`assertEquals`, `assertTrue`, `assertFailsWith`) +for portability across test frameworks. They delegate to the underlying framework +at runtime. + +### 5. Backtick method names + +Kotlin allows backtick-quoted method names for readable test names: +```kotlin +@Test +fun `should return empty list when no users exist`() { ... } +``` + +--- + +## Example: JUnit 4 Test Class to Kotlin with JUnit 5 + +### Java Input + +```java +package com.acme.service; + +import org.junit.Before; +import org.junit.After; +import org.junit.Test; +import org.junit.BeforeClass; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for the UserService class. + */ +public class UserServiceTest { + + private static DatabaseConnection db; + private UserService userService; + + @BeforeClass + public static void setupDatabase() { + db = DatabaseConnection.create("test"); + } + + @Before + public void setUp() { + userService = new UserService(db); + } + + @After + public void tearDown() { + db.clearTestData(); + } + + @Test + public void testFindById() { + User user = userService.findById(1L); + assertNotNull(user); + assertEquals("Alice", user.getName()); + } + + @Test + public void testFindAllReturnsNonEmptyList() { + List users = userService.findAll(); + assertNotNull(users); + assertTrue(users.size() > 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testFindByIdWithNegativeIdThrows() { + userService.findById(-1L); + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.service + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for the UserService class. + */ +class UserServiceTest { + + companion object { + private lateinit var db: DatabaseConnection + + @BeforeAll + @JvmStatic + fun setupDatabase() { + db = DatabaseConnection.create("test") + } + } + + private lateinit var userService: UserService + + @BeforeEach + fun setUp() { + userService = UserService(db) + } + + @AfterEach + fun tearDown() { + db.clearTestData() + } + + @Test + fun `should find user by id`() { + val user = userService.findById(1L) + assertNotNull(user) + assertEquals("Alice", user.name) + } + + @Test + fun `should return non-empty list from findAll`() { + val users = userService.findAll() + assertNotNull(users) + assertTrue(users.isNotEmpty()) + } + + @Test + fun `should throw IllegalArgumentException for negative id`() { + assertFailsWith { + userService.findById(-1L) + } + } +} +``` + +**Key points:** +- JUnit 4 `@Before` / `@After` → JUnit 5 `@BeforeEach` / `@AfterEach`. +- `@BeforeClass` static method → `@BeforeAll` + `@JvmStatic` inside `companion object`. +- `@Test(expected = ...)` → `assertFailsWith { }` with reified generics. +- Static assertions become kotlin.test top-level function imports. +- Test method names use backtick syntax for readability. +- `users.size() > 0` becomes idiomatic `users.isNotEmpty()`. +- The `db` field uses `lateinit var` since it is initialized in `@BeforeAll`. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/LOMBOK.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/LOMBOK.md new file mode 100644 index 0000000..c3546a0 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/LOMBOK.md @@ -0,0 +1,237 @@ +# Lombok Conversion Guide + +## When This Applies + +Detected when imports match `lombok.*`. + +## Core Rule + +**Remove ALL Lombok annotations entirely.** Do not convert Lombok to Lombok — convert +to idiomatic Kotlin equivalents. Lombok has no place in Kotlin code. + +## Annotation Conversion Table + +| Lombok Annotation | Kotlin Equivalent | +|---|---| +| `@Getter` / `@Setter` | Kotlin properties (val/var) — automatic | +| `@Data` | `data class` with primary constructor properties | +| `@Value` (Lombok) | `data class` with `val` properties (immutable) | +| `@Builder` | Default parameter values, or named arguments. For complex builders, use Kotlin builder DSL | +| `@NoArgsConstructor` | Secondary no-arg constructor, or default values for all params | +| `@AllArgsConstructor` | Primary constructor (Kotlin default) | +| `@RequiredArgsConstructor` | Primary constructor with only required (non-default) params | +| `@ToString` | `data class` auto-generates toString, or manual `override fun toString()` | +| `@EqualsAndHashCode` | `data class` auto-generates, or manual `override fun equals/hashCode` | +| `@Slf4j` / `@Log` / `@Log4j2` | Companion object with logger (see example below) | +| `@Cleanup` | Kotlin's `.use {}` extension function | +| `@SneakyThrows` | Kotlin has no checked exceptions — just remove it | +| `@Synchronized` | Kotlin's `@Synchronized` annotation | +| `@With` | `data class` `.copy()` method | +| `@Accessors(chain = true)` | Kotlin's `apply {}` block | + +## Key Rules + +1. **@Slf4j** — Convert to a companion object with an explicit logger: +```kotlin +companion object { + private val log = LoggerFactory.getLogger(MyClass::class.java) +} +``` + +2. **@Data with JPA entities** — Do NOT use `data class` for JPA entities. Use regular + `open class` with properties instead. Data classes break Hibernate proxies. + +3. **@Builder** — Prefer default parameter values. Only create an explicit builder + pattern if the Java code has complex builder logic beyond simple setters. + +4. **Lombok `val`** — Replace with Kotlin's `val` (they serve the same purpose). + +--- + +## Example 1: @Data Class with @Builder + +### Java Input + +```java +package com.acme.model; + +import lombok.Builder; +import lombok.Data; + +/** + * Represents a customer order with shipping details. + */ +@Data +@Builder +public class Order { + private String orderId; + private String customerName; + private int quantity; + private boolean expedited; +} +``` + +### Kotlin Output + +```kotlin +package com.acme.model + +/** + * Represents a customer order with shipping details. + */ +data class Order( + val orderId: String?, + val customerName: String?, + val quantity: Int = 0, + val expedited: Boolean = false +) +``` + +**What changed:** +- `@Data` → `data class` with primary constructor properties. +- `@Builder` → default parameter values. Callers use named arguments: + `Order(orderId = "123", customerName = "Alice", quantity = 2)`. +- All Lombok imports removed. +- Fields become `val` properties (immutable by default; use `var` only if mutation is + required by the original code). +- Reference types are nullable (`String?`) because Java fields default to `null` unless + proven otherwise. + +--- + +## Example 2: @Slf4j Annotated Service Class + +### Java Input + +```java +package com.acme.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Service that processes incoming payment requests. + */ +@Slf4j +@RequiredArgsConstructor +public class PaymentService { + + private final PaymentGateway gateway; + private final NotificationSender notifier; + + /** + * Processes a payment for the given amount. + * + * @param amount the payment amount in cents + * @return true if the payment succeeded + */ + public boolean processPayment(long amount) { + log.info("Processing payment of {} cents", amount); + try { + gateway.charge(amount); + notifier.sendConfirmation(amount); + log.info("Payment of {} cents succeeded", amount); + return true; + } catch (Exception e) { + log.error("Payment failed for amount {}", amount, e); + return false; + } + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.service + +import org.slf4j.LoggerFactory + +/** + * Service that processes incoming payment requests. + */ +open class PaymentService( + private val gateway: PaymentGateway, + private val notifier: NotificationSender +) { + + companion object { + private val log = LoggerFactory.getLogger(PaymentService::class.java) + } + + /** + * Processes a payment for the given amount. + * + * @param amount the payment amount in cents + * @return true if the payment succeeded + */ + fun processPayment(amount: Long): Boolean { + log.info("Processing payment of {} cents", amount) + return try { + gateway.charge(amount) + notifier.sendConfirmation(amount) + log.info("Payment of {} cents succeeded", amount) + true + } catch (e: Exception) { + log.error("Payment failed for amount {}", amount, e) + false + } + } +} +``` + +**What changed:** +- `@Slf4j` → companion object with `LoggerFactory.getLogger(...)`. +- `@RequiredArgsConstructor` → primary constructor with `val` parameters. +- Lombok imports replaced with `org.slf4j.LoggerFactory`. +- `try/catch` used as an expression (idiomatic Kotlin). +- Class is `open` because Java classes are implicitly open. + +--- + +## Example 3: @Value (Lombok) Immutable Class + +### Java Input + +```java +package com.acme.config; + +import lombok.Value; + +/** + * Immutable configuration for connecting to a database. + */ +@Value +public class DatabaseConfig { + String host; + int port; + String databaseName; + boolean useSsl; +} +``` + +### Kotlin Output + +```kotlin +package com.acme.config + +/** + * Immutable configuration for connecting to a database. + */ +data class DatabaseConfig( + val host: String?, + val port: Int, + val databaseName: String?, + val useSsl: Boolean +) +``` + +**What changed:** +- `@Value` → `data class` with `val` properties (all immutable). +- Lombok's `@Value` makes the class final, and Kotlin `data class` is also final by + default — so the semantics match. +- All Lombok imports removed. +- Auto-generated `equals()`, `hashCode()`, `toString()`, and `copy()` come from + `data class` for free. +- Reference types are nullable (`String?`) since the original Java fields have no + nullability annotations. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MICRONAUT.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MICRONAUT.md new file mode 100644 index 0000000..d46dbcd --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MICRONAUT.md @@ -0,0 +1,120 @@ +# Micronaut Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `io.micronaut.*`. +This covers Micronaut HTTP, Micronaut Data, and Micronaut Security. + +## Key Rules + +### 1. Constructor injection is the default + +Micronaut uses compile-time dependency injection via constructor injection by default. +This maps naturally to Kotlin's primary constructor. Remove `@Inject` when there is +only one constructor — Micronaut discovers it automatically. + +### 2. Stereotype annotations + +`@Singleton`, `@Controller`, `@Client`, `@Repository` — preserve these exactly. +No annotation site target is needed. + +### 3. @Value annotation + +Escape `$` in Kotlin to prevent string template interpretation: + +```kotlin +@Value("\${config.key}") val configKey: String +``` + +### 4. @Inject field injection → constructor injection + +Replace `@Inject` on fields with constructor parameters in Kotlin's primary constructor. +This eliminates `lateinit var` and makes dependencies immutable. + +### 5. AOP interceptors require open classes + +Classes using AOP annotations (`@Around`, `@Introduction`, `@Cacheable`) must be `open` +in Kotlin because Micronaut generates subclass proxies for them at compile time. + +### 6. Bean factories + +`@Factory` classes and their `@Bean`-annotated methods should be `open` so Micronaut +can manage their lifecycle through subclassing. + +### 7. @ConfigurationProperties + +Convert to a class with mutable properties. Use `lateinit var` for required `String` +properties and `var` with defaults for primitives. The class must be `open`. + +--- + +## Examples + +### Example 1: Micronaut Controller with Constructor Injection + +**Java:** + +```java +package com.acme.web; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import jakarta.inject.Inject; + +@Controller("/api/orders") +public class OrderController { + + private final OrderService orderService; + private final InventoryClient inventoryClient; + + @Inject + public OrderController(OrderService orderService, InventoryClient inventoryClient) { + this.orderService = orderService; + this.inventoryClient = inventoryClient; + } + + @Get("/{id}") + public OrderDto getOrder(@PathVariable Long id) { + return orderService.findById(id); + } + + @Get + public List listOrders() { + return orderService.findAll(); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.web + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.PathVariable + +@Controller("/api/orders") +class OrderController( + private val orderService: OrderService, + private val inventoryClient: InventoryClient +) { + + @Get("/{id}") + fun getOrder(@PathVariable id: Long): OrderDto? { + return orderService.findById(id) + } + + @Get + fun listOrders(): List { + return orderService.findAll() + } +} +``` + +Key changes: +- `@Inject` is removed — Micronaut auto-discovers the single constructor. +- The `jakarta.inject.Inject` import is removed because it is no longer referenced. +- Constructor parameters become `private val` in the primary constructor. +- Return type `OrderDto` becomes `OrderDto?` where the service may return null. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MOCKITO.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MOCKITO.md new file mode 100644 index 0000000..337f7e3 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/MOCKITO.md @@ -0,0 +1,253 @@ +# Mockito to MockK Conversion Guide + +## When This Applies + +Detected when imports match `org.mockito.*`. + +While Mockito works in Kotlin, MockK is the idiomatic Kotlin mocking library. Convert +to MockK when the project already uses MockK or is doing a full Kotlin migration. If +the project wants to keep Mockito, convert only the Java syntax to Kotlin syntax using +the `mockito-kotlin` helper library. + +## Key Rules + +### 1. MockK conversion table + +| Mockito | MockK | +|---|---| +| `Mockito.mock(Foo.class)` | `mockk()` | +| `@Mock Foo foo` | `@MockK lateinit var foo: Foo` (with `@ExtendWith(MockKExtension::class)`) | +| `when(foo.bar()).thenReturn(x)` | `every { foo.bar() } returns x` | +| `when(foo.bar()).thenThrow(e)` | `every { foo.bar() } throws e` | +| `when(foo.bar()).thenAnswer { }` | `every { foo.bar() } answers { }` | +| `doNothing().when(foo).bar()` | `justRun { foo.bar() }` | +| `verify(foo).bar()` | `verify { foo.bar() }` | +| `verify(foo, times(2)).bar()` | `verify(exactly = 2) { foo.bar() }` | +| `verify(foo, never()).bar()` | `verify(exactly = 0) { foo.bar() }` | +| `ArgumentCaptor` | `slot()` and `capture(slot)` | +| `any()` | `any()` | +| `eq(x)` | `eq(x)` (often not needed — MockK matches exact values by default) | +| `Mockito.spy(obj)` | `spyk(obj)` | +| `@InjectMocks` | No direct equivalent — use constructor injection | +| `verifyNoMoreInteractions(foo)` | `confirmVerified(foo)` | + +### 2. Coroutine support in MockK + +For suspending functions, use `coEvery` and `coVerify` instead of `every` and `verify`: +```kotlin +coEvery { foo.suspendBar() } returns x +coVerify { foo.suspendBar() } +``` + +### 3. Keeping Mockito (syntax-only conversion) + +If keeping Mockito, use the `mockito-kotlin` library (`org.mockito.kotlin`) for +Kotlin-friendly wrappers: +- `mock()` instead of `Mockito.mock(Foo::class.java)` — uses reified generics. +- `whenever(foo.bar())` instead of `` Mockito.`when`(foo.bar()) `` — avoids backtick- + escaping `when` (it is a Kotlin keyword). +- `argumentCaptor()` — type-safe captor via reified generics. +- `any()` — properly handles Kotlin's non-null types. + +### 4. Relaxed mocks + +MockK supports relaxed mocks that return default values without explicit stubbing: +`mockk(relaxed = true)`. This has no direct Mockito equivalent (Mockito's +`RETURNS_DEFAULTS` is the closest). + +--- + +## Example 1: Converting to MockK + +### Java Input + +```java +package com.acme.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.anyLong; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Tests for OrderService using Mockito mocks. + */ +public class OrderServiceTest { + + private UserRepository userRepository; + private OrderRepository orderRepository; + private OrderService orderService; + + @Before + public void setUp() { + userRepository = mock(UserRepository.class); + orderRepository = mock(OrderRepository.class); + orderService = new OrderService(userRepository, orderRepository); + } + + @Test + public void testCreateOrderForUser() { + User user = new User(1L, "Alice"); + when(userRepository.findById(1L)).thenReturn(user); + + orderService.createOrder(1L, "ITEM-100"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Order.class); + verify(orderRepository).save(captor.capture()); + assertEquals("ITEM-100", captor.getValue().getItemCode()); + assertEquals(1L, captor.getValue().getUserId()); + } + + @Test + public void testGetOrderCount() { + when(orderRepository.countByUserId(anyLong())).thenReturn(5); + + int count = orderService.getOrderCount(1L); + + assertEquals(5, count); + verify(orderRepository).countByUserId(1L); + } +} +``` + +### Kotlin Output (MockK) + +```kotlin +package com.acme.service + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +/** + * Tests for OrderService using MockK mocks. + */ +class OrderServiceTest { + + private val userRepository = mockk() + private val orderRepository = mockk() + private val orderService = OrderService(userRepository, orderRepository) + + @Test + fun `should create order for user`() { + val user = User(1L, "Alice") + every { userRepository.findById(1L) } returns user + every { orderRepository.save(any()) } returns Unit + + orderService.createOrder(1L, "ITEM-100") + + val orderSlot = slot() + verify { orderRepository.save(capture(orderSlot)) } + assertEquals("ITEM-100", orderSlot.captured.itemCode) + assertEquals(1L, orderSlot.captured.userId) + } + + @Test + fun `should return order count`() { + every { orderRepository.countByUserId(any()) } returns 5 + + val count = orderService.getOrderCount(1L) + + assertEquals(5, count) + verify { orderRepository.countByUserId(1L) } + } +} +``` + +**Key points:** +- `mock(Foo.class)` → `mockk()` using reified generics. +- `@Before` setUp is eliminated — mocks are initialized inline with property + declarations. This works because MockK mocks do not require a runner. +- `when(...).thenReturn(...)` → `every { ... } returns ...`. +- `ArgumentCaptor` → `slot()` with `capture(slot)`, accessed via `slot.captured`. +- `anyLong()` → `any()` (MockK's `any()` handles all types). +- `verify(foo).bar()` → `verify { foo.bar() }`. + +--- + +## Example 2: Keeping Mockito (mockito-kotlin syntax) + +### Java Input + +```java +package com.acme.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for PricingService using Mockito. + */ +public class PricingServiceTest { + + private PriceRepository priceRepository; + private PricingService pricingService; + + @Before + public void setUp() { + priceRepository = mock(PriceRepository.class); + pricingService = new PricingService(priceRepository); + } + + @Test + public void testGetPrice() { + when(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99); + double price = pricingService.getPrice("ITEM-1"); + assertEquals(9.99, price, 0.001); + verify(priceRepository).findPriceByItemCode("ITEM-1"); + } +} +``` + +### Kotlin Output (mockito-kotlin) + +```kotlin +package com.acme.service + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals + +/** + * Tests for PricingService using Mockito. + */ +class PricingServiceTest { + + private val priceRepository = mock() + private val pricingService = PricingService(priceRepository) + + @Test + fun `should return price for item`() { + whenever(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99) + + val price = pricingService.getPrice("ITEM-1") + + assertEquals(9.99, price, 0.001) + verify(priceRepository).findPriceByItemCode("ITEM-1") + } +} +``` + +**Key points:** +- `mock(Foo.class)` → `mock()` from `org.mockito.kotlin` (reified generics). +- `when(...)` → `whenever(...)` to avoid backtick-escaping the `when` keyword. +- `verify` stays the same — `org.mockito.kotlin.verify` wraps Mockito's verify. +- The `setUp` method is eliminated — mocks are initialized inline. +- `assertEquals` with a delta parameter works the same way from kotlin.test. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/QUARKUS.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/QUARKUS.md new file mode 100644 index 0000000..453f157 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/QUARKUS.md @@ -0,0 +1,138 @@ +# Quarkus Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `io.quarkus.*`, +`javax.enterprise.*`, or `jakarta.enterprise.*`. This covers Quarkus REST, Quarkus CDI, +and Panache ORM. + +## Key Rules + +### 1. CDI beans need a no-arg constructor + +The CDI specification requires beans to have a no-arg constructor (package-private or +public). In Kotlin, satisfy this by giving all constructor parameters default values, +or by adding a secondary no-arg constructor. + +### 2. Scope annotations + +`@ApplicationScoped`, `@RequestScoped`, `@Dependent` — preserve these exactly. +Beans with these scopes must have a no-arg constructor accessible to CDI. + +### 3. @Inject field injection → constructor injection + +Replace `@Inject` on fields with an `@Inject`-annotated primary constructor in Kotlin. +CDI requires the `@Inject` annotation on the constructor when multiple constructors +exist. With a single constructor, Quarkus discovers it automatically. + +### 4. REST endpoint annotations + +`@Path`, `@GET`, `@POST`, `@PUT`, `@DELETE`, `@Produces`, `@Consumes` — preserve +these exactly. No annotation site target is needed. + +### 5. Panache entities + +Panache entities must remain `open` — do NOT use `data class`. Extend `PanacheEntity` +(auto-generated Long ID) or `PanacheEntityBase` (custom ID type). Keep fields as +`open` mutable properties because Panache enhances field access at build time. + +### 6. @ConfigProperty + +Use on constructor parameters with a default value to satisfy CDI's no-arg +constructor requirement: + +```kotlin +@ConfigProperty(name = "app.greeting") val greeting: String = "" +``` + +--- + +## Examples + +### Example 1: REST Resource with CDI Injection + +**Java:** + +```java +package com.acme.web; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.util.List; + +@Path("/api/products") +@ApplicationScoped +@Produces(MediaType.APPLICATION_JSON) +public class ProductResource { + + @Inject + ProductService productService; + + @Inject + PricingService pricingService; + + @GET + public List listProducts() { + return productService.findAll(); + } + + @GET + @Path("/{id}") + public ProductDto getProduct(@PathParam("id") Long id) { + return productService.findById(id); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.web + +import jakarta.enterprise.context.ApplicationScoped +import jakarta.inject.Inject +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.MediaType + +@Path("/api/products") +@ApplicationScoped +@Produces(MediaType.APPLICATION_JSON) +class ProductResource @Inject constructor( + private val productService: ProductService, + private val pricingService: PricingService +) { + + // No-arg constructor required by CDI — default values satisfy this + constructor() : this( + productService = ProductService(), + pricingService = PricingService() + ) + + @GET + fun listProducts(): List { + return productService.findAll() + } + + @GET + @Path("/{id}") + fun getProduct(@PathParam("id") id: Long): ProductDto? { + return productService.findById(id) + } +} +``` + +Key changes: +- `@Inject` field injection is replaced by an `@Inject`-annotated primary constructor. +- A secondary no-arg constructor is added to satisfy the CDI specification. In practice, + CDI will use the `@Inject` constructor — the no-arg constructor exists only to pass + validation. +- Constructor parameters become `private val` in the primary constructor. +- Return type `ProductDto` becomes `ProductDto?` where the service may return null. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RETROFIT.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RETROFIT.md new file mode 100644 index 0000000..9039126 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RETROFIT.md @@ -0,0 +1,151 @@ +# Retrofit / OkHttp Conversion Guide + +## When This Applies + +Detected when imports match `retrofit2.*` or `okhttp3.*`. + +## Key Rules + +### 1. Interface declarations + +Retrofit service interfaces convert directly — Kotlin interfaces are structurally +identical to Java interfaces for this purpose. + +### 2. Call\ to suspend functions + +Replace `Call` return types with `suspend fun` returning `T` directly. This requires +the Retrofit coroutine adapter (built-in since Retrofit 2.6.0). The `Callback` +async pattern is eliminated entirely. + +### 3. Annotation preservation + +All Retrofit annotations transfer directly with no changes: +- HTTP method annotations: `@GET`, `@POST`, `@PUT`, `@DELETE`, `@PATCH`, `@HTTP` +- Header annotations: `@Headers`, `@Header`, `@HeaderMap` +- Parameter annotations: `@Path`, `@Query`, `@QueryMap`, `@Body`, `@Field`, + `@FieldMap`, `@Part`, `@PartMap` +- `@FormUrlEncoded`, `@Multipart`, `@Streaming` + +### 4. Response\ handling + +For endpoints where HTTP status codes matter, keep `Response` as the return type +with `suspend fun`. For simple cases where only the body is needed, return `T` directly +and let Retrofit throw on non-2xx responses. + +### 5. OkHttpClient.Builder + +Java builder chains convert directly. Use `.apply {}` or `.also {}` for grouping +related configuration: + +```kotlin +val client = OkHttpClient.Builder().apply { + connectTimeout(30, TimeUnit.SECONDS) + readTimeout(30, TimeUnit.SECONDS) + addInterceptor(loggingInterceptor) +}.build() +``` + +### 6. Interceptor SAM conversion + +Java `Interceptor` anonymous classes become Kotlin SAM lambdas: +`Interceptor { chain -> chain.proceed(chain.request()) }` + +### 7. Request/Response body handling + +`RequestBody.create(mediaType, content)` → `content.toRequestBody(mediaType)` when +using the `okhttp3-kotlin-extensions` artifact (or `okhttp-bom` with Kotlin extensions). + +--- + +## Example: Retrofit Interface with Coroutine Support + +### Java Input + +```java +package com.acme.api; + +import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.PATCH; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; + +/** + * Retrofit service interface for the Users API. + */ +public interface UserApi { + + @GET("users") + Call> getUsers(@Query("page") int page, @Query("limit") int limit); + + @GET("users/{id}") + Call getUserById(@Path("id") long id); + + @POST("users") + @Headers("Content-Type: application/json") + Call createUser(@Body CreateUserRequest request); + + @PATCH("users/{id}") + Call updateUser(@Path("id") long id, @Body UpdateUserRequest request); + + @DELETE("users/{id}") + Call deleteUser(@Path("id") long id); +} +``` + +### Kotlin Output + +```kotlin +package com.acme.api + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * Retrofit service interface for the Users API. + */ +interface UserApi { + + @GET("users") + suspend fun getUsers(@Query("page") page: Int, @Query("limit") limit: Int): List + + @GET("users/{id}") + suspend fun getUserById(@Path("id") id: Long): UserDto + + @POST("users") + @Headers("Content-Type: application/json") + suspend fun createUser(@Body request: CreateUserRequest): UserDto + + @PATCH("users/{id}") + suspend fun updateUser(@Path("id") id: Long, @Body request: UpdateUserRequest): UserDto + + @DELETE("users/{id}") + suspend fun deleteUser(@Path("id") id: Long): Response +} +``` + +**Key points:** +- `Call` is removed — each method becomes a `suspend fun` returning `T` directly. + Retrofit 2.6.0+ supports this natively without an additional adapter. +- `Call` becomes `Response`. `Unit` is Kotlin's equivalent of `Void`. + `Response` is used here to allow checking the HTTP status code on delete. +- `Call` and `Callback` imports are removed since they are no longer referenced. +- All HTTP method and parameter annotations (`@GET`, `@POST`, `@Path`, `@Query`, + `@Body`, `@Headers`, etc.) are preserved exactly as-is. +- Java `int` → Kotlin `Int`, Java `long` → Kotlin `Long`. +- The `public` modifier on the interface is removed — Kotlin's default visibility + is public. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RXJAVA.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RXJAVA.md new file mode 100644 index 0000000..bb9e5da --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/RXJAVA.md @@ -0,0 +1,181 @@ +# RxJava to Coroutines/Flow Conversion Guide + +## When This Applies + +Detected when imports match `io.reactivex.*` or `rx.*`. This is a significant paradigm +shift — RxJava reactive types map to Kotlin coroutines and Flow. + +## Key Rules + +### 1. Dependency setup + +Add `kotlinx-coroutines-core` and `kotlinx-coroutines-rx3` (or `kotlinx-coroutines-rx2`) +as dependencies if performing a gradual migration. The bridge library provides extension +functions like `asFlow()` and `asObservable()` for interop at module boundaries. + +### 2. Type mapping + +| RxJava | Kotlin | +|---|---| +| `Observable` | `Flow` | +| `Flowable` | `Flow` (backpressure is built-in) | +| `Single` | `suspend fun`: T | +| `Maybe` | `suspend fun`: T? | +| `Completable` | `suspend fun` returning `Unit` | +| `Disposable` | `Job` (from coroutines) | +| `CompositeDisposable` | `CoroutineScope` (structured concurrency) | + +### 3. Operator mapping + +| RxJava | Kotlin Flow | +|---|---| +| `subscribeOn(Schedulers.io())` | `flowOn(Dispatchers.IO)` | +| `observeOn(AndroidSchedulers.mainThread())` | `flowOn(Dispatchers.Main)` or collect on Main | +| `flatMap` | `flatMapMerge` or `flatMapConcat` | +| `map` | `map` (same) | +| `filter` | `filter` (same) | +| `zip` | `combine` or `zip` | +| `merge` | `merge` | +| `concat` | `flatMapConcat` | +| `onErrorReturn` | `catch { emit(default) }` | +| `doOnNext` | `onEach` | +| `subscribe()` | `collect {}` in a coroutine scope | + +### 4. Error handling + +RxJava's `onError` callback maps to Flow's `catch` operator or a try-catch block +wrapping the `collect` call. In suspend functions (replacing `Single`/`Completable`), +use standard try-catch. + +### 5. Backpressure + +Flow has built-in backpressure via suspension. There is no need for a separate +`Flowable` type — all `Flow` instances support backpressure by default. + +### 6. Threading + +`flowOn` changes the upstream dispatcher (analogous to `subscribeOn`). Collection +always happens on the caller's dispatcher. To collect on a specific dispatcher, +launch the collecting coroutine in the desired scope. + +### 7. Lifecycle and cancellation + +RxJava's `Disposable` / `CompositeDisposable` pattern is replaced by structured +concurrency. Cancelling a `CoroutineScope` cancels all child coroutines and flow +collections automatically. + +--- + +## Example: Converting an Observable Chain to Flow + +### Java Input + +```java +package com.acme.data; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.CompositeDisposable; + +/** + * Repository that streams user data from a remote source. + */ +public class UserRepository { + + private final UserApi api; + private final CompositeDisposable disposables = new CompositeDisposable(); + + public UserRepository(UserApi api) { + this.api = api; + } + + public Observable> getActiveUsers() { + return api.getAllUsers() + .subscribeOn(Schedulers.io()) + .map(users -> filterActive(users)) + .doOnNext(users -> logCount(users)) + .onErrorReturn(throwable -> Collections.emptyList()); + } + + public void observeUsers(UserCallback callback) { + disposables.add( + getActiveUsers() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + users -> callback.onUsers(users), + error -> callback.onError(error) + ) + ); + } + + public void clear() { + disposables.clear(); + } + + private List filterActive(List users) { + return users.stream().filter(User::isActive).collect(Collectors.toList()); + } + + private void logCount(List users) { + System.out.println("Active users: " + users.size()); + } +} +``` + +### Kotlin Output + +```kotlin +package com.acme.data + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +/** + * Repository that streams user data from a remote source. + */ +class UserRepository( + private val api: UserApi +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + fun getActiveUsers(): Flow> = + api.getAllUsers() + .map { users -> users.filter { it.isActive } } + .onEach { users -> println("Active users: ${users.size}") } + .catch { emit(emptyList()) } + .flowOn(Dispatchers.IO) + + fun observeUsers(callback: UserCallback) { + scope.launch { + getActiveUsers().collect { users -> + callback.onUsers(users) + } + } + } + + fun clear() { + scope.cancel() + } +} +``` + +**Key points:** +- `Observable>` becomes `Flow>`. +- `subscribeOn(Schedulers.io())` becomes `flowOn(Dispatchers.IO)` at the end of the + chain (it affects all upstream operators). +- `CompositeDisposable` is replaced by a `CoroutineScope` with `SupervisorJob`. + Calling `scope.cancel()` cancels all active collections. +- `doOnNext` becomes `onEach`. +- `onErrorReturn` becomes `catch { emit(emptyList()) }`. +- `observeOn(AndroidSchedulers.mainThread())` is unnecessary because `scope` already + uses `Dispatchers.Main`, and `collect` runs on the collector's dispatcher. +- Java streams (`filter` + `collect`) become Kotlin's `filter` directly on the list. diff --git a/skills/kotlin-tooling-java-to-kotlin/references/frameworks/SPRING.md b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/SPRING.md new file mode 100644 index 0000000..ae07434 --- /dev/null +++ b/skills/kotlin-tooling-java-to-kotlin/references/frameworks/SPRING.md @@ -0,0 +1,238 @@ +# Spring Framework Conversion Guide + +## When This Applies + +This guide applies when the Java source contains imports matching `org.springframework.*`. +This covers Spring Boot, Spring MVC, Spring Data, and Spring Security. + +## Key Rules + +### 1. SpringApplication.run — spread CLI args + +In Kotlin, `String[]` varargs must be spread with the `*` operator. + +- Java: `SpringApplication.run(App.class, args);` +- Kotlin: `SpringApplication.run(App::class.java, *args)` + +### 2. Constructor injection over @Autowired + +Kotlin's primary constructor makes constructor injection natural. When a class has a +single constructor, Spring auto-discovers it — remove `@Autowired`. + +### 3. Stereotype annotations + +`@Component`, `@Service`, `@RestController`, and `@Repository` target the class. +Preserve these annotations exactly. No annotation site target is needed. + +### 4. @Value annotation + +Use `@Value` on constructor parameters. Escape `$` in SpEL expressions to prevent +Kotlin string template interpretation: + +```kotlin +@Value("\${app.name}") val appName: String +``` + +### 5. @ConfigurationProperties + +Convert to a `data class` only if the properties are immutable. For mutable +configuration, use a regular class with `lateinit var`. + +### 6. Spring Data repositories + +Interface declarations convert directly. Replace `Optional` return types with +nullable `T?` in Kotlin for idiomatic usage. + +### 7. @RequestMapping / @GetMapping / @PostMapping etc. + +Preserve exactly. Where Java uses array initializer syntax for annotation parameters, +use `arrayOf()` in Kotlin. + +### 8. @Transactional + +Preserve exactly. The class must remain `open` because Spring creates proxies via +subclassing. Do not make `@Transactional` classes `final`. + +### 9. @Bean methods in @Configuration classes + +`@Bean` methods must be `open` so that Spring can override them in CGLIB proxies. +Alternatively, apply the `allopen` compiler plugin with a Spring preset, which makes +annotated classes and their members open automatically. + +--- + +## Examples + +### Example 1: Spring Boot Application Main Class + +**Java:** + +```java +package com.acme; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +open class Application + +fun main(args: Array) { + runApplication(*args) +} +``` + +Key changes: +- `main` becomes a top-level function (no companion object needed). +- `runApplication` is a Spring Boot Kotlin extension that replaces + `SpringApplication.run(T::class.java, *args)`. +- The `*args` spread operator is required for the varargs parameter. + +--- + +### Example 2: REST Controller with Constructor Injection + +**Java:** + +```java +package com.acme.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/{id}") + public UserDto getUser(@PathVariable Long id) { + return userService.findById(id); + } + + @GetMapping + public List getAllUsers() { + return userService.findAll(); + } +} +``` + +**Kotlin:** + +```kotlin +package com.acme.web + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/users") +class UserController( + private val userService: UserService +) { + + @GetMapping("/{id}") + fun getUser(@PathVariable id: Long): UserDto? { + return userService.findById(id) + } + + @GetMapping + fun getAllUsers(): List { + return userService.findAll() + } +} +``` + +Key changes: +- `@Autowired` is removed — Spring auto-discovers the single constructor. +- The `Autowired` import is removed because it is no longer referenced. +- Constructor parameter becomes a `private val` in the primary constructor. +- Return type `UserDto` becomes `UserDto?` where the service may return null. + +--- + +### Example 3: @ConfigurationProperties Class + +**Java:** + +```java +package com.acme.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "app.mail") +public class MailProperties { + + private String host; + private int port = 587; + private String username; + private String password; + + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public int getPort() { return port; } + public void setPort(int port) { this.port = port; } + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } +} +``` + +**Kotlin (mutable config with lateinit var):** + +```kotlin +package com.acme.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@ConfigurationProperties(prefix = "app.mail") +open class MailProperties { + lateinit var host: String + var port: Int = 587 + lateinit var username: String + lateinit var password: String +} +``` + +Key changes: +- Getters and setters are replaced by Kotlin properties. +- `lateinit var` is used for required `String` properties that Spring populates + after construction. +- `port` keeps its default value and uses a regular `var` (`lateinit` does not + support primitive types). +- The class is `open` so that Spring can create a CGLIB proxy for it.