SLING-11610 - Sling XSS API 2.3.0 does not work on Java 17 (#32)

1. Switch from reflective access to using sun.misc.Unsafe. Even though 'unsafe' looks scary,
it's part of the jdk.unsupported module which both 'exports' and 'opens' its packages.

Reference: https://blogs.oracle.com/javamagazine/post/a-peek-into-java-17-continuing-the-drive-to-encapsulate-the-java-runtime-internals

2. Library updates: update to the latest version of Mockito, drop Powermock

3. Exclude xml-apis transitive dependency, this causes conflicts since we are using the module path

See https://stackoverflow.com/questions/55571046/eclipse-is-confused-by-imports-accessible-from-more-than-one-module

4. Configure the maven-compiler-plugin to use source/target instead of release, even for newer JDK
versions. This has the effect of running javac with fewer checks, and allows compilation on
Java 8 to complete succesfully.
diff --git a/.sling-module.json b/.sling-module.json
new file mode 100644
index 0000000..45d7070
--- /dev/null
+++ b/.sling-module.json
@@ -0,0 +1,5 @@
+{
+    "jenkins": {
+        "jdks": [8, 11, 17, 21]
+    }
+}
diff --git a/pom.xml b/pom.xml
index e7a5090..817aca8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,7 @@
 
     <properties>
         <project.build.outputTimestamp>1680702193</project.build.outputTimestamp>
+        <sling.java.version>8</sling.java.version>
     </properties>
 
 
@@ -213,6 +214,10 @@
                     <groupId>org.owasp.antisamy</groupId>
                     <artifactId>antisamy</artifactId>
                 </exclusion>
+                <exclusion>
+                    <groupId>xml-apis</groupId>
+                    <artifactId>xml-apis</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
         <dependency>
@@ -332,14 +337,8 @@
         </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
-            <artifactId>mockito-all</artifactId>
-            <version>1.10.19</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.powermock</groupId>
-            <artifactId>powermock-api-mockito</artifactId>
-            <version>1.6.5</version>
+            <artifactId>mockito-core</artifactId>
+            <version>4.11.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -378,4 +377,27 @@
         </dependency>
     </dependencies>
 
+    <profiles>
+        <profile>
+            <id>compile-sun-misc</id>
+            <activation>
+                <!-- syntax according to http://maven.apache.org/enforcer/enforcer-rules/versionRanges.html -->
+                <jdk>[9,)</jdk>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <configuration>
+                            <release combine.self="override"/>
+                            <source>${sling.java.version}</source>
+                            <target>${sling.java.version}</target>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
 </project>
diff --git a/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java b/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java
index cb20842..19504f1 100644
--- a/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java
+++ b/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java
@@ -19,7 +19,6 @@
 package org.apache.sling.xss.impl;
 
 import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -27,8 +26,8 @@
 import java.util.regex.Pattern;
 
 import org.apache.sling.xss.impl.style.CssValidator;
-import org.apache.sling.xss.impl.xml.Attribute;
 import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
+import org.apache.sling.xss.impl.xml.Attribute;
 import org.apache.sling.xss.impl.xml.Tag;
 import org.jetbrains.annotations.Nullable;
 import org.owasp.html.AttributePolicy;
@@ -37,6 +36,8 @@
 
 import com.google.common.base.Predicate;
 
+import sun.misc.Unsafe;
+
 public class AntiSamyPolicyAdapter {
     private static final String ALLOW_DYNAMIC_ATTRIBUTES = "allowDynamicAttributes";
     private static final String REMOVE_TAG_ON_INVALID_ACTION = "removeTag";
@@ -222,6 +223,7 @@
 
     private static Predicate<String> matchesPatternsOrLiterals(List<Pattern> patternList, boolean ignoreCase, List<String> literalList) {
         return new Predicate<String>() {
+            @Override
             public boolean apply(String s) {
                 // check if the string matches to the pattern or one of the literal
                 s = ignoreCase ? s.toLowerCase() : s;
@@ -254,20 +256,22 @@
     private void removeAttributeGuards() {
         try {
             Field guards = HtmlPolicyBuilder.class.getDeclaredField("ATTRIBUTE_GUARDS");
-            letMeIn(guards);
-            guards.set(null, new HashMap<>());
+
+            // although it looks distasteful, the 'sun.misc.Unsafe' approach is the only one that
+            // works with Java 8 through 17 .
+            Field f = Unsafe.class.getDeclaredField("theUnsafe");
+            f.setAccessible(true);
+            Unsafe unsafe = (Unsafe) f.get(null);
+            
+            // required to be able to get the static field base
+            unsafe.ensureClassInitialized(HtmlPolicyBuilder.class);
+            
+            Object fieldBase = unsafe.staticFieldBase(guards);
+            long fieldOffset = unsafe.staticFieldOffset(guards);
+            unsafe.putObject(fieldBase, fieldOffset, new HashMap<>());
+
         } catch (ReflectiveOperationException e) {
             throw new IllegalStateException(e);
         }
     }
-
-    private void letMeIn(Field field) throws ReflectiveOperationException {
-        if (!field.isAccessible())
-            field.setAccessible(true);
-        if ((field.getModifiers() & Modifier.FINAL) != 0) {
-            Field modifiersField = Field.class.getDeclaredField("modifiers");
-            modifiersField.setAccessible(true);
-            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
-        }
-    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
index 0855416..8fbfb96 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
@@ -19,13 +19,14 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.HashMap;
 import java.util.regex.Pattern;
 
+import org.apache.commons.lang.reflect.FieldUtils;
 import org.apache.sling.api.resource.observation.ResourceChangeListener;
 import org.apache.sling.commons.metrics.Counter;
 import org.apache.sling.commons.metrics.MetricsService;
@@ -42,7 +43,6 @@
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.osgi.framework.ServiceReference;
-import org.powermock.reflect.Whitebox;
 
 @ExtendWith(SlingContextExtension.class)
 public class XSSAPIImplTest {
@@ -130,7 +130,7 @@
 
     @ParameterizedTest
     @MethodSource("dataForValidHref")
-    public void testGetValidHrefWithoutHrefConfig(String input, String expected) {
+    public void testGetValidHrefWithoutHrefConfig(String input, String expected) throws ReflectiveOperationException {
         context.load().binaryFile("/configWithoutHref.xml", "/apps/sling/xss/configWithoutHref.xml");
         context.registerInjectActivateService(new XSSFilterImpl(), new HashMap<String, Object>(){{
             put("policyPath", "/apps/sling/xss/configWithoutHref.xml");
@@ -140,9 +140,9 @@
         ServiceReference<ResourceChangeListener> xssFilterRCL = context.bundleContext().getServiceReference(ResourceChangeListener.class);
         assertEquals("/apps/sling/xss/configWithoutHref.xml", xssFilterRCL.getProperty(ResourceChangeListener.PATHS));
         // Load AntiSamy configuration without href filter
-        XSSFilterImpl xssFilter = Whitebox.getInternalState(xssAPI, "xssFilter");
+        XSSFilterImpl xssFilter = (XSSFilterImpl) FieldUtils.getField(XSSAPIImpl.class, "xssFilter", true).get(xssAPI);
 
-        Attribute hrefAttribute = Whitebox.getInternalState(xssFilter, "hrefAttribute");
+        Attribute hrefAttribute = (Attribute) FieldUtils.getField(XSSFilterImpl.class, "hrefAttribute", true).get(xssFilter);
         assertEquals(hrefAttribute, XSSFilterImpl.DEFAULT_HREF_ATTRIBUTE);
 
         // Run same tests again to check default configuration
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
index 444501d..5cd7f44 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
@@ -22,7 +22,7 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;