1 /*
2  * Copyright (C) 2007 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.project;
18 
19 import com.android.SdkConstants;
20 import com.android.annotations.NonNull;
21 import com.android.annotations.Nullable;
22 import com.android.ide.eclipse.adt.AdtConstants;
23 import com.android.ide.eclipse.adt.AdtPlugin;
24 import com.google.common.collect.Lists;
25 
26 import org.eclipse.core.resources.IFolder;
27 import org.eclipse.core.resources.IMarker;
28 import org.eclipse.core.resources.IProject;
29 import org.eclipse.core.resources.IResource;
30 import org.eclipse.core.resources.IWorkspaceRoot;
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.NullProgressMonitor;
35 import org.eclipse.jdt.core.Flags;
36 import org.eclipse.jdt.core.IClasspathEntry;
37 import org.eclipse.jdt.core.IJavaModel;
38 import org.eclipse.jdt.core.IJavaProject;
39 import org.eclipse.jdt.core.IMethod;
40 import org.eclipse.jdt.core.IType;
41 import org.eclipse.jdt.core.ITypeHierarchy;
42 import org.eclipse.jdt.core.JavaCore;
43 import org.eclipse.jdt.core.JavaModelException;
44 import org.eclipse.jdt.ui.JavaUI;
45 import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction;
46 import org.eclipse.jface.text.BadLocationException;
47 import org.eclipse.jface.text.IDocument;
48 import org.eclipse.jface.text.IRegion;
49 import org.eclipse.ui.IEditorInput;
50 import org.eclipse.ui.IEditorPart;
51 import org.eclipse.ui.IWorkbench;
52 import org.eclipse.ui.IWorkbenchPage;
53 import org.eclipse.ui.IWorkbenchWindow;
54 import org.eclipse.ui.PartInitException;
55 import org.eclipse.ui.PlatformUI;
56 import org.eclipse.ui.texteditor.IDocumentProvider;
57 import org.eclipse.ui.texteditor.ITextEditor;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 /**
63  * Utility methods to manipulate projects.
64  */
65 public final class BaseProjectHelper {
66 
67     public static final String TEST_CLASS_OK = null;
68 
69     /**
70      * Project filter to be used with {@link BaseProjectHelper#getAndroidProjects(IProjectFilter)}.
71      */
72     public static interface IProjectFilter {
accept(IProject project)73         boolean accept(IProject project);
74     }
75 
76     /**
77      * returns a list of source classpath for a specified project
78      * @param javaProject
79      * @return a list of path relative to the workspace root.
80      */
81     @NonNull
getSourceClasspaths(IJavaProject javaProject)82     public static List<IPath> getSourceClasspaths(IJavaProject javaProject) {
83         List<IPath> sourceList = Lists.newArrayList();
84         IClasspathEntry[] classpaths = javaProject.readRawClasspath();
85         if (classpaths != null) {
86             for (IClasspathEntry e : classpaths) {
87                 if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
88                     sourceList.add(e.getPath());
89                 }
90             }
91         }
92 
93         return sourceList;
94     }
95 
96     /**
97      * returns a list of source classpath for a specified project
98      * @param project
99      * @return a list of path relative to the workspace root.
100      */
getSourceClasspaths(IProject project)101     public static List<IPath> getSourceClasspaths(IProject project) {
102         IJavaProject javaProject = JavaCore.create(project);
103         return getSourceClasspaths(javaProject);
104     }
105 
106     /**
107      * Adds a marker to a file on a specific line. This methods catches thrown
108      * {@link CoreException}, and returns null instead.
109      * @param resource the resource to be marked
110      * @param markerId The id of the marker to add.
111      * @param message the message associated with the mark
112      * @param lineNumber the line number where to put the mark. If line is < 1, it puts the marker
113      * on line 1,
114      * @param severity the severity of the marker.
115      * @return the IMarker that was added or null if it failed to add one.
116      */
markResource(IResource resource, String markerId, String message, int lineNumber, int severity)117     public final static IMarker markResource(IResource resource, String markerId,
118             String message, int lineNumber, int severity) {
119         return markResource(resource, markerId, message, lineNumber, -1, -1, severity);
120     }
121 
122     /**
123      * Adds a marker to a file on a specific line, for a specific range of text. This
124      * methods catches thrown {@link CoreException}, and returns null instead.
125      *
126      * @param resource the resource to be marked
127      * @param markerId The id of the marker to add.
128      * @param message the message associated with the mark
129      * @param lineNumber the line number where to put the mark. If line is < 1, it puts
130      *            the marker on line 1,
131      * @param startOffset the beginning offset of the marker (relative to the beginning of
132      *            the document, not the line), or -1 for no range
133      * @param endOffset the ending offset of the marker
134      * @param severity the severity of the marker.
135      * @return the IMarker that was added or null if it failed to add one.
136      */
137     @Nullable
markResource(IResource resource, String markerId, String message, int lineNumber, int startOffset, int endOffset, int severity)138     public final static IMarker markResource(IResource resource, String markerId,
139                 String message, int lineNumber, int startOffset, int endOffset, int severity) {
140         if (!resource.isAccessible()) {
141             return null;
142         }
143 
144         try {
145             IMarker marker = resource.createMarker(markerId);
146             marker.setAttribute(IMarker.MESSAGE, message);
147             marker.setAttribute(IMarker.SEVERITY, severity);
148 
149             // if marker is text type, enforce a line number so that it shows in the editor
150             // somewhere (line 1)
151             if (lineNumber < 1 && marker.isSubtypeOf(IMarker.TEXT)) {
152                 lineNumber = 1;
153             }
154 
155             if (lineNumber >= 1) {
156                 marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
157             }
158 
159             if (startOffset != -1) {
160                 marker.setAttribute(IMarker.CHAR_START, startOffset);
161                 marker.setAttribute(IMarker.CHAR_END, endOffset);
162             }
163 
164             // on Windows, when adding a marker to a project, it takes a refresh for the marker
165             // to show. In order to fix this we're forcing a refresh of elements receiving
166             // markers (and only the element, not its children), to force the marker display.
167             resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
168 
169             return marker;
170         } catch (CoreException e) {
171             AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$
172                     markerId, resource.getFullPath());
173         }
174 
175         return null;
176     }
177 
178     /**
179      * Adds a marker to a resource. This methods catches thrown {@link CoreException},
180      * and returns null instead.
181      * @param resource the file to be marked
182      * @param markerId The id of the marker to add.
183      * @param message the message associated with the mark
184      * @param severity the severity of the marker.
185      * @return the IMarker that was added or null if it failed to add one.
186      */
187     @Nullable
markResource(IResource resource, String markerId, String message, int severity)188     public final static IMarker markResource(IResource resource, String markerId,
189             String message, int severity) {
190         return markResource(resource, markerId, message, -1, severity);
191     }
192 
193     /**
194      * Adds a marker to an {@link IProject}. This method does not catch {@link CoreException}, like
195      * {@link #markResource(IResource, String, String, int)}.
196      *
197      * @param project the project to be marked
198      * @param markerId The id of the marker to add.
199      * @param message the message associated with the mark
200      * @param severity the severity of the marker.
201      * @param priority the priority of the marker
202      * @return the IMarker that was added.
203      * @throws CoreException if the marker cannot be added
204      */
205     @Nullable
markProject(IProject project, String markerId, String message, int severity, int priority)206     public final static IMarker markProject(IProject project, String markerId,
207             String message, int severity, int priority) throws CoreException {
208         if (!project.isAccessible()) {
209             return null;
210         }
211 
212         IMarker marker = project.createMarker(markerId);
213         marker.setAttribute(IMarker.MESSAGE, message);
214         marker.setAttribute(IMarker.SEVERITY, severity);
215         marker.setAttribute(IMarker.PRIORITY, priority);
216 
217         // on Windows, when adding a marker to a project, it takes a refresh for the marker
218         // to show. In order to fix this we're forcing a refresh of elements receiving
219         // markers (and only the element, not its children), to force the marker display.
220         project.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
221 
222         return marker;
223     }
224 
225     /**
226      * Tests that a class name is valid for usage in the manifest.
227      * <p/>
228      * This tests the class existence, that it can be instantiated (ie it must not be abstract,
229      * nor non static if enclosed), and that it extends the proper super class (not necessarily
230      * directly)
231      * @param javaProject the {@link IJavaProject} containing the class.
232      * @param className the fully qualified name of the class to test.
233      * @param superClassName the fully qualified name of the expected super class.
234      * @param testVisibility if <code>true</code>, the method will check the visibility of the class
235      * or of its constructors.
236      * @return {@link #TEST_CLASS_OK} or an error message.
237      */
testClassForManifest(IJavaProject javaProject, String className, String superClassName, boolean testVisibility)238     public final static String testClassForManifest(IJavaProject javaProject, String className,
239             String superClassName, boolean testVisibility) {
240         try {
241             // replace $ by .
242             String javaClassName = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$
243 
244             // look for the IType object for this class
245             IType type = javaProject.findType(javaClassName);
246             if (type != null && type.exists()) {
247                 // test that the class is not abstract
248                 int flags = type.getFlags();
249                 if (Flags.isAbstract(flags)) {
250                     return String.format("%1$s is abstract", className);
251                 }
252 
253                 // test whether the class is public or not.
254                 if (testVisibility && Flags.isPublic(flags) == false) {
255                     // if its not public, it may have a public default constructor,
256                     // which would then be fine.
257                     IMethod basicConstructor = type.getMethod(type.getElementName(), new String[0]);
258                     if (basicConstructor != null && basicConstructor.exists()) {
259                         int constructFlags = basicConstructor.getFlags();
260                         if (Flags.isPublic(constructFlags) == false) {
261                             return String.format(
262                                     "%1$s or its default constructor must be public for the system to be able to instantiate it",
263                                     className);
264                         }
265                     } else {
266                         return String.format(
267                                 "%1$s must be public, or the system will not be able to instantiate it.",
268                                 className);
269                     }
270                 }
271 
272                 // If it's enclosed, test that it's static. If its declaring class is enclosed
273                 // as well, test that it is also static, and public.
274                 IType declaringType = type;
275                 do {
276                     IType tmpType = declaringType.getDeclaringType();
277                     if (tmpType != null) {
278                         if (tmpType.exists()) {
279                             flags = declaringType.getFlags();
280                             if (Flags.isStatic(flags) == false) {
281                                 return String.format("%1$s is enclosed, but not static",
282                                         declaringType.getFullyQualifiedName());
283                             }
284 
285                             flags = tmpType.getFlags();
286                             if (testVisibility && Flags.isPublic(flags) == false) {
287                                 return String.format("%1$s is not public",
288                                         tmpType.getFullyQualifiedName());
289                             }
290                         } else {
291                             // if it doesn't exist, we need to exit so we may as well mark it null.
292                             tmpType = null;
293                         }
294                     }
295                     declaringType = tmpType;
296                 } while (declaringType != null);
297 
298                 // test the class inherit from the specified super class.
299                 // get the type hierarchy
300                 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
301 
302                 // if the super class is not the reference class, it may inherit from
303                 // it so we get its supertype. At some point it will be null and we
304                 // will stop
305                 IType superType = type;
306                 boolean foundProperSuperClass = false;
307                 while ((superType = hierarchy.getSuperclass(superType)) != null &&
308                         superType.exists()) {
309                     if (superClassName.equals(superType.getFullyQualifiedName())) {
310                         foundProperSuperClass = true;
311                     }
312                 }
313 
314                 // didn't find the proper superclass? return false.
315                 if (foundProperSuperClass == false) {
316                     return String.format("%1$s does not extend %2$s", className, superClassName);
317                 }
318 
319                 return TEST_CLASS_OK;
320             } else {
321                 return String.format("Class %1$s does not exist", className);
322             }
323         } catch (JavaModelException e) {
324             return String.format("%1$s: %2$s", className, e.getMessage());
325         }
326     }
327 
328     /**
329      * Returns the {@link IJavaProject} for a {@link IProject} object.
330      * <p/>
331      * This checks if the project has the Java Nature first.
332      * @param project
333      * @return the IJavaProject or null if the project couldn't be created or if the project
334      * does not have the Java Nature.
335      * @throws CoreException if this method fails. Reasons include:
336      * <ul><li>This project does not exist.</li><li>This project is not open.</li></ul>
337      */
getJavaProject(IProject project)338     public static IJavaProject getJavaProject(IProject project) throws CoreException {
339         if (project != null && project.hasNature(JavaCore.NATURE_ID)) {
340             return JavaCore.create(project);
341         }
342         return null;
343     }
344 
345     /**
346      * Reveals a specific line in the source file defining a specified class,
347      * for a specific project.
348      * @param project
349      * @param className
350      * @param line
351      * @return true if the source was revealed
352      */
revealSource(IProject project, String className, int line)353     public static boolean revealSource(IProject project, String className, int line) {
354         // Inner classes are pointless: All we need is the enclosing type to find the file, and the
355         // line number.
356         // Since the anonymous ones will cause IJavaProject#findType to fail, we remove
357         // all of them.
358         int pos = className.indexOf('$');
359         if (pos != -1) {
360             className = className.substring(0, pos);
361         }
362 
363         // get the java project
364         IJavaProject javaProject = JavaCore.create(project);
365 
366         try {
367             // look for the IType matching the class name.
368             IType result = javaProject.findType(className);
369             if (result != null && result.exists()) {
370                 // before we show the type in an editor window, we make sure the current
371                 // workbench page has an editor area (typically the ddms perspective doesn't).
372                 IWorkbench workbench = PlatformUI.getWorkbench();
373                 IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
374                 IWorkbenchPage page = window.getActivePage();
375                 if (page.isEditorAreaVisible() == false) {
376                     // no editor area? we open the java perspective.
377                     new OpenJavaPerspectiveAction().run();
378                 }
379 
380                 IEditorPart editor = JavaUI.openInEditor(result);
381                 if (editor instanceof ITextEditor) {
382                     // get the text editor that was just opened.
383                     ITextEditor textEditor = (ITextEditor)editor;
384 
385                     IEditorInput input = textEditor.getEditorInput();
386 
387                     // get the location of the line to show.
388                     IDocumentProvider documentProvider = textEditor.getDocumentProvider();
389                     IDocument document = documentProvider.getDocument(input);
390                     IRegion lineInfo = document.getLineInformation(line - 1);
391 
392                     // select and reveal the line.
393                     textEditor.selectAndReveal(lineInfo.getOffset(), lineInfo.getLength());
394                 }
395 
396                 return true;
397             }
398         } catch (JavaModelException e) {
399         } catch (PartInitException e) {
400         } catch (BadLocationException e) {
401         }
402 
403         return false;
404     }
405 
406     /**
407      * Returns the list of android-flagged projects. This list contains projects that are opened
408      * in the workspace and that are flagged as android project (through the android nature)
409      * @param filter an optional filter to control which android project are returned. Can be null.
410      * @return an array of IJavaProject, which can be empty if no projects match.
411      */
getAndroidProjects(@ullable IProjectFilter filter)412     public static @NonNull IJavaProject[] getAndroidProjects(@Nullable IProjectFilter filter) {
413         IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
414         IJavaModel javaModel = JavaCore.create(workspaceRoot);
415 
416         return getAndroidProjects(javaModel, filter);
417     }
418 
419     /**
420      * Returns the list of android-flagged projects for the specified java Model.
421      * This list contains projects that are opened in the workspace and that are flagged as android
422      * project (through the android nature)
423      * @param javaModel the Java Model object corresponding for the current workspace root.
424      * @param filter an optional filter to control which android project are returned. Can be null.
425      * @return an array of IJavaProject, which can be empty if no projects match.
426      */
427     @NonNull
getAndroidProjects(@onNull IJavaModel javaModel, @Nullable IProjectFilter filter)428     public static IJavaProject[] getAndroidProjects(@NonNull IJavaModel javaModel,
429             @Nullable IProjectFilter filter) {
430         // get the java projects
431         IJavaProject[] javaProjectList = null;
432         try {
433             javaProjectList  = javaModel.getJavaProjects();
434         }
435         catch (JavaModelException jme) {
436             return new IJavaProject[0];
437         }
438 
439         // temp list to build the android project array
440         ArrayList<IJavaProject> androidProjectList = new ArrayList<IJavaProject>();
441 
442         // loop through the projects and add the android flagged projects to the temp list.
443         for (IJavaProject javaProject : javaProjectList) {
444             // get the workspace project object
445             IProject project = javaProject.getProject();
446 
447             // check if it's an android project based on its nature
448             if (isAndroidProject(project)) {
449                 if (filter == null || filter.accept(project)) {
450                     androidProjectList.add(javaProject);
451                 }
452             }
453         }
454 
455         // return the android projects list.
456         return androidProjectList.toArray(new IJavaProject[androidProjectList.size()]);
457     }
458 
459     /**
460      * Returns true if the given project is an Android project (e.g. is a Java project
461      * that also has the Android nature)
462      *
463      * @param project the project to test
464      * @return true if the given project is an Android project
465      */
isAndroidProject(IProject project)466     public static boolean isAndroidProject(IProject project) {
467         // check if it's an android project based on its nature
468         try {
469             return project.hasNature(AdtConstants.NATURE_DEFAULT);
470         } catch (CoreException e) {
471             // this exception, thrown by IProject.hasNature(), means the project either doesn't
472             // exist or isn't opened. So, in any case we just skip it (the exception will
473             // bypass the ArrayList.add()
474         }
475 
476         return false;
477     }
478 
479     /**
480      * Returns the {@link IFolder} representing the output for the project for Android specific
481      * files.
482      * <p>
483      * The project must be a java project and be opened, or the method will return null.
484      * @param project the {@link IProject}
485      * @return an IFolder item or null.
486      */
getJavaOutputFolder(IProject project)487     public final static IFolder getJavaOutputFolder(IProject project) {
488         try {
489             if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) {
490                 // get a java project from the normal project object
491                 IJavaProject javaProject = JavaCore.create(project);
492 
493                 IPath path = javaProject.getOutputLocation();
494                 path = path.removeFirstSegments(1);
495                 return project.getFolder(path);
496             }
497         } catch (JavaModelException e) {
498             // Let's do nothing and return null
499         } catch (CoreException e) {
500             // Let's do nothing and return null
501         }
502         return null;
503     }
504 
505     /**
506      * Returns the {@link IFolder} representing the output for the project for compiled Java
507      * files.
508      * <p>
509      * The project must be a java project and be opened, or the method will return null.
510      * @param project the {@link IProject}
511      * @return an IFolder item or null.
512      */
513     @Nullable
getAndroidOutputFolder(IProject project)514     public final static IFolder getAndroidOutputFolder(IProject project) {
515         try {
516             if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) {
517                 return project.getFolder(SdkConstants.FD_OUTPUT);
518             }
519         } catch (JavaModelException e) {
520             // Let's do nothing and return null
521         } catch (CoreException e) {
522             // Let's do nothing and return null
523         }
524         return null;
525     }
526 
527 }
528