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
+}