Merge branch 'master' into SLING-11229-SLING-11230
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 @@
 
     /**
      * Helper method to execute a JCR query.
-     *
+     * 
      * @param session the session
      * @param query the query
      * @param language the language
@@ -52,8 +52,26 @@
      */
     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 @@
         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 @@
             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 @@
         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 bdb7790..d7809d6 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
@@ -32,8 +32,6 @@
 import java.util.TreeMap;
 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;
@@ -64,6 +62,8 @@
 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;
@@ -73,6 +73,9 @@
 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;
 
@@ -88,6 +91,7 @@
                    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 */
@@ -103,6 +107,16 @@
         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_REFERENCE_NAME, service = SlingRepository.class)
     private ServiceReference<SlingRepository> repositoryReference;
 
@@ -122,12 +136,14 @@
 
     private volatile JcrProviderStateFactory stateFactory;
 
+    private Config config;
+  
     private final AtomicReference<DynamicClassLoaderManager> classLoaderManagerReference = new AtomicReference<>();
 
     private AtomicReference<URIProvider[]> uriProviderReference = new AtomicReference<>();
 
     @Activate
-    protected void activate(final ComponentContext context) {
+    protected void activate(final ComponentContext context, final Config config) throws RepositoryException {
         SlingRepository repository = context.locateService(REPOSITORY_REFERENCE_NAME,
                 this.repositoryReference);
         if (repository == null) {
@@ -138,6 +154,8 @@
             return;
         }
 
+        this.config = config;
+
         this.repository = repository;
 
         this.stateFactory = new JcrProviderStateFactory(repositoryReference, repository,
@@ -634,6 +652,9 @@
     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.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.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 @@
         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.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 @@
         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());
+
+    }
+
+}