1/*
2 * Copyright (C) 2015 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 android.databinding
18
19import groovy.io.FileType
20import org.apache.maven.repository.internal.MavenRepositorySystemUtils
21import org.eclipse.aether.DefaultRepositorySystemSession
22import org.eclipse.aether.RepositorySystem
23import org.eclipse.aether.RepositorySystemSession
24import org.eclipse.aether.artifact.Artifact
25import org.eclipse.aether.artifact.DefaultArtifact
26import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory
27import org.eclipse.aether.graph.Dependency
28import org.eclipse.aether.impl.DefaultServiceLocator
29import org.eclipse.aether.repository.LocalRepository
30import org.eclipse.aether.repository.RemoteRepository
31import org.eclipse.aether.resolution.ArtifactDescriptorRequest
32import org.eclipse.aether.resolution.ArtifactDescriptorResult
33import org.eclipse.aether.resolution.ArtifactRequest
34import org.eclipse.aether.spi.connector.RepositoryConnectorFactory
35import org.eclipse.aether.spi.connector.transport.TransporterFactory
36import org.eclipse.aether.transport.file.FileTransporterFactory
37import org.eclipse.aether.transport.http.HttpTransporterFactory
38import org.gradle.api.DefaultTask
39import org.gradle.api.artifacts.Configuration
40import org.gradle.api.artifacts.ModuleVersionIdentifier
41import org.gradle.api.tasks.TaskAction
42
43class LocalizeDependenciesTask extends DefaultTask {
44
45    private Set<String> ids = new HashSet<>();
46
47    private Set<String> fetchTestDependencies = new HashSet<>();
48
49    // force download these if they are seen as a dependency
50    private Set<String>  wildCard = new HashSet<>();
51    {
52        wildCard.add("kotlin-gradle-plugin-core")
53    }
54
55    List<Artifact> artifactsToResolve = new LinkedList<>();
56
57    Set<String> resolvedArtifacts = new HashSet<>();
58
59    Set<String> failed = new HashSet<>();
60
61    HashMap<String, Object> licenses = new HashMap<>();
62
63    Set<String> missingLicenses = new HashSet<>();
64
65    File localRepoDir;
66
67    @TaskAction
68    doIt() {
69        println(ids)
70        LocalizePluginExtension extension = project.extensions.
71                getByName(MavenDependencyCollectorPlugin.EXTENSION_NAME)
72        if (extension.localRepoDir == null || extension.otherRepoDirs == null) {
73
74            def msg = "you must configure " +
75                    "${MavenDependencyCollectorPlugin.EXTENSION_NAME} with localRepoDir and" +
76                    " otherRepoDirs. localRepoDir: " + extension.localRepoDir +
77                    "\notherRepoDir:" + extension.otherRepoDirs;
78            println(msg)
79            println("skipping ${project}")
80            return
81        }
82        localRepoDir = extension.localRepoDir
83        downloadAll(extension.localRepoDir, extension.otherRepoDirs)
84
85        if (!missingLicenses.isEmpty()) {
86            throw new RuntimeException("Missing licenses for $missingLicenses")
87        }
88        println("List of new licenses:")
89        println(ExportLicensesTask.buildNotice(licenses))
90    }
91
92    public void add(MavenDependencyCollectorTask task, ModuleVersionIdentifier id, Configuration conf) {
93        def key = toStringIdentifier(id)
94        ids.add(key)
95        println("adding $key in $conf by $task")
96    }
97
98    public static String toStringIdentifier(ModuleVersionIdentifier id) {
99        return id.group + ":" + id.name + ":" + id.version;
100    }
101
102    private static String artifactKey(Artifact artifact) {
103        return artifact.groupId + ":" + artifact.artifactId + ":" + artifact.version;
104    }
105
106    public downloadAll(File localRepoDir, List<String> otherRepoDirs) {
107        println("downloading all dependencies to $localRepoDir")
108        def mavenCentral = new RemoteRepository.Builder("central", "default",
109                "http://central.maven.org/maven2/").build();
110        def system = newRepositorySystem()
111        localRepoDir = localRepoDir.canonicalFile
112        List<File> otherRepos = new ArrayList<>()
113        otherRepoDirs.each {
114            def repo = new File(it).getCanonicalFile()
115            if (repo.exists() && !repo.equals(localRepoDir)) {
116                otherRepos.add(repo)
117            }
118        }
119        def session = newRepositorySystemSession(system, localRepoDir)
120        ids.each {
121            def artifact = new DefaultArtifact(it)
122            artifactsToResolve.add(artifact)
123        }
124
125        while (!artifactsToResolve.isEmpty()) {
126            println("remaining artifacts to resolve ${artifactsToResolve.size()}")
127            Artifact artifact = artifactsToResolve.remove(0)
128            println("    handling artifact ${artifact.getArtifactId()}")
129            if (shouldSkip(artifact, otherRepos)) {
130                println("skipping $artifact")
131                continue
132            }
133            resolveArtifactWithDependencies(system, session, Arrays.asList(mavenCentral), artifact);
134        }
135    }
136
137    public static boolean shouldSkip(Artifact artifact, List<File> otherRepos) {
138        if (artifact.groupId.startsWith('com.android.databinding') ||
139                artifact.groupId.startsWith('com.android.support') ||
140                artifact.groupId.equals("jdk")){
141            return true
142        }
143        String targetPath = artifact.groupId.replaceAll("\\.", "/") + "/" + artifact.artifactId +
144                "/" + artifact.version
145        for (File repo : otherRepos) {
146            File f = new File(repo, targetPath)
147            if (f.exists()) {
148                println("skipping ${artifact} because it exists in $repo")
149                return true
150            }
151        }
152        return false
153    }
154
155    def boolean isInGit(File file) {
156        if (!file.getCanonicalPath().startsWith(localRepoDir.getCanonicalPath())) {
157            println("$file is in another git repo, ignore for license")
158            return false
159        }
160        def gitSt = ["git", "status", "--porcelain", file.getCanonicalPath()].
161                execute([], localRepoDir)
162        gitSt.waitFor()
163        if (gitSt.exitValue() != 0) {
164            throw new RuntimeException("unable to get git status for $file. ${gitSt.err.text}")
165        }
166        return gitSt.text.trim().isEmpty()
167    }
168
169    public void resolveArtifactWithDependencies(RepositorySystem system,
170            RepositorySystemSession session, List<RemoteRepository> remoteRepositories,
171            Artifact artifact) {
172        def key = artifactKey(artifact)
173        if (resolvedArtifacts.contains(key) || failed.contains(key)) {
174            return
175        }
176        resolvedArtifacts.add(key)
177        ArtifactRequest artifactRequest = new ArtifactRequest();
178        artifactRequest.setArtifact(artifact);
179        artifactRequest.setRepositories(remoteRepositories);
180        def resolved;
181        try {
182            resolved = system.resolveArtifact(session, artifactRequest);
183        } catch (Throwable ignored) {
184            println("cannot find $key, skipping")
185            failed.add(key)
186            return
187        }
188        def alreadyInGit = isInGit(resolved.artifact.file)
189        println("         |-> resolved ${resolved.artifact.file}. Already in git? $alreadyInGit")
190
191
192
193        if (!alreadyInGit) {
194            def license = ExportLicensesTask.findLicenseFor(resolved.artifact.artifactId)
195            if (license == null) {
196                missingLicenses.add(artifactKey(artifact))
197            } else {
198                licenses.put(resolved.artifact.artifactId, license)
199            }
200        }
201
202        ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
203        descriptorRequest.setArtifact(artifact);
204        descriptorRequest.setRepositories(remoteRepositories);
205
206        ArtifactDescriptorResult descriptorResult = system.
207                readArtifactDescriptor(session, descriptorRequest);
208        for (Dependency dependency : descriptorResult.getDependencies()) {
209            println("dependency $dependency for $artifact . scope: ${dependency.scope}")
210            if ("provided".equals(dependency.scope)) {
211                println("skipping $dependency because provided")
212                continue
213            }
214            if ("optional".equals(dependency.scope)) {
215                println("skipping $dependency because optional")
216                continue
217            }
218            if ("test".equals(dependency.scope)) {
219                if (wildCard.contains(dependency.artifact.getArtifactId()) || fetchTestDependencies.contains(key)) {
220                    println("${dependency} is test scope but including because $key is in direct dependencies")
221                } else {
222                    println("skipping $dependency because test and $key is not first level dependency. artifact id: ${dependency.artifact.getArtifactId()}")
223                    continue
224                }
225            }
226
227
228            def dependencyKey = artifactKey(dependency.artifact)
229            if (resolvedArtifacts.contains(dependencyKey)) {
230                println("skipping $dependency because is already resolved as ${dependencyKey}")
231                continue
232            }
233            println("adding to the list ${dependency.artifact}")
234            artifactsToResolve.add(dependency.artifact)
235        }
236        File unwanted = new File(resolved.artifact.file.getParentFile(), "_remote.repositories")
237        if (unwanted.exists()) {
238            unwanted.delete()
239        }
240    }
241
242    public static DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system,
243            File localRepoDir) {
244        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
245        LocalRepository localRepo = new LocalRepository(localRepoDir);
246        session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
247        return session;
248    }
249
250    public static RepositorySystem newRepositorySystem() {
251        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
252        locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
253        locator.addService(TransporterFactory.class, FileTransporterFactory.class);
254        locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
255
256        return locator.getService(RepositorySystem.class);
257    }
258}
259