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 { 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 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 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