1 /*
2  * Copyright 2018 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.gmaven
18 
19 import androidx.build.Version
20 import groovy.util.XmlSlurper
21 import groovy.util.slurpersupport.Node
22 import groovy.util.slurpersupport.NodeChild
23 import org.gradle.api.GradleException
24 import org.gradle.api.logging.Logger
25 import java.io.FileNotFoundException
26 import java.io.IOException
27 
28 /**
29  * Queries maven.google.com to get the version numbers for each artifact.
30  * Due to the structure of maven.google.com, a new query is necessary for each group.
31  *
32  * @param logger Logger of the root project. No reason to create multiple instances of this.
33  */
34 class GMavenVersionChecker(private val logger: Logger) {
35     private val versionCache: MutableMap<String, GroupVersionData> = HashMap()
36 
37     /**
38      * Checks whether the given artifact is already on maven.google.com.
39      *
40      * @param group The project group on maven
41      * @param artifactName The artifact name on maven
42      * @param version The version on maven
43      * @return true if the artifact is already on maven.google.com
44      */
isReleasednull45     fun isReleased(group: String, artifactName: String, version: String): Boolean {
46         return getVersions(group, artifactName)?.contains(Version(version)) ?: false
47     }
48 
49     /**
50      * Return the available versions on maven.google.com for a given artifact
51      *
52      * @param group The group id of the artifact
53      * @param artifactName The name of the artifact
54      * @return The set of versions that are available on maven.google.com. Null if artifact is not
55      *         available.
56      */
getVersionsnull57     private fun getVersions(group: String, artifactName: String): Set<Version>? {
58         val groupData = getVersionData(group)
59         return groupData?.artifacts?.get(artifactName)?.versions
60     }
61 
62     /**
63      * Returns the version data for each artifact in a given group.
64      * <p>
65      * If data is not cached, this will make a web request to get it.
66      *
67      * @param group The group to query
68      * @return A data class which has the versions for each artifact
69      */
getVersionDatanull70     private fun getVersionData(group: String): GroupVersionData? {
71         return versionCache.getOrMaybePut(group) {
72             fetchGroup(group, DEFAULT_RETRY_LIMIT)
73         }
74     }
75 
76     /**
77      * Fetches the group version information from maven.google.com
78      *
79      * @param group The group name to fetch
80      * @param retryCount Number of times we'll retry before failing
81      * @return GroupVersionData that has the data or null if it is a new item.
82      */
fetchGroupnull83     private fun fetchGroup(group: String, retryCount: Int): GroupVersionData? {
84         val url = buildGroupUrl(group)
85         for (run in 0..retryCount) {
86             logger.info("fetching maven XML from $url")
87             try {
88                 val parsedXml = XmlSlurper(false, false).parse(url) as NodeChild
89                 return GroupVersionData.from(parsedXml)
90             } catch (ignored: FileNotFoundException) {
91                 logger.info("could not find version data for $group, seems like a new file")
92                 return null
93             } catch (ioException: IOException) {
94                 logger.warn("failed to fetch the maven info, retrying in 2 seconds. " +
95                         "Run $run of $retryCount")
96                 Thread.sleep(RETRY_DELAY)
97             }
98         }
99         throw GradleException("Could not access maven.google.com")
100     }
101 
102     companion object {
103         /**
104          * Creates the URL which has the XML file that describes the available versions for each
105          * artifact in that group
106          *
107          * @param group Maven group name
108          * @return The URL of the XML file
109          */
buildGroupUrlnull110         private fun buildGroupUrl(group: String) =
111                 "$BASE${group.replace(".","/")}/$GROUP_FILE"
112     }
113 }
114 
115 private fun <K, V> MutableMap<K, V>.getOrMaybePut(key: K, defaultValue: () -> V?): V? {
116     val value = get(key)
117     return if (value == null) {
118         val answer = defaultValue()
119         if (answer != null) put(key, answer)
120         answer
121     } else {
122         value
123     }
124 }
125 
126 /**
127  * Data class that holds the artifacts of a single maven group.
128  *
129  * @param name Maven group name
130  * @param artifacts Map of artifact versions keyed by artifact name
131  */
132 private data class GroupVersionData(
133         val name: String,
134         val artifacts: Map<String, ArtifactVersionData>
135 ) {
136     companion object {
137         /**
138          * Constructs an instance from the given node.
139          *
140          * @param xml The information node fetched from {@code GROUP_FILE}
141          */
fromnull142         fun from(xml: NodeChild): GroupVersionData {
143             /*
144              * sample input:
145              * <android.arch.core>
146              *   <runtime versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
147              *   <common versions="1.0.0-alpha4,1.0.0-alpha5,1.0.0-alpha6,1.0.0-alpha7"/>
148              * </android.arch.core>
149              */
150             val name = xml.name()
151             val artifacts: MutableMap<String, ArtifactVersionData> = HashMap()
152 
153             xml.childNodes().forEach {
154                 val node = it as Node
155                 val versions = (node.attributes()["versions"] as String).split(",").map {
156                     if (it == "0.1" || it == "0.2" || it == "0.3") {
157                         // androidx.core:core-ktx shipped versions 0.1, 0.2, and 0.3 which do not
158                         // comply with our versioning scheme.
159                         Version(it + ".0")
160                     } else {
161                         Version(it)
162                     }
163                 }.toSet()
164                 artifacts.put(it.name(), ArtifactVersionData(it.name(), versions))
165             }
166             return GroupVersionData(name, artifacts)
167         }
168     }
169 }
170 
171 /**
172  * Data class that holds the version information about a single artifact
173  *
174  * @param name Name of the maven artifact
175  * @param versions set of version codes that are already on maven.google.com
176  */
177 private data class ArtifactVersionData(val name: String, val versions: Set<Version>)
178 
179 // wait 2 seconds before retrying if fetch fails
180 private const val RETRY_DELAY: Long = 2000 // ms
181 
182 // number of times we'll try to reach maven.google.com before failing
183 private const val DEFAULT_RETRY_LIMIT = 20
184 
185 private const val BASE = "https://dl.google.com/dl/android/maven2/"
186 private const val GROUP_FILE = "group-index.xml"