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

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


The following commit(s) were added to refs/heads/master by this push:
     new 02265a4  SLING-13166 Update to Parent 66 and apply spotless formatting 
(#5)
02265a4 is described below

commit 02265a4b8dd20281816703d37ae1a1cae43836c8
Author: Eric Norman <[email protected]>
AuthorDate: Tue Apr 14 14:59:31 2026 -0700

    SLING-13166 Update to Parent 66 and apply spotless formatting (#5)
---
 .sling-module.json                                 |   8 +
 pom.xml                                            |  68 +++-
 .../impl/console/AbstractWebConsolePlugin.java     |  27 +-
 .../console/OsgiInstallerWebConsolePlugin.java     | 206 +++++------
 .../impl/console/AbstractWebConsolePluginTest.java |  82 +++++
 .../console/OsgiInstallerWebConsolePluginTest.java | 391 +++++++++++++++++++++
 .../core/impl/console/ReflectionTools.java         |  71 ++++
 7 files changed, 725 insertions(+), 128 deletions(-)

diff --git a/.sling-module.json b/.sling-module.json
new file mode 100644
index 0000000..d4268ec
--- /dev/null
+++ b/.sling-module.json
@@ -0,0 +1,8 @@
+{
+  "jenkins": {
+    "jdks": [
+      17,
+      21
+    ]
+  }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index d876adb..4c146ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,31 +23,35 @@
     <parent>
         <groupId>org.apache.sling</groupId>
         <artifactId>sling-bundle-parent</artifactId>
-        <version>49</version>
+        <version>66</version>
         <relativePath />
     </parent>
 
     <artifactId>org.apache.sling.installer.console</artifactId>
-    <version>1.1.3-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
 
     <name>Apache Sling Installer Web Console Plugin</name>
-    <description> 
-        Provides a web console plugin for the OSGi installer
-    </description>
-
-    <properties>
-        <sling.java.version>8</sling.java.version>
-        
<project.build.outputTimestamp>2023-05-15T10:56:39Z</project.build.outputTimestamp>
-    </properties>
+    <description>Provides a web console plugin for the OSGi 
installer</description>
 
     <scm>
         
<connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-console.git</connection>
         
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-console.git</developerConnection>
-        
<url>https://github.com/apache/sling-org-apache-sling-installer-console.git</url>
-      <tag>HEAD</tag>
-  </scm>
+        <tag>HEAD</tag>
+        
<url>https://github.com/apache/sling-org-apache-sling-installer-console/tree/${project.scm.tag}</url>
+    </scm>
+
+    <properties>
+        <sling.java.version>17</sling.java.version>
+        
<project.build.outputTimestamp>2023-05-15T10:56:39Z</project.build.outputTimestamp>
+    </properties>
 
     <dependencies>
+        <!-- JetBrains annotations for null-analysis (SLING-7798) -->
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.framework</artifactId>
@@ -73,11 +77,49 @@
             <artifactId>org.apache.sling.installer.core</artifactId>
             <version>3.10.0</version>
             <scope>provided</scope>
+            <exclusions>
+                <!-- workaround to exclude the older artifact that is 
incomatible with current sling-mock -->
+                <exclusion>
+                    <groupId>org.apache.sling</groupId>
+                    <artifactId>org.apache.sling.commons.osgi</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock.junit5</artifactId>
+            <version>3.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/src/main/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePlugin.java
index a9e8895..b06bcc3 100644
--- 
a/src/main/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePlugin.java
+++ 
b/src/main/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePlugin.java
@@ -18,15 +18,15 @@
  */
 package org.apache.sling.installer.core.impl.console;
 
-import java.net.URL;
-
 import javax.servlet.GenericServlet;
 
+import java.net.URL;
+
 @SuppressWarnings("serial")
 abstract class AbstractWebConsolePlugin extends GenericServlet {
 
     /**
-     * 
+     *
      * @return the prefix under which resources are requested (must not start 
with "/", must end with "/")
      */
     abstract String getRelativeResourcePrefix();
@@ -38,11 +38,8 @@ abstract class AbstractWebConsolePlugin extends 
GenericServlet {
     private URL getResource(final String path) {
         if (path.startsWith("/" + getRelativeResourcePrefix())) {
             // strip label
-            int index = path.indexOf('/', 1);
-            if (index <= 0) {
-                throw new IllegalStateException("The relativeResourcePrefix 
must contain at least one '/'");
-            }
-            return this.getClass().getResource(path.substring(index));
+            String resPath = path.substring(path.indexOf('/', 1));
+            return this.getClass().getResource(resPath);
         }
         return null;
     }
@@ -57,19 +54,19 @@ abstract class AbstractWebConsolePlugin extends 
GenericServlet {
         if (input == null) {
             return null;
         }
-    
+
         final StringBuilder b = new StringBuilder(input.length());
-        for(int i = 0;i  < input.length(); i++) {
+        for (int i = 0; i < input.length(); i++) {
             final char c = input.charAt(i);
-            if(c == '&') {
+            if (c == '&') {
                 b.append("&amp;");
-            } else if(c == '<') {
+            } else if (c == '<') {
                 b.append("&lt;");
-            } else if(c == '>') {
+            } else if (c == '>') {
                 b.append("&gt;");
-            } else if(c == '"') {
+            } else if (c == '"') {
                 b.append("&quot;");
-            } else if(c == '\'') {
+            } else if (c == '\'') {
                 b.append("&apos;");
             } else {
                 b.append(c);
diff --git 
a/src/main/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePlugin.java
index 97bc12d..5862bc2 100644
--- 
a/src/main/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePlugin.java
+++ 
b/src/main/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePlugin.java
@@ -18,6 +18,9 @@
  */
 package org.apache.sling.installer.core.impl.console;
 
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -27,9 +30,6 @@ import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
 
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
 import org.apache.sling.installer.api.InstallableResource;
 import org.apache.sling.installer.api.info.InfoProvider;
 import org.apache.sling.installer.api.info.InstallationState;
@@ -43,37 +43,36 @@ import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.component.annotations.ReferencePolicyOption;
 
-
-@Component(service=javax.servlet.Servlet.class,
-    property = {
-        Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
-        Constants.SERVICE_DESCRIPTION + "=Apache Sling OSGi Installer Web 
Console Plugin",
-        "felix.webconsole.label=" + OsgiInstallerWebConsolePlugin.LABEL,
-        "felix.webconsole.title=OSGi Installer",
-        "felix.webconsole.category=OSGi",
-        "felix.webconsole.configprinter.modes=zip",
-        "felix.webconsole.configprinter.modes=txt",
-        "felix.webconsole.css=" + OsgiInstallerWebConsolePlugin.RES_LOC + 
"list.css"
-    })
+@Component(
+        service = javax.servlet.Servlet.class,
+        property = {
+            Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+            Constants.SERVICE_DESCRIPTION + "=Apache Sling OSGi Installer Web 
Console Plugin",
+            "felix.webconsole.label=" + OsgiInstallerWebConsolePlugin.LABEL,
+            "felix.webconsole.title=OSGi Installer",
+            "felix.webconsole.category=OSGi",
+            "felix.webconsole.configprinter.modes=zip",
+            "felix.webconsole.configprinter.modes=txt",
+            "felix.webconsole.css=" + OsgiInstallerWebConsolePlugin.RES_LOC + 
"list.css"
+        })
 @SuppressWarnings("serial")
 public class OsgiInstallerWebConsolePlugin extends AbstractWebConsolePlugin {
 
     public static final String LABEL = "osgi-installer";
     protected static final String RES_LOC = LABEL + "/res/ui/";
 
-
-    @Reference(policyOption=ReferencePolicyOption.GREEDY)
-    private InfoProvider installer;
+    @Reference(policyOption = ReferencePolicyOption.GREEDY)
+    private transient InfoProvider installer;
 
     private String getType(final RegisteredResource rsrc) {
         final String type = rsrc.getType();
-        if ( type.equals(InstallableResource.TYPE_BUNDLE) ) {
+        if (type.equals(InstallableResource.TYPE_BUNDLE)) {
             return "Bundles";
-        } else if ( type.equals(InstallableResource.TYPE_CONFIG) ) {
+        } else if (type.equals(InstallableResource.TYPE_CONFIG)) {
             return "Configurations";
-        } else if ( type.equals(InstallableResource.TYPE_FILE) ) {
+        } else if (type.equals(InstallableResource.TYPE_FILE)) {
             return "Files";
-        } else if ( type.equals(InstallableResource.TYPE_PROPERTIES) ) {
+        } else if (type.equals(InstallableResource.TYPE_PROPERTIES)) {
             return "Properties";
         }
         return type;
@@ -82,14 +81,14 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
     private String getEntityId(final RegisteredResource rsrc, String alias) {
         String id = rsrc.getEntityId();
         final int pos = id.indexOf(':');
-        if ( pos != -1 ) {
+        if (pos != -1) {
             id = id.substring(pos + 1);
         }
         return (alias == null ? id : id + '\n' + alias);
     }
 
     private String getURL(final Resource rsrc) {
-        if ( rsrc.getVersion() != null ) {
+        if (rsrc.getVersion() != null) {
             return rsrc.getURL() + " (" + rsrc.getVersion() + ")";
         }
         return rsrc.getURL();
@@ -98,8 +97,8 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
     private String getState(final Resource rsrc) {
         String stateInfo = rsrc.getState().toString();
         // INSTALLED state has some variants
-        if ( rsrc.getState() == ResourceState.INSTALLED) {
-            if (rsrc.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != null 
) {
+        if (rsrc.getState() == ResourceState.INSTALLED) {
+            if (rsrc.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != null) 
{
                 stateInfo = "EXCLUDED";
             }
             if (rsrc.getAttribute(TaskResource.ATTR_INSTALL_INFO) != null) {
@@ -125,55 +124,58 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
      * Format a date
      */
     private synchronized String formatDate(final long time) {
-        if ( time == -1 ) {
-            return "-";
-        }
         final Date d = new Date(time);
         return dateFormat.format(d);
     }
 
     @Override
-    public void service(final ServletRequest req, final ServletResponse res)
-            throws IOException {
+    public void service(final ServletRequest req, final ServletResponse res) 
throws IOException {
         StringWriter bufferedWriter = new StringWriter();
         final PrintWriter bufferedPw = new PrintWriter(bufferedWriter);
 
         PrintWriter pw = res.getWriter();
         final InstallationState state = this.installer.getInstallationState();
         pw.print("<p class='statline ui-state-highlight'>Apache Sling OSGi 
Installer");
-        if ( state.getActiveResources().size() == 0 && 
state.getInstalledResources().size() == 0 && 
state.getUntransformedResources().size() == 0 ) {
+        if (state.getActiveResources().isEmpty()
+                && state.getInstalledResources().isEmpty()
+                && state.getUntransformedResources().isEmpty()) {
             pw.print(" - no resources registered.");
         }
-        
+
         pw.print("</p>");
         pw.println("<ul class=list>");
         pw.println("<li>Active Resources");
         pw.println("<ul>");
-        
+
         String rt = null;
         for (final ResourceGroup group : state.getActiveResources()) {
             final Resource toActivate = group.getResources().get(0);
-            if ( !toActivate.getType().equals(rt) ) {
-                if ( rt != null ) {
+            if (!toActivate.getType().equals(rt)) {
+                if (rt != null) {
                     bufferedPw.println("</tbody></table>");
                 }
                 String anchor = "active-" + escapeXml(getType(toActivate));
                 pw.println("<li><a href='#" + anchor + "'>" + 
escapeXml(getType(toActivate)) + "</a></li>");
-                bufferedPw.println("<div id='" + anchor + "' 
class='ui-widget-header ui-corner-top buttonGroup' style='height: 15px;'>");
-                bufferedPw.printf("<span style='float: left; margin-left: 
1em;'>Active Resources - %s</span>", getType(toActivate));
+                bufferedPw.println("<div id='" + anchor
+                        + "' class='ui-widget-header ui-corner-top 
buttonGroup' style='height: 15px;'>");
+                bufferedPw.printf(
+                        "<span style='float: left; margin-left: 1em;'>Active 
Resources - %s</span>",
+                        getType(toActivate));
                 bufferedPw.println("</div>");
                 bufferedPw.println("<table class='nicetable'><tbody>");
-                bufferedPw.printf("<tr><th>Entity 
ID</th><th>Digest/Priority</th><th>URL 
(Version)</th><th>State</th><th>Error</th></tr>");
+                bufferedPw.printf(
+                        "<tr><th>Entity ID</th><th>Digest/Priority</th><th>URL 
(Version)</th><th>State</th><th>Error</th></tr>");
                 rt = toActivate.getType();
             }
-            
bufferedPw.printf("<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>",
+            bufferedPw.printf(
+                    
"<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>",
                     escapeXml(getEntityId(toActivate, group.getAlias())),
                     escapeXml(getInfo(toActivate)),
                     escapeXml(getURL(toActivate)),
                     escapeXml(toActivate.getState().toString()),
                     escapeXml(getError(toActivate)));
         }
-        if ( rt != null ) {
+        if (rt != null) {
             bufferedPw.println("</tbody></table>");
         } else {
             pw.println("<li>none</li>");
@@ -183,24 +185,28 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("</ul></li>");
         pw.println("<li>Processed Resources");
         pw.println("<ul>");
-        
-        for(final ResourceGroup group : state.getInstalledResources()) {
+
+        for (final ResourceGroup group : state.getInstalledResources()) {
             final Collection<Resource> resources = group.getResources();
-            if (resources.size() > 0) {
+            if (!resources.isEmpty()) {
                 final Iterator<Resource> iter = resources.iterator();
                 final Resource first = iter.next();
-                if ( !first.getType().equals(rt) ) {
-                    if ( rt != null ) {
+                if (!first.getType().equals(rt)) {
+                    if (rt != null) {
                         bufferedPw.println("</tbody></table>");
                     }
                     String anchor = "processed-" + escapeXml(getType(first));
                     pw.println("<li><a href='#" + anchor + "'>" + 
escapeXml(getType(first)) + "</a></li>");
-                    
-                    bufferedPw.println("<div id='" + anchor + "' 
class='ui-widget-header ui-corner-top buttonGroup' style='height: 15px;'>");
-                    bufferedPw.printf("<span style='float: left; margin-left: 
1em;'>Processed Resources - %s</span>", getType(first));
+
+                    bufferedPw.println("<div id='" + anchor
+                            + "' class='ui-widget-header ui-corner-top 
buttonGroup' style='height: 15px;'>");
+                    bufferedPw.printf(
+                            "<span style='float: left; margin-left: 
1em;'>Processed Resources - %s</span>",
+                            getType(first));
                     bufferedPw.println("</div>");
                     bufferedPw.println("<table class='nicetable'><tbody>");
-                    bufferedPw.printf("<tr><th>Entity 
ID</th><th>Digest/Priority</th><th>URL 
(Version)</th><th>State</th><th>Error</th></tr>");
+                    bufferedPw.printf(
+                            "<tr><th>Entity 
ID</th><th>Digest/Priority</th><th>URL 
(Version)</th><th>State</th><th>Error</th></tr>");
                     rt = first.getType();
                 }
                 bufferedPw.print("<tr><td>");
@@ -211,9 +217,9 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 bufferedPw.print(escapeXml(getURL(first)));
                 bufferedPw.print("</td><td>");
                 bufferedPw.print(escapeXml(getState(first)));
-                if ( first.getState() == ResourceState.INSTALLED ) {
+                if (first.getState() == ResourceState.INSTALLED) {
                     final long lastChange = first.getLastChange();
-                    if ( lastChange > 0 ) {
+                    if (lastChange > 0) {
                         bufferedPw.print("<br/>");
                         bufferedPw.print(formatDate(lastChange));
                     }
@@ -221,18 +227,22 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 bufferedPw.print("</td><td>");
                 bufferedPw.print(escapeXml(getError(first)));
                 bufferedPw.print("</td></tr>");
-                if ( first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != 
null ) {
-                    bufferedPw.printf("<tr><td></td><td 
colspan='2'>%s</td><td></td><td></td></tr>",
-                            
escapeXml(first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED).toString()));
+                if (first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != 
null) {
+                    bufferedPw.printf(
+                            "<tr><td></td><td 
colspan='2'>%s</td><td></td><td></td></tr>",
+                            
escapeXml(first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED)
+                                    .toString()));
                 }
-                if ( first.getAttribute(TaskResource.ATTR_INSTALL_INFO) != 
null ) {
-                    bufferedPw.printf("<tr><td></td><td 
colspan='2'>%s</td><td></td><td></td></tr>",
-                            
escapeXml(first.getAttribute(TaskResource.ATTR_INSTALL_INFO).toString()));
-
+                if (first.getAttribute(TaskResource.ATTR_INSTALL_INFO) != 
null) {
+                    bufferedPw.printf(
+                            "<tr><td></td><td 
colspan='2'>%s</td><td></td><td></td></tr>",
+                            
escapeXml(first.getAttribute(TaskResource.ATTR_INSTALL_INFO)
+                                    .toString()));
                 }
-                while ( iter.hasNext() ) {
+                while (iter.hasNext()) {
                     final Resource resource = iter.next();
-                    
bufferedPw.printf("<tr><td></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>",
+                    bufferedPw.printf(
+                            
"<tr><td></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>",
                             escapeXml(getInfo(resource)),
                             escapeXml(getURL(resource)),
                             escapeXml(resource.getState().toString()),
@@ -240,7 +250,7 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 }
             }
         }
-        if ( rt != null ) {
+        if (rt != null) {
             bufferedPw.println("</tbody></table>");
         } else {
             pw.println("<li>none</li>");
@@ -251,27 +261,30 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("<ul>");
 
         rt = null;
-        for(final RegisteredResource registeredResource : 
state.getUntransformedResources()) {
-            if ( !registeredResource.getType().equals(rt) ) {
-                if ( rt != null ) {
+        for (final RegisteredResource registeredResource : 
state.getUntransformedResources()) {
+            if (!registeredResource.getType().equals(rt)) {
+                if (rt != null) {
                     bufferedPw.println("</tbody></table>");
                 }
                 String anchor = "untransformed-" + 
escapeXml(getType(registeredResource));
                 pw.println("<li><a href='#" + anchor + "'>" + 
escapeXml(getType(registeredResource)) + "</a></li>");
-                
-                bufferedPw.println("<div id='" + anchor + "' 
class='ui-widget-header ui-corner-top buttonGroup' style='height: 15px;'>");
-                bufferedPw.printf("<span style='float: left; margin-left: 
1em;'>Untransformed Resources - %s</span>", getType(registeredResource));
+
+                bufferedPw.println("<div id='" + anchor
+                        + "' class='ui-widget-header ui-corner-top 
buttonGroup' style='height: 15px;'>");
+                bufferedPw.printf(
+                        "<span style='float: left; margin-left: 
1em;'>Untransformed Resources - %s</span>",
+                        getType(registeredResource));
                 bufferedPw.println("</div>");
                 bufferedPw.println("<table class='nicetable'><tbody>");
                 
bufferedPw.printf("<tr><th>Digest/Priority</th><th>URL</th></tr>");
 
                 rt = registeredResource.getType();
             }
-            bufferedPw.printf("<tr><td>%s</td><td>%s</td></tr>",
-                    escapeXml(getInfo(registeredResource)),
-                    escapeXml(registeredResource.getURL()));
+            bufferedPw.printf(
+                    "<tr><td>%s</td><td>%s</td></tr>",
+                    escapeXml(getInfo(registeredResource)), 
escapeXml(registeredResource.getURL()));
         }
-        if ( rt != null ) {
+        if (rt != null) {
             bufferedPw.println("</tbody></table>");
         } else {
             pw.println("<li>none</li>");
@@ -286,7 +299,7 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
      * Method for the configuration printer.
      */
     public void printConfiguration(final PrintWriter pw, final String mode) {
-        if ( !"zip".equals(mode) && !"txt".equals(mode) ) {
+        if (!"zip".equals(mode) && !"txt".equals(mode)) {
             return;
         }
         pw.println("Apache Sling OSGi Installer");
@@ -295,13 +308,14 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("Active Resources");
         pw.println("----------------");
         String rt = null;
-        for(final ResourceGroup group : state.getActiveResources()) {
+        for (final ResourceGroup group : state.getActiveResources()) {
             final Resource toActivate = group.getResources().get(0);
-            if ( !toActivate.getType().equals(rt) ) {
+            if (!toActivate.getType().equals(rt)) {
                 pw.printf("%s:%n", getType(toActivate));
                 rt = toActivate.getType();
             }
-            pw.printf("- %s: %s, %s, %s, %s%n",
+            pw.printf(
+                    "- %s: %s, %s, %s, %s%n",
                     getEntityId(toActivate, group.getAlias()),
                     getInfo(toActivate),
                     getURL(toActivate),
@@ -313,37 +327,33 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("Processed Resources");
         pw.println("-------------------");
         rt = null;
-        for(final ResourceGroup group : state.getInstalledResources()) {
+        for (final ResourceGroup group : state.getInstalledResources()) {
             final Collection<Resource> resources = group.getResources();
-            if (resources.size() > 0) {
+            if (!resources.isEmpty()) {
                 final Iterator<Resource> iter = resources.iterator();
                 final Resource first = iter.next();
-                if ( !first.getType().equals(rt) ) {
+                if (!first.getType().equals(rt)) {
                     pw.printf("%s:%n", getType(first));
                     rt = first.getType();
                 }
-                pw.printf("* %s: %s, %s, %s, %s%n",
+                pw.printf(
+                        "* %s: %s, %s, %s, %s%n",
                         getEntityId(first, group.getAlias()),
                         getInfo(first),
                         getURL(first),
                         getState(first),
                         getError(first));
-                if ( first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != 
null ) {
-                    pw.printf("  : %s",
-                            
first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED));
+                if (first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED) != 
null) {
+                    pw.printf("  : %s", 
first.getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED));
                 }
-                if ( first.getAttribute(TaskResource.ATTR_INSTALL_INFO) != 
null ) {
-                    pw.printf("  : %s",
-                            
first.getAttribute(TaskResource.ATTR_INSTALL_INFO));
-
+                if (first.getAttribute(TaskResource.ATTR_INSTALL_INFO) != 
null) {
+                    pw.printf("  : %s", 
first.getAttribute(TaskResource.ATTR_INSTALL_INFO));
                 }
-                while ( iter.hasNext() ) {
+                while (iter.hasNext()) {
                     final Resource resource = iter.next();
-                    pw.printf("  - %s, %s, %s, %s%n",
-                            getInfo(resource),
-                            getURL(resource),
-                            resource.getState(),
-                            getError(resource));
+                    pw.printf(
+                            "  - %s, %s, %s, %s%n",
+                            getInfo(resource), getURL(resource), 
resource.getState(), getError(resource));
                 }
             }
         }
@@ -352,14 +362,12 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("Untransformed Resources");
         pw.println("-----------------------");
         rt = null;
-        for(final RegisteredResource registeredResource : 
state.getUntransformedResources()) {
-            if ( !registeredResource.getType().equals(rt) ) {
+        for (final RegisteredResource registeredResource : 
state.getUntransformedResources()) {
+            if (!registeredResource.getType().equals(rt)) {
                 pw.printf("%s:%n", getType(registeredResource));
                 rt = registeredResource.getType();
             }
-            pw.printf("- %s, %s%n",
-                    getInfo(registeredResource),
-                    registeredResource.getURL());
+            pw.printf("- %s, %s%n", getInfo(registeredResource), 
registeredResource.getURL());
         }
     }
 
@@ -367,6 +375,4 @@ public class OsgiInstallerWebConsolePlugin extends 
AbstractWebConsolePlugin {
     String getRelativeResourcePrefix() {
         return RES_LOC;
     }
-    
-    
 }
diff --git 
a/src/test/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePluginTest.java
 
b/src/test/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePluginTest.java
new file mode 100644
index 0000000..22d7306
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/installer/core/impl/console/AbstractWebConsolePluginTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.installer.core.impl.console;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+class AbstractWebConsolePluginTest {
+    private AbstractWebConsolePlugin plugin = new AbstractWebConsolePlugin() {
+
+        @Override
+        public void service(ServletRequest req, ServletResponse res) throws 
ServletException, IOException {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        String getRelativeResourcePrefix() {
+            return "osgi-installer/res/ui/";
+        }
+    };
+
+    /**
+     * Test method for {@link 
org.apache.sling.installer.core.impl.console.AbstractWebConsolePlugin#escapeXml(java.lang.String)}.
+     */
+    @Test
+    void testEscapeXml() {
+        assertNull(plugin.escapeXml(null));
+        assertEquals("", plugin.escapeXml(""));
+        assertEquals("&lt;hello/&gt;", plugin.escapeXml("<hello/>"));
+        assertEquals("&quot;dog&apos;s &amp; cat&quot;", 
plugin.escapeXml("\"dog's & cat\""));
+    }
+
+    /**
+     * Test method for {@link
+     * 
org.apache.sling.installer.factories.configuration.impl.ConfigurationSerializerWebConsolePlugin#getResource(java.lang.String)}.
+     */
+    @Test
+    void testGetResource() throws SecurityException, IllegalArgumentException {
+        final URL value1 = ReflectionTools.invokeMethodWithReflection(
+                plugin, "getResource", new Class[] {String.class}, URL.class, 
new Object[] {"/invalid"});
+        assertNull(value1);
+
+        final URL value2 = ReflectionTools.invokeMethodWithReflection(
+                plugin, "getResource", new Class[] {String.class}, URL.class, 
new Object[] {
+                    "/osgi-installer/res/ui/list.css"
+                });
+        assertNotNull(value2);
+
+        final URL value3 = ReflectionTools.invokeMethodWithReflection(
+                plugin, "getResource", new Class[] {String.class}, URL.class, 
new Object[] {
+                    "/osgi-installer/res/ui/invalid.css"
+                });
+        assertNull(value3);
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePluginTest.java
 
b/src/test/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePluginTest.java
new file mode 100644
index 0000000..ce789e8
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/installer/core/impl/console/OsgiInstallerWebConsolePluginTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.installer.core.impl.console;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.installer.api.InstallableResource;
+import org.apache.sling.installer.api.info.InfoProvider;
+import org.apache.sling.installer.api.info.InstallationState;
+import org.apache.sling.installer.api.info.Resource;
+import org.apache.sling.installer.api.info.ResourceGroup;
+import org.apache.sling.installer.api.tasks.RegisteredResource;
+import org.apache.sling.installer.api.tasks.ResourceState;
+import org.apache.sling.installer.api.tasks.TaskResource;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import 
org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
+import org.osgi.framework.Version;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+@ExtendWith(SlingContextExtension.class)
+class OsgiInstallerWebConsolePluginTest {
+    public final SlingContext context = new SlingContext();
+
+    private OsgiInstallerWebConsolePlugin plugin;
+
+    private InfoProvider mockInfoProvider;
+
+    @BeforeEach
+    void beforeEach() {
+        mockInfoProvider = context.registerService(InfoProvider.class, 
Mockito.mock(InfoProvider.class));
+
+        plugin = 
context.registerInjectActivateService(OsgiInstallerWebConsolePlugin.class);
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.installer.core.impl.console.OsgiInstallerWebConsolePlugin#getRelativeResourcePrefix()}.
+     */
+    @Test
+    void testGetRelativeResourcePrefix() {
+        assertNotNull(plugin.getRelativeResourcePrefix());
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.installer.core.impl.console.OsgiInstallerWebConsolePlugin#service(jakarta.servlet.ServletRequest,
 jakarta.servlet.ServletResponse)}.
+     */
+    @Test
+    void testServiceWithNoResources() throws IOException {
+        // mock InfoProvider
+        mockInstallationState();
+
+        final @NotNull MockSlingHttpServletRequest req = context.request();
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        plugin.service(req, resp);
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("no resources registered"));
+    }
+
+    @Test
+    void testServiceWithActiveResources() throws IOException {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicActiveResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletRequest req = context.request();
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        plugin.service(req, resp);
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertFalse(outputAsString.contains("no resources registered"));
+    }
+
+    @Test
+    void testServiceWithInstalledResources() throws IOException {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicInstalledResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletRequest req = context.request();
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        plugin.service(req, resp);
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertFalse(outputAsString.contains("no resources registered"));
+    }
+
+    @Test
+    void testServiceWithUntransformedResources() throws IOException {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicUntransformedResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletRequest req = context.request();
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        plugin.service(req, resp);
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertFalse(outputAsString.contains("no resources registered"));
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.installer.core.impl.console.OsgiInstallerWebConsolePlugin#printConfiguration(java.io.PrintWriter,
 java.lang.String)}.
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {"zip", "txt"})
+    void testPrintConfigurationWithActiveResources(String mode) {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicActiveResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        final PrintWriter pw = resp.getWriter();
+        plugin.printConfiguration(pw, mode);
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Apache Sling OSGi Installer"));
+    }
+
+    @Test
+    void testPrintConfigurationWithInstalledResources() {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicInstalledResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        final PrintWriter pw = resp.getWriter();
+        plugin.printConfiguration(pw, "txt");
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Apache Sling OSGi Installer"));
+    }
+
+    @Test
+    void testPrintConfigurationWithUntransformedResources() {
+        // mock InfoProvider
+        final InstallationState mockInstallationState = 
mockInstallationState();
+        mockBasicUntransformedResources(mockInstallationState);
+
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        final PrintWriter pw = resp.getWriter();
+        plugin.printConfiguration(pw, "zip");
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Apache Sling OSGi Installer"));
+    }
+
+    @Test
+    void testPrintConfigurationForInvalidMode() {
+        final @NotNull MockSlingHttpServletResponse resp = context.response();
+        final PrintWriter pw = resp.getWriter();
+        plugin.printConfiguration(pw, "invalid");
+        final String outputAsString = resp.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.isEmpty());
+    }
+
+    private InstallationState mockInstallationState() {
+        InstallationState mockInstallationState = 
Mockito.mock(InstallationState.class);
+        
Mockito.doReturn(mockInstallationState).when(mockInfoProvider).getInstallationState();
+        return mockInstallationState;
+    }
+
+    private RegisteredResource mockInstallRegisteredResource(
+            String type, String entityId, String scheme, Map<String, Object> 
dictionary) {
+        RegisteredResource mockInstalledResource = 
Mockito.mock(RegisteredResource.class);
+        Mockito.doReturn(type).when(mockInstalledResource).getType();
+        Mockito.doReturn(entityId).when(mockInstalledResource).getEntityId();
+        Mockito.doReturn(scheme).when(mockInstalledResource).getScheme();
+        Mockito.doReturn(new Hashtable<>(dictionary))
+                .when(mockInstalledResource)
+                .getDictionary();
+        return mockInstalledResource;
+    }
+
+    private Resource mockInstallResoure(
+            String type,
+            ResourceState state,
+            String entityId,
+            String scheme,
+            Map<String, Object> dictionary,
+            String error,
+            String url,
+            Version version) {
+        Resource mockInstalledResource = Mockito.mock(Resource.class);
+        Mockito.doReturn(type).when(mockInstalledResource).getType();
+        Mockito.doReturn(state).when(mockInstalledResource).getState();
+        Mockito.doReturn(entityId).when(mockInstalledResource).getEntityId();
+        Mockito.doReturn(scheme).when(mockInstalledResource).getScheme();
+        Mockito.doReturn(new Hashtable<>(dictionary))
+                .when(mockInstalledResource)
+                .getDictionary();
+        Mockito.doReturn(error).when(mockInstalledResource).getError();
+        Mockito.doReturn(url).when(mockInstalledResource).getURL();
+        Mockito.doReturn(version).when(mockInstalledResource).getVersion();
+        return mockInstalledResource;
+    }
+
+    private void mockBasicUntransformedResources(final InstallationState 
mockInstallationState) {
+        // mock untransformed resources
+        final RegisteredResource mockUntransformedResource1 = 
mockInstallRegisteredResource(
+                InstallableResource.TYPE_CONFIG, "test1", "launchpad", 
Map.of("key1", "value1"));
+
+        // and a second for code coverage
+        final RegisteredResource mockUntransformedResource2 = 
mockInstallRegisteredResource(
+                InstallableResource.TYPE_CONFIG, "test2", "launchpad", 
Map.of("key2", "value2"));
+
+        // and a third for code coverage
+        final RegisteredResource mockUntransformedResource3 = 
mockInstallRegisteredResource(
+                InstallableResource.TYPE_PROPERTIES, "test3", "launchpad", 
Map.of("key3", "value3"));
+
+        Mockito.doReturn(List.of(mockUntransformedResource1, 
mockUntransformedResource2, mockUntransformedResource3))
+                .when(mockInstallationState)
+                .getUntransformedResources();
+    }
+
+    private void mockBasicInstalledResources(final InstallationState 
mockInstallationState) {
+        // mock installed resources
+        ResourceGroup mockInstalledRG1 = Mockito.mock(ResourceGroup.class);
+        Mockito.doReturn("alias1").when(mockInstalledRG1).getAlias();
+
+        Resource mockInstalledResource1 = mockInstallResoure(
+                InstallableResource.TYPE_CONFIG,
+                ResourceState.INSTALLED,
+                "config:factory1~test1",
+                "launchpad",
+                Map.of("key1", "value1", "key2", "value2"),
+                "error1",
+                "url1",
+                new Version(1, 0, 0));
+        // mock a last change timestamp for code coverage
+        Mockito.doReturn(System.currentTimeMillis())
+                .when(mockInstalledResource1)
+                .getLastChange();
+        // add mock attributes for code coverage
+        
Mockito.doReturn("excluded1").when(mockInstalledResource1).getAttribute(TaskResource.ATTR_INSTALL_EXCLUDED);
+        
Mockito.doReturn("info1").when(mockInstalledResource1).getAttribute(TaskResource.ATTR_INSTALL_INFO);
+
+        Resource mockInstalledResource2 = mockInstallResoure(
+                InstallableResource.TYPE_CONFIG,
+                ResourceState.INSTALLED,
+                "test2",
+                "launchpad",
+                Map.of("key2", "value2"),
+                null,
+                "url2",
+                null);
+        Mockito.doReturn(List.of(mockInstalledResource1, 
mockInstalledResource2))
+                .when(mockInstalledRG1)
+                .getResources();
+
+        // and a second group for code coverage
+        ResourceGroup mockInstalledRG2 = Mockito.mock(ResourceGroup.class);
+
+        Resource mockInstalledResource3 = mockInstallResoure(
+                InstallableResource.TYPE_CONFIG,
+                ResourceState.INSTALLED,
+                "test3",
+                "launchpad",
+                Map.of("key3", "value3"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockInstalledResource3)).when(mockInstalledRG2).getResources();
+
+        // and a third group for code coverage
+        ResourceGroup mockInstalledRG3 = Mockito.mock(ResourceGroup.class);
+
+        Resource mockInstalledResource4 = mockInstallResoure(
+                InstallableResource.TYPE_BUNDLE,
+                ResourceState.INSTALLED,
+                "test4",
+                "launchpad",
+                Map.of("key4", "value4"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockInstalledResource4)).when(mockInstalledRG3).getResources();
+
+        // and a fourth group for code coverage
+        ResourceGroup mockInstalledRG4 = Mockito.mock(ResourceGroup.class);
+
+        Resource mockInstalledResource5 = mockInstallResoure(
+                InstallableResource.TYPE_FILE,
+                ResourceState.UNINSTALLED,
+                "test5",
+                "launchpad",
+                Map.of("key5", "value5"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockInstalledResource5)).when(mockInstalledRG4).getResources();
+
+        // and a fifth group for code coverage
+        ResourceGroup mockInstalledRG5 = Mockito.mock(ResourceGroup.class);
+
+        Resource mockInstalledResource6 = mockInstallResoure(
+                "invalid", ResourceState.INSTALLED, "test6", "launchpad", 
Map.of("key6", "value6"), null, null, null);
+        
Mockito.doReturn(List.of(mockInstalledResource6)).when(mockInstalledRG5).getResources();
+
+        // and a sixth (empty) group for code coverage
+        ResourceGroup mockInstalledRG6 = Mockito.mock(ResourceGroup.class);
+        Mockito.doReturn(List.of()).when(mockInstalledRG6).getResources();
+
+        Mockito.doReturn(List.of(
+                        mockInstalledRG1,
+                        mockInstalledRG2,
+                        mockInstalledRG3,
+                        mockInstalledRG4,
+                        mockInstalledRG5,
+                        mockInstalledRG6))
+                .when(mockInstallationState)
+                .getInstalledResources();
+    }
+
+    private void mockBasicActiveResources(final InstallationState 
mockInstallationState) {
+        // mock active resources
+        ResourceGroup mockActiveRG1 = Mockito.mock(ResourceGroup.class);
+        Resource mockActiveResource1 = mockInstallResoure(
+                InstallableResource.TYPE_CONFIG,
+                ResourceState.INSTALLED,
+                "config:factory1~test1",
+                "launchpad",
+                Map.of("key1", "value1", "key2", "value2"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockActiveResource1)).when(mockActiveRG1).getResources();
+
+        // and a second group for code coverage
+        ResourceGroup mockActiveRG2 = Mockito.mock(ResourceGroup.class);
+        Resource mockActiveResource2 = mockInstallResoure(
+                InstallableResource.TYPE_CONFIG,
+                ResourceState.INSTALLED,
+                "test2",
+                "launchpad",
+                Map.of("key2", "value2"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockActiveResource2)).when(mockActiveRG2).getResources();
+
+        // and a third group for code coverage
+        ResourceGroup mockActiveRG3 = Mockito.mock(ResourceGroup.class);
+        Resource mockActiveResource3 = mockInstallResoure(
+                InstallableResource.TYPE_PROPERTIES,
+                ResourceState.INSTALLED,
+                "test3",
+                "launchpad",
+                Map.of("key3", "value3"),
+                null,
+                null,
+                null);
+        
Mockito.doReturn(List.of(mockActiveResource3)).when(mockActiveRG3).getResources();
+        Mockito.doReturn(List.of(mockActiveRG1, mockActiveRG2, mockActiveRG3))
+                .when(mockInstallationState)
+                .getActiveResources();
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/installer/core/impl/console/ReflectionTools.java
 
b/src/test/java/org/apache/sling/installer/core/impl/console/ReflectionTools.java
new file mode 100644
index 0000000..69cde7d
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/installer/core/impl/console/ReflectionTools.java
@@ -0,0 +1,71 @@
+/*
+ * 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.installer.core.impl.console;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Reflection utilities to facilitate testing
+ */
+public class ReflectionTools {
+
+    private ReflectionTools() {
+        // hide the public constructor
+    }
+
+    public static <T> T invokeMethodWithReflection(
+            Object obj, String methodName, Class<?>[] parameterTypes, Class<T> 
expectedType, Object[] methodArgs) {
+        Object result = null;
+        try {
+            Class<?> clazz = obj.getClass();
+            Method method = null;
+            do {
+                try { // NOSONAR
+                    method = clazz.getDeclaredMethod(methodName, 
parameterTypes);
+                } catch (NoSuchMethodException nsfe) {
+                    clazz = clazz.getSuperclass();
+                }
+            } while (method == null && clazz != null);
+            if (method != null) {
+                if (Modifier.isStatic(method.getModifiers())) {
+                    if (!method.canAccess(null)) {
+                        method.setAccessible(true);
+                    }
+                    result = method.invoke(null, methodArgs);
+                } else {
+                    if (!method.canAccess(obj)) {
+                        method.setAccessible(true);
+                    }
+                    result = method.invoke(obj, methodArgs);
+                }
+            } else {
+                fail("Failed to find method via reflection: " + methodName);
+            }
+        } catch (IllegalArgumentException | IllegalAccessException | 
SecurityException e) {
+            fail("Failed to get method via reflection. Reason: " + 
e.getMessage());
+        } catch (InvocationTargetException e) {
+            fail("Failed to invoke method via reflection. Reason: " + 
e.getMessage());
+        }
+        return expectedType == null ? null : expectedType.cast(result);
+    }
+}

Reply via email to