Skip to main content

document-management-sdm

Invoke this skill to migrate from Neo ECM Service to SAP Document Management Service. Detects com.sap.ecm.api.EcmService resource-ref in web.xml or EcmService JNDI lookups. Uses OpenCMIS client library for CMIS protocol.

Source facts

Repository
SAP-samples/btp-neo-java-app-migration
Last source activity
July 22, 2026 at 13:02
Detected SKILL.md language
English
Stars
6
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
document-management-sdm
description
Invoke this skill to migrate from Neo ECM Service to SAP Document Management Service. Detects com.sap.ecm.api.EcmService resource-ref in web.xml or EcmService JNDI lookups. Uses OpenCMIS client library for CMIS protocol.
disable-model-invocation
false
allowed-tools
Read, Edit, Write, Bash, Grep, Glob
# Document Management Service Migrate from Neo's ECM Service to SAP Document Management Service (SDM). ## Purpose Replace Neo's `com.sap.ecm.api.EcmService` JNDI resource with SAP Document Management Service using the CMIS protocol via OpenCMIS client library. ## Detection This skill applies if any of these patterns are found: ### In web.xml ```xml <resource-ref> <res-ref-name>EcmService</res-ref-name> <res-type>com.sap.ecm.api.EcmService</res-type> </resource-ref> ``` ### In Java source files ```java import com.sap.ecm.api.EcmService; import com.sap.ecm.api.RepositoryOptions; @Resource(name = "EcmService") private EcmService ecmService; // OR JNDI lookup EcmService ecmService = (EcmService) ctx.lookup("java:comp/env/EcmService"); ``` ## 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. **sdk-replacement** - `Use the sdk-replacement skill` - Sets up SAP Cloud SDK - REQUIRED before this skill Also required: - SDM service entitlement in subaccount ## Transformation Steps ### Step 1: Remove Resource Reference from web.xml **Remove this from web.xml:** ```xml <resource-ref> <res-ref-name>EcmService</res-ref-name> <res-type>com.sap.ecm.api.EcmService</res-type> </resource-ref> ``` ### Step 2: Add OpenCMIS Dependencies > **Prerequisite — `sdk-modules-bom` and `scp-cf` must already be present from `sdk-replacement`.** The `ServiceBindingAccessor` asset (Step 3) calls `DefaultServiceBindingAccessor.getInstance()` from `com.sap.cloud.environment.servicebinding.api.*`, which is supplied by `scp-cf` and version-managed by `sdk-modules-bom` at `provided` scope (the buildpack ships its own — newer — implementation at runtime). Both are added by the `sdk-replacement` prerequisite skill; this skill does **not** add them. If they are missing, rerun `sdk-replacement` rather than patching them in by hand. > > **Do NOT** add `com.sap.cloud.environment.servicebinding:*` as a direct `compile`-scope dependency. Bundling a different version of those classes inside the WAR causes a runtime `ServiceConfigurationError: SapVcapServicesServiceBindingAccessor not a subtype` because the buildpack-provided interface and the WAR-bundled implementation are loaded by different classloaders. Add to `pom.xml`: ```xml <dependencies> <!-- OpenCMIS Client --> <dependency> <groupId>org.apache.chemistry.opencmis</groupId> <artifactId>chemistry-opencmis-client-impl</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.apache.chemistry.opencmis</groupId> <artifactId>chemistry-opencmis-client-api</artifactId> <version>1.1.0</version> </dependency> <!-- HTTP Client for REST calls --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> ``` > **What about `scp-cf`?** The `ServiceBindingAccessor` asset depends on > `com.sap.cloud.sdk.cloudplatform:scp-cf` (which transitively brings the > `com.sap.cloud.environment.servicebinding.api.*` classes used by > `DefaultServiceBindingAccessor.getInstance()`). That dependency — along with > `sdk-modules-bom` — is the declared responsibility of the **`sdk-replacement`** > skill, which is a prerequisite of this one. Do **not** add `scp-cf` again > here. If your `pom.xml` is missing it, that means the `sdk-replacement` step > didn't complete; rerun it instead of patching the dependency in by hand. > **Important:** OpenCMIS libraries have NOT been migrated to Jakarta EE. They still use `javax.*` namespaces. This is acceptable as they are self-contained. ### Step 3: Create Service Binding Accessor Copy [assets/ServiceBindingAccessor.java](assets/ServiceBindingAccessor.java) to your project: Key features: - Reads SDM service binding from VCAP_SERVICES - Retrieves OAuth2 token using client credentials - Caches token with automatic refresh ### Step 4: Create Document Service Client Copy these two files to your project (both declare `package com.example.document;`): - [assets/DocumentServiceClient.java](assets/DocumentServiceClient.java) — main client - [assets/RepositoryAlreadyExistsException.java](assets/RepositoryAlreadyExistsException.java) — checked exception thrown when `createRepository` is called for an existing repo Key features: - Creates CMIS sessions for repositories - Repository management — create, delete, list, check existence - OAuth2 authentication integration - Pre-checks existence before POSTing a create, because SDM is idempotent at the HTTP layer (see Common Issues below) — the alternative is silently double-creating, which the Neo `EcmService` never did. > **Identifier rule — the two endpoints take DIFFERENT identifiers, do not collapse them:** > > - **CMIS browser binding** (`SessionParameter.REPOSITORY_ID`) uses the repository **name**. > Verify by hitting `GET {ecmServiceUrl}/browser` — the response is a map keyed by names. > The REST GET response also has a `cmisRepositoryId` field, but that is the *root folder > ID*, not the CMIS repository id; using it produces `Repository 'xxx' not found!` from > OpenCMIS. > > - **REST DELETE** (`DELETE /rest/v2/repositories/{X}`) uses the SDM internal **UUID** — > the `id` field returned by `GET /rest/v2/repositories/`, e.g. `f3023e03-81da-4be4-...`. > Passing the name produces HTTP 500 with body > `{"message":"Repository with id:<name> is invalid. Please enter a valid repository ID."}`. > > The asset has **two** lookup methods to keep this distinction explicit: > `getRepositoryId(name)` → returns the name (for CMIS) and `getRepositoryUuid(name)` → > returns the UUID (for DELETE). Do not "simplify" them into one. If the Neo source called `ecmService.deleteRepository(name, ...)` or `ecmService.forceDeleteRepository(name, ...)`, route it to `documentClient.deleteRepository(name)`. The Neo `EcmService` distinguishes empty vs. non-empty deletion; SDM's REST API does not — use one method on the migrated side and let SDM return the appropriate error if the repo is non-empty. ### Step 5: Update Application Code **Before (Neo):** ```java import com.sap.ecm.api.EcmService; import com.sap.ecm.api.RepositoryOptions; import org.apache.chemistry.opencmis.client.api.Session; @Resource(name = "EcmService") private EcmService ecmService; public void handleDocument() { // Create repository RepositoryOptions options = new RepositoryOptions(); options.setUniqueName("my-repo"); options.setRepositoryKey("secret-key"); String repoId = ecmService.createRepository(options); // Get CMIS session Session session = ecmService.connect(repoId, "secret-key"); // Work with documents... } ``` **After (Cloud Foundry):** > **Note on the package**: the assets declare `package com.example.document;` as a placeholder. > Rename to your project's package (e.g. `com.acme.docs`) when you copy them in, and update the > imports below to match. > > **Note on servlet imports**: this is the Jakarta migration target — use `jakarta.servlet.*`, > not `javax.servlet.*`. The Neo originals will be `javax.*`; the `jakarta-java25-migration` > skill should have already rewritten them. ```java import com.example.document.DocumentServiceClient; import com.example.document.RepositoryAlreadyExistsException; import org.apache.chemistry.opencmis.client.api.Session; import org.apache.chemistry.opencmis.client.api.Folder; import org.apache.chemistry.opencmis.client.api.Document; import org.apache.chemistry.opencmis.client.api.CmisObject; import org.apache.chemistry.opencmis.commons.data.ContentStream; import org.apache.chemistry.opencmis.commons.enums.VersioningState; import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; public class DocumentServlet extends HttpServlet { private final DocumentServiceClient documentClient = new DocumentServiceClient(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String repoName = "my-repo"; // Create repository if not exists. createRepository will also throw // RepositoryAlreadyExistsException on its own pre-check; the explicit // repositoryExists() guard here just avoids the throw on the happy path. if (!documentClient.repositoryExists(repoName)) { try { documentClient.createRepository(repoName); } catch (RepositoryAlreadyExistsException e) { // Race: someone else created it between our check and call. // For the typical "ensure exists" flow this is fine — proceed. } } // Get CMIS session Session session = documentClient.getSession(repoName); // Get root folder Folder rootFolder = session.getRootFolder(); // List documents response.setContentType("application/json"); response.getWriter().println("{\"documents\": ["); boolean first = true; for (CmisObject obj : rootFolder.getChildren()) { if (!first) response.getWriter().println(","); response.getWriter().printf("{\"name\": \"%s\", \"type\": \"%s\"}", obj.getName(), obj.getType().getId()); first = false; } response.getWriter().println("]}"); } // Upload document example protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Session session = documentClient.getSession("my-repo"); Folder rootFolder = session.getRootFolder(); // Create document Map<String, Object> properties = new HashMap<>(); properties.put("cmis:objectTypeId", "cmis:document"); properties.put("cmis:name", "example.txt"); byte[] content = "Hello, World!".getBytes(StandardCharsets.UTF_8); ContentStream contentStream = session.getObjectFactory() .createContentStream("example.txt", content.length, "text/plain", new ByteArrayInputStream(content)); Document doc = rootFolder.createDocument(properties, contentStream, VersioningState.MAJOR); response.getWriter().println("Created document: " + doc.getId()); } // Delete repository example — replaces ecmService.deleteRepository(name, ...) // and ecmService.forceDeleteRepository(name, ...) from the Neo API. // The asset's deleteRepository() resolves the SDM internal UUID for you; // you pass the NAME at the API boundary. protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String repoName = request.getParameter("uniqueName"); try { documentClient.deleteRepository(repoName); response.setStatus(HttpServletResponse.SC_OK); } catch (CmisObjectNotFoundException e) { response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); } } // Create-repository handler — mirrors what Neo's ecmService.createRepository did, // including the "already exists" branch. The Neo EcmService threw on duplicate; // SDM does NOT (POST /rest/v2/repositories returns 201 even on duplicate). The // asset closes that gap by pre-checking and throwing RepositoryAlreadyExistsException, // which we map here to HTTP 412 — matching the contract of the original Neo app and // any integration test that expected PRECONDITION_FAILED on duplicate creation. protected void doPostCreateRepository(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String repoName = request.getParameter("uniqueName"); try { documentClient.createRepository(repoName); response.setStatus(HttpServletResponse.SC_CREATED); } catch (RepositoryAlreadyExistsException e) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, e.getMessage()); } } } ``` ### Step 6: Update MTA Descriptor Bind the SDM service to your application in `mtad.yaml`. **Only add what this skill owns** — the application module's `parameters:`/`properties:` block is set up by the `tomee-runtime` / `jakarta-java25-migration` skills; do not overwrite it here. Add the `requires:` entry to your existing app module: ```yaml modules: - name: ${app-name} # ... existing parameters/properties ... requires: - name: ${app-name}-sdm ``` And add the resource: ```yaml resources: - name: ${app-name}-sdm type: org.cloudfoundry.managed-service parameters: service: sdm service-plan: standard ``` ## Configuration Files No additional configuration files required. Service binding is read at runtime. ## CF Services | Service | Plan | Purpose | |---------|------|---------| | `sdm` | standard | Document Management Service | ## Verification ### 1. Compile Check ```bash mvn clean compile ``` ### 2. Deploy and Test ```bash mvn clean package cf deploy . -f # Test document operations curl "https://${app-url}/documents" ``` ### 3. Check Service Binding ```bash cf env ${app-name} | grep -A 30 "sdm" ``` ## Common Issues ### Issue: `ServiceConfigurationError: SapVcapServicesServiceBindingAccessor not a subtype` at runtime **Cause:** The WAR ships its own copy of `com.sap.cloud.environment.servicebinding.*` (typically because a direct compile-scope dependency on `java-sap-service-operator`, `java-sap-vcap-services`, or the `java-modules-bom` was added). The buildpack already provides those classes, so two versions end up in different classloaders and `ServiceLoader` rejects the in-WAR provider as "not a subtype" of the buildpack-provided interface. The error first surfaces as HTTP 500 on every SDM call, with the stacktrace bottoming out in `DefaultServiceBindingAccessor.<clinit>`.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub