You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by di...@apache.org on 2021/06/07 13:32:50 UTC

[sling-whiteboard] 04/05: add sling sitemap invetory plugin

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

diru pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git

commit 528118cdfda86493addb4e19a28d3e1e53a8047a
Author: Dirk Rudolph <di...@apache.org>
AuthorDate: Mon Jun 7 14:44:31 2021 +0200

    add sling sitemap invetory plugin
---
 sitemap/pom.xml                                    |   6 +
 .../java/org/apache/sling/sitemap/SitemapInfo.java |  10 +
 .../sling/sitemap/impl/SitemapServiceImpl.java     |  33 ++--
 .../apache/sling/sitemap/impl/SitemapServlet.java  |  12 +-
 .../apache/sling/sitemap/impl/SitemapStorage.java  |  16 +-
 .../org/apache/sling/sitemap/impl/SitemapUtil.java |  23 ++-
 .../impl/console/SitemapInventoryPlugin.java       | 205 +++++++++++++++++++++
 7 files changed, 275 insertions(+), 30 deletions(-)

diff --git a/sitemap/pom.xml b/sitemap/pom.xml
index 711bb03..0302154 100644
--- a/sitemap/pom.xml
+++ b/sitemap/pom.xml
@@ -166,6 +166,12 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.inventory</artifactId>
+            <version>1.0.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
             <scope>provided</scope>
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/SitemapInfo.java b/sitemap/src/main/java/org/apache/sling/sitemap/SitemapInfo.java
index da7caaa..6579b23 100644
--- a/sitemap/src/main/java/org/apache/sling/sitemap/SitemapInfo.java
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/SitemapInfo.java
@@ -19,6 +19,7 @@
 package org.apache.sling.sitemap;
 
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.annotation.versioning.ProviderType;
 
 /**
@@ -28,6 +29,15 @@ import org.osgi.annotation.versioning.ProviderType;
 public interface SitemapInfo {
 
     /**
+     * Returns a resource path to the node the sitemap is stored. May return null if the sitemap or sitemap-index is
+     * served on-demand.
+     *
+     * @return
+     */
+    @Nullable
+    String getStoragePath();
+
+    /**
      * Returns the absolute, external url for the sitemap/sitemap-index.
      *
      * @return
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServiceImpl.java b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServiceImpl.java
index 22e6111..aa3703c 100644
--- a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServiceImpl.java
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServiceImpl.java
@@ -25,6 +25,7 @@ import org.apache.sling.sitemap.SitemapInfo;
 import org.apache.sling.sitemap.SitemapService;
 import org.apache.sling.sitemap.common.SitemapLinkExternalizer;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.osgi.service.component.annotations.*;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.Designate;
@@ -58,7 +59,7 @@ public class SitemapServiceImpl implements SitemapService {
     private static final Logger LOG = LoggerFactory.getLogger(SitemapServiceImpl.class);
 
     @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
-    private SitemapLinkExternalizer externalizer = SitemapLinkExternalizer.DEFAULT;
+    private SitemapLinkExternalizer externalizer;
     @Reference
     private JobManager jobManager;
     @Reference
@@ -99,7 +100,7 @@ public class SitemapServiceImpl implements SitemapService {
             return getSitemapUrlsForNestedSitemapRoot(sitemapRoot);
         }
 
-        String url = externalizer.externalize(sitemapRoot);
+        String url = externalize(sitemapRoot);
         Collection<String> names = generatorManager.getGenerators(sitemapRoot).keySet();
 
         if (url == null) {
@@ -121,7 +122,7 @@ public class SitemapServiceImpl implements SitemapService {
             } else {
                 location += storageInfo.getSitemapSelector() + '.' + SitemapServlet.SITEMAP_EXTENSION;
             }
-            infos.add(newSitemapInfo(location, storageInfo.getSize(), storageInfo.getEntries()));
+            infos.add(newSitemapInfo(storageInfo.getPath(), location, storageInfo.getSize(), storageInfo.getEntries()));
         }
 
         return infos;
@@ -172,7 +173,7 @@ public class SitemapServiceImpl implements SitemapService {
     private Collection<SitemapInfo> getSitemapUrlsForNestedSitemapRoot(Resource sitemapRoot) {
         Collection<String> names = generatorManager.getGenerators(sitemapRoot).keySet();
         Resource topLevelSitemapRoot = getTopLevelSitemapRoot(sitemapRoot);
-        String topLevelSitemapRootUrl = externalizer.externalize(topLevelSitemapRoot);
+        String topLevelSitemapRootUrl = externalize(topLevelSitemapRoot);
 
         if (topLevelSitemapRootUrl == null || names.isEmpty()) {
             LOG.debug("Could not create absolute urls for nested sitemaps at: {}", sitemapRoot.getPath());
@@ -187,7 +188,7 @@ public class SitemapServiceImpl implements SitemapService {
             String selector = getSitemapSelector(sitemapRoot, topLevelSitemapRoot, name);
             String location = topLevelSitemapRootUrl + selector + '.' + SitemapServlet.SITEMAP_EXTENSION;
             if (onDemandNames.contains(name)) {
-                infos.add(newSitemapInfo(location, -1, -1));
+                infos.add(newSitemapInfo(null, location, -1, -1));
             } else {
                 if (storageInfos == null) {
                     storageInfos = storage.getSitemaps(sitemapRoot, names);
@@ -197,7 +198,7 @@ public class SitemapServiceImpl implements SitemapService {
                         .findFirst();
                 if (storageInfoOpt.isPresent()) {
                     SitemapStorageInfo storageInfo = storageInfoOpt.get();
-                    infos.add(newSitemapInfo(location, storageInfo.getSize(), storageInfo.getEntries()));
+                    infos.add(newSitemapInfo(storageInfo.getPath(), location, storageInfo.getSize(), storageInfo.getEntries()));
                 }
             }
         }
@@ -205,25 +206,29 @@ public class SitemapServiceImpl implements SitemapService {
         return infos;
     }
 
-
+    private String externalize(Resource resource) {
+        return (externalizer == null ? SitemapLinkExternalizer.DEFAULT : externalizer).externalize(resource);
+    }
 
     private SitemapInfo newSitemapIndexInfo(@NotNull String url) {
-        return new SitemapInfoImpl(url, -1, -1, true, true);
+        return new SitemapInfoImpl(null, url, -1, -1, true, true);
     }
 
-    private SitemapInfo newSitemapInfo(@NotNull String url, int size, int entries) {
-        return new SitemapInfoImpl(url, size, entries, false, isWithinLimits(size, entries));
+    private SitemapInfo newSitemapInfo(@Nullable String path, @NotNull String url, int size, int entries) {
+        return new SitemapInfoImpl(path, url, size, entries, false, isWithinLimits(size, entries));
     }
 
     private static class SitemapInfoImpl implements SitemapInfo {
 
         private final String url;
+        private final String path;
         private final int size;
         private final int entries;
         private final boolean isIndex;
         private final boolean withinLimits;
 
-        private SitemapInfoImpl(@NotNull String url, int size, int entries, boolean isIndex, boolean withinLimits) {
+        private SitemapInfoImpl(@Nullable String path, @NotNull String url, int size, int entries, boolean isIndex, boolean withinLimits) {
+            this.path = path;
             this.url = url;
             this.size = size;
             this.entries = entries;
@@ -231,6 +236,12 @@ public class SitemapServiceImpl implements SitemapService {
             this.withinLimits = withinLimits;
         }
 
+        @Nullable
+        @Override
+        public String getStoragePath() {
+            return path;
+        }
+
         @NotNull
         @Override
         public String getUrl() {
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServlet.java b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServlet.java
index a254a10..a7828b1 100644
--- a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServlet.java
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapServlet.java
@@ -79,7 +79,7 @@ public class SitemapServlet extends SlingSafeMethodsServlet {
     };
 
     @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
-    private SitemapLinkExternalizer externalizer = SitemapLinkExternalizer.DEFAULT;
+    private SitemapLinkExternalizer externalizer;
     @Reference
     private SitemapGeneratorManager generatorManager;
     @Reference
@@ -134,8 +134,8 @@ public class SitemapServlet extends SlingSafeMethodsServlet {
         // add any sitemap from the storage
         for (SitemapStorageInfo storageInfo : storage.getSitemaps(topLevelSitemapRoot)) {
             if (!addedSitemapSelectors.contains(storageInfo.getSitemapSelector())) {
-                String location = externalizer.externalize(request, getSitemapLink(topLevelSitemapRoot,
-                        storageInfo.getSitemapSelector()));
+                String location = externalize(request,
+                        getSitemapLink(topLevelSitemapRoot, storageInfo.getSitemapSelector()));
                 Calendar lastModified = storageInfo.getLastModified();
                 if (location != null && lastModified != null) {
                     sitemapIndex.addSitemap(location, lastModified.toInstant());
@@ -209,7 +209,7 @@ public class SitemapServlet extends SlingSafeMethodsServlet {
             // applicable names we may serve directly, not applicable names, if any, we have to serve from storage
             for (String applicableName : applicableNames) {
                 String sitemapSelector = getSitemapSelector(sitemapRoot, sitemapRoot, applicableName);
-                String location = externalizer.externalize(request, getSitemapLink(sitemapRoot, sitemapSelector));
+                String location = externalize(request, getSitemapLink(sitemapRoot, sitemapSelector));
                 if (location != null) {
                     index.addSitemap(location);
                     addedSitemapSelectors.add(sitemapSelector);
@@ -222,6 +222,10 @@ public class SitemapServlet extends SlingSafeMethodsServlet {
         return addedSitemapSelectors;
     }
 
+    private String externalize(SlingHttpServletRequest request, String uri) {
+        return (externalizer == null ? SitemapLinkExternalizer.DEFAULT : externalizer).externalize(request, uri);
+    }
+
     private static String getSitemapLink(Resource sitemapRoot, String sitemapSelector) {
         String link = sitemapRoot.getPath() + '.' + SITEMAP_SELECTOR + '.';
         if (SITEMAP_SELECTOR.equals(sitemapSelector)) {
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapStorage.java b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapStorage.java
index 4ffa9c4..e6f41ea 100644
--- a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapStorage.java
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapStorage.java
@@ -51,7 +51,7 @@ import static org.apache.sling.sitemap.impl.SitemapUtil.*;
         service = {SitemapStorage.class, Runnable.class},
         property = {
                 Scheduler.PROPERTY_SCHEDULER_NAME + "=sitemap-storage-cleanup",
-                Scheduler.PROPERTY_SCHEDULER_CONCURRENT + "=false",
+                Scheduler.PROPERTY_SCHEDULER_CONCURRENT + ":Boolean=false",
                 Scheduler.PROPERTY_SCHEDULER_RUN_ON + "=" + Scheduler.VALUE_RUN_ON_SINGLE
         }
 )
@@ -125,7 +125,7 @@ public class SitemapStorage implements Runnable {
     }
 
     @NotNull
-    ValueMap getState(@NotNull Resource sitemapRoot, @NotNull String name) throws IOException {
+    public ValueMap getState(@NotNull Resource sitemapRoot, @NotNull String name) throws IOException {
         String statePath = getSitemapFilePath(sitemapRoot, name) + STATE_EXTENSION;
         try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
             Resource state = resolver.getResource(statePath);
@@ -142,7 +142,7 @@ public class SitemapStorage implements Runnable {
         }
     }
 
-    void writeState(@NotNull Resource sitemapRoot, @NotNull String name, @NotNull Map<String, Object> state)
+    public void writeState(@NotNull Resource sitemapRoot, @NotNull String name, @NotNull Map<String, Object> state)
             throws IOException {
         String statePath = getSitemapFilePath(sitemapRoot, name) + STATE_EXTENSION;
         try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
@@ -170,7 +170,7 @@ public class SitemapStorage implements Runnable {
         }
     }
 
-    void removeState(@NotNull Resource sitemapRoot, @NotNull String name) throws IOException {
+    public void removeState(@NotNull Resource sitemapRoot, @NotNull String name) throws IOException {
         String statePath = getSitemapFilePath(sitemapRoot, name) + STATE_EXTENSION;
         try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
             Resource stateResource = resolver.getResource(statePath);
@@ -183,7 +183,7 @@ public class SitemapStorage implements Runnable {
         }
     }
 
-    String writeSitemap(@NotNull Resource sitemapRoot, @NotNull String name, @NotNull InputStream data, int size,
+    public String writeSitemap(@NotNull Resource sitemapRoot, @NotNull String name, @NotNull InputStream data, int size,
                         int entries) throws IOException {
         String sitemapFilePath = getSitemapFilePath(sitemapRoot, name);
         String statePath = sitemapFilePath + STATE_EXTENSION;
@@ -227,7 +227,7 @@ public class SitemapStorage implements Runnable {
         return sitemapFilePath;
     }
 
-    Set<SitemapStorageInfo> getSitemaps(Resource sitemapRoot) {
+    public Set<SitemapStorageInfo> getSitemaps(Resource sitemapRoot) {
         return getSitemaps(sitemapRoot, Collections.emptySet());
     }
 
@@ -242,7 +242,7 @@ public class SitemapStorage implements Runnable {
      * @param names
      * @return
      */
-    Set<SitemapStorageInfo> getSitemaps(Resource sitemapRoot, Collection<String> names) {
+    public Set<SitemapStorageInfo> getSitemaps(Resource sitemapRoot, Collection<String> names) {
         Resource topLevelSitemapRoot = getTopLevelSitemapRoot(sitemapRoot);
         Predicate<SitemapStorageInfo> filter;
 
@@ -279,7 +279,7 @@ public class SitemapStorage implements Runnable {
         }
     }
 
-    boolean copySitemap(Resource sitemapRoot, String sitemapSelector, OutputStream output) throws IOException {
+    public boolean copySitemap(Resource sitemapRoot, String sitemapSelector, OutputStream output) throws IOException {
         if (!isTopLevelSitemapRoot(sitemapRoot)) {
             return false;
         }
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapUtil.java b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapUtil.java
index e5607bc..7ee438e 100644
--- a/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapUtil.java
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/impl/SitemapUtil.java
@@ -19,10 +19,11 @@
 package org.apache.sling.sitemap.impl;
 
 import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.util.ISO9075;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
-import org.apache.sling.sitemap.generator.SitemapGenerator;
 import org.apache.sling.sitemap.SitemapService;
+import org.apache.sling.sitemap.generator.SitemapGenerator;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -31,6 +32,8 @@ import java.util.*;
 
 public class SitemapUtil {
 
+    private static final String JCR_SYSTEM_PATH = "/" + JcrConstants.JCR_SYSTEM + "/";
+
     private SitemapUtil() {
         super();
     }
@@ -151,12 +154,16 @@ public class SitemapUtil {
      */
     @NotNull
     public static Iterator<Resource> findSitemapRoots(ResourceResolver resolver, String searchPath) {
-        return new Iterator<Resource>() {
+        String correctedSearchPath = searchPath == null ? "/" : searchPath;
+        StringBuilder query = new StringBuilder(correctedSearchPath.length() + 35);
+        query.append("/jcr:root").append(ISO9075.encodePath(correctedSearchPath));
+        if (!correctedSearchPath.endsWith("/")) {
+            query.append('/');
+        }
+        query.append("/*[@").append(SitemapService.PROPERTY_SITEMAP_ROOT).append('=').append(Boolean.TRUE).append(']');
 
-            private final Iterator<Resource> hits = resolver.findResources(
-                    "/jcr:root" + searchPath + "//*[@" + SitemapService.PROPERTY_SITEMAP_ROOT + "=true]",
-                    Query.XPATH
-            );
+        return new Iterator<Resource>() {
+            private final Iterator<Resource> hits = resolver.findResources(query.toString(), Query.XPATH);
             private Resource next = seek();
 
             private Resource seek() {
@@ -165,7 +172,9 @@ public class SitemapUtil {
                     // skip a hit on the given searchPath itself. This may be when a search is done for descendant
                     // sitemaps given the normalized sitemap root path and the sitemap root's jcr:content is in the
                     // result set.
-                    if (nextHit == null || nextHit.getPath().equals(searchPath)) {
+                    if (nextHit == null
+                            || nextHit.getPath().equals(correctedSearchPath)
+                            || nextHit.getPath().startsWith(JCR_SYSTEM_PATH)) {
                         continue;
                     }
                     return nextHit;
diff --git a/sitemap/src/main/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPlugin.java b/sitemap/src/main/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPlugin.java
new file mode 100644
index 0000000..8feb7bd
--- /dev/null
+++ b/sitemap/src/main/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPlugin.java
@@ -0,0 +1,205 @@
+/*
+ * 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.sitemap.impl.console;
+
+import org.apache.felix.inventory.Format;
+import org.apache.felix.inventory.InventoryPrinter;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.sitemap.SitemapInfo;
+import org.apache.sling.sitemap.SitemapService;
+import org.apache.sling.sitemap.impl.SitemapUtil;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+@Component(
+        service = InventoryPrinter.class,
+        property = {
+                InventoryPrinter.NAME + "=slingsitemap",
+                InventoryPrinter.TITLE + "=Sling Sitemap",
+                InventoryPrinter.FORMAT + "=JSON",
+                InventoryPrinter.FORMAT + "=TEXT",
+                InventoryPrinter.WEBCONSOLE + "=true"
+
+        }
+)
+public class SitemapInventoryPlugin implements InventoryPrinter {
+
+    private static final Map<String, Object> AUTH = Collections.singletonMap(
+            ResourceResolverFactory.SUBSERVICE, "sitemap-reader");
+    private static final Logger LOG = LoggerFactory.getLogger(SitemapInventoryPlugin.class);
+
+    @Reference
+    private SitemapService sitemapService;
+    @Reference
+    private ResourceResolverFactory resourceResolverFactory;
+
+    private BundleContext bundleContext;
+
+    @Activate
+    protected void activate(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+
+    @Override
+    public void print(PrintWriter printWriter, Format format, boolean isZip) {
+        if (Format.JSON.equals(format)) {
+            printJson(printWriter);
+        } else if (Format.TEXT.equals(format)) {
+            printText(printWriter);
+        }
+    }
+
+    private void printJson(PrintWriter pw) {
+        pw.print('{');
+        pw.print("\"schedulers\":[");
+        boolean hasScheduler = false;
+        for (ServiceReference<?> ref : bundleContext.getBundle().getRegisteredServices()) {
+            Object schedulerExp = ref.getProperty(Scheduler.PROPERTY_SCHEDULER_EXPRESSION);
+            Object schedulerName = ref.getProperty(Scheduler.PROPERTY_SCHEDULER_NAME);
+            if (schedulerExp instanceof String && schedulerName instanceof String) {
+                if (hasScheduler) {
+                    pw.print(',');
+                }
+                hasScheduler = true;
+                pw.print("{\"name\":\"");
+                pw.print(escapeDoubleQuotes((String) schedulerName));
+                pw.print("\",\"expression\":\"");
+                pw.print(escapeDoubleQuotes((String) schedulerExp));
+                pw.print("\"}");
+            }
+        }
+        pw.print("],");
+
+        pw.print("\"roots\":{");
+        try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
+            Iterator<Resource> roots = SitemapUtil.findSitemapRoots(resolver, "/");
+            while (roots.hasNext()) {
+                Resource root = roots.next();
+                pw.print('"');
+                pw.print(escapeDoubleQuotes(root.getPath()));
+                pw.print("\":[");
+                Iterator<SitemapInfo> infoIt = sitemapService.getSitemapInfo(root).iterator();
+                while (infoIt.hasNext()) {
+                    SitemapInfo info = infoIt.next();
+                    pw.print('{');
+                    pw.print("\"url\":\"");
+                    pw.print(escapeDoubleQuotes(info.getUrl()));
+                    pw.print('"');
+                    if (info.getStoragePath() != null) {
+                        pw.print(",\"path\":\"");
+                        pw.print(escapeDoubleQuotes(info.getStoragePath()));
+                        pw.print("\",\"size\":");
+                        pw.print(info.getSize());
+                        pw.print(",\"entries\":");
+                        pw.print(info.getEntries());
+                        pw.print(",\"inLimits\":");
+                        pw.print(info.isWithinLimits());
+                    }
+                    pw.print('}');
+                    if (infoIt.hasNext()) {
+                        pw.print(',');
+                    }
+                }
+                pw.print(']');
+                if (roots.hasNext()) {
+                    pw.print(',');
+                }
+            }
+        } catch (LoginException ex) {
+            pw.println("Failed to list sitemaps: " + ex.getMessage());
+            LOG.warn("Failed to get inventory of sitemaps: {}", ex.getMessage(), ex);
+        }
+        pw.print('}');
+        pw.print('}');
+    }
+
+    private void printText(PrintWriter pw) {
+        pw.println("Apache Sling Sitemap Schedulers");
+        pw.println("-------------------------------");
+
+        for (ServiceReference<?> ref : bundleContext.getBundle().getRegisteredServices()) {
+            Object schedulerExp = ref.getProperty(Scheduler.PROPERTY_SCHEDULER_EXPRESSION);
+            Object schedulerName = ref.getProperty(Scheduler.PROPERTY_SCHEDULER_NAME);
+            if (schedulerExp != null && schedulerName != null) {
+                pw.print(" - Name: ");
+                pw.print(schedulerName);
+                pw.println();
+                pw.print("   Expression: ");
+                pw.print(schedulerExp);
+                pw.println();
+            }
+        }
+
+        pw.println();
+        pw.println();
+        pw.println("Apache Sling Sitemap Roots");
+        pw.println("--------------------------");
+
+        try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
+            Iterator<Resource> roots = SitemapUtil.findSitemapRoots(resolver, "/");
+            while (roots.hasNext()) {
+                Resource root = roots.next();
+                pw.print(root.getPath());
+                pw.print(':');
+                pw.println();
+                for (SitemapInfo info : sitemapService.getSitemapInfo(root)) {
+                    pw.print(" - Url: ");
+                    pw.print(info.getUrl());
+                    pw.println();
+                    if (info.getStoragePath() != null) {
+                        pw.print("   Path: ");
+                        pw.print(info.getStoragePath());
+                        pw.println();
+                        pw.print("   Bytes: ");
+                        pw.print(info.getSize());
+                        pw.println();
+                        pw.print("   Urls: ");
+                        pw.print(info.getEntries());
+                        pw.println();
+                        pw.print("   Within Limits: ");
+                        pw.print(info.isWithinLimits() ? "yes": "no");
+                        pw.println();
+                    }
+                }
+            }
+        } catch (LoginException ex) {
+            pw.println("Failed to list sitemaps: " + ex.getMessage());
+            LOG.warn("Failed to get inventory of sitemaps: {}", ex.getMessage(), ex);
+        }
+    }
+
+    private static String escapeDoubleQuotes(String text) {
+        return text.replace("\"", "\\\"");
+    }
+}