This is an automated email from the ASF dual-hosted git repository. dklco pushed a commit to branch SLING-11229-SLING-11230 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-resource.git
commit 3408056cc42b34c6fa20d07f44c023c447a0dd9d Author: Dan Klco <[email protected]> AuthorDate: Mon Mar 28 09:01:14 2022 -0400 Fixes SLING-11229 / SLING-11230 by adding a configurable limit to the JCR Resource Provider query provider and support parsing the start and limit values out of query comments --- .../resource/internal/helper/JcrResourceUtil.java | 20 +++- .../helper/jcr/BasicQueryLanguageProvider.java | 8 +- .../internal/helper/jcr/JcrResourceProvider.java | 27 ++++- .../helper/jcr/LimitingQueryLanguageProvider.java | 129 +++++++++++++++++++++ .../JcrResourceProviderSessionHandlingTest.java | 17 ++- .../helper/jcr/JcrResourceProviderTest.java | 18 ++- .../jcr/LimitingQueryLanguageProviderTest.java | 112 ++++++++++++++++++ 7 files changed, 322 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrResourceUtil.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrResourceUtil.java index 76b7718..986100d 100644 --- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrResourceUtil.java +++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrResourceUtil.java @@ -43,7 +43,7 @@ public class JcrResourceUtil { /** * Helper method to execute a JCR query. - * + * * @param session the session * @param query the query * @param language the language @@ -52,8 +52,26 @@ public class JcrResourceUtil { */ public static QueryResult query(Session session, String query, String language) throws RepositoryException { + return query(session, query, language, 0, Long.MAX_VALUE); + } + + /** + * Helper method to execute a JCR query. + * + * @param session the session + * @param query the query + * @param language the language + * @param offset the offset to start at + * @param limit the limit to the number of results to return + * @return the query's result + * @throws RepositoryException if the {@link QueryManager} cannot be retrieved + */ + public static QueryResult query(Session session, String query, + String language, long offset, long limit) throws RepositoryException { QueryManager qManager = session.getWorkspace().getQueryManager(); Query q = qManager.createQuery(query, language); + q.setOffset(offset); + q.setLimit(limit); return q.execute(); } diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BasicQueryLanguageProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BasicQueryLanguageProvider.java index 35f9849..5acf880 100644 --- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BasicQueryLanguageProvider.java +++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/BasicQueryLanguageProvider.java @@ -63,6 +63,10 @@ public class BasicQueryLanguageProvider implements QueryLanguageProvider<JcrProv this.providerContext = ctx; } + protected QueryResult query(final ResolveContext<JcrProviderState> ctx, final String query, final String language) throws RepositoryException{ + return JcrResourceUtil.query(ctx.getProviderState().getSession(), query, language); + } + @Override public String[] getSupportedLanguages(final ResolveContext<JcrProviderState> ctx) { try { @@ -77,7 +81,7 @@ public class BasicQueryLanguageProvider implements QueryLanguageProvider<JcrProv final String query, final String language) { try { - final QueryResult res = JcrResourceUtil.query(ctx.getProviderState().getSession(), query, language); + final QueryResult res = query(ctx, query, language); return new JcrNodeResourceIterator(ctx.getResourceResolver(), null, null, res.getNodes(), @@ -97,7 +101,7 @@ public class BasicQueryLanguageProvider implements QueryLanguageProvider<JcrProv final String queryLanguage = ArrayUtils.contains(getSupportedLanguages(ctx), language) ? language : DEFAULT_QUERY_LANGUAGE; try { - final QueryResult result = JcrResourceUtil.query(ctx.getProviderState().getSession(), query, queryLanguage); + final QueryResult result = query(ctx, query, queryLanguage); final String[] colNames = result.getColumnNames(); final RowIterator rows = result.getRows(); diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java index 163ccba..16e39db 100644 --- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java +++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java @@ -30,8 +30,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; import javax.jcr.Item; import javax.jcr.Node; import javax.jcr.NodeIterator; @@ -61,6 +59,8 @@ import org.apache.sling.spi.resource.provider.QueryLanguageProvider; import org.apache.sling.spi.resource.provider.ResolveContext; import org.apache.sling.spi.resource.provider.ResourceContext; import org.apache.sling.spi.resource.provider.ResourceProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.osgi.framework.Constants; import org.osgi.framework.ServiceReference; import org.osgi.service.component.ComponentContext; @@ -70,6 +70,9 @@ import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,6 +88,7 @@ import org.slf4j.LoggerFactory; ResourceProvider.PROPERTY_AUTHENTICATE + "=" + ResourceProvider.AUTHENTICATE_REQUIRED, Constants.SERVICE_VENDOR + "=The Apache Software Foundation" }) +@Designate(ocd = JcrResourceProvider.Config.class) public class JcrResourceProvider extends ResourceProvider<JcrProviderState> { /** Logger */ @@ -100,6 +104,16 @@ public class JcrResourceProvider extends ResourceProvider<JcrProviderState> { IGNORED_PROPERTIES.add("jcr:createdBy"); } + @ObjectClassDefinition(name = "Apache Sling JCR Resource Provider", description = "Provides Sling resources based on the Java Content Repository") + public @interface Config { + + @AttributeDefinition(name = "Enable Query Limit", description = "If set to true, the JcrResourceProvider will support parsing query start and limits from comments in the queries and set a default limit for all other queries using the findResources methods") + boolean enable_query_limit() default false; + + @AttributeDefinition(name = "Default Query Limit", description = "The default query limit for queries using the findResources methods") + long default_query_limit() default 10000L; + } + @Reference(name = REPOSITORY_REFERNENCE_NAME, service = SlingRepository.class) private ServiceReference<SlingRepository> repositoryReference; @@ -115,12 +129,14 @@ public class JcrResourceProvider extends ResourceProvider<JcrProviderState> { private volatile JcrProviderStateFactory stateFactory; + private Config config; + private final AtomicReference<DynamicClassLoaderManager> classLoaderManagerReference = new AtomicReference<DynamicClassLoaderManager>(); private AtomicReference<URIProvider[]> uriProviderReference = new AtomicReference<URIProvider[]>(); @Activate - protected void activate(final ComponentContext context) throws RepositoryException { + protected void activate(final ComponentContext context, final Config config) throws RepositoryException { SlingRepository repository = context.locateService(REPOSITORY_REFERNENCE_NAME, this.repositoryReference); if (repository == null) { @@ -131,6 +147,8 @@ public class JcrResourceProvider extends ResourceProvider<JcrProviderState> { return; } + this.config = config; + this.repository = repository; this.stateFactory = new JcrProviderStateFactory(repositoryReference, repository, @@ -627,6 +645,9 @@ public class JcrResourceProvider extends ResourceProvider<JcrProviderState> { public @Nullable QueryLanguageProvider<JcrProviderState> getQueryLanguageProvider() { final ProviderContext ctx = this.getProviderContext(); if ( ctx != null ) { + if(config.enable_query_limit()){ + return new LimitingQueryLanguageProvider(ctx, config.default_query_limit()); + } return new BasicQueryLanguageProvider(ctx); } return null; diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProvider.java new file mode 100644 index 0000000..be86ae6 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProvider.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.jcr.resource.internal.helper.jcr; + +import java.io.IOException; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.sling.jcr.resource.internal.helper.JcrResourceUtil; +import org.apache.sling.spi.resource.provider.ProviderContext; +import org.apache.sling.spi.resource.provider.ResolveContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LimitingQueryLanguageProvider extends BasicQueryLanguageProvider { + + private final Logger log = LoggerFactory.getLogger(LimitingQueryLanguageProvider.class); + + /** The limit to set for queries */ + private final long defaultLimit; + + public LimitingQueryLanguageProvider(final ProviderContext ctx, long defaultLimit) { + super(ctx); + this.defaultLimit = defaultLimit; + } + + @Override + protected QueryResult query(final ResolveContext<JcrProviderState> ctx, final String query, final String language) + throws RepositoryException { + Triple<String, Long, Long> settings = extractQuerySettings(query); + return JcrResourceUtil.query(ctx.getProviderState().getSession(), settings.getLeft(), language, + settings.getMiddle(), settings.getRight()); + } + + protected Triple<String, Long, Long> extractQuerySettings(String query) { + query = query.trim(); + if (query.endsWith("*/")) { + Pair<Long, Long> settings = parseQueryComment( + query.substring(query.lastIndexOf("/*") + 2, query.lastIndexOf("*/"))); + return new ImmutableTriple<>(query.substring(0, query.lastIndexOf("/*")), + settings.getLeft(), settings.getRight()); + } else { + return new ImmutableTriple<>(query, 0L, defaultLimit); + } + } + + private Pair<Long, Long> parseQueryComment(String query) { + Map<String, Object> parsed = new HashMap<>(); + StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(query)); + int currentToken; + try { + currentToken = tokenizer.nextToken(); + boolean key = true; + Object current = null; + while (currentToken != StreamTokenizer.TT_EOF) { + if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) { + if (!key) { + parsed.put((String) current, tokenizer.nval); + key = true; + current = null; + } else { + throw new IOException( + "Encountered unexpected numeric key: " + tokenizer.toString()); + } + } else if (tokenizer.ttype == StreamTokenizer.TT_WORD) { + if (!key) { + parsed.put((String) current, tokenizer.nval); + key = true; + } else if (current == null) { + current = tokenizer.sval; + } else { + throw new IOException( + "Encountered unmatched key value pair: " + tokenizer.toString()); + } + } else if (((char) currentToken) == '=') { + key = false; + } else if (((char) currentToken) == ',' || ((char) currentToken) == ';') { + // nothing really required, just ignoring as it's a separator + } else { + throw new IOException( + "Encountered unexpected character parsing query comment: " + tokenizer.toString()); + } + currentToken = tokenizer.nextToken(); + } + } catch (Exception e) { + log.warn("Failed to parse query comment due to exception: {}", e.toString()); + return new ImmutablePair<>(0L, defaultLimit); + } + return new ImmutablePair<>(getKeyAsLong(parsed, "slingQueryStart", 0L), + getKeyAsLong(parsed, "slingQueryLimit", defaultLimit)); + } + + private Long getKeyAsLong(Map<String, Object> parsed, String key, Long defaultVal) { + return Optional.ofNullable(parsed.get(key)).map(v -> { + if (v instanceof String) { + return Long.parseLong(v.toString()); + } else { + return ((Double) v).longValue(); + } + }).orElse(defaultVal); + } + +} diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderSessionHandlingTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderSessionHandlingTest.java index 5dbdf7c..c52f271 100644 --- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderSessionHandlingTest.java +++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderSessionHandlingTest.java @@ -31,6 +31,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -47,6 +48,7 @@ import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.jcr.api.SlingRepository; import org.apache.sling.jcr.resource.api.JcrResourceConstants; +import org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProvider.Config; import org.apache.sling.spi.resource.provider.ResolveContext; import org.apache.sling.spi.resource.provider.ResourceProvider; import org.junit.After; @@ -235,7 +237,20 @@ public class JcrResourceProviderSessionHandlingTest { when(ctx.locateService(anyString(), Mockito.<ServiceReference<Object>>any())).thenReturn(repo); jcrResourceProvider = new JcrResourceProvider(); - jcrResourceProvider.activate(ctx); + jcrResourceProvider.activate(ctx, new Config() { + @Override + public Class<? extends Annotation> annotationType() { + return null; + } + @Override + public boolean enable_query_limit() { + return false; + } + @Override + public long default_query_limit() { + return 0; + } + }); jcrProviderState = jcrResourceProvider.authenticate(authInfo); } diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java index 684ce80..de99546 100644 --- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java +++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java @@ -18,6 +18,7 @@ */ package org.apache.sling.jcr.resource.internal.helper.jcr; +import java.lang.annotation.Annotation; import java.security.Principal; import javax.jcr.Node; @@ -26,9 +27,9 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NodeType; -import org.apache.jackrabbit.commons.JcrUtils; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; +import org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProvider.Config; import org.apache.sling.spi.resource.provider.ResolveContext; import org.apache.sling.spi.resource.provider.ResourceContext; import org.junit.Assert; @@ -50,7 +51,20 @@ public class JcrResourceProviderTest extends SlingRepositoryTestBase { ComponentContext ctx = Mockito.mock(ComponentContext.class); Mockito.when(ctx.locateService(Mockito.anyString(), Mockito.any(ServiceReference.class))).thenReturn(repo); jcrResourceProvider = new JcrResourceProvider(); - jcrResourceProvider.activate(ctx); + jcrResourceProvider.activate(ctx, new Config() { + @Override + public Class<? extends Annotation> annotationType() { + return null; + } + @Override + public boolean enable_query_limit() { + return false; + } + @Override + public long default_query_limit() { + return 0; + } + }); } @Override diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProviderTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProviderTest.java new file mode 100644 index 0000000..77dd127 --- /dev/null +++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/LimitingQueryLanguageProviderTest.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.jcr.resource.internal.helper.jcr; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.commons.lang3.tuple.Triple; +import org.apache.sling.spi.resource.provider.ProviderContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class LimitingQueryLanguageProviderTest { + + @Parameters(name = "{0}") + public static Collection<Object[]> testCases() { + return Arrays.asList(new Object[][] { + testCase("JCR-SQL2 No Settings", "SELECT * FROM [nt:folder]", 10L, + "SELECT * FROM [nt:folder]", + 0L, 10L), + testCase("JCR-SQL2 With Limit", "SELECT * FROM [nt:folder] /* slingQueryLimit=20 */", 10L, + "SELECT * FROM [nt:folder] ", + 0L, 20L), + testCase("JCR-SQL2 With Limit", "SELECT * FROM [nt:folder] /* slingQueryStart=2, slingQueryLimit=20 */", + 10L, + "SELECT * FROM [nt:folder] ", + 2L, 20L), + testCase("JCR-SQL2 With Limit", "SELECT * FROM [nt:folder] /* someotherkey=2, slingQueryLimit=20 */", + 10L, + "SELECT * FROM [nt:folder] ", + 0L, 20L), + testCase("JCR-SQL2 With Limit", "SELECT * FROM [nt:folder] /* someotherkey=2, slingQueryLimit=20 */", + 10L, + "SELECT * FROM [nt:folder] ", + 0L, 20L), + testCase("XPath With Limit", + " /jcr:root/content//element(*, sling:Folder)[@sling:resourceType='x'] /* slingQueryStart=2, slingQueryLimit=20 */", + 10L, + "/jcr:root/content//element(*, sling:Folder)[@sling:resourceType='x'] ", + 2L, 20L), + testCase("XPath With Invalid Key", + " /jcr:root/content//element(*, sling:Folder)[@sling:resourceType='x'] /* 2=2, slingQueryLimit=20 */", + 10L, + "/jcr:root/content//element(*, sling:Folder)[@sling:resourceType='x'] ", + 0L, 10L) + }); + } + + public static Object[] testCase(String name, String query, long defaultLimit, String expectedQuery, + long expectedStart, + long expectedLimit) { + return new Object[] { + name, query, defaultLimit, expectedQuery, expectedStart, expectedLimit + }; + + } + + private String name; + private String query; + private Long defaultLimit; + private String expectedQuery; + private Long expectedStart; + private Long expectedLimit; + + public LimitingQueryLanguageProviderTest(String name, String query, long defaultLimit, + String expectedQuery, + long expectedStart, + long expectedLimit) { + this.name = name; + this.query = query; + this.defaultLimit = defaultLimit; + this.expectedQuery = expectedQuery; + this.expectedStart = expectedStart; + this.expectedLimit = expectedLimit; + } + + @Test + public void testQueryLanguageProvider() { + LimitingQueryLanguageProvider provider = new LimitingQueryLanguageProvider(mock(ProviderContext.class), + defaultLimit); + + Triple<String, Long, Long> settings = provider.extractQuerySettings(query); + + assertEquals(expectedQuery, settings.getLeft()); + assertEquals(expectedStart, settings.getMiddle()); + assertEquals(expectedLimit, settings.getRight()); + + } + +}
