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
17package com.android.ide.eclipse.adt.internal.wizards.exportgradle;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.ide.eclipse.adt.AdtConstants;
22import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
23import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
24import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
25import com.android.ide.eclipse.adt.internal.sdk.Sdk;
26import com.google.common.collect.Lists;
27import com.google.common.collect.Maps;
28
29import org.eclipse.core.resources.IProject;
30import org.eclipse.core.resources.IResource;
31import org.eclipse.core.resources.ResourcesPlugin;
32import org.eclipse.core.runtime.CoreException;
33import org.eclipse.core.runtime.IPath;
34import org.eclipse.core.runtime.Path;
35import org.eclipse.jdt.core.IClasspathEntry;
36import org.eclipse.jdt.core.IJavaProject;
37import org.eclipse.jdt.core.IPackageFragmentRoot;
38import org.eclipse.jdt.core.JavaCore;
39import org.eclipse.jdt.core.JavaModelException;
40
41import java.io.File;
42import java.util.Collection;
43import java.util.List;
44import java.util.Map;
45import java.util.regex.Pattern;
46
47/**
48 * Class to setup the project and its modules.
49 */
50public class ProjectSetupBuilder {
51
52    private final static class InternalException extends Exception {
53        private static final long serialVersionUID = 1L;
54
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
67    public ProjectSetupBuilder() {
68
69    }
70
71    public void setCanGenerate(boolean generate) {
72        mCanGenerate = generate;
73    }
74
75    public void setCanFinish(boolean canFinish) {
76        mCanFinish = canFinish;
77    }
78
79    public boolean canFinish() {
80        return mCanFinish;
81    }
82
83    public boolean canGenerate() {
84        return mCanGenerate;
85    }
86
87    public void setStatus(ExportStatus status) {
88        mStatus = status;
89    }
90
91    public ExportStatus getStatus() {
92        return mStatus;
93    }
94
95    @NonNull
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
128    public Collection<GradleModule> getModules() {
129        return mModules.values();
130    }
131
132    public int getModuleCount() {
133        return mModules.size();
134    }
135
136    @Nullable
137    public IPath getCommonRoot() {
138        return mCommonRoot;
139    }
140
141    @Nullable
142    public GradleModule getModule(IJavaProject javaProject) {
143        return mModules.get(javaProject);
144    }
145
146    public boolean isOriginalProject(@NonNull IJavaProject javaProject) {
147        GradleModule module = mModules.get(javaProject);
148        return mOriginalModules.contains(module);
149    }
150
151    @NonNull
152    public List<GradleModule> getOriginalModules() {
153        return mOriginalModules;
154    }
155
156    @Nullable
157    public List<GradleModule> getShortestDependencyTo(GradleModule module) {
158        return findModule(module, mOriginalModules);
159    }
160
161    @Nullable
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
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
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
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
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
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
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
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
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
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
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
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     */
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