Asset Manager API — AEM as a Cloud Service
Overview
com.day.cq.dam.api.AssetManager is not removed on AEM CS — but several of its operations are. The binary-path APIs that relied on direct filesystem access (createAssetForBinary, getAssetForBinary, removeAssetForBinary) do not exist on CS because the cloud runtime does not expose a filesystem path to the AEM JVM. Binary I/O for client uploads moves to Direct Binary Access — bytes flow directly between the client and Adobe's binary store, bypassing the JVM entirely.
API status on AEMaaCS:
| API | Status | What to use instead |
|---|---|---|
AssetManager.getAsset(path) |
✅ Supported | No change — still works for reads |
AssetManager.createAsset(path, InputStream, mimeType, doSave) |
⚠️ Strongly discouraged for client-facing uploads (2 GB binary limit, blocks the JVM, asset-processing pipeline expects Direct Binary Access). Still callable for in-JVM utilities where the binary is small and already in the JVM (bundled resources, fixtures, back-office imports). | Direct Binary Access for client uploads (@adobe/aem-upload JavaScript SDK). In-JVM small-file creation OK. |
AssetManager.createAssetForBinary(binaryFilePath, doSave) |
❌ Removed — relied on filesystem path the cloud runtime does not expose | Direct Binary Access |
AssetManager.getAssetForBinary(binaryFilePath) |
❌ Removed | resolver.getResource(repoPath).adaptTo(Asset.class) — look up by repository path, not binary path |
AssetManager.removeAssetForBinary(binaryFilePath, doSave) |
❌ Removed | In-JVM: resolver.delete(resource) + resolver.commit(). External: HTTP Assets API DELETE /api/assets{path} |
Three CS-specific principles:
| Principle | Why |
|---|---|
| Binary I/O for client uploads goes Direct Binary Access — never through the JVM | The cloud runtime does not have a JVM-accessible filesystem; bytes through the JVM are slow, memory-bound, and blocked by a 2 GB ceiling |
In-JVM delete uses resolver.delete() + resolver.commit() with a service user — never AEM calling its own HTTP API |
Self-looping HTTP adds latency, requires credentials you shouldn't store, and breaks idempotency |
External callers authenticate with IMS / dev-console bearer tokens — never hardcoded passwords or userId:password strings |
AEMaaCS has no admin user with a static password; IMS service credentials are the only supported external auth path |
Classification — choose before making any changes
Uses AssetManager.createAssetForBinary(...) or AssetManager.getAssetForBinary(...) (removed APIs)
→ Apply Path A (C1–C3).
Uses AssetManager.createAsset(path, InputStream, mimeType, doSave) in a client-facing servlet (accepts upload from a browser or external caller)
→ Apply Path A — migrate to Direct Binary Access (C2 client-facing branch).
Uses AssetManager.removeAssetForBinary(...) (removed API)
→ Apply Path B (D1–D3).
Uses both a removed create API AND a removed delete API → Apply Path A first, then Path B. Both transformations are independent.
Uses AssetManager.getAsset(path) only (read access)
→ Already compliant — no migration needed.
Uses AssetManager.createAsset(path, InputStream, mimeType, doSave) in an in-JVM back-office utility (scheduled import, test fixture, asset post-processing — binary is already in the JVM and small)
→ Already supported — verify the surrounding code uses a service-user resolver and closes the stream in try-with-resources, then skip.
One pattern per session. If the file has multiple flows, migrate one direction (create OR delete) at a time.
Before starting: Read ../references/aem-cloud-service-pattern-prerequisites.md and apply SCR→DS, service-user, and SLF4J fixes if present in the same changeset.
Discovery
Detection is performed by the analyzer (../scripts/analyze.sh), run by the runbook:
bash ../scripts/analyze.sh <workspace-root> --pattern asset-manager
Match criteria (what the detector flags): in a file importing com.day.cq.dam.api.AssetManager, a call to createAssetForBinary, getAssetForBinary, removeAssetForBinary, or createAsset. One finding per call site (each call is individually actionable, unlike the class-level patterns), with the call as the snippet. Parse-level only — gated on the import; the receiver type is not resolved, so an unrelated createAsset(...) in the same file could match (rare).
Resolution contract
guided — apply (guided). The analyzer reports each legacy call site; remediation is judgment-based, routed by the call (create/upload → C1–C3, delete → D1–D3) and applied in an apply session.
| Call site | Disposition |
|---|---|
createAssetForBinary / getAssetForBinary (removed on CS) |
apply (guided) → C1–C3 |
removeAssetForBinary (removed on CS) |
apply (guided) → D1–D3 |
Client-facing createAsset(...) upload |
apply (guided) → C1–C3 (Direct Binary Access) |
In-JVM back-office createAsset(...) with a service-user resolver |
skipped: already-compliant |
Test code (src/test/) |
skipped: test-scope |
Complete example — Path A (create / upload)
Before (client-facing upload via AssetManager.createAsset)
package com.example.servlets;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.AssetManager;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
@Component(immediate = true, metatype = false)
public class CreateAssetServlet extends SlingAllMethodsServlet {
@Reference
private AssetManager assetManager;
@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String assetPath = request.getParameter("path");
String mimeType = request.getParameter("mimeType");
InputStream inputStream = request.getInputStream();
try {
Asset asset = assetManager.createAsset(assetPath, inputStream, mimeType, true);
response.setContentType("text/plain");
response.getWriter().write("Asset created: " + asset.getPath());
} catch (Exception e) {
System.err.println("Error creating asset: " + e.getMessage());
e.printStackTrace();
response.setStatus(500);
response.getWriter().write("Error: " + e.getMessage());
}
}
}
After — Cloud Service compatible (client-side Direct Binary Access)
On AEMaaCS the upload happens directly between the client and the binary store; the AEM JVM orchestrates the upload but does not carry the bytes. Delete the upload-accepting servlet entirely, and replace it with a client-side call to the Direct Binary Access HTTP API via @adobe/aem-upload:
import DirectBinary from '@adobe/aem-upload';
/**
* Uploads a file to AEMaaCS via Direct Binary Access.
* @param {File|Blob} file The binary to upload
* @param {string} assetPath Repository destination, e.g. "/content/dam/my-site/file.pdf"
* @param {string} host AEM author host, e.g. "https://author-p123-e456.adobeaemcloud.com"
* @param {string} token IMS bearer token (Adobe Developer Console service credentials,
* or short-lived user token from the AEM login flow)
*/
async function uploadAsset(file, assetPath, host, token) {
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles([{
fileName: file.name,
blob: file,
fileSize: file.size
}])
.withHttpOptions({
headers: {
Authorization: `Bearer ${token}` // IMS / dev-console token — never a static password
}
});
return upload.uploadFiles(options);
}
If the servlet path must remain (routing, ACL, audit reasons), convert it to return 410 Gone with a documented replacement — do not silently call a removed API:
package com.example.servlets;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
@Component(service = Servlet.class, property = {
ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/createasset"
})
public class CreateAssetServlet extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(CreateAssetServlet.class);
@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
LOG.warn("Legacy upload endpoint hit — clients must use Direct Binary Access at /api/assets");
response.setStatus(410); // 410 Gone — the operation moved
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Use Direct Binary Access (POST /api/assets + @adobe/aem-upload) to upload assets.\"}");
}
}
Complete example — Path B (delete)
Before (removed removeAssetForBinary API)
package com.example.servlets;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import com.day.cq.dam.api.AssetManager;
import javax.servlet.ServletException;
import java.io.IOException;
@Component(immediate = true, metatype = false)
public class DeleteAssetServlet extends SlingAllMethodsServlet {
@Reference
private AssetManager assetManager;
@Override
protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String binaryFilePath = request.getParameter("path");
try {
boolean isDeleted = assetManager.removeAssetForBinary(binaryFilePath, true);
response.setContentType("text/plain");
if (isDeleted) {
response.getWriter().write("Asset deleted: " + binaryFilePath);
} else {
response.setStatus(404);
response.getWriter().write("Asset not found: " + binaryFilePath);
}
} catch (Exception e) {
System.err.println("Error deleting asset: " + e.getMessage());
e.printStackTrace();
response.setStatus(500);
response.getWriter().write("Error: " + e.getMessage());
}
}
}
After — in-JVM delete with a service-user resolver (recommended for code already inside AEM)
Server-side code that already has a trusted ResourceResolver should delete assets directly via the resource API. No HTTP, no credentials, no self-loopback through AEM's own API:
package com.example.servlets;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collections;
@Component(service = Servlet.class, property = {
ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/deleteasset"
})
public class DeleteAssetServlet extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(DeleteAssetServlet.class);
private static final String SUBSERVICE = "asset-admin-service";
@Reference
private ResourceResolverFactory resolverFactory;
@Override
protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String assetPath = request.getParameter("path");
if (assetPath == null || assetPath.isEmpty() || !assetPath.startsWith("/content/dam/")) {
response.setStatus(400);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"valid /content/dam path parameter required\"}");
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
response.setStatus(404);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"asset not found\"}");
return;
}
resolver.delete(resource);
resolver.commit();
response.setContentType("application/json");
response.getWriter().write("{\"success\":true}");
LOG.info("Deleted asset at {}", assetPath);
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice '{}'", SUBSERVICE, e);
response.setStatus(500);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"internal error\"}");
} catch (PersistenceException e) {
LOG.error("Commit failed while deleting asset {}", assetPath, e);
response.setStatus(500);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"internal error\"}");
}
}
}
After — external-caller delete via the HTTP Assets API
If the deleter is outside the AEM JVM (integration backend, CLI, worker), call the HTTP API with an IMS or dev-console bearer token — never a hardcoded password:
import axios from 'axios';
/**
* Deletes an asset from AEMaaCS via the HTTP Assets API.
* Caller must provide a bearer token from Adobe Developer Console service credentials
* or a short-lived user token from the AEM login flow.
*/
export async function deleteAsset({ host, assetPath, bearerToken }) {
const response = await axios.delete(`${host}/api/assets${assetPath}`, {
headers: { Authorization: `Bearer ${bearerToken}` },
validateStatus: status => status === 200 || status === 204 || status === 404
});
return response.status !== 404;
}
C1 — Replace createAssetForBinary / getAssetForBinary
These APIs do not exist on AEMaaCS. Remove every call.
// BEFORE (removed APIs)
assetManager.createAssetForBinary(binaryFilePath, doSave);
Asset asset = assetManager.getAssetForBinary(binaryFilePath);
Replacements:
| Legacy call | AEMaaCS replacement |
|---|---|
createAssetForBinary(binaryFilePath, doSave) |
Direct Binary Access via @adobe/aem-upload or HTTP POST /api/assets. The binary never sits on the AEM filesystem. |
getAssetForBinary(binaryFilePath) |
resolver.getResource(repoPath).adaptTo(Asset.class) using the repository path (not a filesystem path) |
// Direct Binary Access — client side
const DirectBinary = require('@adobe/aem-upload');
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles(uploadFiles)
.withHttpOptions({ headers: { Authorization: `Bearer ${token}` } });
await upload.uploadFiles(options);
// Look up by repository path — replaces getAssetForBinary
Resource resource = resolver.getResource("/content/dam/my-site/report.pdf");
Asset asset = resource != null ? resource.adaptTo(Asset.class) : null;
The legacy "binary path" concept (a filesystem path under the AEM install) does not exist on AEMaaCS. Anywhere your legacy code passed a binary path, replace with the asset's repository path (/content/dam/...).
C2 — Decide whether createAsset(path, InputStream, mimeType, overwrite) needs to change
Apply the decision matrix:
| Caller | Keep createAsset(...)? |
|---|---|
Client-facing servlet accepting multipart / InputStream from a browser or external caller |
No — migrate to Direct Binary Access |
| Back-office utility creating small assets from a bundled resource or another already-in-JVM stream (fixtures, reports, migration imports) | Yes, still supported — use a service-user resolver and close the stream |
| Scheduled asset ingestion pulling from an external source | Prefer Direct Binary Access through the HTTP API; the scheduler-triggered job acts as an external client |
When keeping createAsset for in-JVM use:
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"));
InputStream stream = bundleContext.getBundle().getResource(bundledPath).openStream()) {
AssetManager assetManager = resolver.adaptTo(AssetManager.class);
Asset asset = assetManager.createAsset(destPath, stream, mimeType, true);
LOG.info("Seeded asset at {}", asset.getPath());
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice 'asset-admin-service'", e);
} catch (IOException e) {
LOG.error("Failed to read bundled asset {}", bundledPath, e);
}
This is legitimate on AEMaaCS — it does not hit the removed binary APIs and does not accept arbitrary client uploads. Constraints:
- Service-user resolver in try-with-resources (no admin auth, no
USER/PASSWORDmaps) InputStreamin try-with-resources — leaking streams blocks bundle reloaddoSave=trueauto-commits; if you passfalse, callresolver.commit()explicitly- Binary size should be modest — for large files, prefer Direct Binary Access even from in-JVM callers
C3 — Update imports (Path A)
Remove (when createAssetForBinary / getAssetForBinary are gone):
import com.day.cq.dam.api.metadata.MetaDataMap; // if only used for deprecated flow
Keep com.day.cq.dam.api.Asset and com.day.cq.dam.api.AssetManager when createAsset / getAsset are still in use.
Remove (SCR → DS):
import org.apache.felix.scr.annotations.*;
Add — servlet shell:
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
Add — in-JVM createAsset utility:
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Reference;
import java.io.InputStream;
import java.util.Collections;
D1 — Replace removeAssetForBinary with ResourceResolver#delete (in-JVM)
// BEFORE (removed API)
boolean isAssetDeleted = assetManager.removeAssetForBinary(binaryFilePath, doSave);
// AFTER — in-JVM with service-user resolver
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"))) {
Resource resource = resolver.getResource(assetPath); // repository path, not binary path
if (resource == null) {
LOG.info("Asset already gone at {}", assetPath);
return; // idempotent — treat "not found" as success, not failure
}
resolver.delete(resource);
resolver.commit();
LOG.info("Deleted asset at {}", assetPath);
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice 'asset-admin-service'", e);
} catch (PersistenceException e) {
LOG.error("Commit failed while deleting asset {}", assetPath, e);
}
Rules:
assetPathis the repository path (e.g./content/dam/site/file.jpg). The legacy "binary path" concept does not exist on AEMaaCS.- Service-user privileges must include
jcr:removeNodeandjcr:removeChildNodeson the asset tree (see Repoinit setup below). - Do not call AEM's own HTTP API (
/api/assets) from inside the JVM — it adds latency, requires credentials the JVM shouldn't store, and breaks idempotency. Use the resource API directly. - Treat "resource is null" (asset already gone) as a successful no-op, not a failure — delete operations should be idempotent on retry.
- Validate the path parameter before deletion (e.g.
path.startsWith("/content/dam/")) — a poorly-validated delete endpoint is a destructive vulnerability.
D2 — External callers use the HTTP Assets API with a bearer token
For deleters running outside the AEM JVM (integration backends, CLIs, ingestion workers), call the HTTP API:
import axios from 'axios';
export async function deleteAsset({ host, assetPath, bearerToken }) {
const response = await axios.delete(`${host}/api/assets${assetPath}`, {
headers: { Authorization: `Bearer ${bearerToken}` },
validateStatus: status => status === 200 || status === 204 || status === 404
});
return response.status !== 404;
}
Where bearerToken comes from:
- Adobe Developer Console / IMS service credentials (server-to-server JWT exchange producing a short-lived bearer token), or
- A short-lived user token obtained via the AEM login flow (for human-driven CLIs)
Never hardcode usernames and passwords. Never pass ":password" strings or basic auth literals. AEMaaCS has no static admin password.
// WRONG — hardcoded credentials, no admin password exists on AEMaaCS
const response = await axios.delete(url, {
headers: { Authorization: 'Basic ' + Buffer.from('admin:admin').toString('base64') }
});
// RIGHT — IMS bearer token
const response = await axios.delete(url, {
headers: { Authorization: `Bearer ${imsToken}` }
});
D3 — Update imports (Path B)
Remove (when all AssetManager delete calls are gone):
import com.day.cq.dam.api.AssetManager; // remove if no other AssetManager usage remains in the file
Keep AssetManager if the file still uses AssetManager.getAsset(...) for reads.
Remove (SCR → DS):
import org.apache.felix.scr.annotations.*;
Add — in-JVM delete servlet:
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collections;
Repoinit and service-user mapping
The in-JVM create/delete operations acquire a resolver via SUBSERVICE. On AEM CS the backing system user must be created via Repoinit and mapped through ServiceUserMapperImpl.amended. The classic UI-based user admin is not available.
Repoinit (in ui.config):
create service user asset-admin-service
set ACL for asset-admin-service
allow jcr:read,rep:write,jcr:removeNode,jcr:removeChildNodes on /content/dam
end
The jcr:removeNode and jcr:removeChildNodes privileges are required for delete. rep:write alone is not sufficient. Without them, resolver.delete(resource) succeeds in memory but resolver.commit() throws AccessDeniedException.
Service-user mapping (ui.config, file named e.g. org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-asset-admin.cfg.json):
{
"user.mapping": [
"com.example.mybundle:asset-admin-service=[asset-admin-service]"
]
}
Mapping conventions:
- Use the amend form — do not edit the base
ServiceUserMapperImplconfig - Use principal-name mapping: the square-bracket form
=[service-user-name] - Do not use the deprecated
userNameform - Bundle symbolic name on the left of
:must match the bundle owning the servlet / utility - Permissions must be declared directly on the service user — do not rely on group inheritance
See ../references/aem-cloud-service-pattern-prerequisites.md for the full Repoinit workflow.
Greenfield — writing new asset operations
For new asset code (not migrating legacy), the rules collapse to three patterns:
Reading an asset — use the resource API; do not adapt to AssetManager unnecessarily:
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-reader-service"))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
return Optional.empty();
}
Asset asset = resource.adaptTo(Asset.class);
return Optional.ofNullable(asset);
} catch (LoginException e) {
LOG.error("Could not open service resolver", e);
return Optional.empty();
}
Uploading from a client — never write an upload-accepting servlet. The client calls Direct Binary Access:
import DirectBinary from '@adobe/aem-upload';
async function uploadAsset(file, assetPath, host, token) {
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles([{ fileName: file.name, blob: file, fileSize: file.size }])
.withHttpOptions({ headers: { Authorization: `Bearer ${token}` } });
return upload.uploadFiles(options);
}
Deleting from in-JVM code — service-user resolver + delete() + commit():
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
return; // idempotent — already gone
}
resolver.delete(resource);
resolver.commit();
} catch (LoginException e) {
LOG.error("Could not open service resolver", e);
} catch (PersistenceException e) {
LOG.error("Commit failed for {}", assetPath, e);
}
What this captures:
- No client-facing upload servlets — every upload goes Direct Binary Access
getResource().adaptTo(Asset.class)for reads — direct AssetManager.adaptTo is rarely needed- Service-user resolver everywhere — never admin, never
USER/PASSWORDmaps - Idempotent delete — null resource is a successful no-op
Composition with other skills
Trigger replication after a delete: combine with the replication skill:
// Inside the delete code path, after resolver.commit()
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.DELETE, false, assetPath);
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (!response.isSuccessful()) {
LOG.warn("Delete distribution failed for {}: {}", assetPath, response.getMessage());
}
React to asset add/remove events: use the resource-change-listener skill to observe /content/dam, then a JobConsumer to handle the business logic. Do not put asset processing inside onChange().
Hook into the asset processing pipeline: Adobe-managed asset processing runs out-of-band on AEMaaCS. To act after processing completes (renditions ready, metadata extracted), listen for the relevant DAM workflow completion event via the event-migration skill.
Review Checklist
Cross-cutting:
- No
createAssetForBinary(,getAssetForBinary(, orremoveAssetForBinary(calls remain - No
getAdministrativeResourceResolver()orUSER/PASSWORDauth maps - No hardcoded credentials anywhere (
":password", literal bearer tokens, basic-auth literals) -
getServiceResourceResolver(SUBSERVICE)used in try-with-resources for any in-JVM AEM operation - OSGi DS R6 annotations — no
org.apache.felix.scr.annotations.*imports - SLF4J logging — no
System.out/System.err/printStackTrace
Path A (create / upload):
- Client-facing upload endpoints replaced with Direct Binary Access (
@adobe/aem-upload) — any residual servlet returns410 Gonewith a documented replacement -
createAsset(path, InputStream, mimeType, overwrite)remains only in in-JVM utilities where the binary is small and already in the JVM (fixtures, bundled resources, post-processing) -
InputStreamclosed in try-with-resources - Service-user resolver used for in-JVM
createAssetflows - No
getAssetForBinary—resolver.getResource(repoPath).adaptTo(Asset.class)used instead - No reference to a "binary file path" — only repository paths (
/content/dam/...)
Path B (delete):
- In-JVM deletes use
resolver.delete(resource) + resolver.commit()— not AEM calling its own HTTP API -
resource == nulltreated as successful no-op (idempotent delete) - Path parameter validated (e.g.
startsWith("/content/dam/")) before delete -
PersistenceExceptioncaught separately fromLoginExceptionand logged with the path - Service user has
jcr:removeNodeandjcr:removeChildNodes(not justrep:write) - External-caller examples use IMS bearer tokens — not basic auth
Configuration:
- Service user created via Repoinit in
ui.configwith correct ACL privileges for the operation (read for reads, removeNode for deletes) -
ServiceUserMapperImpl.amended-*.cfg.jsonmaps<bundle>:<subservice>→ the service user (principal-name form) - Bundle symbolic name in the mapping matches the bundle owning the servlet / utility
-
mvn clean installsucceeds with no SCR-related or deprecated-API warnings
Troubleshooting
| Symptom | Log to search | Fix direction |
|---|---|---|
NoSuchMethodError: AssetManager.createAssetForBinary at runtime |
the exception name verbatim | Legacy binary-path API isn't on the CS SDK classpath — complete the C1 migration |
NoSuchMethodError: AssetManager.removeAssetForBinary at runtime |
the exception name verbatim | Same as above — complete the D1 migration |
| Upload endpoint returns 500 or times out under load | client-side socket timeout or payload too large; server-side may show memory pressure |
Client is uploading through the JVM via legacy servlet — migrate to Direct Binary Access; the bytes should never flow through AEM's JVM |
LoginException: Unable to retrieve the service resource resolver |
the exception name verbatim | Service user not provisioned via Repoinit, OR ServiceUserMapperImpl.amended-*.cfg.json missing/misnamed, OR bundle symbolic name in mapping doesn't match the bundle owning the servlet |
AccessDeniedException on resolver.commit() after resolver.delete() |
AccessDeniedException near a commit() call |
Service user lacks jcr:removeNode and/or jcr:removeChildNodes on the asset tree — extend Repoinit ACL |
| Delete servlet returns 200 but the asset is still present | none | Code path is missing resolver.commit() — delete() only stages the operation; without commit(), try-with-resources closes the resolver and silently discards the pending change |
External caller gets 401 Unauthorized on /api/assets |
server-side 401 or WWW-Authenticate: Bearer |
Caller is using basic auth with a hardcoded password — switch to an IMS / dev-console bearer token |
| Direct Binary Access upload fails with CORS error | browser console CORS policy: No 'Access-Control-Allow-Origin' |
Client origin not configured in AEMaaCS Cross-Origin Resource Sharing OSGi config — add the origin to the allowlist |
| Direct Binary Access upload completes but processed renditions never appear | none synchronously | Asset processing is asynchronous on AEMaaCS — verify in the DAM admin UI; if stuck, check the asset processing pipeline status in Cloud Manager |
Common Pitfalls
Calling createAssetForBinary / getAssetForBinary / removeAssetForBinary — these APIs do not exist on AEMaaCS. The compile-time signature is gone; if a build succeeds against legacy AEM jars and then deploys to CS, every call throws NoSuchMethodError at runtime.
Accepting client uploads via a Java servlet that calls createAsset(path, InputStream, ...) — funnels every byte through the AEM JVM. Slow, memory-bound, blocked by a 2 GB ceiling, and bypasses the asset processing pipeline's Direct Binary Access path. Migrate to @adobe/aem-upload on the client.
Hardcoding credentials — Authorization: Basic Buffer.from('admin:admin').toString('base64'), literal ":password" strings, plain-text bearer tokens. AEMaaCS has no static admin password; external callers must use IMS / dev-console service credentials that produce short-lived bearer tokens.
AEM calling its own HTTP API from inside the JVM — DELETE /api/assets/... from a Java servlet adds latency, requires credentials the JVM shouldn't hold, and breaks idempotency. Use the resource API directly (resolver.delete() + resolver.commit()).
Forgetting resolver.commit() after resolver.delete() — try-with-resources closes the resolver and silently discards the pending delete. Always commit before the resolver closes.
Treating "asset not found" as an error — delete operations should be idempotent. If the asset is already gone, return success (200/204), not 404 as a failure. Otherwise retries against a now-deleted resource fail unnecessarily.
Service user with only rep:write — rep:write does NOT include node removal. The Repoinit ACL must grant jcr:removeNode and jcr:removeChildNodes for delete operations to succeed.
Confusing repository path with binary path — getAssetForBinary("/var/dam-binaries/foo.jpg") is gone. The new world only has repository paths (/content/dam/site/foo.jpg). Any code path that constructed a "binary path" should be deleted along with the legacy API call.
Not validating the path parameter on a delete endpoint — request.getParameter("path") without validation is a destructive vulnerability. Anchor to /content/dam/ (or your tenant's subtree) before calling resolver.delete().
Catching Exception broadly and swallowing the cause — LoginException, PersistenceException, and IOException carry distinct failure modes. Catch them separately and log with the asset path so audit logs are actionable.
Modern Alternatives
| Need | Use |
|---|---|
| Read an asset (path lookup, metadata, renditions) | resolver.getResource(path).adaptTo(Asset.class) — AssetManager.getAsset(path) is also supported |
| Upload from a browser or external client | Direct Binary Access via @adobe/aem-upload (HTTP POST /api/assets) |
| Create a small asset from a bundled resource (in-JVM) | AssetManager.createAsset(path, InputStream, mimeType, true) with a service-user resolver — still supported |
| Delete from in-JVM code (servlet, scheduler, JobConsumer) | resolver.delete(resource) + resolver.commit() with a service-user resolver |
| Delete from outside AEM (integration backend, CLI) | HTTP DELETE /api/assets{path} with an IMS / dev-console bearer token |
| Update metadata on an existing asset | resource.adaptTo(ModifiableValueMap.class) on /content/dam/.../jcr:content/metadata + resolver.commit(). For external callers: PUT /api/assets{path}.json |
| React to asset events (added, processed, deleted) | ResourceChangeListener on /content/dam (see resource-change-listener skill) |
| Trigger replication after asset operations | Distributor.distribute(...) (see replication skill) |
| Hook the asset processing pipeline | Asset processing runs out-of-band on AEMaaCS — listen for DAM workflow completion events via event-migration skill, not the legacy dam/update_asset workflow internals |
| Bulk asset import / migration | Direct Binary Access from the migration tool, OR Adobe-managed bulk import (Cloud Manager) — not a JVM loop calling createAsset |
Expert Guidance
Binary I/O does not flow through the AEM JVM on AEMaaCS. This is the single biggest architectural shift. Direct Binary Access uploads negotiate a short-lived URL between the client and Adobe's binary store; the AEM JVM only handles metadata and the post-upload complete call. Any pattern that involves the JVM holding bytes (large InputStream over a servlet, createAssetForBinary, in-memory image manipulation before upload) belongs to the legacy AEM 6.x architecture and won't perform on CS.
Asset processing is asynchronous and Adobe-managed. When Direct Binary Access completes the upload, the asset is queued for processing — renditions, metadata extraction, smart tags. The completion of the upload does NOT mean renditions are ready. Code that depends on processed renditions must either:
- Listen for DAM workflow completion events
- Poll the asset's
jcr:content/renditionspath with backoff - Accept eventual consistency and not block on rendition availability
Authentication splits two ways on AEMaaCS. In-JVM code uses a service-user resolver — no credentials, no tokens, just a SUBSERVICE name resolved by ServiceUserMapperImpl.amended. External callers use IMS or dev-console bearer tokens — short-lived, scope-limited, never hardcoded. There is no third option. Hardcoded passwords, basic auth literals, and "admin" accounts do not exist on CS.
Delete operations should be idempotent. Treat resource == null as success; the result of "asset is not present" is the same as "asset was deleted successfully". This matters for retry semantics on JobConsumers and external callers — non-idempotent delete leads to flaky tests and
…(truncated)