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