- name
- jakarta-java25-migration
- description
- Invoke this skill to migrate Java 8/11 to Java 25 and javax.* to jakarta.* namespaces. Detects Java version < 25 in pom.xml or javax.* imports in source files. Foundation skill - invoke before sdk-replacement.
- disable-model-invocation
- false
- allowed-tools
- Read, Edit, Write, Bash, Grep, Glob
# Jakarta EE 10 and Java 25 Migration
Migrate your Neo Java application from Java 8/11 and Java EE to Java 25 and Jakarta EE 10.
## Purpose
Upgrade the Java runtime and migrate from `javax.*` namespaces to `jakarta.*` namespaces for forward compatibility with modern application servers and Cloud Foundry buildpacks.
## Detection
This skill applies if any of these patterns are found:
### In pom.xml
```xml
<!-- Java 8 or 11 source/target -->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- OR -->
<properties>
<java.version>11</java.version>
</properties>
```
### In Java source files
```java
// javax.* imports indicate pre-Jakarta EE
import javax.servlet.*;
import javax.persistence.*;
import javax.annotation.*;
import javax.mail.*;
import javax.ejb.*;
```
## Prerequisites
- None (this is typically the first migration step)
- Maven 3.6+ installed
- Java 25+ available on PATH
## Transformation Steps
### Step 0: Create Migration Copy
Before making any changes, create a copy of the application directory as a sibling folder. All migration work is done on this copy — the original stays untouched.
```bash
APP_DIR=$(pwd)
APP_NAME=$(basename "$APP_DIR")
COPY_DIR="$(dirname "$APP_DIR")/${APP_NAME}-cf-migration"
if [ -d "$COPY_DIR" ]; then
echo "Migration copy already exists at $COPY_DIR — using it."
else
cp -r "$APP_DIR" "$COPY_DIR"
echo "Created migration copy at $COPY_DIR"
fi
cd "$COPY_DIR"
```
Now that we are inside the copy, create the `.migration/` directory and save the config there:
```bash
mkdir -p .migration
```
Save the paths to `.migration/cf-migration-config.json` (create or update the file):
```json
{
"sourceAppDir": "<original APP_DIR>",
"migrationAppDir": "<COPY_DIR>"
}
```
> **All subsequent steps in this skill and all downstream skills (`sdk-replacement`, `authentication-xsuaa`, `mta-descriptor`, etc.) must operate inside `$COPY_DIR`. The `.migration/` directory is inside the copy, not the original.**
### Step 1: Update Java Version in pom.xml
**Before:**
```xml
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
```
**After:**
```xml
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
```
> 🛑 **HARD RULE — pin the runtime JRE in the mtad, or the app 500s on every request.**
> You just set `maven.compiler.target` to 25, so the WAR is Java 25 bytecode (class
> file version 69). `sap_java_buildpack_jakarta`'s **default** JRE is NOT 25 (it is
> currently 21) — if you do not pin it, Tomcat cannot load your servlets and fails with
> `java.lang.UnsupportedClassVersionError: ... class file version 69.0, this version of
> the Java Runtime only recognizes class file versions up to 65.0`, marks the servlet
> unavailable, and **every request returns HTTP 500** (the app still shows `running 1/1`,
> so this is easy to miss until an integration test hits it). The deployed module's
> `properties:` **MUST** include BOTH env vars:
>
> ```yaml
> properties:
> JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']"
> JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 25.+ }'
> ```
>
> The `mta-descriptor` skill's `mtad-base.yaml` template already sets these — if you
> start from it, keep them; if you hand-write the mtad, add them.
>
> ✅ **After writing the mtad, verify:** `grep JBP_CONFIG_SAP_MACHINE_JRE <mtad>` must
> return the line above. Rule of thumb: **`maven.compiler.target` must equal the major
> version in `JBP_CONFIG_SAP_MACHINE_JRE`** (both 25).
### Step 2: Run OpenRewrite Migration
Execute the OpenRewrite recipes to automatically migrate code:
```bash
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.UpgradeToJava25,org.openrewrite.java.migrate.jakarta.JakartaEE10 \
-Drewrite.exportDatatables=true
```
> **Note:**
> - Make sure you have Java SE 25 and the latest Maven version installed
> - The recipe artifact coordinates ensure the latest migration recipes are downloaded
> - **`-Drewrite.activeRecipes` must be passed exactly once.** Maven turns each `-D` into a Java system property, and a property has only one value per key — passing two `-D rewrite.activeRecipes=...` flags silently keeps only the last one and the first recipe is dropped without a warning. Always pass a single comma-separated list. See [the OpenRewrite Maven plugin reference](https://docs.openrewrite.org/reference/rewrite-maven-plugin) for the canonical syntax.
This command will:
- Update Java language features to Java 25
- Replace `javax.*` imports with `jakarta.*`
- Update deprecated API usages
- Migrate web.xml namespace declarations
### Step 2b: Fix OpenRewrite side-effects in pom.xml
OpenRewrite makes two changes that must be corrected after it runs:
#### 2b-1: Restore `jakarta.servlet-api` to `provided` scope
OpenRewrite replaces `javax.servlet:javax.servlet-api` with `jakarta.servlet:jakarta.servlet-api` but sets `<scope>compile</scope>`. This is wrong — `sap_java_buildpack_jakarta` provides the servlet API at runtime, so the scope must be `provided`. If left as `compile`, the servlet API JAR is packaged into the WAR and conflicts with the buildpack's version, causing all servlets to return 404 after a successful deploy.
**Detect and fix:**
```bash
# Check if scope was set to compile
grep -A4 "jakarta.servlet-api" pom.xml | grep "compile"
```
If the above returns a result, change the scope:
```xml
<!-- WRONG — causes 404 after deploy -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>compile</scope>
</dependency>
<!-- Change <scope>compile</scope> to <scope>provided</scope> — see CORRECT block below -->
<!-- CORRECT -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
```
> If `cf-tomcat-bom` is imported in `<dependencyManagement>`, the version tag can be omitted — the BOM manages it.
#### 2b-2: Replace `javax.servlet:jstl` with Jakarta JSTL (Conditional)
OpenRewrite does NOT migrate the old JSTL library. If `javax.servlet:jstl` is present in pom.xml, it must be replaced with the Jakarta EE 10 compatible version.
**Detect:**
```bash
grep -B2 "jstl" pom.xml | grep "javax.servlet"
```
If the above returns a result, replace the dependency:
**Before:**
```xml
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
```
**After:**
```xml
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
```
### Step 3: Handle OpenCMIS Caveat
> **Important:** The `org.apache.chemistry.opencmis` libraries have NOT been migrated to Jakarta EE. If your application uses OpenCMIS (Document Management), you must exclude it from the Jakarta migration.
**Check for OpenCMIS usage:**
```bash
grep -r "opencmis" pom.xml
grep -r "org.apache.chemistry" src/main/java/
```
**If OpenCMIS is used, add these specific exclusions:**
```xml
<dependency>
<groupId>org.apache.chemistry.opencmis</groupId>
<artifactId>chemistry-opencmis-client-impl</artifactId>
<version>${opencmis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-policy</artifactId>
</exclusion>
</exclusions>
</dependency>
```
> **Note:** Add the excluded libraries as separate dependencies using their latest versions, if applicable.
### Step 4: Update web.xml Namespace (if not auto-migrated)
**Before:**
```xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
```
**After:**
```xml
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
```
### Step 5: Common Import Migrations
| Before (javax.*) | After (jakarta.*) |
|------------------|-------------------|
| `javax.servlet.*` | `jakarta.servlet.*` |
| `javax.servlet.http.*` | `jakarta.servlet.http.*` |
| `javax.persistence.*` | `jakarta.persistence.*` |
| `javax.annotation.*` | `jakarta.annotation.*` |
| `javax.ejb.*` | `jakarta.ejb.*` |
| `javax.mail.*` | `jakarta.mail.*` |
| `javax.ws.rs.*` | `jakarta.ws.rs.*` |
| `javax.json.*` | `jakarta.json.*` |
| `javax.validation.*` | `jakarta.validation.*` |
| `com.fasterxml.jackson.jaxrs.*` | `com.fasterxml.jackson.jakarta.rs.*` |
> **Important — Jackson JAX-RS providers:** The Jackson JAX-RS provider artifact changed from `jackson-jaxrs-json-provider` (javax) to `jackson-jakarta-rs-json-provider` (jakarta). In addition to the package rename, the class `JacksonJaxbJsonProvider` was renamed to `JacksonXmlBindJsonProvider`. The simpler `JacksonJsonProvider` (which supports JAXB annotations too) is the recommended replacement. OpenRewrite will update Java `import` statements, but it does **not** update class references in `web.xml`. If your `web.xml` configures JAX-RS providers via `<init-param>`, you must update those manually.
#### Update web.xml JAX-RS Provider References
**Detect:**
```bash
grep -r "com.fasterxml.jackson.jaxrs" --include="*.xml" src/main/webapp/
```
If this returns results, update the class name:
**Before:**
```xml
<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>
com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider
</param-value>
</init-param>
```
**After:**
```xml
<init-param>
<param-name>jaxrs.providers</param-name>
<param-value>
com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider
</param-value>
</init-param>
```
> **Note:** The available classes in the Jakarta artifact are `JacksonJsonProvider` (recommended) and `JacksonXmlBindJsonProvider`. The old name `JacksonJaxbJsonProvider` does **not** exist in the Jakarta artifact — using it will cause `ClassNotFoundException` at runtime even though the JAR is in the WAR.
Also update the Maven dependency:
**Before:**
```xml
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
```
**After:**
```xml
View on GitHub