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