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 }