<lambda>null1 package org.jetbrains.dokka
2
3 import com.google.inject.Inject
4 import com.google.inject.Singleton
5 import com.intellij.psi.PsiElement
6 import com.intellij.psi.PsiMethod
7 import com.intellij.util.io.*
8 import org.jetbrains.dokka.Formats.FileGeneratorBasedFormatDescriptor
9 import org.jetbrains.dokka.Formats.FormatDescriptor
10 import org.jetbrains.dokka.Utilities.ServiceLocator
11 import org.jetbrains.dokka.Utilities.lookup
12 import org.jetbrains.kotlin.descriptors.*
13 import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
14 import org.jetbrains.kotlin.load.java.descriptors.*
15 import org.jetbrains.kotlin.name.FqName
16 import org.jetbrains.kotlin.resolve.DescriptorUtils
17 import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
18 import org.jetbrains.kotlin.resolve.descriptorUtil.parents
19 import java.io.ByteArrayOutputStream
20 import java.io.PrintWriter
21 import java.net.HttpURLConnection
22 import java.net.URL
23 import java.net.URLConnection
24 import java.nio.file.Path
25 import java.security.MessageDigest
26 import javax.inject.Named
27 import kotlin.reflect.full.findAnnotation
28
29 fun ByteArray.toHexString() = this.joinToString(separator = "") { "%02x".format(it) }
30
31 @Singleton
32 class ExternalDocumentationLinkResolver @Inject constructor(
33 val options: DocumentationOptions,
34 @Named("libraryResolutionFacade") val libraryResolutionFacade: DokkaResolutionFacade,
35 val logger: DokkaLogger
36 ) {
37
38 val packageFqNameToLocation = mutableMapOf<FqName, ExternalDocumentationRoot>()
39 val formats = mutableMapOf<String, InboundExternalLinkResolutionService>()
40
41 class ExternalDocumentationRoot(val rootUrl: URL, val resolver: InboundExternalLinkResolutionService, val locations: Map<String, String>) {
toStringnull42 override fun toString(): String = rootUrl.toString()
43 }
44
45 val cacheDir: Path? = options.cacheRoot?.resolve("packageListCache")?.apply { createDirectories() }
46
47 val cachedProtocols = setOf("http", "https", "ftp")
48
doOpenConnectionToReadContentnull49 fun URL.doOpenConnectionToReadContent(timeout: Int = 10000, redirectsAllowed: Int = 16): URLConnection {
50 val connection = this.openConnection()
51 connection.connectTimeout = timeout
52 connection.readTimeout = timeout
53
54 when (connection) {
55 is HttpURLConnection -> {
56 return when (connection.responseCode) {
57 in 200..299 -> {
58 connection
59 }
60 HttpURLConnection.HTTP_MOVED_PERM,
61 HttpURLConnection.HTTP_MOVED_TEMP,
62 HttpURLConnection.HTTP_SEE_OTHER -> {
63 if (redirectsAllowed > 0) {
64 val newUrl = connection.getHeaderField("Location")
65 URL(newUrl).doOpenConnectionToReadContent(timeout, redirectsAllowed - 1)
66 } else {
67 throw RuntimeException("Too many redirects")
68 }
69 }
70 else -> {
71 throw RuntimeException("Unhandled http code: ${connection.responseCode}")
72 }
73 }
74 }
75 else -> return connection
76 }
77 }
78
loadPackageListnull79 fun loadPackageList(link: DokkaConfiguration.ExternalDocumentationLink) {
80
81 val packageListUrl = link.packageListUrl
82 val needsCache = packageListUrl.protocol in cachedProtocols
83
84 val packageListStream = if (cacheDir != null && needsCache) {
85 val packageListLink = packageListUrl.toExternalForm()
86
87 val digest = MessageDigest.getInstance("SHA-256")
88 val hash = digest.digest(packageListLink.toByteArray(Charsets.UTF_8)).toHexString()
89 val cacheEntry = cacheDir.resolve(hash)
90
91 if (cacheEntry.exists()) {
92 try {
93 val connection = packageListUrl.doOpenConnectionToReadContent()
94 val originModifiedDate = connection.date
95 val cacheDate = cacheEntry.lastModified().toMillis()
96 if (originModifiedDate > cacheDate || originModifiedDate == 0L) {
97 if (originModifiedDate == 0L)
98 logger.warn("No date header for $packageListUrl, downloading anyway")
99 else
100 logger.info("Renewing package-list from $packageListUrl")
101 connection.getInputStream().copyTo(cacheEntry.outputStream())
102 }
103 } catch (e: Exception) {
104 logger.error("Failed to update package-list cache for $link")
105 val baos = ByteArrayOutputStream()
106 PrintWriter(baos).use {
107 e.printStackTrace(it)
108 }
109 baos.flush()
110 logger.error(baos.toString())
111 }
112 } else {
113 logger.info("Downloading package-list from $packageListUrl")
114 packageListUrl.openStream().copyTo(cacheEntry.outputStream())
115 }
116 cacheEntry.inputStream()
117 } else {
118 packageListUrl.doOpenConnectionToReadContent().getInputStream()
119 }
120
121 val (params, packages) =
122 packageListStream
123 .bufferedReader()
124 .useLines { lines -> lines.partition { it.startsWith(DOKKA_PARAM_PREFIX) } }
125
126 val paramsMap = params.asSequence()
127 .map { it.removePrefix(DOKKA_PARAM_PREFIX).split(":", limit = 2) }
128 .groupBy({ (key, _) -> key }, { (_, value) -> value })
129
130 val format = paramsMap["format"]?.singleOrNull() ?: "javadoc"
131
132 val locations = paramsMap["location"].orEmpty()
133 .map { it.split("\u001f", limit = 2) }
134 .map { (key, value) -> key to value }
135 .toMap()
136
137
138 val defaultResolverDesc = services["dokka-default"]!!
139 val resolverDesc = services[format]
140 ?: defaultResolverDesc.takeIf { format in formatsWithDefaultResolver }
141 ?: defaultResolverDesc.also {
142 logger.warn("Couldn't find InboundExternalLinkResolutionService(format = `$format`) for $link, using Dokka default")
143 }
144
145
146 val resolverClass = javaClass.classLoader.loadClass(resolverDesc.className).kotlin
147
148 val constructors = resolverClass.constructors
149
150 val constructor = constructors.singleOrNull()
151 ?: constructors.first { it.findAnnotation<Inject>() != null }
152 val resolver = constructor.call(paramsMap) as InboundExternalLinkResolutionService
153
154 val rootInfo = ExternalDocumentationRoot(link.url, resolver, locations)
155
156 packages.map { FqName(it) }.forEach { packageFqNameToLocation[it] = rootInfo }
157 }
158
159 init {
<lambda>null160 options.externalDocumentationLinks.forEach {
161 try {
162 loadPackageList(it)
163 } catch (e: Exception) {
164 throw RuntimeException("Exception while loading package-list from $it", e)
165 }
166 }
167 }
168
buildExternalDocumentationLinknull169 fun buildExternalDocumentationLink(element: PsiElement): String? {
170 return element.extractDescriptor(libraryResolutionFacade)?.let {
171 buildExternalDocumentationLink(it)
172 }
173 }
174
buildExternalDocumentationLinknull175 fun buildExternalDocumentationLink(symbol: DeclarationDescriptor): String? {
176 val packageFqName: FqName =
177 when (symbol) {
178 is PackageFragmentDescriptor -> symbol.fqName
179 is DeclarationDescriptorNonRoot -> symbol.parents.firstOrNull { it is PackageFragmentDescriptor }?.fqNameSafe ?: return null
180 else -> return null
181 }
182
183 val externalLocation = packageFqNameToLocation[packageFqName] ?: return null
184
185 val path = externalLocation.locations[symbol.signature()] ?:
186 externalLocation.resolver.getPath(symbol) ?: return null
187
188 return URL(externalLocation.rootUrl, path).toExternalForm()
189 }
190
191 companion object {
192 const val DOKKA_PARAM_PREFIX = "\$dokka."
<lambda>null193 val services = ServiceLocator.allServices("inbound-link-resolver").associateBy { it.name }
194 private val formatsWithDefaultResolver =
195 ServiceLocator
196 .allServices("format")
<lambda>null197 .filter {
198 val desc = ServiceLocator.lookup<FormatDescriptor>(it) as? FileGeneratorBasedFormatDescriptor
199 desc?.generatorServiceClass == FileGenerator::class
200 }.map { it.name }
201 .toSet()
202 }
203 }
204
205
206 interface InboundExternalLinkResolutionService {
getPathnull207 fun getPath(symbol: DeclarationDescriptor): String?
208
209 class Javadoc(paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService {
210 override fun getPath(symbol: DeclarationDescriptor): String? {
211 if (symbol is EnumEntrySyntheticClassDescriptor) {
212 return getPath(symbol.containingDeclaration)?.let { it + "#" + symbol.name.asString() }
213 } else if (symbol is ClassDescriptor) {
214 return DescriptorUtils.getFqName(symbol).asString().replace(".", "/") + ".html"
215 } else if (symbol is JavaCallableMemberDescriptor) {
216 val containingClass = symbol.containingDeclaration as? JavaClassDescriptor ?: return null
217 val containingClassLink = getPath(containingClass)
218 if (containingClassLink != null) {
219 if (symbol is JavaMethodDescriptor || symbol is JavaClassConstructorDescriptor) {
220 val psi = symbol.sourcePsi() as? PsiMethod
221 if (psi != null) {
222 val params = psi.parameterList.parameters.joinToString { it.type.canonicalText }
223 return containingClassLink + "#" + symbol.name + "(" + params + ")"
224 }
225 } else if (symbol is JavaPropertyDescriptor) {
226 return "$containingClassLink#${symbol.name}"
227 }
228 }
229 }
230 // TODO Kotlin javadoc
231 return null
232 }
233 }
234
235 class Dokka(val paramsMap: Map<String, List<String>>) : InboundExternalLinkResolutionService {
236 val extension = paramsMap["linkExtension"]?.singleOrNull() ?: error("linkExtension not provided for Dokka resolver")
237
getPathnull238 override fun getPath(symbol: DeclarationDescriptor): String? {
239 val leafElement = when (symbol) {
240 is CallableDescriptor, is TypeAliasDescriptor -> true
241 else -> false
242 }
243 val path = getPathWithoutExtension(symbol)
244 if (leafElement) return "$path.$extension"
245 else return "$path/index.$extension"
246 }
247
getPathWithoutExtensionnull248 private fun getPathWithoutExtension(symbol: DeclarationDescriptor): String {
249 return when {
250 symbol.containingDeclaration == null -> identifierToFilename(symbol.name.asString())
251 symbol is PackageFragmentDescriptor -> identifierToFilename(symbol.fqName.asString())
252 else -> getPathWithoutExtension(symbol.containingDeclaration!!) + '/' + identifierToFilename(symbol.name.asString())
253 }
254 }
255
256 }
257 }
258
259