Merge pull request #19 from angulito/SLING-10644-return-servlet-resolver-information-in-json-format

Sling 10644 return servlet resolver information in json format
diff --git a/pom.xml b/pom.xml
index 52b30d4..bc2f799 100644
--- a/pom.xml
+++ b/pom.xml
@@ -431,6 +431,18 @@
             <version>1.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.jayway.jsonpath</groupId>
+            <artifactId>json-path</artifactId>
+            <version>2.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jayway.jsonpath</groupId>
+            <artifactId>json-path-assert</artifactId>
+            <version>2.6.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/src/main/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePlugin.java b/src/main/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePlugin.java
index 542a3c5..a2a9cde 100644
--- a/src/main/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePlugin.java
+++ b/src/main/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePlugin.java
@@ -18,21 +18,7 @@
  */
 package org.apache.sling.servlets.resolver.internal.console;
 
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.concurrent.atomic.AtomicReference;
-
-import javax.servlet.Servlet;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
+import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.request.RequestPathInfo;
 import org.apache.sling.api.request.ResponseUtil;
@@ -57,6 +43,28 @@
 import org.osgi.service.component.annotations.Modified;
 import org.osgi.service.component.annotations.Reference;
 
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * If the servlet request path ends with .json, the information is returned in JSON format.
+ * Otherwise, an HTML code is returned.
+ */
 @SuppressWarnings("serial")
 @Component(service = {Servlet.class},
   configurationPid = ResolverConfig.PID,
@@ -74,6 +82,13 @@
     private static final String PARAMETER_METHOD = "method";
 
     private static final String SERVICE_USER_CONSOLE = "console";
+    private static final String CONSOLE_PATH_WARNING =
+                    "Note that in a real Sling request, the path might vary depending on the existence of"
+                    + " resources that partially match it. "
+                    + "<br/>This utility does not take this into account and uses the first dot to split"
+                    + " between path and selectors/extension. "
+                    + "<br/>As a workaround, you can replace dots with underline characters, for example, when " +
+                            "testing such a URL.";
 
     @Reference(target="("+ServiceUserMapped.SUBSERVICENAME+"=" + SERVICE_USER_CONSOLE + ")")
     private ServiceUserMapped consoleServiceUserMapped; // NOSONAR
@@ -113,139 +128,255 @@
             method = "GET";
         }
 
-        final String CONSOLE_PATH_WARNING =
-                "<em>"
-                + "Note that in a real Sling request, the path might vary depending on the existence of"
-                + " resources that partially match it."
-                + "<br/>This utility does not take this into account and uses the first dot to split"
-                + " between path and selectors/extension."
-                + "<br/>As a workaround, you can replace dots with underline characters, for example, when testing such an URL."
-                + "</em>";
-
+        String requestURI = request.getRequestURI();
         try (final ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object)SERVICE_USER_CONSOLE))) {
-
             final PrintWriter pw = response.getWriter();
 
-            pw.print("<form method='get'>");
-            pw.println("<table class='content' cellpadding='0' cellspacing='0' width='100%'>");
-
-            titleHtml(
-                    pw,
-                    "Servlet Resolver Test",
-                    "To check which servlet is responsible for rendering a response, enter a request path into " +
-                             "the field and click 'Resolve' to resolve it.");
-
-            tr(pw);
-            tdLabel(pw, "URL");
-            tdContent(pw);
-
-            pw.print("<input type='text' name='");
-            pw.print(PARAMETER_URL);
-            pw.print("' value='");
-            if ( url != null ) {
-                pw.print(ResponseUtil.escapeXml(url));
-            }
-            pw.println("' class='input' size='50'>");
-            closeTd(pw);
-            closeTr(pw);
-            closeTr(pw);
-
-            tr(pw);
-            tdLabel(pw, "Method");
-            tdContent(pw);
-            pw.print("<select name='");
-            pw.print(PARAMETER_METHOD);
-            pw.println("'>");
-            pw.println("<option value='GET'>GET</option>");
-            pw.println("<option value='POST'>POST</option>");
-            pw.println("</select>");
-            pw.println("&nbsp;&nbsp;<input type='submit' value='Resolve' class='submit'>");
-
-            closeTd(pw);
-            closeTr(pw);
-
-            if (StringUtils.isNotBlank(url)) {
-                tr(pw);
-                tdLabel(pw, "Decomposed URL");
-                tdContent(pw);
-                pw.println("<dl>");
-                pw.println("<dt>Path</dt>");
-                dd(pw);
-                pw.print(ResponseUtil.escapeXml(requestPathInfo.getResourcePath()));
-                pw.print("<br/>");
-                pw.print(CONSOLE_PATH_WARNING);
-                closeDd(pw);
-                pw.println("<dt>Selectors</dt>");
-                dd(pw);
-                if (requestPathInfo.getSelectors().length == 0) {
-                    pw.print("&lt;none&gt;");
-                } else {
-                    pw.print("[");
-                    pw.print(ResponseUtil.escapeXml(StringUtils.join(requestPathInfo.getSelectors(), ", ")));
-                    pw.print("]");
+            if (requestURI.endsWith("json")) {
+                pw.println("{");
+                if (StringUtils.isNotBlank(url)) {
+                    printJSONDecomposedURLElement(pw, requestPathInfo);
                 }
-                closeDd(pw);
-                pw.println("<dt>Extension</dt>");
-                dd(pw);
-                pw.print(ResponseUtil.escapeXml(requestPathInfo.getExtension()));
-                closeDd(pw);
-                pw.println("</dl>");
-                closeDd(pw);
-                pw.println("<dt>Suffix</dt>");
-                dd(pw);
-                pw.print(ResponseUtil.escapeXml(requestPathInfo.getSuffix()));
-                closeDd(pw);
-                pw.println("</dl>");
-                closeTd(pw);
-                closeTr(pw);
-            }
-
-            if (StringUtils.isNotBlank(requestPathInfo.getResourcePath())) {
-                final Collection<Resource> servlets;
-                Resource resource = resourceResolver.resolve(requestPathInfo.getResourcePath());
-                if (resource.adaptTo(Servlet.class) != null) {
-                    servlets = Collections.singleton(resource);
-                } else {
-                    final ResourceCollector locationUtil = ResourceCollector.create(
-                            resource,
-                            requestPathInfo.getExtension(),
-                            executionPaths.get(),
-                            defaultExtensions.get(),
-                            method,
-                            requestPathInfo.getSelectors());
-                    servlets = locationUtil.getServlets(resourceResolver, resolutionCache.getScriptEngineExtensions());
+                if (StringUtils.isNotBlank(requestPathInfo.getResourcePath())) {
+                    printJSONCandidatesElement(pw, resourceResolver, requestPathInfo, method);
                 }
-                tr(pw);
-                tdLabel(pw, "Candidates");
-                tdContent(pw);
+                pw.printf("  \"warningMsg\" : \"%s\",%n", CONSOLE_PATH_WARNING.replace("<br/>", ""));
+                pw.printf("  \"method\" : \"%s\"%n", StringEscapeUtils.escapeJson(method));
+                pw.print("}");
 
-                if (servlets == null || servlets.isEmpty()) {
-                    pw.println("Could not find a suitable servlet for this request!");
-                } else {
-                    // check for non-existing resources
-                    if (ResourceUtil.isNonExistingResource(resource)) {
-                        pw.println("The resource given by path '");
-                        pw.println(ResponseUtil.escapeXml(resource.getPath()));
-                        pw.println("' does not exist. Therefore no resource type could be determined!<br/>");
+                response.setContentType("application/json");
+            } else {
+                printHTMLInputElements(pw, url);
+                if (StringUtils.isNotBlank(url)) {
+                    printHTMLDecomposedURLElement(pw, requestPathInfo);
+                }
+
+                if (StringUtils.isNotBlank(requestPathInfo.getResourcePath())) {
+                    Resource resource = resourceResolver.resolve(requestPathInfo.getResourcePath());
+                    final Collection<Resource> servlets = resolveServlets(resourceResolver, requestPathInfo, resource,
+                            method);
+
+                    tr(pw);
+                    tdLabel(pw, "Candidates");
+                    tdContent(pw);
+                    if (servlets == null || servlets.isEmpty()) {
+                        pw.println("Could not find a suitable servlet for this request!");
+                    } else {
+                        // check for non-existing resources
+                        if (ResourceUtil.isNonExistingResource(resource)) {
+                            pw.println("The resource given by path '");
+                            pw.println(ResponseUtil.escapeXml(resource.getPath()));
+                            pw.println("' does not exist. Therefore no resource type could be determined!<br/>");
+                        }
+                        pw.print("Candidate servlets and scripts in order of preference for method ");
+                        pw.print(ResponseUtil.escapeXml(method));
+                        pw.println(":<br/>");
+                        pw.println("<ol class='servlets'>");
+                        outputHTMLServlets(pw, servlets.iterator());
+                        pw.println("</ol>");
                     }
-                    pw.print("Candidate servlets and scripts in order of preference for method ");
-                    pw.print(ResponseUtil.escapeXml(method));
-                    pw.println(":<br/>");
-                    pw.println("<ol class='servlets'>");
-                    outputServlets(pw, servlets.iterator());
-                    pw.println("</ol>");
+                    closeTd(pw);
+                    closeTr(pw);
                 }
-                closeTd(pw);
-                closeTr(pw);
-            }
 
-            pw.println("</table>");
-            pw.print("</form>");
+                pw.println("</table>");
+                pw.print("</form>");
+            }
         } catch (final LoginException e) {
             throw new ServletException(e);
         }
     }
 
+    /**
+     * Format an array.
+     */
+    private String formatArrayAsJSON(final String[] array) {
+        if ( array == null || array.length == 0 ) {
+            return "[]";
+        }
+        final StringBuilder sb = new StringBuilder("[");
+        boolean first = true;
+        for(final String s : array) {
+            if ( !first ) {
+                sb.append(", ");
+            }
+            first = false;
+            sb.append("\"");
+            sb.append(StringEscapeUtils.escapeJson(s));
+            sb.append("\"");
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private Map<String, List<String>> getAllowedAndDeniedServlets(Collection<Resource> servlets) {
+        List<String> allowedServlets = new ArrayList<>();
+        List<String> deniedServlets = new ArrayList<>();
+        for (Resource candidateResource : servlets) {
+            Servlet candidate = candidateResource.adaptTo(Servlet.class);
+            if (candidate != null) {
+                final boolean allowed = SlingServletResolver.isPathAllowed(candidateResource.getPath(),
+                        this.executionPaths.get());
+
+                String finalCandidate;
+                if (candidate instanceof SlingScript) {
+                    finalCandidate = candidateResource.getPath();
+                } else {
+                    final boolean isOptingServlet = candidate instanceof OptingServlet;
+                    finalCandidate = candidate.getClass().getName();
+                    if (isOptingServlet) {
+                        finalCandidate += " (OptingServlet)";
+                    }
+                }
+
+                if (allowed) {
+                    allowedServlets.add(finalCandidate);
+                } else {
+                    deniedServlets.add(finalCandidate);
+                }
+            }
+        }
+
+        Map<String, List<String>> result = new HashMap<>();
+        result.put("allowed", allowedServlets);
+        result.put("denied", deniedServlets);
+
+        return result;
+    }
+
+    private void printJSONDecomposedURLElement(PrintWriter pw, RequestPathInfo requestPathInfo) {
+        pw.println("  \"decomposedURL\" : {");
+        pw.printf("    \"path\" : \"%s\",%n",
+                StringEscapeUtils.escapeJson(StringUtils.defaultIfEmpty(requestPathInfo.getResourcePath(), "")));
+        pw.printf("    \"extension\" : \"%s\",%n",
+                StringEscapeUtils.escapeJson(StringUtils.defaultIfEmpty(requestPathInfo.getExtension(), "")));
+        pw.printf("    \"selectors\" : %s,%n",
+                StringUtils.defaultIfEmpty(formatArrayAsJSON(requestPathInfo.getSelectors()), ""));
+        pw.printf("    \"suffix\" : \"%s\"%n",
+                StringEscapeUtils.escapeJson(StringUtils.defaultIfEmpty(requestPathInfo.getSuffix(), "")));
+        pw.println("  },");
+    }
+
+    private void printJSONCandidatesElement(PrintWriter pw, ResourceResolver resourceResolver,
+                                            RequestPathInfo requestPathInfo, String method) {
+        Resource resource = resourceResolver.resolve(requestPathInfo.getResourcePath());
+        final Collection<Resource> servlets = resolveServlets(resourceResolver, requestPathInfo, resource,
+                method);
+        pw.println("  \"candidates\" : {");
+        if (servlets != null) {
+            // check for non-existing resources
+            if (ResourceUtil.isNonExistingResource(resource)) {
+                pw.printf("    \"errorMsg\" : \"%s\",%n", String.format("The resource given by path " +
+                        "'%s' does not exist. Therefore no " +
+                        "resource type could be determined!", StringEscapeUtils.escapeJson(resource.getPath())));
+            }
+
+            Map<String, List<String>> allowedAndDeniedServlets = getAllowedAndDeniedServlets(servlets);
+            List<String> allowedServlets = allowedAndDeniedServlets.getOrDefault("allowed", new ArrayList<>());
+            List<String> deniedServlets = allowedAndDeniedServlets.getOrDefault("denied", new ArrayList<>());
+            pw.printf("    \"allowedServlets\" : %s,%n",
+                    formatArrayAsJSON(allowedServlets.toArray(new String[0])));
+            pw.printf("    \"deniedServlets\" : %s%n",
+                    formatArrayAsJSON(deniedServlets.toArray(new String[0])));
+        }
+        pw.print("  },");
+    }
+
+    private void printHTMLInputElements(PrintWriter pw, String url) {
+        pw.print("<form method='get'>");
+        pw.println("<table class='content' cellpadding='0' cellspacing='0' width='100%'>");
+
+        titleHtml(
+                pw,
+                "Servlet Resolver Test",
+                "To check which servlet is responsible for rendering a response, enter a request path into " +
+                        "the field and click 'Resolve' to resolve it.");
+
+        tr(pw);
+        tdLabel(pw, "URL");
+        tdContent(pw);
+
+        pw.print("<input type='text' name='");
+        pw.print(PARAMETER_URL);
+        pw.print("' value='");
+        if ( url != null ) {
+            pw.print(ResponseUtil.escapeXml(url));
+        }
+        pw.println("' class='input' size='50'>");
+        closeTd(pw);
+        closeTr(pw);
+        closeTr(pw);
+
+        tr(pw);
+        tdLabel(pw, "Method");
+        tdContent(pw);
+        pw.print("<select name='");
+        pw.print(PARAMETER_METHOD);
+        pw.println("'>");
+        pw.println("<option value='GET'>GET</option>");
+        pw.println("<option value='POST'>POST</option>");
+        pw.println("</select>");
+        pw.println("&nbsp;&nbsp;<input type='submit' value='Resolve' class='submit'>");
+
+        closeTd(pw);
+        closeTr(pw);
+    }
+
+    private void printHTMLDecomposedURLElement(PrintWriter pw, RequestPathInfo requestPathInfo) {
+            tr(pw);
+            tdLabel(pw, "Decomposed URL");
+            tdContent(pw);
+            pw.println("<dl>");
+            pw.println("<dt>Path</dt>");
+            dd(pw);
+            pw.print(ResponseUtil.escapeXml(requestPathInfo.getResourcePath()));
+            pw.print("<br/>");
+            pw.print(String.format("<em>%s</em>", CONSOLE_PATH_WARNING));
+            closeDd(pw);
+            pw.println("<dt>Selectors</dt>");
+            dd(pw);
+            if (requestPathInfo.getSelectors().length == 0) {
+                pw.print("&lt;none&gt;");
+            } else {
+                pw.print("[");
+                pw.print(ResponseUtil.escapeXml(StringUtils.join(requestPathInfo.getSelectors(), ", ")));
+                pw.print("]");
+            }
+            closeDd(pw);
+            pw.println("<dt>Extension</dt>");
+            dd(pw);
+            pw.print(ResponseUtil.escapeXml(requestPathInfo.getExtension()));
+            closeDd(pw);
+            pw.println("</dl>");
+            closeDd(pw);
+            pw.println("<dt>Suffix</dt>");
+            dd(pw);
+            pw.print(ResponseUtil.escapeXml(requestPathInfo.getSuffix()));
+            closeDd(pw);
+            pw.println("</dl>");
+            closeTd(pw);
+            closeTr(pw);
+    }
+
+    private Collection<Resource> resolveServlets(ResourceResolver resourceResolver, RequestPathInfo requestPathInfo,
+                                                 Resource resource, String method) {
+        final Collection<Resource> servlets;
+        if (resource.adaptTo(Servlet.class) != null) {
+            servlets = Collections.singleton(resource);
+        } else {
+            final ResourceCollector locationUtil = ResourceCollector.create(
+                    resource,
+                    requestPathInfo.getExtension(),
+                    executionPaths.get(),
+                    defaultExtensions.get(),
+                    method,
+                    requestPathInfo.getSelectors());
+            servlets = locationUtil.getServlets(resourceResolver, resolutionCache.getScriptEngineExtensions());
+        }
+
+        return servlets;
+    }
+
     private void tdContent(final PrintWriter pw) {
         pw.print("<td class='content' colspan='2'>");
     }
@@ -284,29 +415,29 @@
         pw.println("<tr class='content'>");
     }
 
-    private void outputServlets(final PrintWriter pw, final Iterator<Resource> iterator) {
+    private void outputHTMLServlets(final PrintWriter pw, final Iterator<Resource> iterator) {
         while (iterator.hasNext()) {
             Resource candidateResource = iterator.next();
             Servlet candidate = candidateResource.adaptTo(Servlet.class);
             if (candidate != null) {
                 final boolean allowed = SlingServletResolver.isPathAllowed(candidateResource.getPath(), this.executionPaths.get());
                 pw.print("<li>");
-                if ( !allowed ) {
-                    pw.print("<del>");
-                }
 
+                String candidateStr;
                 if (candidate instanceof SlingScript) {
-                    pw.print(ResponseUtil.escapeXml(candidateResource.getPath()));
+                    candidateStr = ResponseUtil.escapeXml(candidateResource.getPath());
                 } else {
                     final boolean isOptingServlet = candidate instanceof OptingServlet;
-                    pw.print(ResponseUtil.escapeXml((candidate.getClass().getName())));
+                    candidateStr = ResponseUtil.escapeXml((candidate.getClass().getName()));
                     if ( isOptingServlet ) {
-                        pw.print(" (OptingServlet)");
+                        candidateStr +=" (OptingServlet)";
                     }
                 }
 
                 if ( !allowed ) {
-                    pw.print("</del>");
+                    pw.print("<del>" + candidateStr + "</del>");
+                } else {
+                    pw.print(candidateStr);
                 }
                 pw.println("</li>");
             }
diff --git a/src/test/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePluginTest.java b/src/test/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePluginTest.java
new file mode 100644
index 0000000..61b886f
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlets/resolver/internal/console/WebConsolePluginTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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.servlets.resolver.internal.console;
+
+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.servlets.resolver.internal.ResolverConfig;
+import org.apache.sling.servlets.resolver.internal.resolution.ResolutionCache;
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import javax.json.Json;
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+public class WebConsolePluginTest {
+    @Mock
+    private HttpServletRequest request;
+    @Mock
+    private HttpServletResponse response;
+    @Mock
+    private ResourceResolverFactory resourceResolverFactory;
+    @Mock
+    private ResolutionCache resolutionCache;
+    @InjectMocks
+    private WebConsolePlugin webConsolePlugin;
+
+    private StringWriter stringWriter;
+    private PrintWriter writer;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.openMocks(this);
+        ResolverConfig resolverConfig = Mockito.mock(ResolverConfig.class);
+        Mockito.when(resolverConfig.servletresolver_defaultExtensions()).thenReturn(new String[]{"html"});
+        Mockito.when(resolverConfig.servletresolver_paths()).thenReturn(new String[]{"/path"});
+        webConsolePlugin.activate(resolverConfig);
+        // mock http responses
+        stringWriter = new StringWriter();
+        writer = new PrintWriter(stringWriter);
+        Mockito.when(response.getWriter()).thenReturn(writer);
+    }
+
+    @After
+    public void closeUp() throws Exception {
+        MockitoAnnotations.openMocks(this).close();
+    }
+
+    @Test
+    public void printJsonFormatFull() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Servlet servlet = Mockito.mock(Servlet.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(servlet);
+        Mockito.when(resource.getPath()).thenReturn("/path");
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("/test.1.json");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // JSON output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver.json");
+
+        webConsolePlugin.service(request, response);
+
+        Mockito.verify(response).setContentType("application/json");
+
+        String jsonString = stringWriter.toString();
+        String json = Json.createReader(new StringReader(jsonString)).readObject().toString();
+
+        Map<String, Object> expectedJsonPaths = new HashMap(){{
+            put("$.candidates", null);
+            put("$.candidates.allowedServlets.length()", 1);
+            put("$.candidates.deniedServlets.length()", 0);
+            put("$.decomposedURL", null);
+            put("$.decomposedURL.path", "/test");
+            put("$.decomposedURL.extension", "json");
+            put("$.decomposedURL.selectors", Arrays.asList("1"));
+            put("$.decomposedURL.suffix", "");
+            put("$.method", "GET");
+        }};
+
+        expectedJsonPaths.forEach((k, v) -> {
+            if (v != null) {
+                assertThat(json, hasJsonPath(k, equalTo(v)));
+            } else {
+                assertThat(json, hasJsonPath(k));
+            }
+        });
+    }
+
+    @Test
+    public void printJsonFormatEmptyURL() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(null);
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // JSON output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver.json");
+
+        webConsolePlugin.service(request, response);
+
+        Mockito.verify(response).setContentType("application/json");
+
+        String jsonString = stringWriter.toString();
+        String json = Json.createReader(new StringReader(jsonString)).readObject().toString();
+
+        Map<String, Object> expectedJsonPaths = new HashMap(){{
+            put("$.method", "GET");
+        }};
+
+        expectedJsonPaths.forEach((k, v) -> {
+            if (v != null) {
+                assertThat(json, hasJsonPath(k, equalTo(v)));
+            } else {
+                assertThat(json, hasJsonPath(k));
+            }
+        });
+    }
+
+    @Test
+    public void printJsonFormatDenyPaths() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Servlet servlet = Mockito.mock(Servlet.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(servlet);
+        Mockito.when(resource.getPath()).thenReturn("/denied");
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("/denied");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // JSON output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver.json");
+
+        webConsolePlugin.service(request, response);
+
+        Mockito.verify(response).setContentType("application/json");
+
+        String jsonString = stringWriter.toString();
+        String json = Json.createReader(new StringReader(jsonString)).readObject().toString();
+
+        Map<String, Object> expectedJsonPaths = new HashMap(){{
+            put("$.candidates", null);
+            put("$.candidates.allowedServlets.length()", 0);
+            put("$.candidates.deniedServlets.length()", 1);
+        }};
+
+        expectedJsonPaths.forEach((k, v) -> {
+            if (v != null) {
+                assertThat(json, hasJsonPath(k, equalTo(v)));
+            } else {
+                assertThat(json, hasJsonPath(k));
+            }
+        });
+    }
+
+    @Test
+    public void printHTMLFormatFull() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Servlet servlet = Mockito.mock(Servlet.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(servlet);
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("/test.1.json");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // HTML output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver");
+
+        webConsolePlugin.service(request, response);
+
+        String htmlString = stringWriter.toString();
+
+        final String expectedInputHTML = "<tr class='content'>\n" +
+                "<td class='content'>URL</td>\n" +
+                "<td class='content' colspan='2'><input type='text' name='url' value='/test.1.json' class='input' " +
+                "size='50'>\n" +
+                "</td></tr>\n" +
+                "</tr>\n" +
+                "<tr class='content'>\n" +
+                "<td class='content'>Method</td>\n" +
+                "<td class='content' colspan='2'><select name='method'>\n" +
+                "<option value='GET'>GET</option>\n" +
+                "<option value='POST'>POST</option>\n" +
+                "</select>\n" +
+                "&nbsp;&nbsp;<input type='submit' value='Resolve' class='submit'>\n" +
+                "</td></tr>";
+        assertThat(htmlString, CoreMatchers.containsString(expectedInputHTML));
+
+        final String expectedDecomposedURLHTML = "<tr class='content'>\n" +
+                "<td class='content'>Decomposed URL</td>\n" +
+                "<td class='content' colspan='2'><dl>\n" +
+                "<dt>Path</dt>\n" +
+                "<dd>\n" +
+                "/test<br/><em>Note that in a real Sling request, the path might vary depending on the existence of " +
+                "resources that partially match it. <br/>This utility does not take this into account and uses the " +
+                "first dot to split between path and selectors/extension. <br/>As a workaround, you can replace dots " +
+                "with underline characters, for example, when testing such a URL.</em></dd><dt>Selectors</dt>\n" +
+                "<dd>\n" +
+                "[1]</dd><dt>Extension</dt>\n" +
+                "<dd>\n" +
+                "json</dd></dl>\n" +
+                "</dd><dt>Suffix</dt>\n" +
+                "<dd>\n" +
+                "null</dd></dl>\n" +
+                "</td></tr>";
+        assertThat(htmlString, CoreMatchers.containsString(expectedDecomposedURLHTML));
+
+        final String expectedCandidatesHTML = "<tr class='content'>\n" +
+                "<td class='content'>Candidates</td>\n" +
+                "<td class='content' colspan='2'>Candidate servlets and scripts in order of preference for method " +
+                "GET:<br/>\n" +
+                "<ol class='servlets'>\n";
+        assertThat(htmlString, CoreMatchers.containsString(expectedCandidatesHTML));
+    }
+
+    @Test
+    public void printHTMLFormatEmptyURL() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(null);
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // HTML output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver");
+
+        webConsolePlugin.service(request, response);
+
+        String htmlString = stringWriter.toString();
+        assertEquals("<form method='get'><table class='content' cellpadding='0' cellspacing='0' width='100%'>\n" +
+                "<tr class='content'>\n" +
+                "<th colspan='3' class='content container'>Servlet Resolver Test</th>\n" +
+                "</tr>\n" +
+                "<tr class='content'>\n" +
+                "<td colspan='3' class='content'>To check which servlet is responsible for rendering a response, " +
+                "enter a request path into the field and click &apos;Resolve&apos; to resolve it.</th>\n" +
+                "</tr>\n" +
+                "<tr class='content'>\n" +
+                "<td class='content'>URL</td>\n" +
+                "<td class='content' colspan='2'><input type='text' name='url' value='' class='input' size='50'>\n" +
+                "</td></tr>\n" +
+                "</tr>\n" +
+                "<tr class='content'>\n" +
+                "<td class='content'>Method</td>\n" +
+                "<td class='content' colspan='2'><select name='method'>\n" +
+                "<option value='GET'>GET</option>\n" +
+                "<option value='POST'>POST</option>\n" +
+                "</select>\n" +
+                "&nbsp;&nbsp;<input type='submit' value='Resolve' class='submit'>\n" +
+                "</td></tr>\n" +
+                "</table>\n" +
+                "</form>", htmlString);
+    }
+
+    @Test
+    public void printHTMLFormatDenyPaths() throws Exception {
+        // Mock resource resolver
+        ResourceResolver resourceResolver = Mockito.mock(ResourceResolver.class);
+        Resource resource = Mockito.mock(Resource.class);
+        Servlet servlet = Mockito.mock(Servlet.class);
+        Mockito.when(resource.adaptTo(Servlet.class)).thenReturn(servlet);
+        Mockito.when(resource.getPath()).thenReturn("/denied");
+        Mockito.when(resourceResolver.resolve(Mockito.anyString())).thenReturn(resource);
+        Mockito.when(resourceResolverFactory.getServiceResourceResolver(Mockito.anyMap())).thenReturn(resourceResolver);
+
+        // Mock request calls
+        Mockito.when(request.getParameter("url")).thenReturn("/denied");
+        Mockito.when(request.getParameter("method")).thenReturn("GET");
+
+        // HTML output
+        Mockito.when(request.getRequestURI()).thenReturn("/path/servletresolver");
+        webConsolePlugin.service(request, response);
+
+        String htmlString = stringWriter.toString();
+        final String expectedDeniedElement = "<ol class='servlets'>\n" +
+                "<li><del>";
+        assertThat(htmlString, CoreMatchers.containsString(expectedDeniedElement));
+    }
+}