diff options
Diffstat (limited to 'plugins/base/src/main/kotlin/resolvers')
21 files changed, 387 insertions, 453 deletions
diff --git a/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProvider.kt new file mode 100644 index 00000000..5e22206b --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProvider.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext + +open class DefaultExternalLocationProvider( + val externalDocumentationInfo: ExternalDocumentationInfo, + val extension: String, + val dokkaContext: DokkaContext +) : ExternalLocationProvider { + override fun resolve(dri: DRI): String? { // TODO: classes without packages? + val docURL = externalDocumentationInfo.documentationURL.toString().removeSuffix("/") + "/" + val classNamesChecked = dri.classNames ?: return "$docURL${dri.packageName ?: ""}/index$extension" + val classLink = (listOfNotNull(dri.packageName) + classNamesChecked.split('.')) + .joinToString("/", transform = ::identifierToFilename) + + val callableChecked = dri.callable ?: return "$docURL$classLink/index$extension" + return "$docURL$classLink/" + identifierToFilename(callableChecked.name) + extension + } +} diff --git a/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProviderFactory.kt new file mode 100644 index 00000000..a6e09bac --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/DefaultExternalLocationProviderFactory.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.plugability.DokkaContext + +class DefaultExternalLocationProviderFactory(val context: DokkaContext) : + ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(docInfo: ExternalDocumentationInfo): ExternalLocationProvider? = + when (docInfo.packageList.linkFormat) { + RecognizedLinkFormat.KotlinWebsiteHtml, + RecognizedLinkFormat.DokkaOldHtml, + RecognizedLinkFormat.DokkaHtml -> DefaultExternalLocationProvider(docInfo, ".html", context) + RecognizedLinkFormat.DokkaGFM, + RecognizedLinkFormat.DokkaJekyll -> DefaultExternalLocationProvider(docInfo, ".md", context) + else -> null + } + } + )
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt deleted file mode 100644 index ff9186f7..00000000 --- a/plugins/base/src/main/kotlin/resolvers/external/DokkaExternalLocationProviderFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.jetbrains.dokka.base.resolvers.external - -import org.jetbrains.dokka.base.resolvers.local.identifierToFilename -import org.jetbrains.dokka.links.DRI - - -class DokkaExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( - object : ExternalLocationProviderFactory { - override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = - when (param) { - "kotlin-website-html", "html" -> DokkaExternalLocationProvider(param, ".html") - "markdown" -> DokkaExternalLocationProvider(param, ".md") - else -> null - } - } -) - -class DokkaExternalLocationProvider(override val param: String, val extension: String) : ExternalLocationProvider { - - override fun DRI.toLocation(): String { // TODO: classes without packages? - - val classNamesChecked = classNames ?: return "${packageName ?: ""}/index$extension" - - val classLink = (listOfNotNull(packageName) + classNamesChecked.split('.')).joinToString( - "/", - transform = ::identifierToFilename - ) - - val callableChecked = callable ?: return "$classLink/index$extension" - - return "$classLink/${identifierToFilename( - callableChecked.name - )}$extension" - } -} diff --git a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProvider.kt new file mode 100644 index 00000000..51df1504 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProvider.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.links.DRI + +interface ExternalLocationProvider { + fun resolve(dri: DRI): String? +} diff --git a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt index 83de9911..10d9fffa 100644 --- a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt +++ b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactory.kt @@ -1,26 +1,7 @@ package org.jetbrains.dokka.base.resolvers.external -import org.jetbrains.dokka.links.DRI -import java.util.concurrent.ConcurrentHashMap - - -interface ExternalLocationProvider { - - val param: String - fun DRI.toLocation(): String -} +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo interface ExternalLocationProviderFactory { - - fun getExternalLocationProvider(param: String): ExternalLocationProvider? -} - -class ExternalLocationProviderFactoryWithCache(val ext: ExternalLocationProviderFactory) : ExternalLocationProviderFactory { - - private val locationProviders = ConcurrentHashMap<String, CacheWrapper>() - - override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = - locationProviders.getOrPut(param) { CacheWrapper(ext.getExternalLocationProvider(param)) }.provider -} - -private class CacheWrapper(val provider: ExternalLocationProvider?)
\ No newline at end of file + fun getExternalLocationProvider(docInfo: ExternalDocumentationInfo): ExternalLocationProvider? +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactoryWithCache.kt b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactoryWithCache.kt new file mode 100644 index 00000000..4fa1f391 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/ExternalLocationProviderFactoryWithCache.kt @@ -0,0 +1,17 @@ +package org.jetbrains.dokka.base.resolvers.external + +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import java.util.concurrent.ConcurrentHashMap + +class ExternalLocationProviderFactoryWithCache(val ext: ExternalLocationProviderFactory) : + ExternalLocationProviderFactory { + + private val locationProviders = ConcurrentHashMap<ExternalDocumentationInfo, CacheWrapper>() + + override fun getExternalLocationProvider(docInfo: ExternalDocumentationInfo): ExternalLocationProvider? = + locationProviders.getOrPut(docInfo) { CacheWrapper(ext.getExternalLocationProvider(docInfo)) }.provider + + private class CacheWrapper(val provider: ExternalLocationProvider?) +} + diff --git a/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt deleted file mode 100644 index c52c9bbb..00000000 --- a/plugins/base/src/main/kotlin/resolvers/external/JavadocExternalLocationProviderFactory.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.jetbrains.dokka.base.resolvers.external - -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.utilities.htmlEscape - -class JavadocExternalLocationProviderFactory : ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( - object : ExternalLocationProviderFactory { - override fun getExternalLocationProvider(param: String): ExternalLocationProvider? = - when(param) { - "javadoc1" -> JavadocExternalLocationProvider(param, "()", ", ") // Covers JDK 1 - 7 - "javadoc8" -> JavadocExternalLocationProvider(param, "--", "-") // Covers JDK 8 - 9 - "javadoc10" -> JavadocExternalLocationProvider(param, "()", ",") // Covers JDK 10 - else -> null - } - } -) - -class JavadocExternalLocationProvider(override val param: String, val brackets: String, val separator: String) : ExternalLocationProvider { - - override fun DRI.toLocation(): String { - - val packageLink = packageName?.replace(".", "/") - if (classNames == null) { - return "$packageLink/package-summary.html".htmlEscape() - } - val classLink = if (packageLink == null) "$classNames.html" else "$packageLink/$classNames.html" - val callableChecked = callable ?: return classLink.htmlEscape() - - val callableLink = "$classLink#" + - callableChecked.name + - "${brackets.first()}" + - callableChecked.params.joinToString(separator) + - "${brackets.last()}" - - return callableLink.htmlEscape() - } -}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProvider.kt new file mode 100644 index 00000000..d42b5b5c --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProvider.kt @@ -0,0 +1,31 @@ +package org.jetbrains.dokka.base.resolvers.external.javadoc + +import org.jetbrains.dokka.base.resolvers.external.DefaultExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.utilities.htmlEscape + +class JavadocExternalLocationProvider( + externalDocumentationInfo: ExternalDocumentationInfo, + val brackets: String, + val separator: String, + dokkaContext: DokkaContext +) : DefaultExternalLocationProvider(externalDocumentationInfo, ".html", dokkaContext) { + + override fun resolve(dri: DRI): String? { + val docURL = externalDocumentationInfo.documentationURL.toString().removeSuffix("/") + "/" + val packageLink = dri.packageName?.replace(".", "/") + if (dri.classNames == null) { + return "$docURL$packageLink/package-summary$extension".htmlEscape() + } + val classLink = if (packageLink == null) "${dri.classNames}$extension" else "$packageLink/${dri.classNames}$extension" + val callableChecked = dri.callable ?: return "$docURL$classLink".htmlEscape() + + return ("$docURL$classLink#" + + callableChecked.name + + "${brackets.first()}" + + callableChecked.params.joinToString(separator) + + "${brackets.last()}").htmlEscape() + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt new file mode 100644 index 00000000..1cfd73a8 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/external/javadoc/JavadocExternalLocationProviderFactory.kt @@ -0,0 +1,25 @@ +package org.jetbrains.dokka.base.resolvers.external.javadoc + +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactory +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProviderFactoryWithCache +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.base.resolvers.shared.RecognizedLinkFormat +import org.jetbrains.dokka.plugability.DokkaContext + +class JavadocExternalLocationProviderFactory(val context: DokkaContext) : + ExternalLocationProviderFactory by ExternalLocationProviderFactoryWithCache( + object : ExternalLocationProviderFactory { + override fun getExternalLocationProvider(docInfo: ExternalDocumentationInfo): ExternalLocationProvider? = + when (docInfo.packageList.linkFormat) { + RecognizedLinkFormat.Javadoc1 -> + JavadocExternalLocationProvider(docInfo, "()", ", ", context) // Covers JDK 1 - 7 + RecognizedLinkFormat.Javadoc8, + RecognizedLinkFormat.DokkaJavadoc -> + JavadocExternalLocationProvider(docInfo, "--", "-", context) // Covers JDK 8 - 9 + RecognizedLinkFormat.Javadoc10 -> + JavadocExternalLocationProvider(docInfo, "()", ",", context) // Covers JDK 10 + else -> null + } + } + )
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt deleted file mode 100644 index 06730641..00000000 --- a/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.jetbrains.dokka.base.resolvers.local - -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.DisplaySourceSet -import org.jetbrains.dokka.model.sourceSetIDs -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.query -import java.net.HttpURLConnection -import java.net.URL -import java.net.URLConnection -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write - -abstract class BaseLocationProvider(protected val dokkaContext: DokkaContext) : LocationProvider { - - protected val externalLocationProviderFactories = - dokkaContext.plugin<DokkaBase>().query { externalLocationProviderFactory } - private val cache: MutableMap<URL, DefaultLocationProvider.LocationInfo> = mutableMapOf() - private val lock = ReentrantReadWriteLock() - - protected fun getExternalLocation( - dri: DRI, - sourceSets: Set<DisplaySourceSet> - ): String? { - val jdkToExternalDocumentationLinks = dokkaContext.configuration.sourceSets - .filter { sourceSet -> sourceSet.sourceSetID in sourceSets.sourceSetIDs } - .groupBy({ it.jdkVersion }, { it.externalDocumentationLinks }) - .map { it.key to it.value.flatten().distinct() }.toMap() - - val toResolve: MutableMap<Int, MutableList<DokkaConfiguration.ExternalDocumentationLink>> = mutableMapOf() - for ((jdk, links) in jdkToExternalDocumentationLinks) { - for (link in links) { - val info = lock.read { cache[link.packageListUrl] } - if (info == null) { - toResolve.getOrPut(jdk) { mutableListOf() }.add(link) - } else if (info.packages.contains(dri.packageName)) { - return link.url.toExternalForm() + getLink(dri, info) - } - } - } - // Not in cache, resolve packageLists - for ((jdk, links) in toResolve) { - for (link in links) { - if (dokkaContext.configuration.offlineMode && link.packageListUrl.protocol.toLowerCase() != "file") - continue - val locationInfo = - loadPackageList(jdk, link.packageListUrl) - if (locationInfo.packages.contains(dri.packageName)) { - return link.url.toExternalForm() + getLink(dri, locationInfo) - } - } - toResolve.remove(jdk) - } - return null - } - - private fun getLink(dri: DRI, locationInfo: DefaultLocationProvider.LocationInfo): String = - locationInfo.locations[dri.packageName + "." + dri.classNames] - ?: // Not sure if it can be here, previously it shadowed only kotlin/dokka related sources, here it shadows both dokka/javadoc, cause I cannot distinguish what LocationProvider has been hypothetically chosen - if (locationInfo.externalLocationProvider != null) - with(locationInfo.externalLocationProvider) { - dri.toLocation() - } - else - throw IllegalStateException("Have not found any convenient ExternalLocationProvider for $dri DRI!") - - private fun loadPackageList(jdk: Int, url: URL): DefaultLocationProvider.LocationInfo = lock.write { - val packageListStream = url.doOpenConnectionToReadContent().getInputStream() - val (params, packages) = - packageListStream - .bufferedReader() - .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } - - val paramsMap = params.asSequence() - .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } - .groupBy({ (key, _) -> key }, { (_, value) -> value }) - - val format = paramsMap["format"]?.singleOrNull() ?: when { - jdk < 8 -> "javadoc1" // Covers JDK 1 - 7 - jdk < 10 -> "javadoc8" // Covers JDK 8 - 9 - else -> "javadoc10" // Covers JDK 10+ - } - - val locations = paramsMap["location"].orEmpty() - .map { it.split("\u001f", limit = 2) } - .map { (key, value) -> key to value } - .toMap() - - val externalLocationProvider = - externalLocationProviderFactories.asSequence().map { it.getExternalLocationProvider(format) } - .filterNotNull().take(1).firstOrNull() - - val info = DefaultLocationProvider.LocationInfo( - externalLocationProvider, - packages.toSet(), - locations - ) - cache[url] = info - return info - } - - private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { - val connection = this.openConnection().apply { - connectTimeout = timeout - readTimeout = timeout - } - - when (connection) { - is HttpURLConnection -> { - return when (connection.responseCode) { - in 200..299 -> { - connection - } - HttpURLConnection.HTTP_MOVED_PERM, - HttpURLConnection.HTTP_MOVED_TEMP, - HttpURLConnection.HTTP_SEE_OTHER -> { - if (redirectsAllowed > 0) { - val newUrl = connection.getHeaderField("Location") - URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) - } else { - throw RuntimeException("Too many redirects") - } - } - else -> { - throw RuntimeException("Unhandled http code: ${connection.responseCode}") - } - } - } - else -> return connection - } - } - - companion object { - const val DOKKA_PARAM_PREFIX = "\$dokka." - } - -} diff --git a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt index 1c27959f..1e20dc7e 100644 --- a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProvider.kt @@ -1,217 +1,51 @@ package org.jetbrains.dokka.base.resolvers.local -import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.base.resolvers.shared.PackageList import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.model.withDescendants import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext -import java.net.HttpURLConnection -import java.net.URL -import java.net.URLConnection +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query import java.util.* -private const val PAGE_WITH_CHILDREN_SUFFIX = "index" - -open class DefaultLocationProvider( +abstract class DefaultLocationProvider( protected val pageGraphRoot: RootPageNode, - dokkaContext: DokkaContext -) : BaseLocationProvider(dokkaContext) { - protected open val extension = ".html" - - protected val pagesIndex: Map<DRI, ContentPage> = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() - .map { it.dri.map { dri -> dri to it } }.flatten() - .groupingBy { it.first } - .aggregate { dri, _, (_, page), first -> - if (first) page else throw AssertionError("Multiple pages associated with dri: $dri") - } - - protected val anchorsIndex = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() - .flatMap { page -> - page.content.withDescendants() - .filter { it.extra[SymbolAnchorHint] != null } - .mapNotNull { it.dci.dri.singleOrNull() } - .distinct() - .map { it to page } - }.toMap() - - - protected val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply { - fun registerPath(page: PageNode, prefix: List<String>) { - val newPrefix = prefix + page.pathName - put(page, newPrefix) - page.children.forEach { registerPath(it, newPrefix) } - } - put(pageGraphRoot, emptyList()) - pageGraphRoot.children.forEach { registerPath(it, emptyList()) } - } - - override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean) = - pathTo(node, context) + if (!skipExtension) extension else "" - - override fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode?) = - pagesIndex[dri]?.let { resolve(it, context) } - ?: anchorsIndex[dri]?.let { resolve(it, context) + "#$dri" } - // Not found in PageGraph, that means it's an external link - ?: getExternalLocation(dri, sourceSets) - - override fun resolveRoot(node: PageNode): String = - pathTo(pageGraphRoot, node).removeSuffix(PAGE_WITH_CHILDREN_SUFFIX) - - override fun ancestors(node: PageNode): List<PageNode> = - generateSequence(node) { it.parent() }.toList() - - protected open fun pathTo(node: PageNode, context: PageNode?): String { - fun pathFor(page: PageNode) = pathsIndex[page] ?: throw AssertionError( - "${page::class.simpleName}(${page.name}) does not belong to current page graph so it is impossible to compute its path" - ) - - val contextNode = - if (context !is ClasslikePageNode && context?.children?.isEmpty() == true && context.parent() != null) context.parent() else context - val nodePath = pathFor(node) - val contextPath = contextNode?.let { pathFor(it) }.orEmpty() - - val commonPathElements = nodePath.asSequence().zip(contextPath.asSequence()) - .takeWhile { (a, b) -> a == b }.count() - - return (List(contextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) + - if (node is ClasslikePageNode || node.children.isNotEmpty()) - listOf(PAGE_WITH_CHILDREN_SUFFIX) - else - emptyList() - ).joinToString("/") - } - - private fun PageNode.parent() = pageGraphRoot.parentMap[this] - - private val cache: MutableMap<URL, LocationInfo> = mutableMapOf() - - private fun getLocation( - dri: DRI, - jdkToExternalDocumentationLinks: Map<Int, List<DokkaConfiguration.ExternalDocumentationLink>> - ): String { - val toResolve: MutableMap<Int, MutableList<DokkaConfiguration.ExternalDocumentationLink>> = mutableMapOf() - for ((jdk, links) in jdkToExternalDocumentationLinks) { - for (link in links) { - val info = cache[link.packageListUrl] - if (info == null) { - toResolve.getOrPut(jdk) { mutableListOf() }.add(link) - } else if (info.packages.contains(dri.packageName)) { - return link.url.toExternalForm() + getLink(dri, info) - } - } - } - // Not in cache, resolve packageLists - for ((jdk, links) in toResolve) { - for (link in links) { - if(dokkaContext.configuration.offlineMode && link.packageListUrl.protocol.toLowerCase() != "file") - continue - val locationInfo = - loadPackageList(jdk, link.packageListUrl) - if (locationInfo.packages.contains(dri.packageName)) { - return link.url.toExternalForm() + getLink(dri, locationInfo) - } + protected val dokkaContext: DokkaContext, + protected val extension: String +) : LocationProvider { + protected val externalLocationProviderFactories = + dokkaContext.plugin<DokkaBase>().query { externalLocationProviderFactory } + + protected val packagesIndex: Map<String, ExternalLocationProvider?> = dokkaContext + .configuration + .sourceSets + .flatMap { sourceSet -> + sourceSet.externalDocumentationLinks.map { + PackageList.load(it.packageListUrl, sourceSet.jdkVersion, dokkaContext) + ?.let { packageList -> ExternalDocumentationInfo(it.url, packageList) } } - toResolve.remove(jdk) - } - return "" - } - - private fun getLink(dri: DRI, locationInfo: LocationInfo): String = - locationInfo.locations[dri.packageName + "." + dri.classNames] - ?: // Not sure if it can be here, previously it shadowed only kotlin/dokka related sources, here it shadows both dokka/javadoc, cause I cannot distinguish what LocationProvider has been hypothetically chosen - if (locationInfo.externalLocationProvider != null) - with(locationInfo.externalLocationProvider) { - dri.toLocation() - } - else - throw IllegalStateException("Have not found any convenient ExternalLocationProvider for $dri DRI!") - - private fun loadPackageList(jdk: Int, url: URL): LocationInfo { - val packageListStream = url.doOpenConnectionToReadContent().getInputStream() - val (params, packages) = - packageListStream - .bufferedReader() - .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } } - - val paramsMap = params.asSequence() - .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) } - .groupBy({ (key, _) -> key }, { (_, value) -> value }) - - val format = paramsMap["format"]?.singleOrNull() ?: when { - jdk < 8 -> "javadoc1" // Covers JDK 1 - 7 - jdk < 10 -> "javadoc8" // Covers JDK 8 - 9 - else -> "javadoc10" // Covers JDK 10+ } - - val locations = paramsMap["location"].orEmpty() - .map { it.split("\u001f", limit = 2) } - .map { (key, value) -> key to value } - .toMap() - - val externalLocationProvider = - externalLocationProviderFactories.asSequence().map { it.getExternalLocationProvider(format) } - .filterNotNull().take(1).firstOrNull() - - val info = LocationInfo( - externalLocationProvider, - packages.toSet(), - locations - ) - cache[url] = info - return info - } - - private fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { - val connection = this.openConnection().apply { - connectTimeout = timeout - readTimeout = timeout - } - - when (connection) { - is HttpURLConnection -> { - return when (connection.responseCode) { - in 200..299 -> { - connection - } - HttpURLConnection.HTTP_MOVED_PERM, - HttpURLConnection.HTTP_MOVED_TEMP, - HttpURLConnection.HTTP_SEE_OTHER -> { - if (redirectsAllowed > 0) { - val newUrl = connection.getHeaderField("Location") - URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) - } else { - throw RuntimeException("Too many redirects") - } - } - else -> { - throw RuntimeException("Unhandled http code: ${connection.responseCode}") - } - } + .filterNotNull() + .flatMap { extDocInfo -> + extDocInfo.packageList.packages.map { packageName -> + val externalLocationProvider = (externalLocationProviderFactories.asSequence() + .mapNotNull { it.getExternalLocationProvider(extDocInfo) }.firstOrNull() + ?: run { dokkaContext.logger.error("No ExternalLocationProvider for '${extDocInfo.packageList.url}' found"); null }) + run { null } + packageName to externalLocationProvider } - else -> return connection } - } - - data class LocationInfo( - val externalLocationProvider: ExternalLocationProvider?, - val packages: Set<String>, - val locations: Map<String, String> - ) -} - -private val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + .toMap() + .filterKeys(String::isNotBlank) -internal fun identifierToFilename(name: String): String { - if (name.isEmpty()) return "--root--" - val escaped = name.replace("<|>".toRegex(), "-") - val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } - return if (lowercase in reservedFilenames) "--$lowercase--" else lowercase -} + protected open fun getExternalLocation(dri: DRI, sourceSets: Set<DokkaSourceSet>): String? = + packagesIndex[dri.packageName]?.resolve(dri) -private val PageNode.pathName: String - get() = if (this is PackagePageNode) name else identifierToFilename( - name - ) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt new file mode 100644 index 00000000..4233ffa1 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProvider.kt @@ -0,0 +1,103 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.DokkaBase +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.resolvers.external.ExternalLocationProvider +import org.jetbrains.dokka.base.resolvers.shared.ExternalDocumentationInfo +import org.jetbrains.dokka.base.resolvers.shared.PackageList +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.withDescendants +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.query +import java.util.* + +open class DokkaLocationProvider( + pageGraphRoot: RootPageNode, + dokkaContext: DokkaContext, + extension: String = ".html" +) : DefaultLocationProvider(pageGraphRoot, dokkaContext, extension) { + protected open val PAGE_WITH_CHILDREN_SUFFIX = "index" + + protected open val pathsIndex: Map<PageNode, List<String>> = IdentityHashMap<PageNode, List<String>>().apply { + fun registerPath(page: PageNode, prefix: List<String>) { + val newPrefix = prefix + page.pathName + put(page, newPrefix) + page.children.forEach { registerPath(it, newPrefix) } + } + put(pageGraphRoot, emptyList()) + pageGraphRoot.children.forEach { registerPath(it, emptyList()) } + } + + protected open val pagesIndex: Map<DRI, ContentPage> = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .flatMap { it.dri.map { dri -> dri to it } } + .groupingBy { it.first } + .aggregate { dri, _, (_, page), first -> + if (first) page else throw AssertionError("Multiple pages associated with dri: $dri") + } + + protected open val anchorsIndex = pageGraphRoot.withDescendants().filterIsInstance<ContentPage>() + .flatMap { page -> + page.content.withDescendants() + .filter { it.extra[SymbolAnchorHint] != null } + .mapNotNull { it.dci.dri.singleOrNull() } + .distinct() + .map { it to page } + }.toMap() + + override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean) = + pathTo(node, context) + if (!skipExtension) extension else "" + + override fun resolve(dri: DRI, sourceSets: Set<DokkaSourceSet>, context: PageNode?) = + pagesIndex[dri]?.let { resolve(it, context) } + ?: anchorsIndex[dri]?.let { resolve(it, context) + "#$dri" } + // Not found in PageGraph, that means it's an external link + ?: getExternalLocation(dri, sourceSets) + + override fun pathToRoot(from: PageNode): String = + pathTo(pageGraphRoot, from).removeSuffix(PAGE_WITH_CHILDREN_SUFFIX) + + override fun ancestors(node: PageNode): List<PageNode> = + generateSequence(node) { it.parent() }.toList() + + protected open fun pathTo(node: PageNode, context: PageNode?): String { + fun pathFor(page: PageNode) = pathsIndex[page] ?: throw AssertionError( + "${page::class.simpleName}(${page.name}) does not belong to current page graph so it is impossible to compute its path" + ) + + val contextNode = + if (context !is ClasslikePageNode && context?.children?.isEmpty() == true && context.parent() != null) context.parent() else context + val nodePath = pathFor(node) + val contextPath = contextNode?.let { pathFor(it) }.orEmpty() + + val commonPathElements = nodePath.asSequence().zip(contextPath.asSequence()) + .takeWhile { (a, b) -> a == b }.count() + + return (List(contextPath.size - commonPathElements) { ".." } + nodePath.drop(commonPathElements) + + if (node is ClasslikePageNode || node.children.isNotEmpty()) + listOf(PAGE_WITH_CHILDREN_SUFFIX) + else + emptyList() + ).joinToString("/") + } + + private fun PageNode.parent() = pageGraphRoot.parentMap[this] + + private val PageNode.pathName: String + get() = if (this is PackagePageNode) name else identifierToFilename(name) + + companion object { + internal val reservedFilenames = setOf("index", "con", "aux", "lst", "prn", "nul", "eof", "inp", "out") + + internal fun identifierToFilename(name: String): String { + if (name.isEmpty()) return "--root--" + val escaped = name.replace("[<>]".toRegex(), "-") + val lowercase = escaped.replace("[A-Z]".toRegex()) { matchResult -> "-" + matchResult.value.toLowerCase() } + return if (lowercase in reservedFilenames) "--$lowercase--" else lowercase + } + } +} + + diff --git a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProviderFactory.kt index 442d2e6d..99cc0687 100644 --- a/plugins/base/src/main/kotlin/resolvers/local/DefaultLocationProviderFactory.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/DokkaLocationProviderFactory.kt @@ -3,21 +3,18 @@ package org.jetbrains.dokka.base.resolvers.local import org.jetbrains.dokka.pages.MultimoduleRootPageNode import org.jetbrains.dokka.pages.RootPageNode import org.jetbrains.dokka.plugability.DokkaContext -import java.util.* import java.util.concurrent.ConcurrentHashMap -class DefaultLocationProviderFactory(private val context: DokkaContext) : LocationProviderFactory { - +class DokkaLocationProviderFactory(private val context: DokkaContext) : LocationProviderFactory { private val cache = ConcurrentHashMap<CacheWrapper, LocationProvider>() override fun getLocationProvider(pageNode: RootPageNode) = cache.computeIfAbsent(CacheWrapper(pageNode)) { if (pageNode.children.first() is MultimoduleRootPageNode) MultimoduleLocationProvider(pageNode, context) - else DefaultLocationProvider(pageNode, context) + else DokkaLocationProvider(pageNode, context) } -} - -private class CacheWrapper(val pageNode: RootPageNode) { - override fun equals(other: Any?) = other is CacheWrapper && other.pageNode == this.pageNode - override fun hashCode() = System.identityHashCode(pageNode) -}
\ No newline at end of file + private class CacheWrapper(val pageNode: RootPageNode) { + override fun equals(other: Any?) = other is CacheWrapper && other.pageNode == this.pageNode + override fun hashCode() = System.identityHashCode(pageNode) + } +} diff --git a/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt index 391af004..3775e94e 100644 --- a/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/LocationProvider.kt @@ -3,16 +3,10 @@ package org.jetbrains.dokka.base.resolvers.local import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.pages.PageNode -import org.jetbrains.dokka.pages.RootPageNode interface LocationProvider { fun resolve(dri: DRI, sourceSets: Set<DisplaySourceSet>, context: PageNode? = null): String? fun resolve(node: PageNode, context: PageNode? = null, skipExtension: Boolean = false): String? - fun resolveRoot(node: PageNode): String + fun pathToRoot(from: PageNode): String fun ancestors(node: PageNode): List<PageNode> } - -interface LocationProviderFactory { - fun getLocationProvider(pageNode: RootPageNode): LocationProvider -} - diff --git a/plugins/base/src/main/kotlin/resolvers/local/LocationProviderFactory.kt b/plugins/base/src/main/kotlin/resolvers/local/LocationProviderFactory.kt new file mode 100644 index 00000000..fb72fc60 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/local/LocationProviderFactory.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.base.resolvers.local + +import org.jetbrains.dokka.pages.RootPageNode + +interface LocationProviderFactory { + fun getLocationProvider(pageNode: RootPageNode): LocationProvider +} diff --git a/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt b/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt index 5d2a96d5..f8c8fc6c 100644 --- a/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt +++ b/plugins/base/src/main/kotlin/resolvers/local/MultimoduleLocationProvider.kt @@ -1,5 +1,7 @@ package org.jetbrains.dokka.base.resolvers.local +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.resolvers.local.DokkaLocationProvider.Companion.identifierToFilename import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.pages.PageNode @@ -8,7 +10,7 @@ import org.jetbrains.dokka.plugability.DokkaContext class MultimoduleLocationProvider(private val root: RootPageNode, context: DokkaContext) : LocationProvider { - private val defaultLocationProvider = DefaultLocationProvider(root, context) + private val defaultLocationProvider = DokkaLocationProvider(root, context) val paths = context.configuration.modules.map { it.name to it.path @@ -22,7 +24,7 @@ class MultimoduleLocationProvider(private val root: RootPageNode, context: Dokka override fun resolve(node: PageNode, context: PageNode?, skipExtension: Boolean) = defaultLocationProvider.resolve(node, context, skipExtension) - override fun resolveRoot(node: PageNode): String = defaultLocationProvider.resolveRoot(node) + override fun pathToRoot(from: PageNode): String = defaultLocationProvider.pathToRoot(from) override fun ancestors(node: PageNode): List<PageNode> = listOf(root) diff --git a/plugins/base/src/main/kotlin/resolvers/shared/ExternalDocumentationInfo.kt b/plugins/base/src/main/kotlin/resolvers/shared/ExternalDocumentationInfo.kt new file mode 100644 index 00000000..473d46d1 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/shared/ExternalDocumentationInfo.kt @@ -0,0 +1,5 @@ +package org.jetbrains.dokka.base.resolvers.shared + +import java.net.URL + +data class ExternalDocumentationInfo(val documentationURL: URL, val packageList: PackageList)
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/shared/LinkFormat.kt b/plugins/base/src/main/kotlin/resolvers/shared/LinkFormat.kt new file mode 100644 index 00000000..42be72c4 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/shared/LinkFormat.kt @@ -0,0 +1,6 @@ +package org.jetbrains.dokka.base.resolvers.shared + +interface LinkFormat { + val formatName: String + val linkExtension: String +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/shared/PackageList.kt b/plugins/base/src/main/kotlin/resolvers/shared/PackageList.kt new file mode 100644 index 00000000..a9f0e618 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/shared/PackageList.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.base.resolvers.shared + +import org.jetbrains.dokka.base.renderers.PackageListService +import org.jetbrains.dokka.plugability.DokkaContext +import java.net.URL + +data class PackageList( + val linkFormat: RecognizedLinkFormat, + val packages: Set<String>, + val locations: Map<String, String>, + val url: URL +) { + companion object { + fun load(url: URL, jdkVersion: Int, dokkaContext: DokkaContext): PackageList? { + if (dokkaContext.configuration.offlineMode && url.protocol.toLowerCase() != "file") + return null + + val packageListStream = url.doOpenConnectionToReadContent().getInputStream() + val (params, packages) = + packageListStream + .bufferedReader() + .useLines { lines -> lines.partition { it.startsWith(PackageListService.DOKKA_PARAM_PREFIX) } } + + val paramsMap = params.asSequence() + .map { it.removePrefix("${PackageListService.DOKKA_PARAM_PREFIX}.").split(":", limit = 2) } + .groupBy({ (key, _) -> key }, { (_, value) -> value }) + + val format = paramsMap["format"]?.singleOrNull()?.let { RecognizedLinkFormat.fromString(it) } ?: when { + jdkVersion < 8 -> RecognizedLinkFormat.Javadoc1 // Covers JDK 1 - 7 + jdkVersion < 10 -> RecognizedLinkFormat.Javadoc8 // Covers JDK 8 - 9 + else -> RecognizedLinkFormat.Javadoc10 // Covers JDK 10+ + } + + val locations = paramsMap["location"].orEmpty() + .map { it.split("\u001f", limit = 2) } + .map { (key, value) -> key to value } + .toMap() + + return PackageList(format, packages.toSet(), locations, url) + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/shared/RecognizedLinkFormat.kt b/plugins/base/src/main/kotlin/resolvers/shared/RecognizedLinkFormat.kt new file mode 100644 index 00000000..e8044b4f --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/shared/RecognizedLinkFormat.kt @@ -0,0 +1,18 @@ +package org.jetbrains.dokka.base.resolvers.shared + +enum class RecognizedLinkFormat(override val formatName: String, override val linkExtension: String) : LinkFormat { + DokkaHtml("html-v1", "html"), + DokkaJavadoc("javadoc-v1", "html"), + DokkaGFM("gfm-v1", "md"), + DokkaJekyll("jekyll-v1", "md"), + Javadoc1("javadoc1", "html"), + Javadoc8("javadoc8", "html"), + Javadoc10("javadoc10", "html"), + DokkaOldHtml("html", "html"), + KotlinWebsiteHtml("kotlin-website-html", "html"); + + companion object { + fun fromString(formatName: String) = + values().asSequence().filter { it.formatName == formatName }.firstOrNull() + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/resolvers/shared/utils.kt b/plugins/base/src/main/kotlin/resolvers/shared/utils.kt new file mode 100644 index 00000000..cb737041 --- /dev/null +++ b/plugins/base/src/main/kotlin/resolvers/shared/utils.kt @@ -0,0 +1,36 @@ +package org.jetbrains.dokka.base.resolvers.shared + +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection + +internal fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection { + val connection = this.openConnection().apply { + connectTimeout = timeout + readTimeout = timeout + } + + when (connection) { + is HttpURLConnection -> { + return when (connection.responseCode) { + in 200..299 -> { + connection + } + HttpURLConnection.HTTP_MOVED_PERM, + HttpURLConnection.HTTP_MOVED_TEMP, + HttpURLConnection.HTTP_SEE_OTHER -> { + if (redirectsAllowed > 0) { + val newUrl = connection.getHeaderField("Location") + URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1) + } else { + throw RuntimeException("Too many redirects") + } + } + else -> { + throw RuntimeException("Unhandled http code: ${connection.responseCode}") + } + } + } + else -> return connection + } +} |