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.

Quellinformationen

Repository
SAP-samples/btp-neo-java-app-migration
Letzte Quellaktivität
22. Juli 2026 um 13:02
Erkannte Sprache von SKILL.md
Englisch
Sterne
6
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
4 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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>`.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen