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
17package androidx.build.gmaven
18
19import androidx.build.Version
20import groovy.util.XmlSlurper
21import groovy.util.slurpersupport.Node
22import groovy.util.slurpersupport.NodeChild
23import org.gradle.api.GradleException
24import org.gradle.api.logging.Logger
25import java.io.FileNotFoundException
26import 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 */
34class 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     */
45    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     */
57    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     */
70    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     */
83    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         */
110        private fun buildGroupUrl(group: String) =
111                "$BASE${group.replace(".","/")}/$GROUP_FILE"
112    }
113}
114
115private 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 */
132private 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         */
142        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 */
177private data class ArtifactVersionData(val name: String, val versions: Set<Version>)
178
179// wait 2 seconds before retrying if fetch fails
180private const val RETRY_DELAY: Long = 2000 // ms
181
182// number of times we'll try to reach maven.google.com before failing
183private const val DEFAULT_RETRY_LIMIT = 20
184
185private const val BASE = "https://dl.google.com/dl/android/maven2/"
186private const val GROUP_FILE = "group-index.xml"