1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.build.importMaven
18 
19 import okhttp3.MediaType.Companion.toMediaType
20 import okhttp3.OkHttpClient
21 import okhttp3.Request
22 import okhttp3.RequestBody.Companion.toRequestBody
23 import org.apache.logging.log4j.kotlin.logger
24 import org.w3c.dom.Node
25 import javax.xml.parsers.DocumentBuilderFactory
26 import javax.xml.xpath.XPathConstants
27 import javax.xml.xpath.XPathFactory
28 
29 /**
30  * Pulls the license for a given project.
31  * A license might be fetched:
32  * * directly, if it is a txt url
33  * * from github, if it is a github project
34  * * via license server, as the fallback.
35  */
36 class LicenseDownloader(
37     /**
38      * If set, we'll also query github API to get the license.
39      * Note that, even though this provides better license files, it might potentially fetch the
40      * wrong license if the project changed its license.
41      */
42     private val enableGithubApi: Boolean = false
43 ) {
44     private val logger = logger("LicenseDownloader")
45     private val mediaType = "application/json; charset=utf-8".toMediaType()
46     private val licenseEndpoint = "https://fetch-licenses.appspot.com/convert/licenses"
47     private val githubLicenseApiClient = GithubLicenseApiClient()
48     private val licenseXPath = XPathFactory.newInstance().newXPath()
49         .compile("/project/licenses/license/url")
50     private val scmUrlXPath = XPathFactory.newInstance().newXPath()
51         .compile("/project/scm/url")
52     private val client = OkHttpClient()
53 
54     /**
55      * Fetches license information for external dependencies.
56      */
fetchLicenseFromPomnull57     fun fetchLicenseFromPom(bytes: ByteArray): ByteArray? {
58         val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
59         val document = bytes.inputStream().use {
60             builder.parse(it)
61         }
62         val licenseUrl = (licenseXPath.evaluate(document, XPathConstants.NODE) as? Node)
63             ?.textContent
64         val scmUrl = (scmUrlXPath.evaluate(document, XPathConstants.NODE) as? Node)?.textContent
65         val fetchers = listOf(
66             {
67                 // directly download if it is a txt file
68                 licenseUrl?.let(this::tryFetchTxtLicense)
69             },
70             {
71                 // download via github API if it is hosted on github
72                 if (enableGithubApi) {
73                     scmUrl?.let(githubLicenseApiClient::getProjectLicense)
74                 } else {
75                     null
76                 }
77             },
78             {
79                 // fallback to license server
80                 licenseUrl?.let(this::fetchViaLicenseProxy)
81             }
82         )
83         val licenseContents = fetchers.firstNotNullOfOrNull {
84             it()
85         } ?: return null
86         // get rid of any windows style line endings or extra newlines
87         val cleanedUp = licenseContents.replace("\r", "").dropLastWhile {
88             it == '\n'
89         } + "\n"
90         return cleanedUp.toByteArray(Charsets.UTF_8)
91     }
92 
tryFetchTxtLicensenull93     private fun tryFetchTxtLicense(url: String): String? {
94         if (!url.endsWith(".txt")) {
95             return null
96         }
97         logger.trace {
98             "Fetching license directly from $url"
99         }
100         val request = Request.Builder().url(url).build()
101         return client.newCall(request).execute().use {
102             it.body?.string()
103         }
104     }
105 
fetchViaLicenseProxynull106     private fun fetchViaLicenseProxy(url: String): String? {
107         logger.trace {
108             "Fetching license ($url) via license server"
109         }
110         val payload = "{\"url\": \"$url\"}".toRequestBody(mediaType)
111         val request = Request.Builder().url(licenseEndpoint).post(payload).build()
112         return client.newCall(request).execute().use {
113             it.body?.string()
114         }
115     }
116 }