- name
- tomee-runtime
- description
- Invoke this skill to configure TomEE container for EJB applications. Detects @Stateless, @Singleton, @EJB annotations, javax.ejb/jakarta.ejb imports, or neo-javaee7-wp-api dependency. Use instead of Tomcat for EJB support.
- disable-model-invocation
- false
- allowed-tools
- Read, Edit, Write, Bash, Grep, Glob
# TomEE Runtime
Configure TomEE container for JavaEE/EJB applications in Cloud Foundry.
## Purpose
Migrate Neo applications using TomEE runtime (with EJB support) to Cloud Foundry's TomEE 10 container instead of Tomcat.
## Detection
This skill applies if any of these patterns are found:
### In pom.xml
```xml
<dependency>
<groupId>com.sap.cloud</groupId>
<artifactId>neo-javaee7-wp-api</artifactId>
</dependency>
```
### In Java source files
```java
import javax.ejb.Stateless;
import javax.ejb.Singleton;
import javax.ejb.EJB;
import javax.ejb.Schedule;
@Stateless
public class MyService { }
@Singleton
public class MySingleton { }
```
### In web.xml or deployment descriptors
```xml
<!-- EJB references -->
<ejb-local-ref>
<ejb-ref-name>ejb/MyService</ejb-ref-name>
<local>com.example.MyService</local>
</ejb-local-ref>
```
## Prerequisites
> **Working directory:** This skill must run inside the `-cf-migration` copy of your app, created by `jakarta-java25-migration` or `neo-to-cf-migration-orchestrator`. If your current directory does not end in `-cf-migration`, switch to it before proceeding.
Before invoking this skill, ensure you have invoked:
1. **jakarta-java25-migration** - `Use the jakarta-java25-migration skill`
- Migrates to Jakarta EE 10
- REQUIRED before this skill
> **Note:** TomEE is an alternative to the Tomcat migration path. Choose TomEE if your application uses EJB features.
## Transformation Steps
### Step 1: Replace Neo Dependencies
**Before (Neo):**
```xml
<dependency>
<groupId>com.sap.cloud</groupId>
<artifactId>neo-javaee7-wp-api</artifactId>
<version>${neo.version}</version>
<scope>provided</scope>
</dependency>
```
**After (Cloud Foundry TomEE):**
See [assets/pom-cf-tomee.xml](assets/pom-cf-tomee.xml) for a complete template.
Key changes:
```xml
<dependencyManagement>
<dependencies>
<!-- CF TomEE BOM instead of cf-tomcat-bom -->
<dependency>
<groupId>com.sap.cloud.sjb.cf</groupId>
<artifactId>cf-tomee-bom</artifactId>
<version>${cf-tomee-bom-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-modules-bom</artifactId>
<version>${sdk-modules-bom-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<!-- Resolve via sdk-replacement Step 3 (substitute `cf-tomee-bom` for `cf-tomcat-bom`). -->
<cf-tomee-bom-version>RESOLVED_CF_TOMEE_BOM_VERSION</cf-tomee-bom-version>
<sdk-modules-bom-version>RESOLVED_SDK_MODULES_BOM_VERSION</sdk-modules-bom-version>
<jakarta.persistence-api-version>3.2.0</jakarta.persistence-api-version>
</properties>
<dependencies>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>scp-cf</artifactId>
</dependency>
<!-- Jakarta EE API provided by TomEE -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
```
### Step 2: Migrate EJB Annotations
**Before (Java EE 7):**
```java
import javax.ejb.Stateless;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.annotation.PostConstruct;
@Stateless
public class MyBusinessService {
@EJB
private OtherService otherService;
public String doWork() {
return otherService.process();
}
}
@Singleton
@Startup
public class ApplicationInitializer {
@PostConstruct
public void init() {
System.out.println("Application started");
}
@Schedule(hour = "*", minute = "*/5", persistent = false)
public void scheduledTask() {
System.out.println("Running scheduled task");
}
}
```
**After (Jakarta EE 10):**
```java
import jakarta.ejb.Stateless;
import jakarta.ejb.EJB;
import jakarta.ejb.Schedule;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.annotation.PostConstruct;
@Stateless
public class MyBusinessService {
@EJB
private OtherService otherService;
public String doWork() {
return otherService.process();
}
}
@Singleton
@Startup
public class ApplicationInitializer {
@PostConstruct
public void init() {
System.out.println("Application started");
}
@Schedule(hour = "*", minute = "*/5", persistent = false)
public void scheduledTask() {
System.out.println("Running scheduled task");
}
}
```
### Step 3: Create resources.xml for Data Sources
For TomEE, use `resources.xml` instead of Tomcat's `context.xml`.
Create `src/main/webapp/WEB-INF/resources.xml`:
```xml
<?xml version='1.0' encoding='utf-8'?>
<resources>
<Resource id="jdbc/DefaultDB"
provider="xs.openejb:XS Default JDBC Database"
type="javax.sql.DataSource">
service=${service_name_for_DefaultDB}
</Resource>
</resources>
```
> **Note:** TomEE uses `xs.openejb:XS Default JDBC Database` provider instead of Tomcat's `TomcatDataSourceFactory`. The `service` placeholder is specified as element content, not as an XML attribute.
### Step 4: Configure persistence.xml for TomEE JTA
If the application uses JPA, the `persistence.xml` must be updated for TomEE's JTA environment. There are four critical changes:
1. **File location** — Move `persistence.xml` from `src/main/resources/META-INF/` to `src/main/webapp/META-INF/`. TomEE processes DDL generation correctly only when the persistence.xml is at the WAR root `META-INF/` location, not at `WEB-INF/classes/META-INF/`.
2. **Explicit EclipseLink provider** — TomEE defaults to OpenJPA. Without an explicit `<provider>`, EclipseLink may not be properly initialized as the JPA provider.
3. **Short JNDI datasource name** — TomEE resolves datasources differently than Neo. Use `jdbc/DefaultDB` without the `java:comp/env/` prefix.
4. **HANA Cloud NCLOB for large text columns** — HANA Cloud limits `NVARCHAR` to 5000 characters. Any `@Lob` field with `length > 5000` must use `columnDefinition = "NCLOB"` instead of `length`, otherwise DDL generation silently fails for those tables.
**Before (Neo):**
```xml
<!-- Location: src/main/resources/META-INF/persistence.xml -->
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="MyApp" transaction-type="JTA">
<jta-data-source>java:comp/env/jdbc/DefaultDB</jta-data-source>
<class>com.example.MyEntity</class>
<properties>
<property name="eclipselink.ddl-generation" value="create-tables" />
<property name="eclipselink.ddl-generation.output-mode" value="database" />
</properties>
</persistence-unit>
</persistence>
```
**After (Cloud Foundry TomEE):**
```xml
<!-- Location: src/main/webapp/META-INF/persistence.xml (MUST be under webapp, not resources) -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="MyApp" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/DefaultDB</jta-data-source>
<class>com.example.MyEntity</class>
<properties>
<property name="eclipselink.ddl-generation" value="create-or-extend-tables" />
<property name="eclipselink.target-database" value="HANA"/>
</properties>
</persistence-unit>
</persistence>
```
**Key changes:**
- **File moved** from `src/main/resources/META-INF/` to `src/main/webapp/META-INF/` — TomEE's DDL generation only runs correctly when persistence.xml is at the WAR root `META-INF/` location. Placing it under `resources/META-INF/` (which maps to `WEB-INF/classes/META-INF/` in the WAR) causes DDL generation to silently fail.
- `<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>` — explicit EclipseLink provider (TomEE defaults to OpenJPA)
- `<jta-data-source>jdbc/DefaultDB</jta-data-source>` — short JNDI name without `java:comp/env/` prefix
- `eclipselink.ddl-generation=create-or-extend-tables` — use this value instead of `create-tables` for robustness during schema evolution
> **Warning:** If `persistence.xml` is placed under `src/main/resources/META-INF/` instead of `src/main/webapp/META-INF/`, EclipseLink's DDL generation will **silently fail** in TomEE — no tables will be created and no error will be logged at startup. The failure only becomes visible at runtime when queries hit missing tables. Always verify by adding `<property name="eclipselink.logging.level" value="FINE"/>` temporarily and checking for `CREATE TABLE` statements in the logs.
#### HANA Cloud @Lob Column Migration
HANA Cloud enforces a maximum of **5000 characters** for `NVARCHAR` columns. EclipseLink maps `@Lob @Column(length = N)` to `NVARCHAR(N)` — if `N > 5000`, the `CREATE TABLE` statement fails silently.
**Before (works on Neo HANA on-premise, fails on HANA Cloud):**
```java
@Lob
@Column(nullable = false, length = 16 * 1024) // 16384 > 5000 limit
private String longText = "";
@Lob
@Column(length = 512 * 1024) // 524288 > 5000 limit
private String introduction = "";
```
**After (works on HANA Cloud):**
```java
@Lob
@Column(nullable = false, columnDefinition = "NCLOB")
private String longText = "";
@Lob
@Column(columnDefinition = "NCLOB")
private String introduction = "";
```
Replace `length = N` with `columnDefinition = "NCLOB"` on every `@Lob` field where the original length exceeds 5000. Keep other `@Column` attributes (`nullable`, `updatable`) as-is.
> **Note:** `NCLOB` has no practical size limit on HANA Cloud and is the correct type for large text fields. Fields with `length <= 5000` (e.g., `@Column(length = 1024)`) can remain as `NVARCHAR` and do not need this change.
> 🛑 **HARD RULE — a JPA/EJB app REQUIRES a HANA schema service. "No services
> required" is wrong for any app with a `persistence.xml` or a JPA datasource.**
> The `resources.xml` from Step 3 binds `jdbc/DefaultDB` to
> `service=${service_name_for_DefaultDB}`. If the mtad does not bind a
> `com.sap.xs.hana-schema` service (Steps 5–6), TomEE cannot initialise the
> datasource / persistence unit at startup, and the app **stages successfully
> but crashes on boot** (`cf deploy` → "Some instances have crashed", no useful
> HTTP response). Steps 5 and 6 are **mandatory** whenever the app uses JPA —
> do not skip them and do not declare "no services required".
>
> ✅ **Verify before deploy:** the mtad must contain a
> `type: com.sap.xs.hana-schema` resource AND the module must `requires:` it.
> `grep -q 'com.sap.xs.hana-schema' <mtad>` must succeed for a persistence app.
### Step 5: Create resource_configuration.yml
Create `src/main/webapp/META-INF/sap_java_buildpack/config/resource_configuration.yml`:
```yaml
---
tomee/webapps/ROOT/WEB-INF/resources.xml:
service_name_for_DefaultDB: ${app-name}-hana
```
> **Note:** The path is `tomee/webapps/ROOT/WEB-INF` instead of `tomcat/webapps/ROOT`.
>
> 🛑 **SUBSTITUTE `${app-name}` with the REAL app name — it is a placeholder, not
> a literal.** This value MUST exactly equal the `hana-schema` resource name you
> bind in the mtad (Step 6). If you leave it literal, the buildpack looks up a
> service called `app-name-hana` that does not exist, logs
> `Could not find service with name 'app-name-hana', known services
> '[<real>-hana]'`, the JDBC pool fails to initialise, and the app **crashes on
> startup**. Example for an app named `persistence-with-ejb`:
>
> ```yaml
> ---
> tomee/webapps/ROOT/WEB-INF/resources.xml:
> service_name_for_DefaultDB: persistence-with-ejb-hana
> ```
>
> ✅ **Cross-check:** the value here must be identical to the `resources:` entry
Ver no GitHub