Unpublish Action in AEM AEM

1. Kiến trúc luồng Replication & Unpublish trong AEM 6.5

Trong mô hình Author-Publish:

  1. Replication Action Type: ReplicationActionType.DEACTIVATE gửi một gói replication chứa chỉ thị xóa hoặc đánh dấu deactivation của node tương ứng trên Publish instance.
  2. Replication Agent: Đọc payload từ hàng đợi (Replication Queue), đóng gói thành HTTP POST request (thường qua servlet /bin/receive) gửi sang Publish.
  3. Dispatcher Flush: Khi một node bị de-activate, Agent Flush của Dispatcher cần nhận tín hiệu để xóa file tĩnh hoặc vô hiệu hóa .stat file tương ứng.

2. Các yêu cầu kỹ thuật & Rủi ro cần lưu ý

  1. JCR Mixin Type:
  • Node cần có mixin cq:ReplicationStatus (mix:versionable nếu cần kiểm soát version history trước khi xóa).
  • Mixin này cung cấp các thuộc tính: cq:lastReplicated, cq:lastReplicationAction, cq:lastReplicatedBy.
  1. Sự tồn tại của Node trên Author sau khi Unpublish:
  • Trường hợp 1 (Chỉ Unpublish): Node vẫn tồn tại trên Author nhưng bị xóa khỏi Publish. Cần cập nhật cq:lastReplicationAction = Deactivate.
  • Trường hợp 2 (Unpublish rồi Xóa hẳn trên Author): Phải đảm bảo replication sang Publish hoàn tất thành công trước khi tiến hành xóa node trên Author. Nếu xóa trên Author trước, Replication Agent sẽ không tìm thấy path để tạo replication payload.
  1. Quyền hạn (ACLs / Service User):
  • Workflow Process Step phải sử dụng ResourceResolverFactory.getServiceResourceResolver(...) với System User có đủ quyền:
  • jcr:read, jcr:modifyProperties trên path của Author.
  • Quyền replicate (crx:replicate).
  • Quyền xóa/sửa trên Publish instance thông qua user cấu hình trong Transport của Replication Agent.

3. Thiết kế Custom Workflow Process Step

Để xử lý unpublish an toàn, ta xây dựng một OSGi Workflow Process Step bằng Java:

3.1. Java Implementation

package com.custom.aem.workflows.process;

import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.Replicator;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.util.Collections;
import java.util.Map;

@Component(
    service = WorkflowProcess.class,
    property = {
        "process.label=Custom Node Unpublish (Deactivate) Process"
    }
)
public class UnpublishCustomNodeProcess implements WorkflowProcess {

    private static final Logger LOG = LoggerFactory.getLogger(UnpublishCustomNodeProcess.class);
    private static final String SUBSERVICE_NAME = "workflow-replication-service";

    @Reference
    private Replicator replicator;

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) throws WorkflowException {
        String payloadPath = workItem.getWorkflowData().getPayload().toString();
        LOG.info("Bắt đầu xử lý Deactivate cho node: {}", payloadPath);

        Map<String, Object> authInfo = Collections.singletonMap(
            ResourceResolverFactory.SUBSERVICE, SUBSERVICE_NAME
        );

        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
            Resource targetResource = resolver.getResource(payloadPath);
            
            if (targetResource == null) {
                LOG.warn("Resource không tồn tại tại đường dẫn: {}", payloadPath);
                return;
            }

            Node targetNode = targetResource.adaptTo(Node.class);
            if (targetNode != null) {
                // Đảm bảo node có mixin cq:ReplicationStatus để theo dõi metadata
                if (targetNode.canAddMixin("cq:ReplicationStatus")) {
                    targetNode.addMixin("cq:ReplicationStatus");
                }
            }

            Session session = resolver.adaptTo(Session.class);
            if (session != null) {
                // 1. Thực hiện deactivation sang Publish instance
                replicator.replicate(session, ReplicationActionType.DEACTIVATE, payloadPath);
                LOG.info("Đã gửi lệnh Replication DEACTIVATE cho: {}", payloadPath);

                // 2. Commit session (lưu mixin nếu có thay đổi)
                session.save();
            }

        } catch (ReplicationException e) {
            LOG.error("Lỗi Replication khi deactivating node: " + payloadPath, e);
            throw new WorkflowException("Replication failed", e);
        } catch (Exception e) {
            LOG.error("Lỗi không xác định trong UnpublishCustomNodeProcess", e);
            throw new WorkflowException(e.getMessage(), e);
        }
    }
}

4. Cấu hình OSGi Service User & Permissions

Để workflow chạy ổn định mà không phụ thuộc vào session của người kích hoạt (thường thiếu quyền replicate hoặc admin):

  1. Tạo Service User: workflow-replication-service thông qua Apache Sling Service User Mapper.
  2. Cấu hình Service User Mapping (org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended):
com.custom.aem.core:workflow-replication-service=[custom-workflow-replication-subservice]
  1. Phân quyền Repository (ACLs):
  • Gán quyền cho principal custom-workflow-replication-subservice:
  • Read, Write, Modify Properties trên thư mục chứa custom node.
  • Quyền crx:replicate trên / hoặc trên subtree tương ứng.

5. Cấu hình Dispatcher Invalidation cho Custom Node

Khác với cq:Page tự động gửi request .stat invalidation lên thư mục HTML, custom node thường phục vụ API (JSON, XML) hoặc model dữ liệu backend:

  • Dispatcher Flush Agent: Kiểm tra replication agent loại Dispatcher Flush trên Author hoặc Publish.
  • Nếu node payload nằm ngoài /content thông thường (ví dụ: /var/data/...), cần cấu hình custom header hoặc servlet re-flush:
  • Trong cấu hình /filter và /invalidate của file dispatcher.any, khai báo path của custom node:
/invalidate {
    /0000 {
        /glob "*"
        /type "deny"
    }
    /0001 {
        /glob "*.html"
        /type "allow"
    }
    /0002 {
        /glob "/bin/custom/data/*"
        /type "allow"
    }
}

6. Checklist Kiểm thử & Vận hành (Verification Plan)

BướcHành động kiểm traKết quả kỳ vọng
1. Kích hoạt WFTrigger workflow với custom node payloadWorkflow kết thúc với trạng thái COMPLETED.
2. Kiểm tra AuthorMở CRXDE Lite tại AuthorNode vẫn còn (hoặc đã xóa theo nghiệp vụ), property cq:lastReplicationAction có giá trị Deactivate.
3. Kiểm tra PublishMở CRXDE Lite tại Publish instanceCustom node tương ứng không còn tồn tại (đã bị removed khỏi JCR).
4. Kiểm tra QueueVào /etc/replication/agents.author/publish.htmlQueue = 0, status OK, log hiển thị ReplicationActionType.DEACTIVATE trả về code 200.
5. Dispatcher CacheGọi API/URL đọc dữ liệu custom nodeTrả về HTTP 404 Not Found (không trả về dữ liệu cache cũ).