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.links.DRI 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().query { externalLocationProviderFactory } private val cache: MutableMap = mutableMapOf() private val lock = ReentrantReadWriteLock() protected fun getExternalLocation( dri: DRI, sourceSets: Set ): String { val jdkToExternalDocumentationLinks = dokkaContext.configuration.sourceSets .filter { sourceSet -> sourceSets.contains(sourceSet) } .groupBy({ it.jdkVersion }, { it.externalDocumentationLinks }) .map { it.key to it.value.flatten().distinct() }.toMap() val toResolve: MutableMap> = 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 "" } 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." } }