diff --git a/.gitignore b/.gitignore index 598e895..780ab41 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ node_modules/ # Temporary files drafts/tmp/ +# OMC state +.omc/ + # Release artifacts *.skill dist/ diff --git a/skills/aem/cloud-service/skills/content-distribution/README.md b/skills/aem/cloud-service/skills/content-distribution/README.md new file mode 100644 index 0000000..66d6c2e --- /dev/null +++ b/skills/aem/cloud-service/skills/content-distribution/README.md @@ -0,0 +1,382 @@ +# AEM as a Cloud Service Content Distribution Skills + +Programmatic content publishing and distribution monitoring for Adobe Experience Manager as a Cloud Service. + +## Overview + +These skills cover the official APIs for content replication and distribution event handling in AEM Cloud Service: + +1. **Replication API** (`com.day.cq.replication`) - Programmatic content publishing +2. **Sling Distribution Events** (`org.apache.sling.distribution.event`) - Distribution lifecycle monitoring + +## Official Documentation + +- **Replication API Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/com/day/cq/replication/package-summary.html +- **Sling Distribution Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html +- **Adobe Replication Guide**: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/operations/replication.html + +## Skills Included + +### 1. Replication API (`replication/SKILL.md`) + +Use the official AEM Replication API to programmatically publish and unpublish content. + +**What it covers**: +- `Replicator` service usage with code examples +- Publishing to Publish and Preview tiers +- Bulk replication (with rate limits) +- `ReplicationOptions` for advanced configuration +- Replication status checks +- Permission validation +- Workflow integration +- Event handling for replication events + +**When to use**: +- Custom OSGi services that publish content +- Workflow process steps +- Automated publishing pipelines +- Integration with external systems + +**Official API**: `com.day.cq.replication.Replicator` + +### 2. Sling Distribution Event Handling (`sling-distribution/SKILL.md`) + +Monitor and react to content distribution lifecycle events. + +**What it covers**: +- Distribution event topics (package created, queued, distributed, dropped, imported) +- Event properties and metadata +- OSGi event handler examples +- Common use cases (alerting, cache warming, analytics) +- Queue monitoring patterns +- Distribution request types + +**When to use**: +- Monitor distribution lifecycle +- Trigger post-distribution actions (cache warming, notifications) +- Distribution auditing and logging +- Handle distribution failures +- Integration workflows + +**Official API**: `org.apache.sling.distribution.event` + +## How They Work Together + +### Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Code: Replication API │ +│ replicator.replicate(session, ACTIVATE, "/content/...") │ +└──────────────────────┬──────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Sling Distribution: Underlying Transport │ +│ │ +│ [AGENT_PACKAGE_CREATED] ← Event fires │ +│ ↓ │ +│ [AGENT_PACKAGE_QUEUED] ← Event fires │ +│ ↓ │ +│ [AGENT_PACKAGE_DISTRIBUTED] ← Event fires │ +│ ↓ │ +│ Adobe Developer Pipeline Service │ +│ ↓ │ +│ [IMPORTER_PACKAGE_IMPORTED] ← Event fires on target │ +└─────────────────────────────────────────────────────────┘ + ↓ + Content is live on Publish/Preview +``` + +### Workflow Example + +**Scenario**: Publish a page and warm the CDN cache after distribution completes. + +**Step 1**: Use Replication API to publish: +```java +@Reference +private Replicator replicator; + +// Publish content +replicator.replicate(session, ReplicationActionType.ACTIVATE, "/content/mysite/en/page"); +``` + +**Step 2**: Listen for distribution completion: +```java +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED + } +) +public class CacheWarmingHandler implements EventHandler { + + @Override + public void handleEvent(Event event) { + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + for (String path : paths) { + warmCache(path); // Custom cache warming logic + } + } +} +``` + +## Key Differences from AEM 6.x + +| Aspect | AEM 6.x | AEM Cloud Service | +|--------|---------|-------------------| +| **Replication API** | `com.day.cq.replication.Replicator` | ✅ Same API available | +| **Transport** | Direct JCR replication | Sling Distribution via Adobe Developer pipeline | +| **Agents** | Manual configuration | Automatic (managed by Adobe) | +| **Preview Tier** | Not available | Available with agent filtering | +| **Distribution Events** | Limited | Full lifecycle events via `org.apache.sling.distribution.event` | + +## Rate Limits and Constraints + +### Replication API Limits + +| Constraint | Limit | +|-----------|-------| +| Paths per API call (recommended) | 100 | +| Paths per API call (hard limit) | 500 | +| Transactional guarantee | ≤100 paths only | +| Payload size | 10 MB maximum | + +### Best Practices + +1. **Respect rate limits**: ≤100 paths per call for transactional guarantee +2. **Use workflow for large operations**: Tree Activation workflow step for >500 paths +3. **Handle failures gracefully**: Always catch `ReplicationException` +4. **Use service users**: Never replicate with admin credentials +5. **Publish only what's needed**: Avoid unnecessary bulk operations +6. **Monitor events**: Use Sling Distribution events for observability + +## Common Use Cases + +### Use Case 1: Auto-Publish Content Fragments + +```java +// Listen for content fragment changes +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + SlingConstants.TOPIC_RESOURCE_CHANGED +}) +public class AutoPublishHandler implements EventHandler { + + @Reference + private Replicator replicator; + + @Reference + private ResourceResolverFactory resolverFactory; + + @Override + public void handleEvent(Event event) { + String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); + + if (isContentFragment(path)) { + try (ResourceResolver resolver = getServiceResolver()) { + Session session = resolver.adaptTo(Session.class); + replicator.replicate(session, ReplicationActionType.ACTIVATE, path); + } catch (Exception e) { + LOG.error("Auto-publish failed", e); + } + } + } + + private ResourceResolver getServiceResolver() throws LoginException { + Map param = Map.of( + ResourceResolverFactory.SUBSERVICE, "contentPublisher" + ); + return resolverFactory.getServiceResourceResolver(param); + } + + private boolean isContentFragment(String path) { + return path != null && path.startsWith("/content/dam") && + path.contains("/jcr:content"); + } +} +``` + +### Use Case 2: Alert on Distribution Failures + +```java +// Monitor for dropped packages +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED +}) +public class FailureAlertHandler implements EventHandler { + + @Override + public void handleEvent(Event event) { + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + LOG.error("Distribution failed: packageId={}, paths={}", + packageId, String.join(",", paths)); + + // Send alert to operations team + alertService.sendAlert("Distribution failure", packageId); + } +} +``` + +### Use Case 3: CDN Cache Warming + +```java +// Warm cache after successful distribution +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED +}) +public class CacheWarmingHandler implements EventHandler { + + @Override + public void handleEvent(Event event) { + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + for (String path : paths) { + String url = convertToPublicUrl(path); + httpClient.execute(new HttpGet(url)); // Warm cache + } + } +} +``` + +### Use Case 4: Publish to Preview for Approval Workflow + +```java +// Workflow step: Publish to Preview for review +@Component(service = WorkflowProcess.class, property = { + "process.label=Publish to Preview for Review" +}) +public class PublishToPreviewStep implements WorkflowProcess { + + @Reference + private Replicator replicator; + + @Override + public void execute(WorkItem workItem, WorkflowSession workflowSession, + MetaDataMap args) throws WorkflowException { + + String payloadPath = workItem.getWorkflowData().getPayload().toString(); + Session session = workflowSession.adaptTo(Session.class); + + // Create options to target Preview tier + ReplicationOptions options = new ReplicationOptions(); + options.setFilter(agent -> "preview".equals(agent.getId())); + + try { + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + payloadPath, + options + ); + } catch (ReplicationException e) { + throw new WorkflowException("Failed to publish to Preview", e); + } + } +} +``` + +## Quick Reference + +### Replication API - Key Classes + +```java +// Inject the service +@Reference +private Replicator replicator; + +// Single path +replicator.replicate(session, ReplicationActionType.ACTIVATE, "/content/page"); + +// Multiple paths +replicator.replicate(session, ReplicationActionType.ACTIVATE, + new String[]{"/content/page1", "/content/page2"}, null); + +// With options +ReplicationOptions options = new ReplicationOptions(); +options.setFilter(agent -> "preview".equals(agent.getId())); +replicator.replicate(session, ReplicationActionType.ACTIVATE, "/content/page", options); + +// Check status +ReplicationStatus status = replicator.getReplicationStatus(session, "/content/page"); +boolean isPublished = status != null && status.isActivated(); +``` + +### Distribution Events - Key Topics + +```java +// Listen for events +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_QUEUED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED +}) +public class MyDistributionHandler implements EventHandler { + + @Override + public void handleEvent(Event event) { + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + // Handle event + } +} +``` + +## Troubleshooting + +### Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| ReplicationException | Permission denied | Check service user has `crx:replicate` permission | +| Content not appearing | Wrong tier selected | Verify agent filter targets correct tier (preview vs publish) | +| Event handler not firing | Wrong event topic | Use exact constants from `DistributionEventTopics` | +| "Too many paths" error | Exceeded 500 path limit | Batch requests or use Tree Activation workflow | + +### Debug Checklist + +1. **Replication fails**: + - Check logs for `ReplicationException` + - Verify user has `crx:replicate` permission on content path + - Check Sling jobs console: `/system/console/slingjobs` + +2. **Events not firing**: + - Verify component is active: `/system/console/components` + - Check event topic matches exactly + - Verify OSGi event handler properties + +3. **Content not on target tier**: + - Check replication status: `replicator.getReplicationStatus()` + - Verify agent filter targets correct tier + - Check distribution logs in Cloud Manager + +## References + +- **Replication Skill**: [replication/SKILL.md](./replication/SKILL.md) +- **Sling Distribution Skill**: [sling-distribution/SKILL.md](./sling-distribution/SKILL.md) +- **Official Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/ +- **Adobe Documentation**: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/operations/replication.html diff --git a/skills/aem/cloud-service/skills/content-distribution/SKILL.md b/skills/aem/cloud-service/skills/content-distribution/SKILL.md new file mode 100644 index 0000000..d84abf4 --- /dev/null +++ b/skills/aem/cloud-service/skills/content-distribution/SKILL.md @@ -0,0 +1,347 @@ +--- +name: aem-content-distribution +status: beta +description: | + [BETA] AEM as a Cloud Service content distribution and replication. Covers programmatic publishing + using the Replication API and distribution event monitoring using Sling Distribution events. + This skill is in beta. Verify all outputs before applying them to production projects. +--- + +# AEM Cloud Service Content Distribution + +> **Beta Skill**: This skill is in beta and under active development. +> Results should be reviewed carefully before use in production. +> Report issues at https://github.com/adobe/skills/issues + +Programmatic content publishing and distribution monitoring using official AEM Cloud Service APIs. + +## When to Use This Skill + +Use this skill collection for: +- **Programmatic publishing**: Publish content via `Replicator` API +- **Distribution monitoring**: Track distribution lifecycle events +- **Automated workflows**: Integration with workflow process steps +- **Event handling**: React to distribution events (failures, completions) +- **Custom publishing logic**: Bulk operations, Preview tier publishing + +## Sub-Skills + +This is a parent skill that routes to specialized sub-skills based on your task: + +| Task | Sub-Skill | File | +|------|-----------|------| +| Programmatically publish/unpublish content | Replication API | [replication/SKILL.md](./replication/SKILL.md) | +| Monitor distribution events and lifecycle | Sling Distribution Events | [sling-distribution/SKILL.md](./sling-distribution/SKILL.md) | + +## Quick Decision Guide + +**Choose Replication API** when you need to: +- Publish content from custom OSGi services +- Integrate publishing into workflow steps +- Perform bulk publishing operations +- Publish to Preview tier for review +- Check replication status programmatically + +**Choose Sling Distribution Events** when you need to: +- Monitor distribution lifecycle (created, queued, distributed, imported) +- React to distribution failures +- Trigger post-distribution actions (cache warming, notifications) +- Audit distribution operations +- Track distribution metrics + +## Official APIs + +Both skills use official, supported AEM Cloud Service APIs: + +1. **Replication API**: `com.day.cq.replication` + - **Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/com/day/cq/replication/package-summary.html + - **Main Classes**: `Replicator`, `ReplicationOptions`, `ReplicationStatus`, `ReplicationActionType` + +2. **Sling Distribution API**: `org.apache.sling.distribution` + - **Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html + - **Main Packages**: `org.apache.sling.distribution.event` (event topics and properties) + +## Architecture Overview + +``` +┌──────────────────────────────────────────────────┐ +│ Replication API (Your Code) │ +│ com.day.cq.replication.Replicator │ +│ │ +│ replicator.replicate(session, ACTIVATE, path) │ +└────────────────────┬─────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────┐ +│ Sling Distribution (Underlying Transport) │ +│ org.apache.sling.distribution │ +│ │ +│ [AGENT_PACKAGE_CREATED] ← Distribution events │ +│ ↓ fire at each stage │ +│ [AGENT_PACKAGE_QUEUED] │ +│ ↓ │ +│ [AGENT_PACKAGE_DISTRIBUTED] │ +│ ↓ │ +│ Adobe Developer Pipeline Service │ +│ ↓ │ +│ [IMPORTER_PACKAGE_IMPORTED] │ +└──────────────────────────────────────────────────┘ + ↓ + Content live on Publish/Preview +``` + +### How It Works + +1. **Your code** calls `Replicator.replicate()` to publish content +2. **Sling Distribution** packages content and fires `AGENT_PACKAGE_CREATED` event +3. Package is **queued** and `AGENT_PACKAGE_QUEUED` event fires +4. Package is **sent** to Adobe Developer pipeline and `AGENT_PACKAGE_DISTRIBUTED` event fires +5. Target tier **imports** content and `IMPORTER_PACKAGE_IMPORTED` event fires +6. Content is **live** on target tier (Publish or Preview) + +## Common Patterns + +### Pattern 1: Publish and Monitor + +Publish content and track when it goes live: + +```java +// Step 1: Publish using Replication API +@Reference +private Replicator replicator; + +public void publishContent(Session session, String path) throws ReplicationException { + replicator.replicate(session, ReplicationActionType.ACTIVATE, path); +} + +// Step 2: Monitor completion using Distribution Events +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED +}) +public class PublishCompletionHandler implements EventHandler { + + @Override + public void handleEvent(Event event) { + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + LOG.info("Content is now live: {}", String.join(",", paths)); + // Trigger post-publish actions (cache warming, notifications, etc.) + } +} +``` + +### Pattern 2: Preview-First Workflow + +Publish to Preview for approval, then to Publish: + +```java +// Workflow Step 1: Publish to Preview +public void publishToPreview(Session session, String path) throws ReplicationException { + ReplicationOptions options = new ReplicationOptions(); + options.setFilter(agent -> "preview".equals(agent.getId())); + + replicator.replicate(session, ReplicationActionType.ACTIVATE, path, options); +} + +// Workflow Step 2: After approval, publish to Publish tier +public void publishToProduction(Session session, String path) throws ReplicationException { + ReplicationOptions options = new ReplicationOptions(); + options.setFilter(agent -> "publish".equals(agent.getId())); + + replicator.replicate(session, ReplicationActionType.ACTIVATE, path, options); +} +``` + +### Pattern 3: Auto-Publish with Failure Handling + +Auto-publish content and alert on failures: + +```java +// Publish handler +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + SlingConstants.TOPIC_RESOURCE_CHANGED +}) +public class AutoPublishHandler implements EventHandler { + + @Reference + private Replicator replicator; + + @Override + public void handleEvent(Event event) { + String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); + + if (shouldAutoPublish(path)) { + try (ResourceResolver resolver = getServiceResolver()) { + Session session = resolver.adaptTo(Session.class); + replicator.replicate(session, ReplicationActionType.ACTIVATE, path); + } catch (Exception e) { + LOG.error("Auto-publish failed", e); + } + } + } +} + +// Failure monitoring +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED +}) +public class FailureAlertHandler implements EventHandler { + + @Reference + private AlertService alertService; + + @Override + public void handleEvent(Event event) { + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + + alertService.sendAlert("Distribution failed", packageId); + } +} +``` + +## Rate Limits and Constraints + +| Constraint | Limit | Impact | +|-----------|-------|--------| +| Paths per API call (recommended) | 100 | Transactional guarantee | +| Paths per API call (hard limit) | 500 | Beyond this, call fails | +| Payload size | 10 MB | Excluding binaries | +| Use atomic calls | false | For >100 paths, enables auto-bucketing | + +**Best Practice**: For operations >500 paths, use Tree Activation workflow step instead of custom code. + +## Key Differences from AEM 6.x + +| Feature | AEM 6.x | AEM Cloud Service | +|---------|---------|-------------------| +| Replication API | `com.day.cq.replication.Replicator` | ✅ Same API | +| Replication agents | Manual configuration | ✅ Automatic (managed by Adobe) | +| Transport mechanism | Direct JCR replication | ✅ Sling Distribution via Adobe pipeline | +| Preview tier | Not available | ✅ Available (requires agent filtering) | +| Distribution events | Limited | ✅ Full lifecycle via `org.apache.sling.distribution.event` | +| Agent configuration | Manual OSGi config | ❌ Not exposed (managed by Adobe) | + +## When NOT to Use These Skills + +**Use UI workflows instead** when: +- Publishing small amounts of content manually +- One-off publishing operations +- Content authors can use Quick Publish or Manage Publication + +**Use Tree Activation workflow** when: +- Publishing >500 paths +- Large hierarchical content trees +- Don't need custom logic + +## Quick Reference + +### Replication API Basics + +```java +// Inject service +@Reference +private Replicator replicator; + +// Publish single page +replicator.replicate(session, ReplicationActionType.ACTIVATE, "/content/mysite/page"); + +// Unpublish +replicator.replicate(session, ReplicationActionType.DEACTIVATE, "/content/mysite/page"); + +// Bulk publish (≤100 for transactional guarantee) +replicator.replicate(session, ReplicationActionType.ACTIVATE, + new String[]{"/content/page1", "/content/page2"}, null); + +// Publish to Preview +ReplicationOptions options = new ReplicationOptions(); +options.setFilter(agent -> "preview".equals(agent.getId())); +replicator.replicate(session, ReplicationActionType.ACTIVATE, "/content/page", options); + +// Check status +ReplicationStatus status = replicator.getReplicationStatus(session, "/content/page"); +boolean isPublished = status != null && status.isActivated(); +``` + +### Distribution Event Handling Basics + +```java +// Listen for distribution events +@Component(service = EventHandler.class, property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED +}) +public class DistributionMonitor implements EventHandler { + + @Override + public void handleEvent(Event event) { + String topic = event.getTopic(); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + // Handle event based on topic + if (DistributionEventTopics.AGENT_PACKAGE_DROPPED.equals(topic)) { + LOG.error("Distribution failed: {}", packageId); + } else if (DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED.equals(topic)) { + LOG.info("Content is live: {}", String.join(",", paths)); + } + } +} +``` + +## Best Practices + +1. **Use the right API**: Replication API for publishing, Distribution events for monitoring +2. **Respect rate limits**: ≤100 paths for transactional guarantee +3. **Handle failures**: Always catch `ReplicationException`, monitor `AGENT_PACKAGE_DROPPED` events +4. **Use service users**: Never use admin credentials +5. **Filter events appropriately**: Only listen to events you need +6. **Validate permissions**: Call `replicator.checkPermission()` before replication +7. **Publish only what's needed**: Avoid unnecessary bulk operations + +## Troubleshooting + +### Replication Issues + +| Issue | Solution | +|-------|----------| +| `ReplicationException` | Check service user has `crx:replicate` permission | +| Content not on target tier | Verify agent filter, check replication status | +| "Too many paths" error | Reduce to ≤500 paths or use Tree Activation workflow | + +### Event Handling Issues + +| Issue | Solution | +|-------|----------| +| Event handler not firing | Verify event topic constant matches exactly | +| Missing event properties | Always null-check event properties | +| Handler slowing distribution | Use async job processing, don't block | + +## Detailed Documentation + +For detailed examples, code samples, and advanced usage: + +- **Replication API**: See [replication/SKILL.md](./replication/SKILL.md) +- **Sling Distribution Events**: See [sling-distribution/SKILL.md](./sling-distribution/SKILL.md) + +## References + +- **Replication Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/com/day/cq/replication/package-summary.html +- **Sling Distribution Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html +- **Adobe Documentation**: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/operations/replication.html +- **Sling Distribution**: https://sling.apache.org/documentation/bundles/content-distribution.html diff --git a/skills/aem/cloud-service/skills/content-distribution/replication/SKILL.md b/skills/aem/cloud-service/skills/content-distribution/replication/SKILL.md new file mode 100644 index 0000000..35f3ee1 --- /dev/null +++ b/skills/aem/cloud-service/skills/content-distribution/replication/SKILL.md @@ -0,0 +1,708 @@ +--- +name: aem-replication-api +description: | + Programmatic content publishing using AEM Cloud Service Replication API (com.day.cq.replication). + Covers Replicator service, ReplicationOptions, status checks, and event handling. +--- + +# AEM Cloud Service Replication API + +Programmatic content publishing and unpublishing using the official AEM Replication API. + +## When to Use This Skill + +Use the Replication API for programmatic content distribution: +- Custom OSGi services that publish content +- Workflow process steps requiring activation +- Automated publishing pipelines +- Integration with external systems +- Bulk operations (with proper constraints) + +**For UI-based publishing**, use Manage Publication or Quick Publish instead. + +## Official API Documentation + +**Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/com/day/cq/replication/package-summary.html + +**Key Classes**: +- `com.day.cq.replication.Replicator` - Main replication service +- `com.day.cq.replication.ReplicationOptions` - Configuration options +- `com.day.cq.replication.ReplicationStatus` - Publication status +- `com.day.cq.replication.ReplicationActionType` - Action types (ACTIVATE, DEACTIVATE, DELETE, TEST) + +## Architecture: How Replication Works in Cloud Service + +AEM Cloud Service uses **Sling Content Distribution** as the underlying transport mechanism: + +1. Author tier: Replication API call triggers content packaging +2. Content is sent to Adobe Developer pipeline service (external to AEM runtime) +3. Pipeline distributes to target tier (Publish or Preview) +4. Target tier imports and activates content + +**Key Difference from AEM 6.x**: No direct JCR replication; content flows through external pipeline service. + +## Basic Replication: Single Path + +### Example: Activate a Page + +```java +import com.day.cq.replication.Replicator; +import com.day.cq.replication.ReplicationActionType; +import com.day.cq.replication.ReplicationException; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import javax.jcr.Session; + +@Component(service = ContentPublisher.class) +public class ContentPublisher { + + @Reference + private Replicator replicator; + + /** + * Activate a single page to Publish tier + */ + public void publishPage(Session session, String pagePath) + throws ReplicationException { + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePath + ); + } + + /** + * Deactivate a page from Publish tier + */ + public void unpublishPage(Session session, String pagePath) + throws ReplicationException { + + replicator.replicate( + session, + ReplicationActionType.DEACTIVATE, + pagePath + ); + } + + /** + * Delete content from Publish tier + */ + public void deleteFromPublish(Session session, String path) + throws ReplicationException { + + replicator.replicate( + session, + ReplicationActionType.DELETE, + path + ); + } +} +``` + +### ReplicationActionType Values + +| Action Type | Description | Use Case | +|-------------|-------------|----------| +| `ACTIVATE` | Publish content to target tier | Make content live | +| `DEACTIVATE` | Unpublish content from target tier | Remove from production | +| `DELETE` | Delete content from target tier | Permanent removal | +| `TEST` | Test replication connection | Health checks | +| `INTERNAL_POLL` | Internal polling (reverse replication) | System use only | + +## Bulk Replication: Multiple Paths + +**IMPORTANT CONSTRAINTS**: +- **Recommended limit**: 100 paths per call +- **Hard limit**: 500 paths per call +- **Transactional guarantee**: Only for ≤100 paths +- **Payload size**: 10 MB maximum (excluding binaries) + +### Example: Bulk Activation + +```java +import com.day.cq.replication.ReplicationOptions; + +public class BulkPublisher { + + @Reference + private Replicator replicator; + + /** + * Publish multiple pages (up to 100 for transactional guarantee) + */ + public void publishMultiplePages(Session session, String[] pagePaths) + throws ReplicationException { + + // Validate path count + if (pagePaths.length > 100) { + throw new IllegalArgumentException( + "For transactional guarantee, limit to 100 paths. Got: " + pagePaths.length + ); + } + + // Replicate all paths atomically + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePaths, + null // No additional options + ); + } + + /** + * Bulk publish with automatic bucketing (>100 paths) + */ + public void publishLargeSet(Session session, String[] pagePaths) + throws ReplicationException { + + if (pagePaths.length > 500) { + throw new IllegalArgumentException( + "Hard limit is 500 paths per call. Got: " + pagePaths.length + ); + } + + // Use non-atomic calls for automatic bucketing + ReplicationOptions options = new ReplicationOptions(); + options.setUseAtomicCalls(false); // Enable automatic bucketing + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePaths, + options + ); + } +} +``` + +**Best Practice**: For bulk operations >500 paths, use Tree Activation workflow step instead of custom code. + +## Publishing to Preview Tier + +The Preview tier requires explicit agent filtering. + +### Example: Publish to Preview + +```java +import com.day.cq.replication.Agent; +import com.day.cq.replication.AgentFilter; + +public class PreviewPublisher { + + @Reference + private Replicator replicator; + + /** + * Publish to Preview tier only + */ + public void publishToPreview(Session session, String pagePath) + throws ReplicationException { + + ReplicationOptions options = new ReplicationOptions(); + + // Filter to target Preview agent only + options.setFilter(new AgentFilter() { + @Override + public boolean isIncluded(Agent agent) { + return "preview".equals(agent.getId()); + } + }); + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePath, + options + ); + } + + /** + * Publish to BOTH Preview and Publish tiers + */ + public void publishToBothTiers(Session session, String pagePath) + throws ReplicationException { + + ReplicationOptions options = new ReplicationOptions(); + + // Include both preview and publish agents + options.setFilter(new AgentFilter() { + @Override + public boolean isIncluded(Agent agent) { + String agentId = agent.getId(); + return "preview".equals(agentId) || "publish".equals(agentId); + } + }); + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePath, + options + ); + } +} +``` + +**Note**: Preview agent is disabled by default and must be configured in Cloud Manager. + +## Advanced Options: ReplicationOptions + +### Synchronous vs. Asynchronous Replication + +```java +public class SynchronousPublisher { + + @Reference + private Replicator replicator; + + /** + * Synchronous replication (wait for completion) + */ + public void publishSynchronously(Session session, String pagePath) + throws ReplicationException { + + ReplicationOptions options = new ReplicationOptions(); + options.setSynchronous(true); // Block until complete + + // Optional: Add listener for synchronous feedback + options.setListener(new ReplicationListener() { + @Override + public void onStart(ReplicationAction action) { + // Called when replication starts + } + + @Override + public void onMessage(ReplicationLog.Level level, String message) { + // Called for progress updates + } + + @Override + public void onEnd(ReplicationAction action, ReplicationResult result) { + // Called when replication completes + } + + @Override + public void onError(ReplicationAction action, Exception error) { + // Called if replication fails + } + }); + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + pagePath, + options + ); + } +} +``` + +**Important**: `ReplicationListener` only works with synchronous replication. + +### Suppress Status Updates + +```java +/** + * Replicate without updating replication metadata properties + */ +public void replicateWithoutStatusUpdate(Session session, String path) + throws ReplicationException { + + ReplicationOptions options = new ReplicationOptions(); + options.setSuppressStatusUpdate(true); // Don't update cq:lastReplicated, etc. + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + path, + options + ); +} +``` + +### Suppress Versioning + +```java +/** + * Replicate without creating implicit versions + */ +public void replicateWithoutVersioning(Session session, String path) + throws ReplicationException { + + ReplicationOptions options = new ReplicationOptions(); + options.setSuppressVersions(true); // Don't auto-version on replication + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + path, + options + ); +} +``` + +## Checking Replication Status + +### Example: Check if Page is Published + +```java +import com.day.cq.replication.ReplicationStatus; +import org.apache.sling.api.resource.Resource; + +public class StatusChecker { + + @Reference + private Replicator replicator; + + /** + * Check if content is currently published + */ + public boolean isPublished(Session session, String path) + throws ReplicationException { + + ReplicationStatus status = replicator.getReplicationStatus(session, path); + + if (status == null) { + return false; // No replication metadata + } + + return status.isActivated(); + } + + /** + * Get detailed replication information + */ + public void printReplicationInfo(Session session, String path) + throws ReplicationException { + + ReplicationStatus status = replicator.getReplicationStatus(session, path); + + if (status != null) { + System.out.println("Activated: " + status.isActivated()); + System.out.println("Deactivated: " + status.isDeactivated()); + System.out.println("Last Published: " + status.getLastPublished()); + System.out.println("Published By: " + status.getLastPublishedBy()); + System.out.println("Last Action: " + status.getLastReplicationAction()); + System.out.println("Pending: " + status.isPending()); + } + } + + /** + * Alternative: Adapt resource to ReplicationStatus + */ + public boolean isPublished(Resource resource) { + ReplicationStatus status = resource.adaptTo(ReplicationStatus.class); + return status != null && status.isActivated(); + } +} +``` + +### Batch Status Queries + +For checking status of multiple resources, use `ReplicationStatusProvider.getBatchReplicationStatus()` for better performance. + +## Permission Checks + +### Example: Validate User Can Replicate + +```java +public class PermissionValidator { + + @Reference + private Replicator replicator; + + /** + * Check if user has permission to replicate + */ + public boolean canUserReplicate(Session session, String path) { + try { + replicator.checkPermission( + session, + ReplicationActionType.ACTIVATE, + path + ); + return true; + } catch (ReplicationException e) { + // User lacks permission + return false; + } + } + + /** + * Validate before replicating + */ + public void safeReplicate(Session session, String path) + throws ReplicationException { + + // Check permission first + replicator.checkPermission(session, ReplicationActionType.ACTIVATE, path); + + // Proceed with replication + replicator.replicate(session, ReplicationActionType.ACTIVATE, path); + } +} +``` + +## Workflow Integration + +### Example: Workflow Process Step + +```java +import com.adobe.granite.workflow.WorkflowException; +import com.adobe.granite.workflow.WorkflowSession; +import com.adobe.granite.workflow.exec.WorkItem; +import com.adobe.granite.workflow.exec.WorkflowProcess; +import com.adobe.granite.workflow.metadata.MetaDataMap; +import org.osgi.service.component.annotations.Component; + +@Component( + service = WorkflowProcess.class, + property = { + "process.label=Publish Content to Publish Tier" + } +) +public class PublishWorkflowProcess implements WorkflowProcess { + + @Reference + private Replicator replicator; + + @Override + public void execute(WorkItem workItem, WorkflowSession workflowSession, + MetaDataMap args) throws WorkflowException { + + String payloadPath = workItem.getWorkflowData().getPayload().toString(); + + try { + Session session = workflowSession.adaptTo(Session.class); + + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + payloadPath + ); + + } catch (ReplicationException e) { + throw new WorkflowException("Failed to publish content", e); + } + } +} +``` + +**Process Arguments** (configured in workflow model): +- Can use `MetaDataMap args` to pass custom parameters like target tier + +## Listening to Replication Events + +Use OSGi Event Handlers to react to replication events: + +```java +import org.osgi.service.component.annotations.Component; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + "com/day/cq/replication" + } +) +public class ReplicationEventLogger implements EventHandler { + + private static final Logger LOG = LoggerFactory.getLogger(ReplicationEventLogger.class); + + @Override + public void handleEvent(Event event) { + String[] paths = (String[]) event.getProperty("paths"); + String action = (String) event.getProperty("action"); + String userId = (String) event.getProperty("userId"); + + if (paths != null) { + for (String path : paths) { + LOG.info("Replication: action={}, path={}, user={}", action, path, userId); + } + } + } +} +``` + +**Event Properties**: +- `paths` - Array of replicated paths +- `action` - Replication action (Activate, Deactivate, etc.) +- `userId` - User who triggered replication + +## Best Practices + +### 1. Respect Rate Limits +- ≤100 paths per call for transactional guarantee +- ≤500 paths hard limit +- ≤10 MB payload size + +### 2. Use Workflow for Large Operations +Don't build custom bulk publishing code. Use Tree Activation workflow step. + +### 3. Validate Permissions +Always check permissions before replication to avoid exceptions. + +### 4. Handle Exceptions +```java +try { + replicator.replicate(session, ReplicationActionType.ACTIVATE, path); +} catch (ReplicationException e) { + LOG.error("Replication failed for path: " + path, e); + // Handle failure (retry, notify, etc.) +} +``` + +### 5. Use Service Users +Never replicate with admin credentials. Create service users with minimum required permissions. + +```xml + + + +{ + "user.mapping": [ + "com.myapp.core:contentPublisher=myapp-replication-service" + ] +} +``` + +### 6. Publish Only What's Needed +"It is always a good practice to only publish content that must be published." + +## Common Patterns + +### Pattern 1: Auto-Publish on Content Fragment Save + +```java +import org.apache.sling.api.SlingConstants; +import org.apache.sling.api.resource.ResourceResolverFactory; + +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + SlingConstants.TOPIC_RESOURCE_CHANGED + } +) +public class AutoPublishContentFragmentHandler implements EventHandler { + + @Reference + private Replicator replicator; + + @Reference + private ResourceResolverFactory resolverFactory; + + @Override + public void handleEvent(Event event) { + String path = (String) event.getProperty(SlingConstants.PROPERTY_PATH); + + // Only process content fragments + if (path != null && path.startsWith("/content/dam") && + isContentFragment(path)) { + + try (ResourceResolver resolver = getServiceResolver()) { + Session session = resolver.adaptTo(Session.class); + replicator.replicate( + session, + ReplicationActionType.ACTIVATE, + path + ); + } catch (Exception e) { + LOG.error("Auto-publish failed", e); + } + } + } + + private ResourceResolver getServiceResolver() throws Exception { + Map param = Map.of( + ResourceResolverFactory.SUBSERVICE, "contentPublisher" + ); + return resolverFactory.getServiceResourceResolver(param); + } + + private boolean isContentFragment(String path) { + // Implementation to check if path is a content fragment + return true; + } +} +``` + +### Pattern 2: External Cache Purge After Publication + +```java +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + "com/day/cq/replication" + } +) +public class ExternalCachePurgeHandler implements EventHandler { + + @Reference + private HttpClient httpClient; + + @Override + public void handleEvent(Event event) { + String action = (String) event.getProperty("action"); + + if ("Activate".equals(action) || "Deactivate".equals(action)) { + String[] paths = (String[]) event.getProperty("paths"); + + if (paths != null) { + for (String path : paths) { + purgeExternalCache(path); + } + } + } + } + + private void purgeExternalCache(String path) { + // Call external CDN purge API + try { + HttpPost request = new HttpPost("https://cdn.example.com/purge"); + request.setHeader("Content-Type", "application/json"); + request.setEntity(new StringEntity("{\"path\":\"" + path + "\"}")); + httpClient.execute(request); + } catch (Exception e) { + LOG.error("Cache purge failed", e); + } + } +} +``` + +## Troubleshooting + +### Issue: Replication Fails Silently + +**Check**: Verify replication queues in Felix console +``` +https://author-p12345-e67890.adobeaemcloud.com/system/console/slingjobs +``` + +Look for failed jobs with topic: `com/day/cq/replication` + +### Issue: Permission Errors + +**Error**: `javax.jcr.AccessDeniedException` + +**Solution**: Verify service user has replication permissions: +``` +/content/mysite -> jcr:read, crx:replicate +``` + +### Issue: Content Not Appearing + +**Check**: +1. Verify replication status: `replicator.getReplicationStatus()` +2. Check logs for distribution errors +3. Verify target tier (Preview vs. Publish) + +## References + +- **Official Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/com/day/cq/replication/package-summary.html +- **Adobe Docs**: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/operations/replication.html +- **Sling Distribution**: https://sling.apache.org/documentation/bundles/content-distribution.html diff --git a/skills/aem/cloud-service/skills/content-distribution/sling-distribution/SKILL.md b/skills/aem/cloud-service/skills/content-distribution/sling-distribution/SKILL.md new file mode 100644 index 0000000..13b472f --- /dev/null +++ b/skills/aem/cloud-service/skills/content-distribution/sling-distribution/SKILL.md @@ -0,0 +1,738 @@ +--- +name: aem-sling-distribution +description: | + Monitor and react to content distribution events using Sling Distribution API (org.apache.sling.distribution). + Covers distribution event handling, queue monitoring, and distribution lifecycle tracking. +--- + +# AEM Cloud Service Sling Distribution Event Handling + +Monitor and react to content distribution lifecycle events using the Sling Distribution API. + +## When to Use This Skill + +Use Sling Distribution event handling for: +- **Monitoring distribution lifecycle**: Track package creation, queueing, and delivery +- **Custom post-distribution actions**: Cache warming, notifications, analytics +- **Distribution auditing**: Log and track all distribution events +- **Failure handling**: React to dropped packages and distribution failures +- **Integration workflows**: Trigger external systems after successful distribution + +**For programmatic publishing**, use the [Replication API](../replication/SKILL.md) instead. + +## Official API Documentation + +**Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html + +**Key Packages**: +- `org.apache.sling.distribution` - Core distribution interfaces +- `org.apache.sling.distribution.event` - Event topics and properties +- `org.apache.sling.distribution.queue` - Queue monitoring + +## Understanding Sling Distribution in Cloud Service + +### What is Sling Distribution? + +Sling Distribution is the **underlying transport mechanism** for content replication in AEM Cloud Service. When you use the Replication API (`Replicator.replicate()`), Sling Distribution handles: + +1. **Package Creation**: Content is assembled into distribution packages +2. **Queueing**: Packages are queued for delivery +3. **Transport**: Packages are sent to Adobe Developer pipeline service +4. **Import**: Target tier imports and applies the content + +### Architecture Flow + +``` +Replication API Call + ↓ +[AGENT_PACKAGE_CREATED] - Package assembled + ↓ +[AGENT_PACKAGE_QUEUED] - Package added to queue + ↓ +[AGENT_PACKAGE_DISTRIBUTED] - Package sent to pipeline + ↓ +[IMPORTER_PACKAGE_IMPORTED] - Package imported on target tier +``` + +**OR** + +``` +[AGENT_PACKAGE_DROPPED] - Package failed and was removed +``` + +## Distribution Event Topics + +### Available Event Topics + +Sling Distribution fires events at each stage of the distribution lifecycle: + +| Event Topic | When It Fires | Use Case | +|-------------|---------------|----------| +| `AGENT_PACKAGE_CREATED` | After package creation | Track what's being published | +| `AGENT_PACKAGE_QUEUED` | After package is queued | Monitor queue depth | +| `AGENT_PACKAGE_DISTRIBUTED` | After successful distribution | Trigger post-publish actions | +| `AGENT_PACKAGE_DROPPED` | When package fails and is dropped | Handle failures, alert on-call | +| `IMPORTER_PACKAGE_IMPORTED` | After successful import on target | Confirm content is live | + +### Event Topic Constants + +```java +import org.apache.sling.distribution.event.DistributionEventTopics; + +// Event topic strings +DistributionEventTopics.AGENT_PACKAGE_CREATED // Package created +DistributionEventTopics.AGENT_PACKAGE_QUEUED // Package queued +DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED // Package distributed +DistributionEventTopics.AGENT_PACKAGE_DROPPED // Package dropped +DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED // Package imported +``` + +## Listening to Distribution Events + +### Example: Basic Distribution Event Logger + +```java +import org.apache.sling.distribution.event.DistributionEventTopics; +import org.apache.sling.distribution.event.DistributionEventProperties; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_QUEUED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED + } +) +public class DistributionEventLogger implements EventHandler { + + private static final Logger LOG = LoggerFactory.getLogger(DistributionEventLogger.class); + + @Override + public void handleEvent(Event event) { + String topic = event.getTopic(); + + // Extract event properties + String componentName = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME + ); + String componentKind = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_COMPONENT_KIND + ); + String distributionType = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_TYPE + ); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + Long timestamp = (Long) event.getProperty( + DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP + ); + + LOG.info("Distribution Event: topic={}, component={}, type={}, packageId={}, paths={}, timestamp={}", + topic, componentName, distributionType, packageId, + paths != null ? String.join(",", paths) : "null", + timestamp + ); + } +} +``` + +## Event Properties + +### Available Event Properties + +Every distribution event contains these properties: + +| Property | Type | Description | +|----------|------|-------------| +| `DISTRIBUTION_COMPONENT_NAME` | String | Name of component generating the event | +| `DISTRIBUTION_COMPONENT_KIND` | String | Kind of component (agent, importer, etc.) | +| `DISTRIBUTION_TYPE` | String | Type of distribution request (ADD, DELETE, etc.) | +| `DISTRIBUTION_PACKAGE_ID` | String | Unique package identifier | +| `DISTRIBUTION_PATHS` | String[] | Content paths being distributed | +| `DISTRIBUTION_DEEP_PATHS` | String[] | Deep paths (full subtree) | +| `DISTRIBUTION_ENQUEUE_TIMESTAMP` | Long | When item was enqueued (milliseconds) | + +### Accessing Event Properties + +```java +import org.apache.sling.distribution.event.DistributionEventProperties; + +@Override +public void handleEvent(Event event) { + // Component information + String componentName = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME + ); + String componentKind = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_COMPONENT_KIND + ); + + // Distribution details + String type = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_TYPE + ); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + + // Content paths + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + String[] deepPaths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_DEEP_PATHS + ); + + // Timing + Long enqueueTime = (Long) event.getProperty( + DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP + ); +} +``` + +## Common Use Cases + +### Use Case 1: Alert on Distribution Failures + +```java +import org.apache.sling.distribution.event.DistributionEventTopics; + +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED + } +) +public class DistributionFailureAlertHandler implements EventHandler { + + private static final Logger LOG = LoggerFactory.getLogger( + DistributionFailureAlertHandler.class + ); + + @Reference + private AlertService alertService; // Custom alert service + + @Override + public void handleEvent(Event event) { + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + LOG.error("Distribution package dropped: packageId={}, paths={}", + packageId, String.join(",", paths)); + + // Send alert to operations team + alertService.sendAlert( + "CRITICAL: Distribution Failed", + String.format("Package %s was dropped. Paths: %s", + packageId, String.join(",", paths)) + ); + } +} +``` + +### Use Case 2: CDN Cache Warming After Distribution + +```java +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED + } +) +public class CdnCacheWarmingHandler implements EventHandler { + + private static final Logger LOG = LoggerFactory.getLogger( + CdnCacheWarmingHandler.class + ); + + @Reference + private HttpClient httpClient; + + @Override + public void handleEvent(Event event) { + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + String distributionType = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_TYPE + ); + + // Only warm cache on ADD (activation) + if ("ADD".equals(distributionType) && paths != null) { + for (String path : paths) { + warmCache(path); + } + } + } + + private void warmCache(String path) { + try { + // Convert JCR path to public URL + String publicUrl = "https://www.example.com" + + path.replace("/content/mysite", "") + ".html"; + + LOG.info("Warming CDN cache for: {}", publicUrl); + + HttpGet request = new HttpGet(publicUrl); + httpClient.execute(request); + + } catch (Exception e) { + LOG.error("Cache warming failed for path: " + path, e); + } + } +} +``` + +### Use Case 3: Distribution Analytics Tracking + +```java +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED + } +) +public class DistributionAnalyticsHandler implements EventHandler { + + @Reference + private AnalyticsService analyticsService; + + @Override + public void handleEvent(Event event) { + String topic = event.getTopic(); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + Long timestamp = (Long) event.getProperty( + DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP + ); + + // Track metrics + if (DistributionEventTopics.AGENT_PACKAGE_CREATED.equals(topic)) { + analyticsService.trackEvent("distribution.package.created", + Map.of("packageId", packageId, "pathCount", paths.length)); + } + else if (DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED.equals(topic)) { + long duration = System.currentTimeMillis() - timestamp; + analyticsService.trackEvent("distribution.package.distributed", + Map.of("packageId", packageId, "duration", duration)); + } + else if (DistributionEventTopics.AGENT_PACKAGE_DROPPED.equals(topic)) { + analyticsService.trackEvent("distribution.package.failed", + Map.of("packageId", packageId, "paths", String.join(",", paths))); + } + } +} +``` + +### Use Case 4: Slack Notifications for Production Publishes + +```java +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED + } +) +public class ProductionPublishNotificationHandler implements EventHandler { + + @Reference + private SlackService slackService; + + @Reference + private SlingSettingsService slingSettings; + + @Override + public void handleEvent(Event event) { + // Only notify for production environment + if (!slingSettings.getRunModes().contains("prod")) { + return; + } + + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + String componentName = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME + ); + + // Only notify for publish agent (not preview) + if (!"publish".equals(componentName)) { + return; + } + + String message = String.format( + ":rocket: Content published to production: %s", + String.join(", ", paths) + ); + + slackService.postToChannel("#content-releases", message); + } +} +``` + +### Use Case 5: Audit Log for All Distribution Events + +```java +import javax.jcr.Node; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; + +@Component( + service = EventHandler.class, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_QUEUED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED + } +) +public class DistributionAuditHandler implements EventHandler { + + @Reference + private ResourceResolverFactory resolverFactory; + + @Override + public void handleEvent(Event event) { + try (ResourceResolver resolver = getServiceResolver()) { + + String topic = event.getTopic(); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + Long timestamp = (Long) event.getProperty( + DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP + ); + + // Create audit log entry under /var/audit/distribution + String auditPath = "/var/audit/distribution/" + + System.currentTimeMillis(); + + Node auditNode = resolver.getResource("/var/audit/distribution") + .adaptTo(Node.class) + .addNode(String.valueOf(System.currentTimeMillis()), "nt:unstructured"); + + auditNode.setProperty("topic", topic); + auditNode.setProperty("packageId", packageId); + auditNode.setProperty("paths", paths); + auditNode.setProperty("timestamp", timestamp); + auditNode.setProperty("auditTime", System.currentTimeMillis()); + + resolver.commit(); + + } catch (Exception e) { + LOG.error("Failed to create audit log", e); + } + } + + private ResourceResolver getServiceResolver() throws Exception { + Map param = Map.of( + ResourceResolverFactory.SUBSERVICE, "distributionAuditor" + ); + return resolverFactory.getServiceResourceResolver(param); + } +} +``` + +## Distribution Request Types + +When monitoring events, the `DISTRIBUTION_TYPE` property indicates the type of operation: + +| Request Type | Description | When Used | +|--------------|-------------|-----------| +| `ADD` | Content is being added/activated | Normal publishing | +| `DELETE` | Content is being deleted | Unpublishing, deletion | +| `PULL` | Content is being pulled from target | Reverse replication | +| `INVALIDATE` | Cache invalidation only | CDN purge | +| `TEST` | Connection test | Health checks | + +### Example: Handle Different Distribution Types + +```java +@Override +public void handleEvent(Event event) { + String distributionType = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_TYPE + ); + String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS + ); + + switch (distributionType) { + case "ADD": + LOG.info("Content activated: {}", String.join(",", paths)); + break; + case "DELETE": + LOG.info("Content deleted: {}", String.join(",", paths)); + break; + case "INVALIDATE": + LOG.info("Cache invalidated: {}", String.join(",", paths)); + break; + case "TEST": + LOG.debug("Distribution test executed"); + break; + default: + LOG.warn("Unknown distribution type: {}", distributionType); + } +} +``` + +## Best Practices + +### 1. Filter Events Appropriately + +Only listen to events you need: + +```java +// Good: Listen to specific events +property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED +} + +// Avoid: Listening to all events if not needed +``` + +### 2. Handle Events Asynchronously + +Event handlers should be fast. Offload heavy processing: + +```java +@Reference +private JobManager jobManager; + +@Override +public void handleEvent(Event event) { + // Queue a job for async processing + Map jobProperties = new HashMap<>(); + jobProperties.put("packageId", event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID)); + + jobManager.addJob("com/myapp/distribution/process", jobProperties); +} +``` + +### 3. Use Service Users + +Event handlers should use service users, not admin sessions: + +```xml + +{ + "user.mapping": [ + "com.myapp.core:distributionEventHandler=myapp-distribution-service" + ] +} +``` + +### 4. Handle Null Properties + +Not all properties are available in all events: + +```java +String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS +); + +if (paths != null && paths.length > 0) { + // Process paths +} +``` + +### 5. Log Appropriately + +Use appropriate log levels: + +```java +// INFO for normal flow +LOG.info("Package distributed: {}", packageId); + +// WARN for unexpected situations +LOG.warn("Package queued longer than expected: {}", packageId); + +// ERROR for failures +LOG.error("Package dropped: {}", packageId); +``` + +## Monitoring Distribution Queue + +While you can't directly query distribution queues via the API in Cloud Service, you can monitor via: + +1. **Events**: Track `AGENT_PACKAGE_QUEUED` and `AGENT_PACKAGE_DISTRIBUTED` events +2. **Felix Console**: View Sling jobs at `/system/console/slingjobs` +3. **Logs**: Check distribution logs in Cloud Manager + +### Example: Queue Depth Monitoring + +```java +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +@Component( + service = {EventHandler.class, QueueMonitor.class}, + property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_QUEUED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED, + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_DROPPED + } +) +public class QueueMonitor implements EventHandler { + + private final Map queuedPackages = new ConcurrentHashMap<>(); + + @Override + public void handleEvent(Event event) { + String topic = event.getTopic(); + String packageId = (String) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PACKAGE_ID + ); + + if (DistributionEventTopics.AGENT_PACKAGE_QUEUED.equals(topic)) { + queuedPackages.put(packageId, System.currentTimeMillis()); + LOG.info("Queue depth: {}", queuedPackages.size()); + + // Alert if queue is too deep + if (queuedPackages.size() > 50) { + LOG.warn("Distribution queue depth exceeds threshold: {}", + queuedPackages.size()); + } + } + else if (DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED.equals(topic) || + DistributionEventTopics.AGENT_PACKAGE_DROPPED.equals(topic)) { + Long queuedTime = queuedPackages.remove(packageId); + + if (queuedTime != null) { + long duration = System.currentTimeMillis() - queuedTime; + LOG.info("Package processed in {}ms. Queue depth: {}", + duration, queuedPackages.size()); + } + } + } + + public int getQueueDepth() { + return queuedPackages.size(); + } +} +``` + +## Troubleshooting + +### Issue: Event Handler Not Firing + +**Causes**: +1. Wrong event topic constant +2. Component not activated +3. Event topic not registered + +**Solution**: +```java +// Verify event topic matches exactly +property = { + org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" + + DistributionEventTopics.AGENT_PACKAGE_CREATED // Exact constant +} + +// Check component is active in Felix console +// /system/console/components +``` + +### Issue: Missing Event Properties + +**Cause**: Not all properties are available in all events + +**Solution**: Always null-check: +```java +String[] paths = (String[]) event.getProperty( + DistributionEventProperties.DISTRIBUTION_PATHS +); + +if (paths == null) { + LOG.warn("No paths in distribution event"); + return; +} +``` + +### Issue: Event Handler Slowing Down Distribution + +**Cause**: Synchronous processing in event handler + +**Solution**: Use async job processing: +```java +@Reference +private JobManager jobManager; + +@Override +public void handleEvent(Event event) { + // Don't do heavy work here + jobManager.addJob("com/myapp/process", eventData); +} +``` + +## Relationship with Replication API + +**Key Understanding**: +- **Replication API** (`com.day.cq.replication.Replicator`) - What you call to publish content +- **Sling Distribution Events** (`org.apache.sling.distribution.event`) - What fires during the distribution lifecycle + +**Workflow**: +``` +Your Code: replicator.replicate(...) + ↓ +Sling Distribution: Creates package → [AGENT_PACKAGE_CREATED] + ↓ +Sling Distribution: Queues package → [AGENT_PACKAGE_QUEUED] + ↓ +Sling Distribution: Sends package → [AGENT_PACKAGE_DISTRIBUTED] + ↓ +Target Tier: Imports package → [IMPORTER_PACKAGE_IMPORTED] +``` + +## References + +- **Sling Distribution Javadoc**: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html +- **Sling Distribution Docs**: https://sling.apache.org/documentation/bundles/content-distribution.html +- **Replication API Skill**: [replication/SKILL.md](../replication/SKILL.md) +- **Adobe AEM Replication**: https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/operations/replication.html