Modify and Query the JCR
JCR Queries
I highly recommend downloading and "studying" the JCR Query Cheatsheet 1.
QueryBuilder
The QueryBuilder API executes a query which can be customized via a predicates hashmap. The below configuration creates a query that searches for nodes below a given path, that have two predefined property key/value pairs.
Using the QueryBuilder is strongly advised when you need to sanitze input. For example in a servlet, where the user can customize the query via request parameter.
final HashMap<String, String> properties = new HashMap<>();
// predicates
properties.put("path", "/content/mysite");
properties.put("group.1_property", "some-property-name1");
properties.put("group.1_property.value", "some-property-value1");
properties.put("group.2_property", "some-property-name2");
properties.put("group.2_property.value", "some-property-value2");
// config
properties.put("p.offset", "0");
properties.put("p.limit", "-1");
The above configuration can be passed to the QueryBuilder.
@Reference
private QueryBuilder builder;
private List<Hit> executeQuery(SlingHttpServletRequest request, HashMap<String, String> properties) {
final Session session = request.getResourceResolver().adaptTo(Session.class);
final com.day.cq.search.Query query = builder.createQuery(PredicateGroup.create(properties), session);
final SearchResult result = query.getResult();
return result.getHits();
}
SQL2
resourceResolver.findResources() runs a JCR-SQL2 query directly. Use it when all query inputs
are constants under your control - the JCR-SQL2 grammar has no parameter binding, so any
user-controlled string concatenated into the statement is a query injection risk. For any query
fed by request parameters, use the QueryBuilder above (or pre-validate inputs against a strict
allowlist before building the statement).
The example below only references constants (the resource type and a fixed path), so string construction is safe:
final String myQuery = "SELECT * FROM [nt:base] AS s " +
"WHERE ISDESCENDANTNODE([/content/experience-fragments]) " +
"AND [sling:resourceType] = '" + TestModel.RESOURCE_TYPE + "'";
final Iterator<Resource> results = request.getResourceResolver().findResources(myQuery, Query.JCR_SQL2);
If you need to narrow by locale or another caller-supplied value, validate it against an allowlist first:
// Only accept known locales -- never concatenate raw request input.
private static final Set<String> ALLOWED_LOCALES = Set.of("en", "de", "fr");
if (!ALLOWED_LOCALES.contains(locale)) {
return Collections.emptyIterator();
}
final String myQuery = "SELECT * FROM [nt:base] AS s " +
"WHERE ISDESCENDANTNODE([/content/experience-fragments/" + locale + "]) " +
"AND [sling:resourceType] = '" + TestModel.RESOURCE_TYPE + "'";
Session API / JCR API
Inside an AEM bundle, never open a JCR Session directly with credentials. Instead, obtain a
service ResourceResolver and adapt it to a Session - this gives you a session scoped to a
service user with the minimum privileges it needs. See
Security basics for setting up the service user mapping.
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import java.util.Map;
@Reference
private ResourceResolverFactory resolverFactory;
private static final Map<String, Object> AUTH_INFO = Map.of(
ResourceResolverFactory.SUBSERVICE, "my-write-service");
public void writeMessage() {
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(AUTH_INFO)) {
Session session = resolver.adaptTo(Session.class);
if (session == null) {
return;
}
Node root = session.getRootNode();
Node adobe = root.hasNode("adobe") ? root.getNode("adobe") : root.addNode("adobe");
Node day = adobe.hasNode("day") ? adobe.getNode("day") : adobe.addNode("day");
day.setProperty("message", "Hello from a service user");
session.save();
} catch (LoginException | RepositoryException e) {
LOG.error("Failed to write message", e);
}
}
:::danger Never use admin:admin or loginAdministrative()
Hardcoded admin credentials and loginAdministrative() both grant unrestricted repository
access. Always route access through a sub-service scoped to the minimum privileges it needs.
:::
Remote access from outside AEM
If you truly need to talk to a running AEM instance from an external Java client (migrations,
tooling, tests), connect over HTTP via JcrUtils.getRepository() - but treat it as dev-only and
pull credentials from configuration, never source. The default local-SDK author port is 4502.
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import org.apache.jackrabbit.commons.JcrUtils;
// Credentials come from environment / config -- never hardcode in committed code.
Repository repository = JcrUtils.getRepository("http://localhost:4502/crx/server");
Session session = repository.login(
new SimpleCredentials(System.getenv("AEM_USER"), System.getenv("AEM_PASS").toCharArray()));
Groovy Console
:::warning Dev / ops only The Groovy Console runs arbitrary code as the admin user. Never expose it on Publish, and restrict access on Author to trusted operators. These examples are written for ad-hoc debugging and maintenance - don't copy their string-concatenation shape into production servlets. :::
Simple query example
Find all pages below a given path that use a specific sling:resourceType. Uses JCR-SQL2 (the
legacy sql language is deprecated and gone in modern Oak).
def findByResourceType(rootPath, resourceType) {
def queryManager = session.workspace.queryManager
def statement = "SELECT * FROM [nt:base] " +
"WHERE ISDESCENDANTNODE([${rootPath}]) " +
"AND [sling:resourceType] = '${resourceType}'"
queryManager.createQuery(statement, 'JCR-SQL2')
}
final def query = findByResourceType('/content/geometrixx/en', 'geometrixx/components/contentpage')
final def result = query.execute()
println "No of pages found = ${result.nodes.size()}"
result.nodes.each { node ->
println "nodePath:: ${node.path}"
}
Bulk cleanup example
Remove jcr:language properties from every node below /content/eurowings/backoffice.
import javax.jcr.Session
Session session = resourceResolver.adaptTo(Session.class)
def queryManager = session.workspace.queryManager
def statement = "SELECT * FROM [nt:base] " +
"WHERE ISDESCENDANTNODE([/content/eurowings/backoffice]) " +
"AND [jcr:language] IS NOT NULL"
def result = queryManager.createQuery(statement, 'JCR-SQL2').execute()
result.nodes.each { node ->
println "Deleting jcr:language on :: ${node.path}"
node.getProperty('jcr:language').remove()
}
session.save()
Further Groovy Examples on Github
Delete Inbox Notifications
To delete AEM Inbox Notifications, just delete /var/taskmanagement/tasks.
Locally via CRX/DE and on deployed environments via "empty" Content-Package.
References
See also
- Architecture
- Node operations
- Content fragments
- Replication and activation
- GraphQL
- Sling models
- Groovy console
- Security basics