1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 com.android.ide.eclipse.adt.internal.wizards.exportgradle;
18 
19 import com.android.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.ide.eclipse.adt.AdtConstants;
22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
23 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
24 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
25 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
26 import com.google.common.collect.Lists;
27 import com.google.common.collect.Maps;
28 
29 import org.eclipse.core.resources.IProject;
30 import org.eclipse.core.resources.IResource;
31 import org.eclipse.core.resources.ResourcesPlugin;
32 import org.eclipse.core.runtime.CoreException;
33 import org.eclipse.core.runtime.IPath;
34 import org.eclipse.core.runtime.Path;
35 import org.eclipse.jdt.core.IClasspathEntry;
36 import org.eclipse.jdt.core.IJavaProject;
37 import org.eclipse.jdt.core.IPackageFragmentRoot;
38 import org.eclipse.jdt.core.JavaCore;
39 import org.eclipse.jdt.core.JavaModelException;
40 
41 import java.io.File;
42 import java.util.Collection;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.regex.Pattern;
46 
47 /**
48  * Class to setup the project and its modules.
49  */
50 public class ProjectSetupBuilder {
51 
52     private final static class InternalException extends Exception {
53         private static final long serialVersionUID = 1L;
54 
InternalException(String message)55         InternalException(String message) {
56             super(message);
57         }
58     }
59 
60     private boolean mCanFinish = false;
61     private boolean mCanGenerate = false;
62     private final List<GradleModule> mOriginalModules = Lists.newArrayList();
63     private final Map<IJavaProject, GradleModule> mModules = Maps.newHashMap();
64     private IPath mCommonRoot;
65     private ExportStatus mStatus;
66 
ProjectSetupBuilder()67     public ProjectSetupBuilder() {
68 
69     }
70 
setCanGenerate(boolean generate)71     public void setCanGenerate(boolean generate) {
72         mCanGenerate = generate;
73     }
74 
setCanFinish(boolean canFinish)75     public void setCanFinish(boolean canFinish) {
76         mCanFinish = canFinish;
77     }
78 
canFinish()79     public boolean canFinish() {
80         return mCanFinish;
81     }
82 
canGenerate()83     public boolean canGenerate() {
84         return mCanGenerate;
85     }
86 
setStatus(ExportStatus status)87     public void setStatus(ExportStatus status) {
88         mStatus = status;
89     }
90 
getStatus()91     public ExportStatus getStatus() {
92         return mStatus;
93     }
94 
95     @NonNull
setProject(@onNull List<IJavaProject> selectedProjects)96     public String setProject(@NonNull List<IJavaProject> selectedProjects)
97             throws CoreException {
98         mModules.clear();
99 
100         // build a list of all projects that must be included. This is in case
101         // some dependencies have not been included in the selected projects. We also include
102         // parent projects so that the full multi-project setup is correct.
103         // Note that if two projects are selected that are not related, both will be added
104         // in the same multi-project anyway.
105         try {
106             for (IJavaProject javaProject : selectedProjects) {
107                 GradleModule module;
108 
109                 if (javaProject.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) {
110                     module = processAndroidProject(javaProject);
111                 } else {
112                     module = processJavaProject(javaProject);
113                 }
114 
115                 mOriginalModules.add(module);
116             }
117 
118             Collection<GradleModule> modules = mModules.values();
119             computeRootAndPaths(modules);
120 
121             return null;
122         } catch (InternalException e) {
123             return e.getMessage();
124         }
125     }
126 
127     @NonNull
getModules()128     public Collection<GradleModule> getModules() {
129         return mModules.values();
130     }
131 
getModuleCount()132     public int getModuleCount() {
133         return mModules.size();
134     }
135 
136     @Nullable
getCommonRoot()137     public IPath getCommonRoot() {
138         return mCommonRoot;
139     }
140 
141     @Nullable
getModule(IJavaProject javaProject)142     public GradleModule getModule(IJavaProject javaProject) {
143         return mModules.get(javaProject);
144     }
145 
isOriginalProject(@onNull IJavaProject javaProject)146     public boolean isOriginalProject(@NonNull IJavaProject javaProject) {
147         GradleModule module = mModules.get(javaProject);
148         return mOriginalModules.contains(module);
149     }
150 
151     @NonNull
getOriginalModules()152     public List<GradleModule> getOriginalModules() {
153         return mOriginalModules;
154     }
155 
156     @Nullable
getShortestDependencyTo(GradleModule module)157     public List<GradleModule> getShortestDependencyTo(GradleModule module) {
158         return findModule(module, mOriginalModules);
159     }
160 
161     @Nullable
findModule(GradleModule toFind, GradleModule rootModule)162     public List<GradleModule> findModule(GradleModule toFind, GradleModule rootModule) {
163         if (toFind == rootModule) {
164             List<GradleModule> list = Lists.newArrayList();
165             list.add(toFind);
166             return list;
167         }
168 
169         List<GradleModule> shortestChain = findModule(toFind, rootModule.getDependencies());
170 
171         if (shortestChain != null) {
172             shortestChain.add(0, rootModule);
173         }
174 
175         return shortestChain;
176     }
177 
178     @Nullable
findModule(GradleModule toFind, List<GradleModule> modules)179     public List<GradleModule> findModule(GradleModule toFind, List<GradleModule> modules) {
180         List<GradleModule> currentChain = null;
181 
182         for (GradleModule child : modules) {
183             List<GradleModule> newChain = findModule(toFind, child);
184             if (currentChain == null) {
185                 currentChain = newChain;
186             } else if (newChain != null) {
187                 if (currentChain.size() > newChain.size()) {
188                     currentChain = newChain;
189                 }
190             }
191         }
192 
193         return currentChain;
194     }
195 
196     @NonNull
processAndroidProject(@onNull IJavaProject javaProject)197     private GradleModule processAndroidProject(@NonNull IJavaProject javaProject)
198             throws InternalException, CoreException {
199 
200         // get/create the module
201         GradleModule module = createModuleOnDemand(javaProject);
202         if (module.isConfigured()) {
203             return module;
204         }
205 
206         module.setType(GradleModule.Type.ANDROID);
207 
208         ProjectState projectState = Sdk.getProjectState(javaProject.getProject());
209         assert projectState != null;
210 
211         // add library project dependencies
212         List<LibraryState> libraryProjects = projectState.getLibraries();
213         for (LibraryState libraryState : libraryProjects) {
214             ProjectState libProjectState = libraryState.getProjectState();
215             if (libProjectState != null) {
216                 IJavaProject javaLib = getJavaProject(libProjectState);
217                 if (javaLib != null) {
218                     GradleModule libModule = processAndroidProject(javaLib);
219                     module.addDependency(libModule);
220                 } else {
221                     throw new InternalException(String.format(
222                             "Project %1$s is missing. Needed by %2$s.\n" +
223                             "Make sure all dependencies are opened.",
224                             libraryState.getRelativePath(),
225                             javaProject.getProject().getName()));
226                 }
227             } else {
228                 throw new InternalException(String.format(
229                         "Project %1$s is missing. Needed by %2$s.\n" +
230                         "Make sure all dependencies are opened.",
231                         libraryState.getRelativePath(),
232                         javaProject.getProject().getName()));
233             }
234         }
235 
236         // add java project dependencies
237         List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject);
238         for (IJavaProject javaDep : javaDepProjects) {
239             GradleModule libModule = processJavaProject(javaDep);
240             module.addDependency(libModule);
241         }
242 
243         return module;
244     }
245 
246     @NonNull
processJavaProject(@onNull IJavaProject javaProject)247     private GradleModule processJavaProject(@NonNull IJavaProject javaProject)
248             throws InternalException, CoreException {
249         // get/create the module
250         GradleModule module = createModuleOnDemand(javaProject);
251 
252         if (module.isConfigured()) {
253             return module;
254         }
255 
256         module.setType(GradleModule.Type.JAVA);
257 
258         // add java project dependencies
259         List<IJavaProject> javaDepProjects = getReferencedProjects(javaProject);
260         for (IJavaProject javaDep : javaDepProjects) {
261             // Java project should not reference Android project!
262             if (javaDep.getProject().hasNature(AdtConstants.NATURE_DEFAULT)) {
263                 throw new InternalException(String.format(
264                         "Java project %1$s depends on Android project %2$s!\n" +
265                         "This is not a valid dependency",
266                         javaProject.getProject().getName(), javaDep.getProject().getName()));
267             }
268             GradleModule libModule = processJavaProject(javaDep);
269             module.addDependency(libModule);
270         }
271 
272         return module;
273     }
274 
computeRootAndPaths(Collection<GradleModule> modules)275     private void computeRootAndPaths(Collection<GradleModule> modules) throws InternalException {
276         // compute the common root.
277         mCommonRoot = determineCommonRoot(modules);
278 
279         // compute all the relative paths.
280         for (GradleModule module : modules) {
281             String path = getGradlePath(module.getJavaProject().getProject().getLocation(),
282                     mCommonRoot);
283 
284             module.setPath(path);
285         }
286     }
287 
288     /**
289      * Finds the common parent directory shared by this project and all its dependencies.
290      * If there's only one project, returns the single project's folder.
291      * @throws InternalException
292      */
293     @NonNull
determineCommonRoot(Collection<GradleModule> modules)294     private static IPath determineCommonRoot(Collection<GradleModule> modules)
295             throws InternalException {
296         IPath commonRoot = null;
297         for (GradleModule module : modules) {
298             if (commonRoot == null) {
299                 commonRoot = module.getJavaProject().getProject().getLocation();
300             } else {
301                 commonRoot = findCommonRoot(commonRoot,
302                         module.getJavaProject().getProject().getLocation());
303             }
304         }
305 
306         return commonRoot;
307     }
308 
309     /**
310      * Converts the given path to be relative to the given root path, and converts it to
311      * Gradle project notation, such as is used in the settings.gradle file.
312      */
313     @NonNull
getGradlePath(IPath path, IPath root)314     private static String getGradlePath(IPath path, IPath root) {
315         IPath relativePath = path.makeRelativeTo(root);
316         String relativeString = relativePath.toOSString();
317         return ":" + relativeString.replaceAll(Pattern.quote(File.separator), ":"); //$NON-NLS-1$
318     }
319 
320     /**
321      * Given two IPaths, finds the parent directory of both of them.
322      * @throws InternalException
323      */
324     @NonNull
findCommonRoot(@onNull IPath path1, @NonNull IPath path2)325     private static IPath findCommonRoot(@NonNull IPath path1, @NonNull IPath path2)
326             throws InternalException {
327         if (path1.getDevice() != null && !path1.getDevice().equals(path2.getDevice())) {
328             throw new InternalException(
329                     "Different modules have been detected on different drives.\n" +
330                     "This prevents finding a common root to all modules.");
331         }
332 
333         IPath result = path1.uptoSegment(0);
334 
335         final int count = Math.min(path1.segmentCount(), path2.segmentCount());
336         for (int i = 0; i < count; i++) {
337             if (path1.segment(i).equals(path2.segment(i))) {
338                 result = result.append(Path.SEPARATOR + path2.segment(i));
339             }
340         }
341         return result;
342     }
343 
344     @Nullable
getJavaProject(ProjectState projectState)345     private IJavaProject getJavaProject(ProjectState projectState) {
346         try {
347             return BaseProjectHelper.getJavaProject(projectState.getProject());
348         } catch (CoreException e) {
349             return null;
350         }
351     }
352 
353     @NonNull
createModuleOnDemand(@onNull IJavaProject javaProject)354     private GradleModule createModuleOnDemand(@NonNull IJavaProject javaProject) {
355         GradleModule module = mModules.get(javaProject);
356         if (module == null) {
357             module = new GradleModule(javaProject);
358             mModules.put(javaProject, module);
359         }
360 
361         return module;
362     }
363 
364     @NonNull
getReferencedProjects(IJavaProject javaProject)365     private static List<IJavaProject> getReferencedProjects(IJavaProject javaProject)
366             throws JavaModelException, InternalException {
367 
368         List<IJavaProject> projects = Lists.newArrayList();
369 
370         IClasspathEntry entries[] = javaProject.getRawClasspath();
371         for (IClasspathEntry classpathEntry : entries) {
372             if (classpathEntry.getContentKind() == IPackageFragmentRoot.K_SOURCE
373                     && classpathEntry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
374                 // found required project on build path
375                 String subProjectRoot = classpathEntry.getPath().toString();
376                 IJavaProject subProject = getJavaProject(subProjectRoot);
377                 // is project available in workspace?
378                 if (subProject != null) {
379                     projects.add(subProject);
380                 } else {
381                     throw new InternalException(String.format(
382                             "Project '%s' is missing project dependency '%s' in Eclipse workspace.\n" +
383                             "Make sure all dependencies are opened.",
384                             javaProject.getProject().getName(),
385                             classpathEntry.getPath().toString()));
386                 }
387             }
388         }
389 
390         return projects;
391     }
392 
393     /**
394      * Get Java project for given root.
395      */
396     @Nullable
getJavaProject(String root)397     private static IJavaProject getJavaProject(String root) {
398         IPath path = new Path(root);
399         if (path.segmentCount() == 1) {
400             return getJavaProjectByName(root);
401         }
402         IResource resource = ResourcesPlugin.getWorkspace().getRoot()
403                 .findMember(path);
404         if (resource != null && resource.getType() == IResource.PROJECT) {
405             if (resource.exists()) {
406                 return (IJavaProject) JavaCore.create(resource);
407             }
408         }
409         return null;
410     }
411 
412     /**
413      * Get Java project from resource.
414      */
getJavaProjectByName(String name)415     private static IJavaProject getJavaProjectByName(String name) {
416         try {
417             IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
418             if (project.exists()) {
419                 return JavaCore.create(project);
420             }
421         } catch (IllegalArgumentException iae) {
422         }
423         return null;
424     }
425 }
426