Add a filter doclet which works with the Java 11 doclet API. (#1216)

* Add a filter doclet which works with the Java 11 doclet API.

MVP with some KIs but I ran out of weekend but allows us to
bump the compiler version everywhere.

Pros:
* Kotlin
* Has potential to be project independent without being as
heavyweight as doclava
* Nifty HTML DSL :)

KI:
* HTML DSL incomplete
* Nested class paths are wrong
* No links on types in signatures

* Minor bugfixes.

* Another minor fix.
diff --git a/api-doclet/build.gradle b/api-doclet/build.gradle
index 2ada3d5..d83fffc 100644
--- a/api-doclet/build.gradle
+++ b/api-doclet/build.gradle
@@ -1,21 +1,18 @@
-description = 'Conscrypt: API Doclet'
-
-
-java {
-    toolchain {
-        // Force Java 8 for the doclet.
-        languageVersion = JavaLanguageVersion.of(8)
-    }
-    // Java 8 doclets depend on the JDK's tools.jar
-    def compilerMetadata = javaToolchains.compilerFor(toolchain).get().metadata
-    def jdkHome = compilerMetadata.getInstallationPath()
-    def toolsJar = jdkHome.file("lib/tools.jar")
-    dependencies {
-        implementation files(toolsJar)
-    }
+plugins {
+    id 'org.jetbrains.kotlin.jvm' version '2.0.0'
 }
 
-tasks.withType(Javadoc) {
-    // TODO(prb): Update doclet to Java 11.
+description = 'Conscrypt: API Doclet'
+
+kotlin {
+    jvmToolchain(11)
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
+}
+
+tasks.withType(Javadoc).configureEach {
+    // No need to javadoc the Doclet....
     enabled = false
 }
diff --git a/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java b/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java
deleted file mode 100644
index abf8339..0000000
--- a/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2010 Google Inc.
- *
- * Licensed 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.
- */
-/*
- * Originally from Doclava project at
- * https://android.googlesource.com/platform/external/doclava/+/master/src/com/google/doclava/Doclava.java
- */
-
-package org.conscrypt.doclet;
-
-import com.sun.javadoc.*;
-import com.sun.tools.doclets.standard.Standard;
-import com.sun.tools.javadoc.Main;
-import java.io.FileNotFoundException;
-import java.lang.reflect.Array;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * This Doclet filters out all classes, methods, fields, etc. that have the {@code @Internal}
- * annotation on them.
- */
-public class FilterDoclet extends com.sun.tools.doclets.standard.Standard {
-    public static void main(String[] args) throws FileNotFoundException {
-        String name = FilterDoclet.class.getName();
-        Main.execute(name, args);
-    }
-
-    public static boolean start(RootDoc rootDoc) {
-        return Standard.start((RootDoc) filterHidden(rootDoc, RootDoc.class));
-    }
-
-    /**
-     * Returns true if the given element has an @Internal annotation.
-     */
-    private static boolean hasHideAnnotation(ProgramElementDoc doc) {
-        for (AnnotationDesc ann : doc.annotations()) {
-            if (ann.annotationType().qualifiedTypeName().equals("org.conscrypt.Internal")) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if the given element is hidden.
-     */
-    private static boolean isHidden(Doc doc) {
-        // Methods, fields, constructors.
-        if (doc instanceof MemberDoc) {
-            return hasHideAnnotation((MemberDoc) doc);
-        }
-        // Classes, interfaces, enums, annotation types.
-        if (doc instanceof ClassDoc) {
-            // Check the class doc and containing class docs if this is a
-            // nested class.
-            ClassDoc current = (ClassDoc) doc;
-            do {
-                if (hasHideAnnotation(current)) {
-                    return true;
-                }
-                current = current.containingClass();
-            } while (current != null);
-        }
-        return false;
-    }
-
-    /**
-     * Filters out hidden elements.
-     */
-    private static Object filterHidden(Object o, Class<?> expected) {
-        if (o == null) {
-            return null;
-        }
-
-        Class<?> type = o.getClass();
-        if (type.getName().startsWith("com.sun.")) {
-            // TODO: Implement interfaces from superclasses, too.
-            return Proxy.newProxyInstance(
-                    type.getClassLoader(), type.getInterfaces(), new HideHandler(o));
-        } else if (o instanceof Object[]) {
-            Class<?> componentType = expected.getComponentType();
-            if (componentType == null) {
-                return o;
-            }
-
-            Object[] array = (Object[]) o;
-            List<Object> list = new ArrayList<Object>(array.length);
-            for (Object entry : array) {
-                if ((entry instanceof Doc) && isHidden((Doc) entry)) {
-                    continue;
-                }
-                list.add(filterHidden(entry, componentType));
-            }
-            return list.toArray((Object[]) Array.newInstance(componentType, list.size()));
-        } else {
-            return o;
-        }
-    }
-
-    /**
-     * Filters hidden elements.
-     */
-    private static class HideHandler implements InvocationHandler {
-        private final Object target;
-
-        public HideHandler(Object target) {
-            this.target = target;
-        }
-
-        @Override
-        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-            String methodName = method.getName();
-            if (args != null) {
-                if (methodName.equals("compareTo") || methodName.equals("equals")
-                        || methodName.equals("overrides") || methodName.equals("subclassOf")) {
-                    args[0] = unwrap(args[0]);
-                }
-            }
-
-            try {
-                return filterHidden(method.invoke(target, args), method.getReturnType());
-            } catch (InvocationTargetException e) {
-                e.printStackTrace();
-                throw e.getTargetException();
-            }
-        }
-
-        private static Object unwrap(Object proxy) {
-            if (proxy instanceof Proxy)
-                return ((HideHandler) Proxy.getInvocationHandler(proxy)).target;
-            return proxy;
-        }
-    }
-}
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt
new file mode 100644
index 0000000..811d13a
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import javax.lang.model.element.Element
+import javax.lang.model.element.TypeElement
+import kotlin.streams.toList
+
+class ClassIndex {
+    private val index = mutableMapOf<String, ClassInfo>()
+
+    private fun put(classInfo: ClassInfo) {
+        index[classInfo.qualifiedName] = classInfo
+    }
+
+    fun put(element: Element) {
+        put(ClassInfo(element as TypeElement))
+    }
+
+    fun get(qualifiedName: String) = index[qualifiedName]
+    fun contains(qualifiedName: String) = index.containsKey(qualifiedName)
+    fun find(name: String) = if (contains(name)) get(name) else findSimple(name)
+    private fun findSimple(name: String) = classes().firstOrNull { it.simpleName == name } // XXX dups
+
+    fun classes(): Collection<ClassInfo> = index.values
+
+    fun addVisible(elements: Set<Element>) {
+        elements
+            .filterIsInstance<TypeElement>()
+            .filter(Element::isVisibleType)
+            .forEach(::put)
+    }
+
+    private fun packages(): List<String> = index.values.stream()
+        .map { it.packageName }
+        .distinct()
+        .sorted()
+        .toList()
+
+    private fun classesForPackage(packageName: String) = index.values.stream()
+        .filter { it.packageName == packageName }
+        .sorted()
+        .toList()
+
+    fun generateHtml():String = html {
+        packages().forEach { packageName ->
+            div("package-section") {
+                h2("Package $packageName", "package-name")
+                ul("class-list") {
+                    classesForPackage(packageName)
+                        .forEach { c ->
+                            li {
+                                a(c.fileName, c.simpleName)
+                            }
+                        }
+
+                }
+            }
+        }
+    }
+}
+
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt
new file mode 100644
index 0000000..ffe58b5
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.TypeElement
+
+
+data class ClassInfo(val element: TypeElement) : Comparable<ClassInfo> {
+    val simpleName = element.simpleName.toString()
+    val qualifiedName = element.qualifiedName.toString()
+    val packageName = FilterDoclet.elementUtils.getPackageOf(element).qualifiedName.toString()
+    val fileName = qualifiedName.replace('.', '/') + ".html"
+
+    override fun compareTo(other: ClassInfo) = qualifiedName.compareTo(other.qualifiedName)
+
+    private fun description() = html {
+        div("class-description") {
+            compose { element.commentTree() + element.tagTree() }
+        }
+    }
+
+    private fun fields() = html {
+        val fields = element.children(Element::isVisibleField)
+        if (fields.isNotEmpty()) {
+            h2("Fields")
+            fields.forEach { field ->
+                div("member") {
+                    h4(field.simpleName.toString())
+                    compose {
+                        field.commentTree() + field.tagTree()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun nestedClasses() = html {
+        val nested = element.children(Element::isVisibleType)
+        nested.takeIf { it.isNotEmpty() }?.let {
+            h2("Nested Classes")
+            nested.forEach { cls ->
+                div("member") {
+                    h4(cls.simpleName.toString())
+                    compose {
+                        cls.commentTree() + cls.tagTree()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun method(method: ExecutableElement) = html {
+        div("member") {
+            h4(method.simpleName.toString())
+            pre(method.methodSignature(), "method-signature")
+            div("description") {
+                compose {
+                    method.commentTree()
+                }
+                val params = method.paramTags()
+                val throwns = method.throwTags()
+                val returns = if (method.isConstructor())
+                    emptyList()
+                else
+                    method.returnTag(method.returnType)
+
+                if(params.size + returns.size + throwns.size > 0) {
+                    div("params") {
+                        table("params-table") {
+                            rowGroup(params, title = "Parameters", colspan = 2) {
+                                td {text(it.first)}
+                                td {text(it.second)}
+                            }
+                            rowGroup(returns, title = "Returns", colspan = 2) {
+                                td {text(it.first)}
+                                td {text(it.second)}
+                            }
+                            rowGroup(throwns, title = "Throws", colspan = 2) {
+                                td {text(it.first)}
+                                td {text(it.second)}
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun executables(title: String, filter: (Element) -> Boolean) = html {
+        val methods = element.children(filter)
+        if (methods.isNotEmpty()) {
+            h2(title)
+            methods.forEach {
+                compose {
+                    method(it as ExecutableElement)
+                }
+            }
+        }
+    }
+
+    private fun constructors() = executables("Constructors", Element::isVisibleConstructor)
+    private fun methods() = executables("Public Methods", Element::isVisibleMethod)
+
+    fun generateHtml() = html {
+        div("package-name") { text("Package: $packageName") }
+        h1(simpleName)
+        pre(element.signature(), "class-signature")
+
+        compose {
+            description() +
+                    fields() +
+                    constructors() +
+                    methods() +
+                    nestedClasses()
+        }
+    }
+}
+
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt
new file mode 100644
index 0000000..2b82f01
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import org.conscrypt.doclet.FilterDoclet.Companion.baseUrl
+import com.sun.source.doctree.DocCommentTree
+import com.sun.source.doctree.DocTree
+import com.sun.source.doctree.EndElementTree
+import com.sun.source.doctree.LinkTree
+import com.sun.source.doctree.LiteralTree
+import com.sun.source.doctree.ParamTree
+import com.sun.source.doctree.ReturnTree
+import com.sun.source.doctree.SeeTree
+import com.sun.source.doctree.StartElementTree
+import com.sun.source.doctree.TextTree
+import com.sun.source.doctree.ThrowsTree
+import org.conscrypt.doclet.FilterDoclet.Companion.classIndex
+import org.conscrypt.doclet.FilterDoclet.Companion.docTrees
+import javax.lang.model.element.Element
+import javax.lang.model.type.TypeMirror
+
+fun renderDocTreeList(treeList: List<DocTree>):String =
+    treeList.joinToString("\n", transform = ::renderDocTree)
+
+fun renderDocTree(docTree: DocTree): String = when (docTree) {
+    is TextTree -> docTree.body
+    is LinkTree -> {
+        val reference = docTree.reference.toString()
+        val label = if (docTree.label.isEmpty()) {
+            reference
+        } else {
+            renderDocTreeList(docTree.label)
+        }
+        createLink(reference, label)
+    }
+    is StartElementTree, is EndElementTree -> docTree.toString()
+    is LiteralTree -> "<code>${docTree.body}</code>"
+    else -> error("[${docTree.javaClass} / ${docTree.kind} --- ${docTree}]")
+}
+
+fun createLink(reference: String, label: String) = html {
+    val parts = reference.split('#')
+    val className = parts[0]
+    val anchor = if (parts.size > 1) "#${parts[1]}" else ""
+    val classInfo = classIndex.find(className)
+    val href = if (classInfo != null)
+        "${classInfo.simpleName}.html$anchor"
+    else
+        "$baseUrl${className.replace('.', '/')}.html$anchor"
+
+    a(href, label)
+}
+
+fun renderBlockTagList(tagList: List<DocTree>): String =
+    tagList.joinToString("\n", transform = ::renderBlockTag)
+
+fun renderBlockTag(tag: DocTree) = when (tag) {
+    is ParamTree, is ReturnTree, is ThrowsTree -> error("Unexpected block tag: $tag")
+    is SeeTree -> html {
+        br()
+        p {
+            strong("See: ")
+            text(renderDocTreeList(tag.reference))
+        }
+    }
+    else -> tag.toString()
+}
+
+inline fun <reified T> Element.filterTags() =
+    docTree()?.blockTags?.filterIsInstance<T>() ?: emptyList()
+
+fun Element.paramTags() = filterTags<ParamTree>()
+    .map { it.name.toString() to renderDocTreeList(it.description) }
+    .toList()
+
+
+fun Element.returnTag(returnType: TypeMirror): List<Pair<String, String>> {
+    val list = mutableListOf<Pair<String, String>>()
+    val descriptions  = filterTags<ReturnTree>()
+        .map {  renderDocTreeList(it.description) }
+        .singleOrNull()
+
+    if (descriptions != null) {
+        list.add(returnType.toString() to descriptions)
+    }
+    return list
+}
+
+fun Element.throwTags() = filterTags<ThrowsTree>()
+    .map { it.exceptionName.toString() to renderDocTreeList(it.description) }
+    .toList()
+
+fun Element.docTree(): DocCommentTree? {
+    return docTrees.getDocCommentTree(this)
+}
+
+fun Element.commentTree() = html {
+    text {
+        docTree()?.let { renderDocTreeList(it.fullBody) } ?: ""
+    }
+}
+
+fun Element.tagTree() = html {
+    text {
+        docTree()?.let { renderBlockTagList(it.blockTags) } ?: ""
+    }
+}
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt
new file mode 100644
index 0000000..a9fcd00
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import com.sun.source.doctree.UnknownBlockTagTree
+import java.util.Locale
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.TypeMirror
+
+fun Element.isType() = isClass() || isInterface() || isEnum()
+fun Element.isClass() = this is TypeElement && kind == ElementKind.CLASS
+fun Element.isEnum() = this is TypeElement && kind == ElementKind.ENUM
+fun Element.isInterface() = this is TypeElement && kind == ElementKind.INTERFACE
+fun Element.isExecutable() = this is ExecutableElement
+fun Element.isField() = this is VariableElement
+
+fun Element.isVisibleType() = isType() && isVisible()
+fun Element.isVisibleMethod() = isExecutable() && isVisible() && kind == ElementKind.METHOD
+fun Element.isVisibleConstructor() = isExecutable() && isVisible() && kind == ElementKind.CONSTRUCTOR
+fun Element.isVisibleField() = isField() && isVisible()
+fun Element.isPublic() = modifiers.contains(Modifier.PUBLIC)
+fun Element.isPrivate() = !isPublic() // Ignore protected for now :)
+fun Element.isHidden() = isPrivate() || hasHideMarker() || parentIsHidden()
+fun Element.isVisible() = !isHidden()
+fun Element.hasHideMarker() = hasAnnotation("org.conscrypt.Internal") || hasHideTag()
+fun Element.children(filterFunction: (Element) -> Boolean) = enclosedElements
+    .filter(filterFunction)
+    .toList()
+
+fun Element.parentIsHidden(): Boolean
+        = if (enclosingElement.isType()) enclosingElement.isHidden() else false
+
+fun Element.hasAnnotation(annotationName: String): Boolean = annotationMirrors
+    .map { it.annotationType.toString() }
+    .any { it == annotationName }
+
+
+fun Element.hasHideTag(): Boolean {
+    return docTree()?.blockTags?.any {
+        tag -> tag is UnknownBlockTagTree && tag.tagName == "hide"
+    } ?: false
+}
+
+fun ExecutableElement.isConstructor() = kind == ElementKind.CONSTRUCTOR
+fun ExecutableElement.name() = if (isConstructor()) parentName() else simpleName.toString()
+fun ExecutableElement.parentName() = enclosingElement.simpleName.toString()
+
+fun ExecutableElement.methodSignature(): String {
+    val modifiers = modifiers.joinToString(" ")
+    val returnType = if (isConstructor()) "" else "${formatType(returnType)} "
+
+    val typeParams = typeParameters.takeIf { it.isNotEmpty() }
+        ?.joinToString(separator = ", ", prefix = "<", postfix = ">") {
+            it.asType().toString() } ?: ""
+
+    val parameters = parameters.joinToString(", ") { param ->
+        "${formatType(param.asType())} ${param.simpleName}"
+    }
+
+    val exceptions = thrownTypes
+        .joinToString(", ")
+        .prefixIfNotEmpty(" throws ")
+    return "$modifiers $typeParams$returnType${simpleName}($parameters)$exceptions"
+}
+
+fun formatType(typeMirror: TypeMirror): String {
+    return if (typeMirror.kind.isPrimitive) {
+        typeMirror.toString()
+    } else {
+        typeMirror.toString()
+            .split('.')
+            .last()
+    }
+}
+
+fun TypeElement.signature(): String {
+    val modifiers = modifiers.joinToString(" ")
+    val kind = this.kind.toString().lowercase(Locale.getDefault())
+
+    val superName = superDisplayName(superclass)
+
+    val interfaces = interfaces
+        .joinToString(", ")
+        .prefixIfNotEmpty(" implements ")
+
+    return "$modifiers $kind $simpleName$superName$interfaces"
+}
+
+fun superDisplayName(mirror: TypeMirror): String {
+    return when (mirror.toString()) {
+        "none", "java.lang.Object" -> ""
+        else -> " extends $mirror "
+    }
+}
+
+private fun String.prefixIfNotEmpty(prefix: String): String
+        = if (isNotEmpty()) prefix + this else this
+
+private fun String.suffixIfNotEmpty(prefix: String): String
+        = if (isNotEmpty()) this + prefix else this
\ No newline at end of file
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt
new file mode 100644
index 0000000..77db33f
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import com.sun.source.util.DocTrees
+import jdk.javadoc.doclet.Doclet
+import jdk.javadoc.doclet.DocletEnvironment
+import jdk.javadoc.doclet.Reporter
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.Locale
+import javax.lang.model.SourceVersion
+import javax.lang.model.util.Elements
+import javax.lang.model.util.Types
+
+class FilterDoclet : Doclet {
+    companion object {
+        lateinit var docTrees: DocTrees
+        lateinit var elementUtils: Elements
+        lateinit var typeUtils: Types
+        lateinit var outputPath: Path
+        var baseUrl: String = "https://docs.oracle.com/javase/8/docs/api/"
+        val CSS_FILENAME = "styles.css"
+        var outputDir = "."
+        var docTitle = "DTITLE"
+        var windowTitle = "WTITLE"
+        var noTimestamp: Boolean = false
+        val classIndex = ClassIndex()
+    }
+
+    override fun init(locale: Locale?, reporter: Reporter?) = Unit // TODO
+    override fun getName() = "FilterDoclet"
+    override fun getSupportedSourceVersion() = SourceVersion.latest()
+
+    override fun run(environment: DocletEnvironment): Boolean {
+        docTrees = environment.docTrees
+        elementUtils = environment.elementUtils
+        typeUtils = environment.typeUtils
+        outputPath = Paths.get(outputDir)
+        Files.createDirectories(outputPath)
+
+        classIndex.addVisible(environment.includedElements)
+
+        try {
+            generateClassFiles()
+            generateIndex()
+            return true
+        } catch (e: Exception) {
+            System.err.println("Error generating documentation: " + e.message)
+            e.printStackTrace()
+            return false
+        }
+    }
+
+    private fun generateClassFiles() = classIndex.classes().forEach(::generateClassFile)
+
+    private fun generateIndex() {
+        val indexPath = outputPath.resolve("index.html")
+
+        html {
+            body(
+                title = docTitle,
+                stylesheet = relativePath(indexPath, CSS_FILENAME),
+            ) {
+                div("index-container") {
+                    h1(docTitle, "index-title")
+                    compose {
+                        classIndex.generateHtml()
+                    }
+                }
+            }
+        }.let {
+            Files.newBufferedWriter(indexPath).use { writer ->
+                writer.write(it)
+            }
+        }
+    }
+
+    private fun generateClassFile(classInfo: ClassInfo) {
+        val classFilePath = outputPath.resolve(classInfo.fileName)
+        Files.createDirectories(classFilePath.parent)
+        val simpleName = classInfo.simpleName
+
+        html {
+            body(
+                title = "$simpleName - conscrypt-openjdk API",
+                stylesheet = relativePath(classFilePath, CSS_FILENAME),
+            ) {
+                compose {
+                    classInfo.generateHtml()
+                }
+            }
+        }.let {
+            Files.newBufferedWriter(classFilePath).use { writer ->
+                writer.write(it)
+            }
+        }
+    }
+
+    private fun relativePath(from: Path, to: String): String {
+        val fromDir = from.parent
+        val toPath = Paths.get(outputDir).resolve(to)
+
+        if (fromDir == null) {
+            return to
+        }
+
+        val relativePath = fromDir.relativize(toPath)
+        return relativePath.toString().replace('\\', '/')
+    }
+
+    override fun getSupportedOptions(): Set<Doclet.Option> {
+        return setOf<Doclet.Option>(
+            StringOption(
+                "-d",
+                "<directory>",
+                "Destination directory for output files"
+            ) { d: String -> outputDir = d },
+            StringOption(
+                "-doctitle",
+                "<title>",
+                "Document title"
+            ) { t: String -> docTitle = t },
+            StringOption(
+                "-windowtitle",
+                "<title>",
+                "Window title"
+            ) { w: String -> windowTitle = w },
+            StringOption(
+                "-link",
+                "<link>",
+                "Link"
+            ) { l: String -> baseUrl = l },
+            BooleanOption(
+                "-notimestamp",
+                "Something"
+            ) { noTimestamp = true })
+    }
+}
\ No newline at end of file
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt
new file mode 100644
index 0000000..0c2758b
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+private typealias Block = HtmlBuilder.() -> Unit
+private fun Block.render(): String = HtmlBuilder().apply(this).toString()
+
+class HtmlBuilder {
+    private val content = StringBuilder()
+    override fun toString() = content.toString()
+
+    fun text(fragment: () -> String): StringBuilder = text(fragment())
+    fun text(text: String): StringBuilder = content.append(text)
+    fun compose(fragment: () -> String) {
+        content.append(fragment())
+    }
+
+    fun body(title: String, stylesheet: String, content: Block) {
+        text("""
+             <!DOCTYPE html>
+             <html><head>
+               <link rel="stylesheet" type="text/css" href="$stylesheet">
+               <meta charset="UTF-8">
+               <title>$title</title>
+             </head>
+             <body>""".trimIndent() +
+             content.render() +
+             "</body></html>")
+    }
+
+    private fun tagBlock(
+        tag: String, cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block)
+    {
+        content.append("\n<$tag")
+        cssClass?.let { content.append(""" class="$it"""") }
+        colspan?.let { content.append(""" colspan="$it"""") }
+        id?.let { content.append(""" id="$it"""") }
+        content.append(">")
+        content.append(block.render())
+        content.append("</$tag>\n")
+    }
+
+    fun div(cssClass: String? = null, id: String? = null, block: Block) =
+        tagBlock("div", cssClass = cssClass, colspan = null, id, block)
+    fun ul(cssClass: String? = null, id: String? = null, block: Block) =
+        tagBlock("ul", cssClass = cssClass, colspan = null, id, block)
+    fun ol(cssClass: String? = null, id: String? = null, block: Block) =
+        tagBlock("ol", cssClass = cssClass, colspan = null, id, block)
+    fun table(cssClass: String? = null, id: String? = null, block: Block) =
+        tagBlock("table", cssClass = cssClass, colspan = null, id, block)
+    fun tr(cssClass: String? = null, id: String? = null, block: Block) =
+        tagBlock("tr", cssClass = cssClass, colspan = null, id, block)
+    fun th(cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block) =
+        tagBlock("th", cssClass, colspan, id, block)
+    fun td(cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block) =
+        tagBlock("td", cssClass, colspan, id, block)
+
+    private fun tagValue(tag: String, value: String, cssClass: String? = null) {
+        val classText = cssClass?.let { """ class="$it"""" } ?: ""
+        content.append("<$tag$classText>$value</$tag>\n")
+    }
+
+    fun h1(heading: String, cssClass: String? = null) = tagValue("h1", heading, cssClass)
+    fun h1(cssClass: String? = null, block: Block) = h1(block.render(), cssClass)
+    fun h2(heading: String, cssClass: String? = null) = tagValue("h2", heading, cssClass)
+    fun h2(cssClass: String? = null, block: Block) = h2(block.render(), cssClass)
+    fun h3(heading: String, cssClass: String? = null) = tagValue("h3", heading, cssClass)
+    fun h3(cssClass: String? = null, block: Block) = h2(block.render(), cssClass)
+    fun h4(heading: String, cssClass: String? = null) = tagValue("h4", heading, cssClass)
+    fun h4(cssClass: String? = null, block: Block) = h2(block.render(), cssClass)
+    fun h5(heading: String, cssClass: String? = null) = tagValue("h5", heading, cssClass)
+    fun h5(cssClass: String? = null, block: Block) = h2(block.render(), cssClass)
+
+    fun p(text: String, cssClass: String? = null) = tagValue("p", text, cssClass)
+    fun p(cssClass: String? = null, block: Block) = p(block.render(), cssClass)
+    fun b(text: String, cssClass: String? = null) = tagValue("b", text, cssClass)
+    fun b(cssClass: String? = null, block: Block) = b(block.render(), cssClass)
+    fun pre(text: String, cssClass: String? = null) = tagValue("pre", text, cssClass)
+    fun pre(cssClass: String? = null, block: Block) = pre(block.render(), cssClass)
+    fun code(text: String, cssClass: String? = null) = tagValue("code", text, cssClass)
+    fun code(cssClass: String? = null, block: Block) = code(block.render(), cssClass)
+    fun strong(text: String, cssClass: String? = null) = tagValue("strong", text, cssClass)
+    fun strong(cssClass: String? = null, block: Block) = strong(block.render(), cssClass)
+
+    fun br() = content.append("<br/>\n")
+    fun a(href: String, label: String) {
+        content.append("""<a href="$href">$label</a>""")
+    }
+    fun a(href: String, block: Block) = a(href, block.render())
+    fun a(href: String) = a(href, href)
+
+    fun li(text: String, cssClass: String? = null) = tagValue("li", text, cssClass)
+    fun li(cssClass: String? = null, block: Block) = li(block.render(), cssClass)
+
+    fun <T> items(collection: Iterable<T>, cssClass: String? = null,
+                  transform: HtmlBuilder.(T) -> Unit = { text(it.toString()) }) {
+        collection.forEach {
+            li(cssClass = cssClass) { transform(it) }
+        }
+    }
+
+    fun <T> row(item: T, rowClass: String? = null, cellClass: String? = null,
+                span: Int? = null,
+                transform: HtmlBuilder.(T) -> Unit = { td {it.toString() } }) {
+        tr(cssClass = rowClass) {
+            transform(item)
+        }
+    }
+    fun <T> rowGroup(rows: Collection<T>, title: String? = null, rowClass: String? = null, cellClass: String? = null,
+                 colspan: Int? = null,
+                transform: HtmlBuilder.(T) -> Unit) {
+        if(rows.isNotEmpty()) {
+            title?.let {
+                tr {
+                    th(colspan = colspan) {
+                        strong(it)
+                    }
+                }
+            }
+            rows.forEach {
+                tr {
+                    transform(it)
+                }
+            }
+        }
+    }
+}
+
+fun html(block: Block) = block.render()
+
+fun exampleSubfunction() = html {
+    h1("Headings from exampleSubfunction")
+    listOf("one", "two", "three").forEach {
+        h1(it)
+    }
+}
+
+fun example() = html {
+    val fruits = listOf("Apple", "Banana", "Cherry")
+    body(
+        stylesheet = "path/to/stylesheet.css",
+        title = "Page Title"
+    ) {
+        div(cssClass = "example-class") {
+            text {
+                "This is a div"
+            }
+            h1 {
+                text("Heading1a")
+            }
+            h2 {
+                a("www.google.com", "Heading with a link")
+            }
+            h3("Heading with CSS class", "my-class")
+            h2("h2", "my-class")
+            p("Hello world")
+            compose {
+                exampleSubfunction()
+            }
+            br()
+            a("www.google.com") {
+                text("a link with ")
+                b("bold")
+                text(" text.")
+            }
+
+        }
+        h1("Lists")
+
+        h2("Unordered list:")
+        ul {
+            li("First item")
+            li("Second item")
+            li {
+                text { "Complex item with " }
+                b { text { "bold text" } }
+            }
+            ul {
+                li("First nested item")
+                li("Second nested item")
+            }
+        }
+
+        h2("Ordered list:")
+        ol {
+            li("First item")
+            li("Second item")
+            li {
+                text { "Item with a " }
+                a(href = "https://example.com") { text { "link" } }
+            }
+        }
+        h2("List item iteration")
+        ul {
+            // Default
+            items(fruits)
+            // Text transform
+            items(fruits) {
+                text("I like ${it}.")
+            }
+            // HTML transform with a CSS class
+            items(fruits, "myclass") {
+                a("www.google.com") {
+                    b(it)
+                }
+            }
+        }
+        ol("ol-class") {
+            items((1..5).asIterable()) {
+                text("Item $it")
+            }
+        }
+    }
+    val data1 = listOf(1, 2)
+    val data2 = "3" to "4"
+    val data3 = listOf(
+        "tag1" to "Some value",
+        "tag2" to "Next Value",
+        "tag3" to "Another value"
+    )
+
+    table("table-class") {
+        tr {
+            th {
+                text("First column")
+            }
+            th {
+                text("Second column")
+
+            }
+        }
+        tr("tr-class") {
+            td("td-class") {
+                text("Data 1")
+            }
+            td(colspan = 2, id = "foo") {
+                    text("Data 2")
+            }
+        }
+        tr {
+            td() {
+                text("Data 3")
+            }
+        }
+        row(data1, "c1") {
+            a(href="www.google.com") { text("$it") }
+        }
+        row(data2) { p:Pair<String, String> ->
+            td {
+                text(p.first)
+            }
+            td {
+                text(p.second)
+            }
+
+        }
+        rowGroup(data3, title = "Row Group", colspan=2) { p: Pair<String, String> ->
+            td {
+                text(p.first)
+            }
+            td {
+                text(p.second)
+            }
+        }
+    }
+}
+
+fun main() {
+    example().let(::println)
+}
diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt
new file mode 100644
index 0000000..3fc57b0
--- /dev/null
+++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed 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.conscrypt.doclet
+
+import jdk.javadoc.doclet.Doclet.Option
+import java.util.function.Consumer
+
+abstract class BaseOption(private val name: String) : Option {
+    override  fun getKind() = Option.Kind.STANDARD
+    override fun getNames(): List<String> = listOf(name)
+}
+
+class StringOption(name: String,
+                   private val parameters: String,
+                   private val description: String,
+                   private val action: Consumer<String>
+) : BaseOption(name) {
+    override fun getArgumentCount() = 1
+    override fun getDescription(): String = description
+    override fun getParameters(): String = parameters
+
+    override fun process(option: String, arguments: MutableList<String>): Boolean {
+        action.accept(arguments[0])
+        return true
+    }
+}
+
+class BooleanOption(name: String,
+                    private val description: String,
+                    private val action: Runnable): BaseOption(name) {
+    override fun getArgumentCount() = 0
+    override fun getDescription(): String = description
+    override fun getParameters(): String = ""
+
+    override fun process(option: String, arguments: MutableList<String>): Boolean {
+        action.run()
+        return true
+    }
+}
diff --git a/api-doclet/src/main/resources/styles.css b/api-doclet/src/main/resources/styles.css
new file mode 100644
index 0000000..262f64e
--- /dev/null
+++ b/api-doclet/src/main/resources/styles.css
@@ -0,0 +1,147 @@
+body {
+    font-family: Arial, sans-serif;
+    line-height: 1.2;
+    color: #333;
+    /* max-width: 800px; */
+    margin: 0 auto;
+    padding: 10px;
+}
+.method {
+    margin-bottom: 30px;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 20px;
+}
+.body h3 {
+    font-size: 24px;
+    underline: true
+}
+.method-name {
+    color: #2c3e50;
+    font-size: 24px;
+    margin-bottom: 10px;
+}
+.method-signature .class-signature {
+    background-color: #f7f9fa;
+    border: 1px solid #e1e4e8;
+    border-radius: 3px;
+    padding: 12px;
+    font-family: monospace;
+    font-size: 14px;
+    overflow-x: auto;
+}
+.description {
+    margin: 15px 0;
+    padding: 10px;
+    background-color: #f8f8f8;
+}
+.params {
+    margin-top: 20px;
+}
+.params h5 {
+    color: #2c3e50;
+    font-size: 16px;
+}
+.params-table {
+    border-collapse: collapse;
+}
+.params-table th, .params-table td {
+    border: 1px solid #ddd;
+    padding: 12px;
+    text-align: left;
+}
+.params-table th {
+    background-color: #f2f2f2;
+    font-weight: bold;
+}
+.params-table tr:nth-child(even) {
+    background-color: #f8f8f8;
+}
+.constructor {
+    margin-bottom: 30px;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 20px;
+}
+.constructor-name {
+    color: #2c3e50;
+    font-size: 24px;
+    margin-bottom: 10px;
+}
+.constructor-signature {
+    background-color: #f7f9fa;
+    border: 1px solid #e1e4e8;
+    border-radius: 3px;
+    padding: 10px;
+    font-family: monospace;
+    font-size: 14px;
+    overflow-x: auto;
+}
+/* Index page styles */
+.index-container {
+    margin: 0 auto;
+    padding: 20px;
+}
+.index-title {
+    color: #2c3e50;
+    font-size: 32px;
+    margin-bottom: 20px;
+    border-bottom: 2px solid #3498db;
+    padding-bottom: 10px;
+}
+.package-section {
+    margin-bottom: 30px;
+}
+.package-name {
+    color: #2c3e50;
+    font-size: 12px;
+    margin-bottom: 10px;
+    padding: 10px;
+}
+.class-list {
+    list-style-type: none;
+    padding-left: 20px;
+}
+.class-list li {
+    margin-bottom: 5px;
+}
+.class-list a {
+    color: #3498db;
+    text-decoration: none;
+}
+.class-list a:hover {
+    text-decoration: underline;
+}
+.header {
+    font-size: 28px;
+    color: #2c3e50;
+    margin-bottom: 20px;
+}
+
+.class-description {
+    margin: 20px 0;
+    padding: 15px;
+    background-color: #f8f9fa;
+    font-size: 16px;
+    line-height: 1.6;
+}
+
+.class-description p {
+    margin-bottom: 10px;
+}
+
+.class-description code {
+    background-color: #e9ecef;
+    padding: 2px 4px;
+    border-radius: 4px;
+    font-family: monospace;
+}
+
+.package-name {
+    font-family: monospace;
+    font-size: 14px;
+    color: #6c757d;
+    background-color: #f1f3f5;
+    padding: 5px 10px;
+    border-radius: 4px;
+    margin-bottom: 20px;
+    display: inline-block;
+}
diff --git a/build.gradle b/build.gradle
index 1a8bc5b..1f54bb9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,8 +3,7 @@
 
 buildscript {
     ext.android_tools = 'com.android.tools.build:gradle:7.4.0'
-    ext.errorproneVersion = '2.4.0'
-    ext.errorproneJavacVersion = '9+181-r4173-1'
+    ext.errorproneVersion = '2.31.0'
     repositories {
         google()
         mavenCentral()
@@ -20,7 +19,7 @@
     // Add dependency for build script so we can access Git from our
     // build script.
     id 'org.ajoberstar.grgit' version '5.2.2'
-    id 'net.ltgt.errorprone' version '1.3.0'
+    id 'net.ltgt.errorprone' version '4.0.0'
     id "com.google.osdetector" version "1.7.3"
     id "biz.aQute.bnd.builder" version "6.4.0" apply false
 }
@@ -139,7 +138,6 @@
 
     dependencies {
         errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
-        errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion")
     }
 
     tasks.register("generateProperties", WriteProperties) {
@@ -156,9 +154,7 @@
     if (!androidProject) {
         java {
             toolchain {
-                // Compile with a real JDK 8 so we don't end up with accidental dependencies
-                // on Java 11 bootclasspath, e.g. ByteBuffer.flip().
-                languageVersion = JavaLanguageVersion.of(8)
+                languageVersion = JavaLanguageVersion.of(11)
             }
         }
 
@@ -166,6 +162,8 @@
             t.configure {
                 options.compilerArgs += ["-Xlint:all", "-Xlint:-options", '-Xmaxwarns', '9999999']
                 options.encoding = "UTF-8"
+                options.release = 8
+
                 if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) {
                     options.compilerArgs += ["-Werror"]
                 }
@@ -190,14 +188,7 @@
 
         javadoc.options {
             encoding = 'UTF-8'
-            links 'https://docs.oracle.com/javase/8/docs/api/'
-        }
-
-        // All non-Android projects build with Java 8, so disable doclint as it's noisy.
-        allprojects {
-            tasks.withType(Javadoc) {
-                options.addStringOption('Xdoclint:none', '-quiet')
-            }
+            links 'https://docs.oracle.com/en/java/javase/21/docs/api/java.base/'
         }
 
         tasks.register("javadocJar", Jar) {
diff --git a/openjdk/build.gradle b/openjdk/build.gradle
index cda93c3..2d8b4cf 100644
--- a/openjdk/build.gradle
+++ b/openjdk/build.gradle
@@ -129,7 +129,6 @@
 }
 
 sourceSets {
-
     main {
         java {
             srcDirs += "${rootDir}/common/src/main/java"
@@ -346,9 +345,17 @@
 
 javadoc {
     dependsOn(configurations.publicApiDocs)
-    // TODO(prb): Update doclet to Java 11.
-    // options.doclet = "org.conscrypt.doclet.FilterDoclet"
-    // options.docletpath = configurations.publicApiDocs.files as List
+    options.showFromPublic()
+    options.doclet = "org.conscrypt.doclet.FilterDoclet"
+    options.docletpath = configurations.publicApiDocs.files as List
+    failOnError false
+
+    doLast {
+        copy {
+            from "$rootDir/api-doclet/src/main/resources/styles.css"
+            into "$buildDir/docs/javadoc"
+        }
+    }
 }
 
 def jniIncludeDir() {
diff --git a/openjdk/src/main/java/org/conscrypt/Platform.java b/openjdk/src/main/java/org/conscrypt/Platform.java
index a4bcc74..f5770ca 100644
--- a/openjdk/src/main/java/org/conscrypt/Platform.java
+++ b/openjdk/src/main/java/org/conscrypt/Platform.java
@@ -80,7 +80,6 @@
 import javax.net.ssl.X509TrustManager;
 import org.conscrypt.ct.CTLogStore;
 import org.conscrypt.ct.CTPolicy;
-import sun.security.x509.AlgorithmId;
 
 /**
  * Platform-specific methods for OpenJDK.
@@ -539,13 +538,26 @@
     @SuppressWarnings("unused")
     static String oidToAlgorithmName(String oid) {
         try {
-            return AlgorithmId.get(oid).getName();
-        } catch (Exception e) {
-            return oid;
-        } catch (IllegalAccessError e) {
-            // This can happen under JPMS because AlgorithmId isn't exported by java.base
-            return oid;
+            Class<?> algorithmIdClass = Class.forName("sun.security.x509.AlgorithmId");
+            Method getMethod = algorithmIdClass.getDeclaredMethod("get", String.class);
+            getMethod.setAccessible(true);
+            Method getNameMethod = algorithmIdClass.getDeclaredMethod("getName");
+            getNameMethod.setAccessible(true);
+
+            Object algIdObj = getMethod.invoke(null, oid);
+            return (String) getNameMethod.invoke(algIdObj);
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof RuntimeException) {
+                throw(RuntimeException) cause;
+            } else if (cause instanceof Error) {
+                throw(Error) cause;
+            }
+            throw new RuntimeException(e);
+        } catch (Exception ignored) {
+            //Ignored
         }
+        return oid;
     }
 
     /*
diff --git a/testing/build.gradle b/testing/build.gradle
index 37969bc..fafd5fa 100644
--- a/testing/build.gradle
+++ b/testing/build.gradle
@@ -24,3 +24,8 @@
             libraries.bouncycastle_provider,
             libraries.junit
 }
+
+// No public methods here.
+tasks.withType(Javadoc).configureEach {
+    enabled = false
+}