aboutsummaryrefslogtreecommitdiff
path: root/plugins/base/src/main/kotlin/resolvers/local/BaseLocationProvider.kt
blob: a9a5e498b138295a53d9d0750f2d810203e3c40f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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.pages.ContentSourceSet
import org.jetbrains.dokka.pages.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<ContentSourceSet>
    ): 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 ""
    }

    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."
    }

}