Skip to content

Commit 80c3c50

Browse files
committed
Improve detection of index for "rep:ACL" nodes
Rely on EXPLAIN MEASURE and evaluate the estimated cost. This closes #714
1 parent 46b8c46 commit 80c3c50

File tree

3 files changed

+122
-10
lines changed

3 files changed

+122
-10
lines changed

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/QueryHelper.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import static biz.netcentric.cq.tools.actool.history.impl.PersistableInstallationLogger.msHumanReadable;
1212

13+
import java.io.IOException;
1314
import java.util.Collection;
1415
import java.util.HashSet;
1516
import java.util.Iterator;
@@ -29,6 +30,7 @@
2930
import javax.jcr.query.InvalidQueryException;
3031
import javax.jcr.query.Query;
3132
import javax.jcr.query.QueryResult;
33+
import javax.jcr.query.Row;
3234
import javax.jcr.security.AccessControlList;
3335
import javax.jcr.security.AccessControlManager;
3436
import javax.jcr.security.AccessControlPolicy;
@@ -40,13 +42,18 @@
4042
import org.slf4j.Logger;
4143
import org.slf4j.LoggerFactory;
4244

45+
import com.fasterxml.jackson.databind.JsonNode;
46+
import com.fasterxml.jackson.databind.ObjectMapper;
47+
4348
public class QueryHelper {
4449
public static final Logger LOG = LoggerFactory.getLogger(QueryHelper.class);
4550

4651
private static final String ROOT_REP_POLICY_NODE = "/rep:policy";
4752
private static final String ROOT_REPO_POLICY_NODE = "/" + Constants.REPO_POLICY_NODE;
4853
private static final String HOME_REP_POLICY = "/home/rep:policy";
49-
private static final String OAK_INDEX_PATH_REP_ACL = "/oak:index/repACL-custom-1";
54+
55+
/** every query cost below that threshold means a dedicated index exists, above that threshold means: fallback to traversal */
56+
private static final double COST_THRESHOLD_FOR_QUERY_INDEX = 100d;
5057

5158
/** Method that returns a set containing all rep:policy nodes from repository excluding those contained in paths which are excluded from
5259
* search
@@ -98,7 +105,12 @@ public static Set<String> getRepPolicyNodePaths(final Session session,
98105
paths.add(HOME_REP_POLICY);
99106
}
100107

101-
boolean indexForRepACLExists = session.nodeExists(OAK_INDEX_PATH_REP_ACL);
108+
boolean indexForRepACLExists = false;
109+
try {
110+
indexForRepACLExists = hasQueryIndexForACLs(session);
111+
} catch(IOException|RepositoryException e) {
112+
LOG.warn("Cannot figure out if query index for rep:ACL nodes exist", e);
113+
}
102114
LOG.debug("Index for repACL exists: {}",indexForRepACLExists);
103115
String queryForAClNodes = indexForRepACLExists ?
104116
"SELECT * FROM [rep:ACL] WHERE ISDESCENDANTNODE([%s])" :
@@ -125,10 +137,32 @@ public static Set<String> getRepPolicyNodePaths(final Session session,
125137
return paths;
126138
}
127139

140+
static boolean hasQueryIndexForACLs(final Session session) throws RepositoryException, IOException {
141+
Query query = session.getWorkspace().getQueryManager().createQuery("EXPLAIN MEASURE SELECT * FROM [rep:ACL] AS s WHERE ISDESCENDANTNODE([/])", Query.JCR_SQL2);
142+
QueryResult queryResult = query.execute();
143+
Row row = queryResult.getRows().nextRow();
144+
// inspired by https://github.com/apache/jackrabbit-oak/blob/cc8adb42d89bc4625138a62ab074e7794a4d39ab/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java#L1092
145+
String plan = row.getValue("plan").getString();
146+
String costJson = plan.substring(plan.lastIndexOf('{'));
147+
148+
// use jackson for JSON parsing
149+
ObjectMapper mapper = new ObjectMapper();
150+
151+
// read the json strings and convert it into JsonNode
152+
JsonNode node = mapper.readTree(costJson);
153+
double cost = node.get("s").asDouble(Double.MAX_VALUE);
154+
// look at https://jackrabbit.apache.org/oak/docs/query/query-engine.html#cost-calculation for the threshold
155+
// https://github.com/apache/jackrabbit-oak/blob/cc8adb42d89bc4625138a62ab074e7794a4d39ab/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java#L75
156+
157+
// for traversing cost = estimation of node count
158+
// for property index = between 2 and 100
159+
LOG.debug("Cost for rep:ACL query is estimated with {}", cost);
160+
return cost <= COST_THRESHOLD_FOR_QUERY_INDEX;
161+
}
162+
128163
/** Get Nodes with XPATH Query. */
129164
public static Set<String> getNodePathsFromQuery(final Session session,
130-
final String xpathQuery) throws InvalidQueryException,
131-
RepositoryException {
165+
final String xpathQuery) throws RepositoryException {
132166
return getNodePathsFromQuery(session, xpathQuery, Query.XPATH);
133167
}
134168

accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/extensions/OakRepository.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
import java.nio.file.Paths;
1616
import java.util.Comparator;
1717
import java.util.Properties;
18+
import java.util.function.Consumer;
1819
import java.util.stream.Stream;
1920

21+
import javax.jcr.LoginException;
2022
import javax.jcr.Repository;
2123
import javax.jcr.RepositoryException;
2224
import javax.jcr.Session;
@@ -77,19 +79,39 @@ public class OakRepository implements BeforeAllCallback, BeforeEachCallback, Aft
7779

7880
private static final Logger LOGGER = LoggerFactory.getLogger(OakRepository.class);
7981

82+
/**
83+
* Optionally uses a dedicated BlobStore with Oak, otherwise just in memory
84+
*/
85+
private final boolean useFileStore;
86+
87+
private final Consumer<Jcr> jcrInitCallback;
88+
89+
public OakRepository() {
90+
this(true);
91+
}
92+
93+
public OakRepository(boolean useFileStore) {
94+
this(useFileStore, null);
95+
}
96+
97+
public OakRepository(boolean useFileStore, Consumer<Jcr> jcrInitCallback) {
98+
this.useFileStore = useFileStore;
99+
this.jcrInitCallback = jcrInitCallback;
100+
}
101+
80102
@Override
81103
public void afterAll(ExtensionContext context) throws Exception {
82104
shutdownRepository();
83105
}
84106

85107
@Override
86108
public void beforeAll(ExtensionContext context) throws Exception {
87-
initRepository(true);
109+
initRepository();
88110
}
89111

90112
@Override
91113
public void beforeEach(ExtensionContext context) throws Exception {
92-
admin = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
114+
admin = createAdminSession();
93115
}
94116

95117
@Override
@@ -110,11 +132,11 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
110132
}
111133

112134
/**
113-
* @param useFileStore only evaluated for Oak. Optionally uses a dedicated BlobStore with Oak
135+
* @param useFileStore only evaluated for Oak. O
114136
* @throws RepositoryException
115137
* @throws IOException
116138
* @throws InvalidFileStoreVersionException */
117-
private void initRepository(boolean useFileStore) throws RepositoryException, IOException, InvalidFileStoreVersionException {
139+
private void initRepository() throws RepositoryException, IOException, InvalidFileStoreVersionException {
118140
Jcr jcr;
119141
if (useFileStore) {
120142
BlobStore blobStore = createBlobStore();
@@ -128,7 +150,9 @@ private void initRepository(boolean useFileStore) throws RepositoryException, IO
128150
// in-memory repo
129151
jcr = new Jcr();
130152
}
131-
153+
if (jcrInitCallback != null) {
154+
jcrInitCallback.accept(jcr);
155+
}
132156
repository = jcr
133157
.with(createSecurityProvider())
134158
.withAtomicCounter()
@@ -151,8 +175,8 @@ private void shutdownRepository() throws IOException {
151175
if (fileStore != null) {
152176
fileStore.close();
153177
fileStore = null;
178+
deleteDirectory(DIR_OAK_REPO_HOME);
154179
}
155-
deleteDirectory(DIR_OAK_REPO_HOME);
156180
}
157181
repository = null;
158182
}
@@ -206,4 +230,8 @@ public static ConfigurationParameters getSecurityConfigurationParameters() {
206230
UserConfiguration.NAME, ConfigurationParameters.of(userProps),
207231
AuthorizationConfiguration.NAME, ConfigurationParameters.of(authzProps));
208232
}
233+
234+
public Session createAdminSession() throws RepositoryException {
235+
return repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
236+
}
209237
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* (C) Copyright 2023 Cognizant Netcentric.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/legal/epl-v10.html
8+
*/
9+
package biz.netcentric.cq.tools.actool.helper;
10+
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
import java.io.IOException;
15+
16+
import javax.jcr.Node;
17+
import javax.jcr.PropertyType;
18+
import javax.jcr.RepositoryException;
19+
import javax.jcr.Session;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
import biz.netcentric.cq.tools.actool.extensions.OakRepository;
25+
26+
class QueryHelperIT {
27+
28+
@RegisterExtension
29+
static final OakRepository repository = new OakRepository(false);
30+
31+
@Test
32+
void testHasQueryIndexForACLsWithoutIndex(Session session) throws RepositoryException, IOException {
33+
// adjust nodetype index definition (at /oak:index/nodetype) to reflect what is configured in AEM
34+
Node ntIndexDefNode = session.getNode("/oak:index/nodetype");
35+
ntIndexDefNode.setProperty("declaringNodeTypes", new String[] { "oak:QueryIndexDefinition", "rep:User", "rep:Authorizable" }, PropertyType.NAME);
36+
session.save();
37+
assertFalse(QueryHelper.hasQueryIndexForACLs(session));
38+
}
39+
40+
@Test
41+
void testHasQueryIndexForACLsWithIndex(Session session) throws RepositoryException, IOException {
42+
// adjust nodetype index definition (at /oak:index/nodetype) to include rep:ACL
43+
// this is a different type than shipped with ACTool (Lucene), but lucene based index providers are hard to set up in an IT
44+
Node ntIndexDefNode = session.getNode("/oak:index/nodetype");
45+
ntIndexDefNode.setProperty("declaringNodeTypes", new String[] { "oak:QueryIndexDefinition", "rep:User", "rep:Authorizable", "rep:ACL" }, PropertyType.NAME);
46+
session.save();
47+
assertTrue(QueryHelper.hasQueryIndexForACLs(session));
48+
}
49+
50+
}

0 commit comments

Comments
 (0)