You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by re...@apache.org on 2025/01/13 12:05:58 UTC

(sling-org-apache-sling-resourceresolver) 03/03: SLING-12611: Resource Resolver: refactor MapEntries - extract PagedQueryIterator

This is an automated email from the ASF dual-hosted git repository.

reschke pushed a commit to branch SLING-12611
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git

commit 89838659e1bd988ca2e94ccea266728ddf000df1
Author: Julian Reschke <re...@apache.org>
AuthorDate: Mon Jan 13 13:05:26 2025 +0100

    SLING-12611: Resource Resolver: refactor MapEntries - extract PagedQueryIterator
---
 .../impl/mapping/PagedQueryIterator.java           | 175 +++++++++++++++++++++
 1 file changed, 175 insertions(+)

diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java
new file mode 100644
index 0000000..090ee43
--- /dev/null
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/mapping/PagedQueryIterator.java
@@ -0,0 +1,175 @@
+/*
+ * 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.resourceresolver.impl.mapping;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.sling.resourceresolver.impl.mapping.MapEntries.queryLiteral;
+
+/**
+ * Utility class for running paged queries.
+ */
+public class PagedQueryIterator implements Iterator<Resource> {
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(PagedQueryIterator.class);
+
+    private final String subject;
+    private final String propertyName;
+    private final ResourceResolver resolver;
+    private final String query;
+    private final int pageSize;
+
+    private String lastKey = "";
+    private String lastValue = null;
+    private Iterator<Resource> it;
+    private int count = 0;
+    private int page = 0;
+    private Resource next = null;
+    private final String[] defaultValue = new String[0];
+    private int largestPage = 0;
+    private String largestKeyValue = "";
+    private int largestKeyCount = 0;
+    private int currentKeyCount = 0;
+
+    /**
+     * @param subject name of the query, will be used only for logging
+     * @param propertyName name of multivalued string property to query on (used for diagnostics)
+     * @param resolver resource resolver
+     * @param query query string in SQL2 syntax
+     * @param pageSize page size (start a new query after page size is exceeded)
+     */
+    public PagedQueryIterator(String subject, String propertyName, ResourceResolver resolver, String query, int pageSize) {
+        this.subject = subject;
+        this.propertyName = propertyName;
+        this.resolver = resolver;
+        this.query = query;
+        this.pageSize = pageSize;
+        nextPage();
+    }
+
+    private void nextPage() {
+        count = 0;
+        String formattedQuery = String.format(query, queryLiteral(lastKey));
+        log.debug("start {} query (page {}): {}", subject, page, formattedQuery);
+        long queryStart = System.nanoTime();
+        this.it = resolver.findResources(formattedQuery, "JCR-SQL2");
+        long queryElapsed = System.nanoTime() - queryStart;
+        log.debug("end {} query (page {}); elapsed {}ms", subject, page, TimeUnit.NANOSECONDS.toMillis(queryElapsed));
+        page += 1;
+    }
+
+    private Resource getNext() throws NoSuchElementException {
+        Resource resource = it.next();
+        count += 1;
+        final String[] values = resource.getValueMap().get(propertyName, defaultValue);
+
+        if (values.length > 0) {
+            String value = values[0];
+            if (value.compareTo(lastKey) < 0) {
+                String message = String.format("unexpected query result in page %d, %s of '%s' despite querying for > '%s'",
+                        (page - 1), propertyName, value, lastKey);
+                log.error(message);
+                throw new RuntimeException(message);
+            }
+            if (lastValue != null && value.compareTo(lastValue) < 0) {
+                String message = String.format("unexpected query result in page %d, property name '%s', got '%s', last value was '%s'",
+                        (page - 1), propertyName, value, lastValue);
+                log.error(message);
+                throw new RuntimeException(message);
+            }
+
+            // keep information about large key counts
+            if (value.equals(lastValue)) {
+                currentKeyCount += 1;
+            } else {
+                if (currentKeyCount > largestKeyCount) {
+                    largestKeyCount = currentKeyCount + 1;
+                    largestKeyValue = lastValue;
+                }
+                currentKeyCount = 0;
+            }
+
+            // start next page?
+            if (count > pageSize && !value.equals(lastValue)) {
+                updatePageStats();
+                lastKey = value;
+                nextPage();
+                return getNext();
+            }
+            lastValue = value;
+        }
+
+        return resource;
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (next == null) {
+            try {
+                next = getNext();
+            } catch (NoSuchElementException ex) {
+                if (currentKeyCount > largestKeyCount) {
+                    largestKeyCount = currentKeyCount + 1;
+                    largestKeyValue = lastValue;
+                }
+                updatePageStats();
+                // there are no more
+                next = null;
+            }
+        }
+        return next != null;
+    }
+
+    @Override
+    public Resource next() throws NoSuchElementException {
+        Resource result = next != null ? next : getNext();
+        next = null;
+        return result;
+    }
+
+    private void updatePageStats() {
+        largestPage = Math.max(largestPage, count - 1);
+        log.debug("read {} query (page {}); {} entries, last key was: {}, largest page so far: {}", subject, page - 1,
+                count, lastKey, largestPage);
+    }
+
+    public @NotNull String getStatistics() {
+        return String.format(" (max. page size: %d, number of pages: %d)", largestPage, page);
+    }
+
+    public @NotNull String getWarning() {
+        int warnAt = pageSize * 10;
+        if (largestKeyCount > warnAt) {
+            return String.format(
+                    "Largest number of %s entries with the same 'first' selector exceeds expectation of %d (value '%s' appears %d times)",
+                    subject, warnAt, largestKeyValue, largestKeyCount);
+        } else {
+            return "";
+        }
+    }
+}