blob: a81297939a609622e3df00a23b73d91355eaf32e [file] [log] [blame]
package org.jetbrains.dokka
import com.google.inject.Inject
import com.google.inject.Singleton
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import com.intellij.util.io.*
import org.jetbrains.dokka.Formats.FileGeneratorBasedFormatDescriptor
import org.jetbrains.dokka.Formats.FormatDescriptor
import org.jetbrains.dokka.Utilities.ServiceLocator
import org.jetbrains.dokka.Utilities.lookup
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
import org.jetbrains.kotlin.load.java.descriptors.*
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.parents
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLConnection
import java.nio.file.Path
import java.security.MessageDigest
import javax.inject.Named
import kotlin.reflect.full.findAnnotation
fun ByteArray.toHexString() = this.joinToString(separator = "") { "%02x".format(it) }
@Singleton
class ExternalDocumentationLinkResolver @Inject constructor(
val options: DocumentationOptions,
@Named("libraryResolutionFacade") val libraryResolutionFacade: DokkaResolutionFacade,
val logger: DokkaLogger
) {
val packageFqNameToLocation = mutableMapOf<FqName, ExternalDocumentationRoot>()
val formats = mutableMapOf<String, InboundExternalLinkResolutionService>()
class ExternalDocumentationRoot(val rootUrl: URL, val resolver: InboundExternalLinkResolutionService, val locations: Map<String, String>) {
override fun toString(): String = rootUrl.toString()
}
val cacheDir: Path? = options.cacheRoot?.resolve("packageListCache")?.apply { createDirectories() }
val cachedProtocols = setOf("http", "https", "ftp")
fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection {
val connection = this.openConnection()
connection.connectTimeout = timeout
connection.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
}
}
fun loadPackageList(link: DokkaConfiguration.ExternalDocumentationLink) {
val packageListUrl = link.packageListUrl
val needsCache = packageListUrl.protocol in cachedProtocols
val packageListStream = if (cacheDir != null && needsCache) {
val packageListLink = packageListUrl.toExternalForm()
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(packageListLink.toByteArray(Charsets.UTF_8)).toHexString()
val cacheEntry = cacheDir.resolve(hash)
if (cacheEntry.exists()) {
try {
val connection = packageListUrl.doOpenConnectionToReadContent()
val originModifiedDate = connection.date
val cacheDate = cacheEntry.lastModified().toMillis()
if (originModifiedDate > cacheDate || originModifiedDate == 0L) {
if (originModifiedDate == 0L)
logger.warn("No date header for $packageListUrl, downloading anyway")
else
logger.info("Renewing package-list from $packageListUrl")
connection.getInputStream().copyTo(cacheEntry.outputStream())
}
} catch (e: Exception) {
logger.error("Failed to update package-list cache for $link")
val baos = ByteArrayOutputStream()
PrintWriter(baos).use {
e.printStackTrace(it)
}
baos.flush()
logger.error(baos.toString())
}
} else {
logger.info("Downloading package-list from $packageListUrl")
packageListUrl.openStream().copyTo(cacheEntry.outputStream())
}
cacheEntry.inputStream()
} else {
packageListUrl.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() ?: "javadoc"
val locations = paramsMap["location"].orEmpty()
.map { it.split("\u001f", limit = 2) }
.map { (key, value) -> key to value }
.toMap()
val defaultResolverDesc = services["dokka-default"]!!
val resolverDesc = services[format]
?: defaultResolverDesc.takeIf { format in formatsWithDefaultResolver }
?: defaultResolverDesc.also {
logger.warn("Couldn't find InboundExternalLinkResolutionService(format = `$format`) for $link, using Dokka default")
}
val resolverClass = javaClass.classLoader.loadClass(resolverDesc.className).kotlin
val constructors = resolverClass.constructors
val constructor = constructors.singleOrNull()
?: constructors.first { it.findAnnotation<Inject>() != null }
val resolver = constructor.call(paramsMap) as InboundExternalLinkResolutionService
val rootInfo = ExternalDocumentationRoot(link.url, resolver, locations)
packages.map { FqName(it) }.forEach { packageFqNameToLocation[it] = rootInfo }
}
init {
options.externalDocumentationLinks.forEach {
try {
loadPackageList(it)
} catch (e: Exception) {
throw RuntimeException("Exception while loading package-list from $it", e)
}
}
}
fun buildExternalDocumentationLink(element: PsiElement): String? {
return element.extractDescriptor(libraryResolutionFacade)?.let {
buildExternalDocumentationLink(it)
}
}
fun buildExternalDocumentationLink(symbol: DeclarationDescriptor): String? {
val packageFqName: FqName =
when (symbol) {
is PackageFragmentDescriptor -> symbol.fqName
is DeclarationDescriptorNonRoot -> symbol.parents.firstOrNull { it is PackageFragmentDescriptor }?.fqNameSafe ?: return null
else -> return null
}
val externalLocation = packageFqNameToLocation[packageFqName] ?: return null
val path = externalLocation.locations[symbol.signature()] ?:
externalLocation.resolver.getPath(symbol) ?: return null
return URL(externalLocation.rootUrl, path).toExternalForm()
}
companion object {
const val DOKKA_PARAM_PREFIX = "\$dokka."
val services = ServiceLocator.allServices("inbound-link-resolver").associateBy { it.name }
private val formatsWithDefaultResolver =
ServiceLocator
.allServices("format")
.filter {
val desc = ServiceLocator.lookup<FormatDescriptor>(it) as? FileGeneratorBasedFormatDescriptor
desc?.generatorServiceClass == FileGenerator::class
}.map { it.name }
.toSet()
}
}
interface InboundExternalLinkResolutionService {
fun getPath(symbol: DeclarationDescriptor): String?
class Javadoc(paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService {
override fun getPath(symbol: DeclarationDescriptor): String? {
if (symbol is EnumEntrySyntheticClassDescriptor) {
return getPath(symbol.containingDeclaration)?.let { it + "#" + symbol.name.asString() }
} else if (symbol is JavaClassDescriptor) {
return DescriptorUtils.getFqName(symbol).asString().replace(".", "/") + ".html"
} else if (symbol is JavaCallableMemberDescriptor) {
val containingClass = symbol.containingDeclaration as? JavaClassDescriptor ?: return null
val containingClassLink = getPath(containingClass)
if (containingClassLink != null) {
if (symbol is JavaMethodDescriptor || symbol is JavaClassConstructorDescriptor) {
val psi = symbol.sourcePsi() as? PsiMethod
if (psi != null) {
val params = psi.parameterList.parameters.joinToString { it.type.canonicalText }
return containingClassLink + "#" + symbol.name + "(" + params + ")"
}
} else if (symbol is JavaPropertyDescriptor) {
return "$containingClassLink#${symbol.name}"
}
}
}
// TODO Kotlin javadoc
return null
}
}
class Dokka(val paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService {
val extension = paramsMap["linkExtension"]?.singleOrNull() ?: error("linkExtension not provided for Dokka resolver")
override fun getPath(symbol: DeclarationDescriptor): String? {
val leafElement = when (symbol) {
is CallableDescriptor, is TypeAliasDescriptor -> true
else -> false
}
val path = getPathWithoutExtension(symbol)
if (leafElement) return "$path.$extension"
else return "$path/index.$extension"
}
private fun getPathWithoutExtension(symbol: DeclarationDescriptor): String {
return when {
symbol.containingDeclaration == null -> identifierToFilename(symbol.name.asString())
symbol is PackageFragmentDescriptor -> identifierToFilename(symbol.fqName.asString())
else -> getPathWithoutExtension(symbol.containingDeclaration!!) + '/' + identifierToFilename(symbol.name.asString())
}
}
}
}