For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/content-and-data/node-operation.md.

Comparing Sling Resource API and JCR Node API for repository CRUD operations.

JCR Node Operations


1. Hai API Làm Việc Với JCR Node

Sling Resource APIJCR Node API
Mức độCao (abstraction)Thấp (trực tiếp)
Object chínhResource, ValueMapNode, Property, Session
TestabilityDễ mockKhó mock hơn
Nên dùng khiMặc địnhKhi Sling API không đủ

Nguyên tắc: Dùng Sling Resource API cho mọi CRUD thông thường. Chỉ xuống JCR API khi cần: node ordering, versioning, workspace copy.


2. Tạo Node

Sling API (khuyến nghị)

public void createNode(ResourceResolver resolver) throws PersistenceException {
    Resource parent = resolver.getResource("/content/mysite/en");
    if (parent == null) return;

    Map<String, Object> props = new HashMap<>();
    props.put("jcr:primaryType", "nt:unstructured");
    props.put("myProperty", "Hello World");
    props.put("myNumber", 42L);

    resolver.create(parent, "newChild", props);
    resolver.commit();
}

JCR API

public void createNode(Session session) throws Exception {
    Node parent = session.getNode("/content/mysite/en");
    Node newNode = parent.addNode("newChild", "nt:unstructured");
    newNode.setProperty("myProperty", "Hello World");
    newNode.setProperty("myNumber", 42L);
    session.save();
}

Lưu ý AEM 6.5: ResourceResolver và Session lấy từ service user, không dùng admin session trong production. Khai báo service user mapping trong OSGi config org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~myproject.cfg.json dưới ui.config.


3. Đọc Properties

Sling API — ValueMap

public void readProperties(Resource resource) {
    ValueMap props = resource.getValueMap();

    // Đọc với default value để tránh NPE
    String title   = props.get("jcr:title", "Untitled");
    Long count     = props.get("visitCount", 0L);
    Boolean hidden = props.get("hideInNav", false);

    // Multi-value property
    String[] tags = props.get("cq:tags", String[].class);
}

JCR API — Node

public void readProperties(Node node) throws Exception {
    String title = node.getProperty("jcr:title").getString();
    long count   = node.getProperty("visitCount").getLong();
    boolean hidden = node.getProperty("hideInNav").getBoolean();

    // Multi-value
    javax.jcr.Value[] values = node.getProperty("cq:tags").getValues();
    for (javax.jcr.Value v : values) {
        System.out.println(v.getString());
    }
}

Bảng kiểu dữ liệu

JCR TypeSling ValueMap classJCR getter
StringString.class.getString()
LongLong.class.getLong()
BooleanBoolean.class.getBoolean()
DateCalendar.class.getDate()
BinaryInputStream.class.getBinary()
String[]String[].class.getValues()

4. Cập Nhật Properties

Sling API — ModifiableValueMap

public void updateProperties(Resource resource, ResourceResolver resolver)
        throws PersistenceException {

    ModifiableValueMap mvp = resource.adaptTo(ModifiableValueMap.class);
    if (mvp == null) {
        // Thường do service user thiếu quyền write
        log.warn("Cannot adapt to ModifiableValueMap: {}", resource.getPath());
        return;
    }

    mvp.put("jcr:title", "Updated Title");
    mvp.put("visitCount", 100L);
    mvp.remove("obsoleteProperty");

    resolver.commit();
}

JCR API

public void updateProperties(Session session) throws Exception {
    Node node = session.getNode("/content/mysite/en/jcr:content");
    node.setProperty("jcr:title", "Updated Title");
    node.setProperty("visitCount", 100L);

    if (node.hasProperty("obsoleteProperty")) {
        node.getProperty("obsoleteProperty").remove();
    }

    session.save();
}

Lưu ý: adaptTo(ModifiableValueMap.class) trả về null khi service user thiếu quyền jcr:write trên path đó. Kiểm tra ACL trong CRX/DE: /home/users/system/&lt;service-user&gt;.


5. Xóa Node

Sling API

public void deleteNode(ResourceResolver resolver) throws PersistenceException {
    Resource target = resolver.getResource("/content/mysite/en/obsolete-page");
    if (target != null) {
        resolver.delete(target);
        resolver.commit();
    }
}

JCR API

public void deleteNode(Session session) throws Exception {
    if (session.nodeExists("/content/mysite/en/obsolete-page")) {
        Node node = session.getNode("/content/mysite/en/obsolete-page");
        node.remove();
        session.save();
    }
}

Xóa nhiều node trong vòng lặp — tránh ConcurrentModificationException

// SAI: xóa trong khi đang iterate
for (Resource child : parent.getChildren()) {
    resolver.delete(child); // ConcurrentModificationException!
}

// ĐÚNG: collect trước, xóa sau
List<String> toDelete = new ArrayList<>();
for (Resource child : parent.getChildren()) {
    if (shouldDelete(child)) {
        toDelete.add(child.getPath());
    }
}
for (String path : toDelete) {
    Resource r = resolver.getResource(path);
    if (r != null) resolver.delete(r);
}
resolver.commit();

6. Move và Copy

Move (atomic, cần save)

public void moveNode(Session session) throws Exception {
    session.move(
        "/content/mysite/en/old-page",
        "/content/mysite/en/new-page"
    );
    session.save();
}

Copy (immediate, không cần save)

public void copyNode(Session session) throws Exception {
    Workspace workspace = session.getWorkspace();
    workspace.copy(
        "/content/mysite/en/source-page",
        "/content/mysite/en/copy-of-source"
    );
    // workspace.copy() commit ngay lập tức, không cần session.save()
}
session.move()workspace.copy()
Cần save()CóKhông
AtomicCóKhông (copy ngay)
Kết quảDi chuyển nodeDuplicate toàn bộ subtree

7. Duyệt Cây Node

Lên/xuống/ngang

public void navigate(Resource resource) {
    Resource parent = resource.getParent();
    Resource child  = resource.getChild("jcr:content");

    // Iterate direct children
    for (Resource c : resource.getChildren()) {
        log.info("Child: {} ({})", c.getName(), c.getResourceType());
    }
}

Tìm node config theo cây cha (inheritance pattern)

Dùng khi config được set ở page cha và cần tìm từ page con đi lên:

@Self
private Resource currentResource;

private Resource findInheritedConfig(Page page) {
    String relPath = "/jcr:content/customConfig";
    int depth = page.getDepth();

    for (int i = 1; i < depth; i++) {
        Resource config = currentResource.getChild(page.getPath() + relPath);
        if (config != null) return config;
        page = page.getParent();
    }
    return null;
}

Duyệt đệ quy toàn bộ subtree

// Java 11+: String.repeat() có sẵn
public void traverseRecursively(Resource resource, int depth) {
    String indent = "  ".repeat(depth);
    String type   = resource.getValueMap().get("jcr:primaryType", "unknown");
    log.info("{}{} [{}]", indent, resource.getName(), type);

    for (Resource child : resource.getChildren()) {
        traverseRecursively(child, depth + 1);
    }
}

// Java 8: dùng StringBuilder thay thế String.repeat()
public void traverseRecursively(Resource resource, int depth) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < depth; i++) sb.append("  ");
    String type = resource.getValueMap().get("jcr:primaryType", "unknown");
    log.info("{}{} [{}]", sb, resource.getName(), type);

    for (Resource child : resource.getChildren()) {
        traverseRecursively(child, depth + 1);
    }
}

Cảnh báo: Duyệt đệ quy trên cây lớn (DAM, /content) có thể gây OOM. Luôn giới hạn depth hoặc dùng QueryBuilder thay thế.


8. Sắp Xếp Thứ Tự Child Node

Mặc định nt:unstructured và cq:Page giữ thứ tự insertion. Có thể sắp xếp lại bằng JCR API:

public void reorderChildren(Session session) throws Exception {
    Node parent = session.getNode("/content/mysite/en");

    // Chuyển "page-b" lên trước "page-a"
    parent.orderBefore("page-b", "page-a");

    // Chuyển "page-c" xuống cuối (null = cuối danh sách)
    parent.orderBefore("page-c", null);

    session.save();
}

Sling Resource API không hỗ trợ node ordering. Bắt buộc dùng JCR API cho tính năng này.


9. Versioning

AEM 6.5 dùng JCR versioning để quản lý lịch sử nội dung. Node cần có mixin mix:versionable.

public void versioningExample(Session session) throws Exception {
    VersionManager vm = session.getWorkspace().getVersionManager();
    String path = "/content/mysite/en/jcr:content";

    // Tạo version mới (check-in + check-out)
    vm.checkpoint(path);

    // Liệt kê lịch sử versions
    VersionHistory history = vm.getVersionHistory(path);
    VersionIterator versions = history.getAllVersions();
    while (versions.hasNext()) {
        Version v = versions.nextVersion();
        log.info("Version {} — {}", v.getName(), v.getCreated().getTime());
    }

    // Restore về version cụ thể
    vm.restore(path, "1.0", true);
    // true = xóa conflicting nodes nếu có (thường là true)
}

Lưu ý: vm.restore() ghi đè nội dung hiện tại. Dùng cẩn thận trong môi trường production. Versioning chỉ áp dụng cho node có mixin mix:versionable (mặc định có trên cq:PageContent).


10. Batch Operations

Khi update hàng nghìn node, không save sau mỗi thay đổi — sẽ rất chậm và gây nhiều transaction nhỏ.

public void batchUpdate(ResourceResolver resolver, List<Resource> resources)
        throws PersistenceException {

    final int BATCH_SIZE = 1000;
    int count = 0;

    for (Resource resource : resources) {
        ModifiableValueMap mvp = resource.adaptTo(ModifiableValueMap.class);
        if (mvp == null) continue;

        mvp.put("migrationFlag", Boolean.TRUE);
        count++;

        if (count % BATCH_SIZE == 0) {
            resolver.commit();
            log.info("Committed {} nodes", count);
        }
    }

    // Commit phần còn lại
    if (count % BATCH_SIZE != 0) {
        resolver.commit();
    }

    log.info("Batch update hoàn tất: {} nodes", count);
}

Chọn batch size:

  • Node thông thường: 1000
  • Node có binary property lớn: giảm xuống 100–200
  • Groovy Console: dùng built-in batching utilities của Groovy Console

11. Sling API vs JCR API — Tóm Tắt

Thao tácSling Resource APIJCR Node API
Tạo noderesolver.create()node.addNode()
Đọc propertyresource.getValueMap()node.getProperty()
Cập nhật propertyModifiableValueMap.put()node.setProperty()
Xóa noderesolver.delete()node.remove()
Persistresolver.commit()session.save()
Node orderingKhông hỗ trợnode.orderBefore()
VersioningKhông hỗ trợVersionManager
Moveresolver.move()session.move()
CopyKhông hỗ trợ trực tiếpworkspace.copy()
TestabilityDễ mock (MockResource)Khó mock hơn

12. Patterns Thường Gặp Trong AEM 6.5

Lấy jcr:content từ một Page resource

Resource pageResource = resolver.getResource("/content/mysite/en/about");
Resource jcrContent = pageResource.getChild("jcr:content");
ValueMap props = jcrContent.getValueMap();
String title = props.get("jcr:title", "");

Adapt Resource sang Page (CQ API)

Page page = resource.adaptTo(Page.class);
if (page != null) {
    String title = page.getTitle();
    String path  = page.getPath();
    Page parent  = page.getParent();
}

Lấy Session từ ResourceResolver

Session session = resolver.adaptTo(Session.class);

Không hardcode credentials trong OSGi Service

// SAI trong production
Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));

// ĐÚNG: dùng service user
@Reference
private ResourceResolverFactory resolverFactory;

Map<String, Object> authInfo = Collections.singletonMap(
    ResourceResolverFactory.SUBSERVICE, "my-service-user"
);
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
    // thao tác với resolver
}

Tham Khảo