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