diff --git a/sitemap/pom.xml b/sitemap/pom.xml
index a50d53203..2b96385c1 100644
--- a/sitemap/pom.xml
+++ b/sitemap/pom.xml
@@ -247,6 +247,18 @@
2.1
test
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.12.3
+ test
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ 2.12.3
+ test
+
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
index f0f32fec2..ef7f10634 100644
--- 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
@@ -29,6 +29,7 @@
import org.apache.sling.sitemap.SitemapService;
import org.apache.sling.sitemap.common.SitemapUtil;
import org.apache.sling.sitemap.impl.SitemapServiceConfiguration;
+import org.jetbrains.annotations.Nullable;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.annotations.Activate;
@@ -115,17 +116,20 @@ private void printJson(PrintWriter pw) {
while (infoIt.hasNext()) {
SitemapInfo info = infoIt.next();
pw.print('{');
- pw.print("\"url\":\"");
+ pw.print("\"name\":\"");
+ pw.print(escapeDoubleQuotes(info.getName()));
+ pw.print('"');
+ pw.print(",\"url\":\"");
pw.print(escapeDoubleQuotes(info.getUrl()));
+ pw.print("\",\"status\":\"");
+ pw.print(info.getStatus());
pw.print('"');
if (info.getStoragePath() != null) {
pw.print(",\"path\":\"");
pw.print(escapeDoubleQuotes(info.getStoragePath()));
- pw.print("\",\"status\":\"");
- pw.print(info.getStatus());
pw.print("\",\"size\":");
pw.print(info.getSize());
- pw.print(",\"entries\":");
+ pw.print(",\"urls\":");
pw.print(info.getEntries());
pw.print(",\"inLimits\":");
pw.print(isWithinLimits(info));
@@ -149,8 +153,9 @@ private void printJson(PrintWriter pw) {
}
private void printText(PrintWriter pw) {
- pw.println("Apache Sling Sitemap Schedulers");
- pw.println("-------------------------------");
+ pw.println("# Apache Sling Sitemap Schedulers");
+ pw.println("# -------------------------------");
+ pw.println("schedulers:");
for (ServiceReference> ref : bundleContext.getBundle().getRegisteredServices()) {
Object schedulerExp = ref.getProperty(Scheduler.PROPERTY_SCHEDULER_EXPRESSION);
@@ -167,35 +172,40 @@ private void printText(PrintWriter pw) {
pw.println();
pw.println();
- pw.println("Apache Sling Sitemap Roots");
- pw.println("--------------------------");
+ pw.println("# Apache Sling Sitemap Roots");
+ pw.println("# --------------------------");
+ pw.println("roots:");
try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) {
Iterator roots = SitemapUtil.findSitemapRoots(resolver, "/");
while (roots.hasNext()) {
Resource root = roots.next();
+ pw.print(" ");
pw.print(root.getPath());
pw.print(':');
pw.println();
for (SitemapInfo info : sitemapService.getSitemapInfo(root)) {
- pw.print(" - Url: ");
+ pw.print(" - Name: ");
+ pw.print(info.getName());
+ pw.println();
+ pw.print(" Url: ");
pw.print(info.getUrl());
pw.println();
+ pw.print(" Status: ");
+ pw.print(info.getStatus());
+ pw.println();
if (info.getStoragePath() != null) {
- pw.print(" Path: ");
+ pw.print(" Path: ");
pw.print(info.getStoragePath());
pw.println();
- pw.print(" Status: ");
- pw.print(info.getStatus());
- pw.println();
- pw.print(" Bytes: ");
+ pw.print(" Size: ");
pw.print(info.getSize());
pw.println();
- pw.print(" Urls: ");
+ pw.print(" Urls: ");
pw.print(info.getEntries());
pw.println();
- pw.print(" Within Limits: ");
- pw.print(isWithinLimits(info) ? "yes": "no");
+ pw.print(" Within Limits: ");
+ pw.print(isWithinLimits(info) ? "yes" : "no");
pw.println();
}
}
@@ -210,7 +220,7 @@ private boolean isWithinLimits(SitemapInfo info) {
return info.getSize() <= configuration.getMaxSize() && info.getEntries() <= configuration.getMaxEntries();
}
- private static String escapeDoubleQuotes(String text) {
- return text.replace("\"", "\\\"");
+ private static String escapeDoubleQuotes(@Nullable String text) {
+ return text == null ? "" : text.replace("\"", "\\\"");
}
}
diff --git a/sitemap/src/test/java/org/apache/sling/sitemap/impl/SitemapServiceImplTest.java b/sitemap/src/test/java/org/apache/sling/sitemap/impl/SitemapServiceImplTest.java
index 7ca09dc54..dc78fae14 100644
--- a/sitemap/src/test/java/org/apache/sling/sitemap/impl/SitemapServiceImplTest.java
+++ b/sitemap/src/test/java/org/apache/sling/sitemap/impl/SitemapServiceImplTest.java
@@ -60,6 +60,7 @@ public class SitemapServiceImplTest {
private final SitemapServiceImpl subject = new SitemapServiceImpl();
private final SitemapStorage storage = new SitemapStorage();
private final SitemapGeneratorManagerImpl generatorManager = new SitemapGeneratorManagerImpl();
+ private final SitemapScheduler scheduler = new SitemapScheduler();
private final SitemapServiceConfiguration sitemapServiceConfiguration = new SitemapServiceConfiguration();
private TestGenerator generator = new TestGenerator() {
@@ -73,6 +74,8 @@ public class SitemapServiceImplTest {
@Mock
private ServiceUserMapped serviceUser;
+ @Mock
+ private JobManager jobManager;
private Resource deRoot;
private Resource enRoot;
@@ -105,11 +108,17 @@ public void setup() {
noRoot = context.create().resource("/content/site/nothing");
context.registerService(ServiceUserMapped.class, serviceUser, "subServiceName", "sitemap-writer");
+ context.registerService(ServiceUserMapped.class, serviceUser, "subServiceName", "sitemap-reader");
context.registerService(SitemapGenerator.class, generator, "service.ranking", 1);
context.registerService(SitemapGenerator.class, newsGenerator, "service.ranking", 2);
+ context.registerService(JobManager.class, jobManager);
context.registerInjectActivateService(sitemapServiceConfiguration);
context.registerInjectActivateService(generatorManager);
context.registerInjectActivateService(storage);
+ context.registerInjectActivateService(scheduler,
+ "scheduler.name", "default",
+ "scheduler.expression", "never",
+ "names", new String[] { SitemapService.DEFAULT_SITEMAP_NAME });
context.registerInjectActivateService(subject);
}
@@ -137,7 +146,7 @@ public void testSitemapIndexUrlReturned() {
assertThat(enInfo, hasSize(2));
assertThat(enInfo, hasItems(
eqSitemapInfo("/site/en.sitemap-index.xml", -1, -1, SitemapInfo.Status.ON_DEMAND),
- eqSitemapInfo("/site/en.sitemap.xml", -1, -1, SitemapInfo.Status.UNKNOWN)
+ eqSitemapInfo("/site/en.sitemap.xml", -1, -1, SitemapInfo.Status.SCHEDULED)
));
assertThat(frInfo, hasSize(3));
assertThat(frInfo, hasItems(
@@ -160,13 +169,17 @@ public void testSitemapUrlReturnEmptyCollections() {
@Test
public void testSitemapUrlReturnsProperSelectors() throws IOException {
// given
- storage.writeSitemap(deRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100, 1);
+ storage.writeSitemap(deRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100,
+ 1);
storage.writeSitemap(frRoot, "foobar", new ByteArrayInputStream(new byte[0]), 1, 100, 1);
- storage.writeSitemap(enRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100, 1);
+ storage.writeSitemap(enRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100,
+ 1);
storage.writeSitemap(enNews, "foo", new ByteArrayInputStream(new byte[0]), 1, 100, 1);
storage.writeSitemap(enNews, "bar", new ByteArrayInputStream(new byte[0]), 1, 100, 1);
- storage.writeSitemap(itRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100, 1);
- storage.writeSitemap(itRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 2, 100, 1);
+ storage.writeSitemap(itRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 1, 100,
+ 1);
+ storage.writeSitemap(itRoot, SitemapService.DEFAULT_SITEMAP_NAME, new ByteArrayInputStream(new byte[0]), 2, 100,
+ 1);
generator.setNames(deRoot, SitemapService.DEFAULT_SITEMAP_NAME);
generator.setNames(frRoot, "foobar");
diff --git a/sitemap/src/test/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPluginTest.java b/sitemap/src/test/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPluginTest.java
new file mode 100644
index 000000000..2503fc3e1
--- /dev/null
+++ b/sitemap/src/test/java/org/apache/sling/sitemap/impl/console/SitemapInventoryPluginTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.IOUtils;
+import org.apache.felix.inventory.Format;
+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.SitemapServiceConfiguration;
+import org.apache.sling.testing.mock.jcr.MockJcr;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
+
+@ExtendWith({SlingContextExtension.class, MockitoExtension.class})
+public class SitemapInventoryPluginTest {
+
+ public final SlingContext context = new SlingContext(ResourceResolverType.JCR_MOCK);
+ private final SitemapInventoryPlugin subject = new SitemapInventoryPlugin();
+ private final SitemapServiceConfiguration configuration = new SitemapServiceConfiguration();
+
+ // some terms are different in text then in json for better readability
+ private static Map YAML_TO_JSON = ImmutableMap.of(
+ "within limits", "inlimits"
+ );
+
+ @Mock
+ private SitemapService sitemapService;
+ @Mock
+ private SitemapInfo deInfo;
+ @Mock
+ private SitemapInfo enInfo1;
+ @Mock
+ private SitemapInfo enInfo2;
+ @Mock
+ private ServiceReference schedulerReference1;
+ @Mock
+ private ServiceReference schedulerReference2;
+ @Mock
+ private ServiceReference unknownReference;
+ // we have to use a mock bundle as the osgi-mock one does not implement Bundle#getRegisteredServices()
+ @Mock
+ private Bundle bundle;
+ @Mock
+ private BundleContext bundleContext;
+ // we have to use mock rrf in order to provide the context#resourceResolver() rr to the implementation with the
+ // mocked jcr query responses
+ @Mock
+ private ResourceResolverFactory resourceResolverFactory;
+
+ @BeforeEach
+ public void setup() throws LoginException {
+ ResourceResolver resourceResolver = spy(context.resourceResolver());
+
+ context.registerService(SitemapService.class, sitemapService);
+ context.registerInjectActivateService(configuration, "maxEntries", 999);
+ context.registerService(ResourceResolverFactory.class, resourceResolverFactory, "service.ranking", 100);
+ context.registerInjectActivateService(subject);
+ subject.activate(bundleContext);
+
+ Resource deRoot = context.create().resource("/content/site/de",
+ SitemapService.PROPERTY_SITEMAP_ROOT, Boolean.TRUE);
+ Resource enRoot = context.create().resource("/content/site/en",
+ SitemapService.PROPERTY_SITEMAP_ROOT, Boolean.TRUE);
+
+ MockJcr.setQueryResult(
+ context.resourceResolver().adaptTo(Session.class),
+ "/jcr:root//*[@sling:sitemapRoot=true] option(index tag slingSitemaps)",
+ Query.XPATH,
+ Arrays.asList(deRoot.adaptTo(Node.class), enRoot.adaptTo(Node.class))
+ );
+
+ when(deInfo.getName()).thenReturn(SitemapService.DEFAULT_SITEMAP_NAME);
+ when(deInfo.getUrl()).thenReturn("/site/de.sitemap.xml");
+ when(deInfo.getStatus()).thenReturn(SitemapInfo.Status.STORAGE);
+ when(deInfo.getSize()).thenReturn(1000);
+ when(deInfo.getEntries()).thenReturn(10);
+ when(deInfo.getStoragePath()).thenReturn("/var/sitemaps/content/site/de/sitemap.xml");
+ when(enInfo1.getName()).thenReturn(SitemapService.SITEMAP_INDEX_NAME);
+ when(enInfo1.getUrl()).thenReturn("/site/en.sitemap-index.xml");
+ when(enInfo1.getStatus()).thenReturn(SitemapInfo.Status.ON_DEMAND);
+ when(enInfo2.getName()).thenReturn(SitemapService.DEFAULT_SITEMAP_NAME);
+ when(enInfo2.getUrl()).thenReturn("/site/en.sitemap.xml");
+ when(enInfo2.getStatus()).thenReturn(SitemapInfo.Status.STORAGE);
+ when(enInfo2.getSize()).thenReturn(10000);
+ when(enInfo2.getEntries()).thenReturn(1000);
+ when(enInfo2.getStoragePath()).thenReturn("/var/sitemaps/content/site/en/sitemap.xml");
+
+ when(bundleContext.getBundle()).thenReturn(bundle);
+ when(bundle.getRegisteredServices()).thenReturn(new ServiceReference[]{
+ schedulerReference1, schedulerReference2, unknownReference
+ });
+ when(schedulerReference1.getProperty(Scheduler.PROPERTY_SCHEDULER_NAME)).thenReturn("sitemap-default");
+ when(schedulerReference1.getProperty(Scheduler.PROPERTY_SCHEDULER_EXPRESSION)).thenReturn("0 0 0 * * * ?");
+ when(schedulerReference2.getProperty(Scheduler.PROPERTY_SCHEDULER_NAME)).thenReturn("sitemap-news");
+ when(schedulerReference2.getProperty(Scheduler.PROPERTY_SCHEDULER_EXPRESSION)).thenReturn("0 */30 * * * * ?");
+ when(resourceResolverFactory.getServiceResourceResolver(any())).thenReturn(resourceResolver);
+
+ doNothing().when(resourceResolver).close();
+ doReturn(Collections.singleton(deInfo))
+ .when(sitemapService).getSitemapInfo(argThat(resourceWithPath(deRoot.getPath())));
+ doReturn(Arrays.asList(enInfo1, enInfo2))
+ .when(sitemapService).getSitemapInfo(argThat(resourceWithPath(enRoot.getPath())));
+ }
+
+ @Test
+ public void testJson() {
+ // given
+ StringWriter writer = new StringWriter();
+
+ // when
+ subject.print(new PrintWriter(writer), Format.JSON, false);
+
+ // then
+ assertJson("SitemapInventoryPluginTest/inventory.json", writer.toString());
+ }
+
+ @Test
+ public void testText() {
+ // given
+ StringWriter writer = new StringWriter();
+
+ // when
+ subject.print(new PrintWriter(writer), Format.TEXT, false);
+
+ // then
+ assertYaml("SitemapInventoryPluginTest/inventory.json", writer.toString());
+ }
+
+ private static void assertYaml(String expected, String given) {
+ assertJson(new YAMLMapper(), expected, given);
+ }
+
+ private static void assertJson(String expected, String given) {
+ assertJson(new ObjectMapper(), expected, given);
+ }
+
+ private static void assertJson(ObjectMapper objectMapper, String expected, String given) {
+ try {
+ InputStream expectedResource = SitemapInventoryPluginTest.class.getClassLoader()
+ .getResourceAsStream(expected);
+ StringWriter expectedContent = new StringWriter();
+ IOUtils.copy(expectedResource, expectedContent, StandardCharsets.UTF_8);
+ JsonNode expectedJson = objectMapper.readTree(normalizeJson(expectedContent.toString()));
+ JsonNode givenJson = objectMapper.readTree(normalizeJson(given));
+ assertEquals(expectedJson, givenJson);
+ } catch (IOException ex) {
+ fail(ex.getMessage());
+ }
+ }
+
+ private static String normalizeJson(String json) {
+ String lowercase = json.toLowerCase(Locale.ROOT);
+ for (Map.Entry entry : YAML_TO_JSON.entrySet()) {
+ lowercase = lowercase.replace(entry.getKey(), entry.getValue());
+ }
+ return lowercase;
+ }
+
+ private static ArgumentMatcher resourceWithPath(String path) {
+ return r -> r.getPath().equals(path);
+ }
+}
diff --git a/sitemap/src/test/resources/SitemapInventoryPluginTest/inventory.json b/sitemap/src/test/resources/SitemapInventoryPluginTest/inventory.json
new file mode 100644
index 000000000..bf9319639
--- /dev/null
+++ b/sitemap/src/test/resources/SitemapInventoryPluginTest/inventory.json
@@ -0,0 +1,41 @@
+{
+ "schedulers": [
+ {
+ "name": "sitemap-default",
+ "expression": "0 0 0 * * * ?"
+ },
+ {
+ "name": "sitemap-news",
+ "expression": "0 */30 * * * * ?"
+ }
+ ],
+ "roots": {
+ "/content/site/de": [
+ {
+ "name": "",
+ "url": "/site/de.sitemap.xml",
+ "status": "STORAGE",
+ "path": "/var/sitemaps/content/site/de/sitemap.xml",
+ "size": 1000,
+ "urls": 10,
+ "inLimits": true
+ }
+ ],
+ "/content/site/en": [
+ {
+ "name": "",
+ "url": "/site/en.sitemap-index.xml",
+ "status": "ON_DEMAND"
+ },
+ {
+ "name": "",
+ "url": "/site/en.sitemap.xml",
+ "status": "STORAGE",
+ "path": "/var/sitemaps/content/site/en/sitemap.xml",
+ "size": 10000,
+ "urls": 1000,
+ "inLimits": false
+ }
+ ]
+ }
+}
\ No newline at end of file