• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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