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