1 /* 2 * Copyright (C) 2010 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.editors.layout.gle2; 18 19 import static com.android.AndroidConstants.FD_RES_LAYOUT; 20 import static com.android.ide.eclipse.adt.AdtConstants.EXT_XML; 21 import static com.android.ide.eclipse.adt.AdtConstants.WS_LAYOUTS; 22 import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; 23 import static com.android.resources.ResourceType.LAYOUT; 24 import static org.eclipse.core.resources.IResourceDelta.ADDED; 25 import static org.eclipse.core.resources.IResourceDelta.CHANGED; 26 import static org.eclipse.core.resources.IResourceDelta.CONTENT; 27 import static org.eclipse.core.resources.IResourceDelta.REMOVED; 28 29 import com.android.annotations.VisibleForTesting; 30 import com.android.ide.common.resources.ResourceFile; 31 import com.android.ide.common.resources.ResourceFolder; 32 import com.android.ide.common.resources.ResourceItem; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; 35 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 36 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 37 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 38 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener; 39 import com.android.ide.eclipse.adt.io.IFileWrapper; 40 import com.android.io.IAbstractFile; 41 import com.android.resources.ResourceType; 42 import com.android.sdklib.SdkConstants; 43 44 import org.eclipse.core.resources.IFile; 45 import org.eclipse.core.resources.IMarker; 46 import org.eclipse.core.resources.IProject; 47 import org.eclipse.core.resources.IResource; 48 import org.eclipse.core.runtime.CoreException; 49 import org.eclipse.core.runtime.IStatus; 50 import org.eclipse.core.runtime.QualifiedName; 51 import org.eclipse.swt.widgets.Display; 52 import org.eclipse.wst.sse.core.StructuredModelManager; 53 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 54 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 55 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 56 import org.w3c.dom.Document; 57 import org.w3c.dom.Element; 58 import org.w3c.dom.NodeList; 59 60 import java.util.ArrayList; 61 import java.util.Collection; 62 import java.util.Collections; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 import java.util.LinkedList; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Set; 69 70 /** 71 * The include finder finds other XML files that are including a given XML file, and does 72 * so efficiently (caching results across IDE sessions etc). 73 */ 74 @SuppressWarnings("restriction") // XML model 75 public class IncludeFinder { 76 /** Qualified name for the per-project persistent property include-map */ 77 private final static QualifiedName CONFIG_INCLUDES = new QualifiedName(AdtPlugin.PLUGIN_ID, 78 "includes");//$NON-NLS-1$ 79 80 /** 81 * Qualified name for the per-project non-persistent property storing the 82 * {@link IncludeFinder} for this project 83 */ 84 private final static QualifiedName INCLUDE_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, 85 "includefinder"); //$NON-NLS-1$ 86 87 /** Project that the include finder locates includes for */ 88 private final IProject mProject; 89 90 /** Map from a layout resource name to a set of layouts included by the given resource */ 91 private Map<String, List<String>> mIncludes = null; 92 93 /** 94 * Reverse map of {@link #mIncludes}; points to other layouts that are including a 95 * given layouts 96 */ 97 private Map<String, List<String>> mIncludedBy = null; 98 99 /** Flag set during a refresh; ignore updates when this is true */ 100 private static boolean sRefreshing; 101 102 /** Global (cross-project) resource listener */ 103 private static ResourceListener sListener; 104 105 /** 106 * Constructs an {@link IncludeFinder} for the given project. Don't use this method; 107 * use the {@link #get} factory method instead. 108 * 109 * @param project project to create an {@link IncludeFinder} for 110 */ IncludeFinder(IProject project)111 private IncludeFinder(IProject project) { 112 mProject = project; 113 } 114 115 /** 116 * Returns the {@link IncludeFinder} for the given project 117 * 118 * @param project the project the finder is associated with 119 * @return an {@IncludeFinder} for the given project, never null 120 */ get(IProject project)121 public static IncludeFinder get(IProject project) { 122 IncludeFinder finder = null; 123 try { 124 finder = (IncludeFinder) project.getSessionProperty(INCLUDE_FINDER); 125 } catch (CoreException e) { 126 // Not a problem; we will just create a new one 127 } 128 129 if (finder == null) { 130 finder = new IncludeFinder(project); 131 try { 132 project.setSessionProperty(INCLUDE_FINDER, finder); 133 } catch (CoreException e) { 134 AdtPlugin.log(e, "Can't store IncludeFinder"); 135 } 136 } 137 138 return finder; 139 } 140 141 /** 142 * Returns a list of resource names that are included by the given resource 143 * 144 * @param includer the resource name to return included layouts for 145 * @return the layouts included by the given resource 146 */ getIncludesFrom(String includer)147 private List<String> getIncludesFrom(String includer) { 148 ensureInitialized(); 149 150 return mIncludes.get(includer); 151 } 152 153 /** 154 * Gets the list of all other layouts that are including the given layout. 155 * 156 * @param included the file that is included 157 * @return the files that are including the given file, or null or empty 158 */ getIncludedBy(IResource included)159 public List<Reference> getIncludedBy(IResource included) { 160 ensureInitialized(); 161 String mapKey = getMapKey(included); 162 List<String> result = mIncludedBy.get(mapKey); 163 if (result == null) { 164 String name = getResourceName(included); 165 if (!name.equals(mapKey)) { 166 result = mIncludedBy.get(name); 167 } 168 } 169 170 if (result != null && result.size() > 0) { 171 List<Reference> references = new ArrayList<Reference>(result.size()); 172 for (String s : result) { 173 references.add(new Reference(mProject, s)); 174 } 175 return references; 176 } else { 177 return null; 178 } 179 } 180 181 /** 182 * Returns true if the given resource is included from some other layout in the 183 * project 184 * 185 * @param included the resource to check 186 * @return true if the file is included by some other layout 187 */ isIncluded(IResource included)188 public boolean isIncluded(IResource included) { 189 ensureInitialized(); 190 String mapKey = getMapKey(included); 191 List<String> result = mIncludedBy.get(mapKey); 192 if (result == null) { 193 String name = getResourceName(included); 194 if (!name.equals(mapKey)) { 195 result = mIncludedBy.get(name); 196 } 197 } 198 199 return result != null && result.size() > 0; 200 } 201 202 @VisibleForTesting getIncludedBy(String included)203 /* package */ List<String> getIncludedBy(String included) { 204 ensureInitialized(); 205 return mIncludedBy.get(included); 206 } 207 208 /** Initialize the inclusion data structures, if not already done */ ensureInitialized()209 private void ensureInitialized() { 210 if (mIncludes == null) { 211 // Initialize 212 if (!readSettings()) { 213 // Couldn't read settings: probably the first time this code is running 214 // so there is no known data about includes. 215 216 // Yes, these should be multimaps! If we start using Guava replace 217 // these with multimaps. 218 mIncludes = new HashMap<String, List<String>>(); 219 mIncludedBy = new HashMap<String, List<String>>(); 220 221 scanProject(); 222 saveSettings(); 223 } 224 } 225 } 226 227 // ----- Persistence ----- 228 229 /** 230 * Create a String serialization of the includes map. The map attempts to be compact; 231 * it strips out the @layout/ prefix, and eliminates the values for empty string 232 * values. The map can be restored by calling {@link #decodeMap}. The encoded String 233 * will have sorted keys. 234 * 235 * @param map the map to be serialized 236 * @return a serialization (never null) of the given map 237 */ 238 @VisibleForTesting encodeMap(Map<String, List<String>> map)239 public static String encodeMap(Map<String, List<String>> map) { 240 StringBuilder sb = new StringBuilder(); 241 242 if (map != null) { 243 // Process the keys in sorted order rather than just 244 // iterating over the entry set to ensure stable output 245 List<String> keys = new ArrayList<String>(map.keySet()); 246 Collections.sort(keys); 247 for (String key : keys) { 248 List<String> values = map.get(key); 249 250 if (sb.length() > 0) { 251 sb.append(','); 252 } 253 sb.append(key); 254 if (values.size() > 0) { 255 sb.append('=').append('>'); 256 sb.append('{'); 257 boolean first = true; 258 for (String value : values) { 259 if (first) { 260 first = false; 261 } else { 262 sb.append(','); 263 } 264 sb.append(value); 265 } 266 sb.append('}'); 267 } 268 } 269 } 270 271 return sb.toString(); 272 } 273 274 /** 275 * Decodes the encoding (produced by {@link #encodeMap}) back into the original map, 276 * modulo any key sorting differences. 277 * 278 * @param encoded an encoding of a map created by {@link #encodeMap} 279 * @return a map corresponding to the encoded values, never null 280 */ 281 @VisibleForTesting decodeMap(String encoded)282 public static Map<String, List<String>> decodeMap(String encoded) { 283 HashMap<String, List<String>> map = new HashMap<String, List<String>>(); 284 285 if (encoded.length() > 0) { 286 int i = 0; 287 int end = encoded.length(); 288 289 while (i < end) { 290 291 // Find key range 292 int keyBegin = i; 293 int keyEnd = i; 294 while (i < end) { 295 char c = encoded.charAt(i); 296 if (c == ',') { 297 break; 298 } else if (c == '=') { 299 i += 2; // Skip => 300 break; 301 } 302 i++; 303 keyEnd = i; 304 } 305 306 List<String> values = new ArrayList<String>(); 307 // Find values 308 if (i < end && encoded.charAt(i) == '{') { 309 i++; 310 while (i < end) { 311 int valueBegin = i; 312 int valueEnd = i; 313 char c = 0; 314 while (i < end) { 315 c = encoded.charAt(i); 316 if (c == ',' || c == '}') { 317 valueEnd = i; 318 break; 319 } 320 i++; 321 } 322 if (valueEnd > valueBegin) { 323 values.add(encoded.substring(valueBegin, valueEnd)); 324 } 325 326 if (c == '}') { 327 if (i < end-1 && encoded.charAt(i+1) == ',') { 328 i++; 329 } 330 break; 331 } 332 assert c == ','; 333 i++; 334 } 335 } 336 337 String key = encoded.substring(keyBegin, keyEnd); 338 map.put(key, values); 339 i++; 340 } 341 } 342 343 return map; 344 } 345 346 /** 347 * Stores the settings in the persistent project storage. 348 */ saveSettings()349 private void saveSettings() { 350 // Serialize the mIncludes map into a compact String. The mIncludedBy map can be 351 // inferred from it. 352 String encoded = encodeMap(mIncludes); 353 354 try { 355 if (encoded.length() >= 2048) { 356 // The maximum length of a setting key is 2KB, according to the javadoc 357 // for the project class. It's unlikely that we'll 358 // hit this -- even with an average layout root name of 20 characters 359 // we can still store over a hundred names. But JUST IN CASE we run 360 // into this, we'll clear out the key in this name which means that the 361 // information will need to be recomputed in the next IDE session. 362 mProject.setPersistentProperty(CONFIG_INCLUDES, null); 363 } else { 364 String existing = mProject.getPersistentProperty(CONFIG_INCLUDES); 365 if (!encoded.equals(existing)) { 366 mProject.setPersistentProperty(CONFIG_INCLUDES, encoded); 367 } 368 } 369 } catch (CoreException e) { 370 AdtPlugin.log(e, "Can't store include settings"); 371 } 372 } 373 374 /** 375 * Reads previously stored settings from the persistent project storage 376 * 377 * @return true iff settings were restored from the project 378 */ readSettings()379 private boolean readSettings() { 380 try { 381 String encoded = mProject.getPersistentProperty(CONFIG_INCLUDES); 382 if (encoded != null) { 383 mIncludes = decodeMap(encoded); 384 385 // Set up a reverse map, pointing from included files to the files that 386 // included them 387 mIncludedBy = new HashMap<String, List<String>>(2 * mIncludes.size()); 388 for (Map.Entry<String, List<String>> entry : mIncludes.entrySet()) { 389 // File containing the <include> 390 String includer = entry.getKey(); 391 // Files being <include>'ed by the above file 392 List<String> included = entry.getValue(); 393 setIncludedBy(includer, included); 394 } 395 396 return true; 397 } 398 } catch (CoreException e) { 399 AdtPlugin.log(e, "Can't read include settings"); 400 } 401 402 return false; 403 } 404 405 // ----- File scanning ----- 406 407 /** 408 * Scan the whole project for XML layout resources that are performing includes. 409 */ scanProject()410 private void scanProject() { 411 ProjectResources resources = ResourceManager.getInstance().getProjectResources(mProject); 412 if (resources != null) { 413 Collection<ResourceItem> layouts = resources.getResourceItemsOfType(LAYOUT); 414 for (ResourceItem layout : layouts) { 415 List<ResourceFile> sources = layout.getSourceFileList(); 416 for (ResourceFile source : sources) { 417 updateFileIncludes(source, false); 418 } 419 } 420 421 return; 422 } 423 } 424 425 /** 426 * Scans the given {@link ResourceFile} and if it is a layout resource, updates the 427 * includes in it. 428 * 429 * @param resourceFile the {@link ResourceFile} to be scanned for includes (doesn't 430 * have to be only layout XML files; this method will filter the type) 431 * @param singleUpdate true if this is a single file being updated, false otherwise 432 * (e.g. during initial project scanning) 433 * @return true if we updated the includes for the resource file 434 */ updateFileIncludes(ResourceFile resourceFile, boolean singleUpdate)435 private boolean updateFileIncludes(ResourceFile resourceFile, boolean singleUpdate) { 436 Collection<ResourceType> resourceTypes = resourceFile.getResourceTypes(); 437 for (ResourceType type : resourceTypes) { 438 if (type == ResourceType.LAYOUT) { 439 ensureInitialized(); 440 441 List<String> includes = Collections.emptyList(); 442 if (resourceFile.getFile() instanceof IFileWrapper) { 443 IFile file = ((IFileWrapper) resourceFile.getFile()).getIFile(); 444 445 // See if we have an existing XML model for this file; if so, we can 446 // just look directly at the parse tree 447 boolean hadXmlModel = false; 448 IStructuredModel model = null; 449 try { 450 IModelManager modelManager = StructuredModelManager.getModelManager(); 451 model = modelManager.getExistingModelForRead(file); 452 if (model instanceof IDOMModel) { 453 IDOMModel domModel = (IDOMModel) model; 454 Document document = domModel.getDocument(); 455 includes = findIncludesInDocument(document); 456 hadXmlModel = true; 457 } 458 } finally { 459 if (model != null) { 460 model.releaseFromRead(); 461 } 462 } 463 464 // If no XML model we have to read the XML contents and (possibly) parse it. 465 // The actual file may not exist anymore (e.g. when deleting a layout file 466 // or when the workspace is out of sync.) 467 if (!hadXmlModel) { 468 String xml = AdtPlugin.readFile(file); 469 if (xml != null) { 470 includes = findIncludes(xml); 471 } 472 } 473 } else { 474 String xml = AdtPlugin.readFile(resourceFile); 475 if (xml != null) { 476 includes = findIncludes(xml); 477 } 478 } 479 480 String key = getMapKey(resourceFile); 481 if (includes.equals(getIncludesFrom(key))) { 482 // Common case -- so avoid doing settings flush etc 483 return false; 484 } 485 486 boolean detectCycles = singleUpdate; 487 setIncluded(key, includes, detectCycles); 488 489 if (singleUpdate) { 490 saveSettings(); 491 } 492 493 return true; 494 } 495 } 496 497 return false; 498 } 499 500 /** 501 * Finds the list of includes in the given XML content. It attempts quickly return 502 * empty if the file does not include any include tags; it does this by only parsing 503 * if it detects the string <include in the file. 504 */ findIncludes(String xml)505 private List<String> findIncludes(String xml) { 506 int index = xml.indexOf("<include"); //$NON-NLS-1$ 507 if (index != -1) { 508 return findIncludesInXml(xml); 509 } 510 511 return Collections.emptyList(); 512 } 513 514 /** 515 * Parses the given XML content and extracts all the included URLs and returns them 516 * 517 * @param xml layout XML content to be parsed for includes 518 * @return a list of included urls, or null 519 */ findIncludesInXml(String xml)520 private List<String> findIncludesInXml(String xml) { 521 Document document = DomUtilities.parseDocument(xml, false /*logParserErrors*/); 522 if (document != null) { 523 return findIncludesInDocument(document); 524 } 525 526 return Collections.emptyList(); 527 } 528 529 /** Searches the given DOM document and returns the list of includes, if any */ findIncludesInDocument(Document document)530 private List<String> findIncludesInDocument(Document document) { 531 NodeList includes = document.getElementsByTagName(LayoutDescriptors.VIEW_INCLUDE); 532 if (includes.getLength() > 0) { 533 List<String> urls = new ArrayList<String>(); 534 for (int i = 0; i < includes.getLength(); i++) { 535 Element element = (Element) includes.item(i); 536 String url = element.getAttribute(LayoutDescriptors.ATTR_LAYOUT); 537 if (url.length() > 0) { 538 String resourceName = urlToLocalResource(url); 539 if (resourceName != null) { 540 urls.add(resourceName); 541 } 542 } 543 } 544 545 return urls; 546 } 547 548 return Collections.emptyList(); 549 } 550 551 /** 552 * Returns the layout URL to a local resource name (provided the URL is a local 553 * resource, not something in @android etc.) Returns null otherwise. 554 */ urlToLocalResource(String url)555 private static String urlToLocalResource(String url) { 556 if (!url.startsWith("@")) { //$NON-NLS-1$ 557 return null; 558 } 559 int typeEnd = url.indexOf('/', 1); 560 if (typeEnd == -1) { 561 return null; 562 } 563 int nameBegin = typeEnd + 1; 564 int typeBegin = 1; 565 int colon = url.lastIndexOf(':', typeEnd); 566 if (colon != -1) { 567 String packageName = url.substring(typeBegin, colon); 568 if ("android".equals(packageName)) { //$NON-NLS-1$ 569 // Don't want to point to non-local resources 570 return null; 571 } 572 573 typeBegin = colon + 1; 574 assert "layout".equals(url.substring(typeBegin, typeEnd)); //$NON-NLS-1$ 575 } 576 577 return url.substring(nameBegin); 578 } 579 580 /** 581 * Record the list of included layouts from the given layout 582 * 583 * @param includer the layout including other layouts 584 * @param included the layouts that were included by the including layout 585 * @param detectCycles if true, check for cycles and report them as project errors 586 */ 587 @VisibleForTesting setIncluded(String includer, List<String> included, boolean detectCycles)588 /* package */ void setIncluded(String includer, List<String> included, boolean detectCycles) { 589 // Remove previously linked inverse mappings 590 List<String> oldIncludes = mIncludes.get(includer); 591 if (oldIncludes != null && oldIncludes.size() > 0) { 592 for (String includee : oldIncludes) { 593 List<String> includers = mIncludedBy.get(includee); 594 if (includers != null) { 595 includers.remove(includer); 596 } 597 } 598 } 599 600 mIncludes.put(includer, included); 601 // Reverse mapping: for included items, point back to including file 602 setIncludedBy(includer, included); 603 604 if (detectCycles) { 605 detectCycles(includer); 606 } 607 } 608 609 /** Record the list of included layouts from the given layout */ setIncludedBy(String includer, List<String> included)610 private void setIncludedBy(String includer, List<String> included) { 611 for (String target : included) { 612 List<String> list = mIncludedBy.get(target); 613 if (list == null) { 614 list = new ArrayList<String>(2); // We don't expect many includes 615 mIncludedBy.put(target, list); 616 } 617 if (!list.contains(includer)) { 618 list.add(includer); 619 } 620 } 621 } 622 623 /** Start listening on project resources */ start()624 public static void start() { 625 assert sListener == null; 626 sListener = new ResourceListener(); 627 ResourceManager.getInstance().addListener(sListener); 628 } 629 stop()630 public static void stop() { 631 assert sListener != null; 632 ResourceManager.getInstance().addListener(sListener); 633 } 634 getMapKey(ResourceFile resourceFile)635 private static String getMapKey(ResourceFile resourceFile) { 636 IAbstractFile file = resourceFile.getFile(); 637 String name = file.getName(); 638 String folderName = file.getParentFolder().getName(); 639 return getMapKey(folderName, name); 640 } 641 getMapKey(IResource resourceFile)642 private static String getMapKey(IResource resourceFile) { 643 String folderName = resourceFile.getParent().getName(); 644 String name = resourceFile.getName(); 645 return getMapKey(folderName, name); 646 } 647 getResourceName(IResource resourceFile)648 private static String getResourceName(IResource resourceFile) { 649 String name = resourceFile.getName(); 650 int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot 651 if (baseEnd > 0) { 652 name = name.substring(0, baseEnd); 653 } 654 655 return name; 656 } 657 getMapKey(String folderName, String name)658 private static String getMapKey(String folderName, String name) { 659 int baseEnd = name.length() - EXT_XML.length() - 1; // -1: the dot 660 if (baseEnd > 0) { 661 name = name.substring(0, baseEnd); 662 } 663 664 // Create a map key for the given resource file 665 // This will map 666 // /res/layout/foo.xml => "foo" 667 // /res/layout-land/foo.xml => "-land/foo" 668 669 if (FD_RES_LAYOUT.equals(folderName)) { 670 // Normal case -- keep just the basename 671 return name; 672 } else { 673 // Store the relative path from res/ on down, so 674 // /res/layout-land/foo.xml becomes "layout-land/foo" 675 //if (folderName.startsWith(FD_LAYOUT)) { 676 // folderName = folderName.substring(FD_LAYOUT.length()); 677 //} 678 679 return folderName + WS_SEP + name; 680 } 681 } 682 683 /** Listener of resource file saves, used to update layout inclusion data structures */ 684 private static class ResourceListener implements IResourceListener { 685 @Override fileChanged(IProject project, ResourceFile file, int eventType)686 public void fileChanged(IProject project, ResourceFile file, int eventType) { 687 if (sRefreshing) { 688 return; 689 } 690 691 if ((eventType & (CHANGED | ADDED | REMOVED | CONTENT)) == 0) { 692 return; 693 } 694 695 IncludeFinder finder = get(project); 696 if (finder != null) { 697 if (finder.updateFileIncludes(file, true)) { 698 finder.saveSettings(); 699 } 700 } 701 } 702 703 @Override folderChanged(IProject project, ResourceFolder folder, int eventType)704 public void folderChanged(IProject project, ResourceFolder folder, int eventType) { 705 // We only care about layout resource files 706 } 707 } 708 709 // ----- Cycle detection ----- 710 detectCycles(String from)711 private void detectCycles(String from) { 712 // Perform DFS on the include graph and look for a cycle; if we find one, produce 713 // a chain of includes on the way back to show to the user 714 if (mIncludes.size() > 0) { 715 Set<String> visiting = new HashSet<String>(mIncludes.size()); 716 String chain = dfs(from, visiting); 717 if (chain != null) { 718 addError(from, chain); 719 } else { 720 // Is there an existing error for us to clean up? 721 removeErrors(from); 722 } 723 } 724 } 725 726 /** Format to chain include cycles in: a=>b=>c=>d etc */ 727 private final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$ 728 dfs(String from, Set<String> visiting)729 private String dfs(String from, Set<String> visiting) { 730 visiting.add(from); 731 732 List<String> includes = mIncludes.get(from); 733 if (includes != null && includes.size() > 0) { 734 for (String include : includes) { 735 if (visiting.contains(include)) { 736 return String.format(CHAIN_FORMAT, from, include); 737 } 738 String chain = dfs(include, visiting); 739 if (chain != null) { 740 return String.format(CHAIN_FORMAT, from, chain); 741 } 742 } 743 } 744 745 visiting.remove(from); 746 747 return null; 748 } 749 removeErrors(String from)750 private void removeErrors(String from) { 751 final IResource resource = findResource(from); 752 if (resource != null) { 753 try { 754 final String markerId = IMarker.PROBLEM; 755 756 IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO); 757 758 for (final IMarker marker : markers) { 759 String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); 760 if (tmpMsg == null || tmpMsg.startsWith(MESSAGE)) { 761 // Remove 762 runLater(new Runnable() { 763 @Override 764 public void run() { 765 try { 766 sRefreshing = true; 767 marker.delete(); 768 } catch (CoreException e) { 769 AdtPlugin.log(e, "Can't delete problem marker"); 770 } finally { 771 sRefreshing = false; 772 } 773 } 774 }); 775 } 776 } 777 } catch (CoreException e) { 778 // if we couldn't get the markers, then we just mark the file again 779 // (since markerAlreadyExists is initialized to false, we do nothing) 780 } 781 } 782 } 783 784 /** Error message for cycles */ 785 private static final String MESSAGE = "Found cyclical <include> chain"; 786 addError(String from, String chain)787 private void addError(String from, String chain) { 788 final IResource resource = findResource(from); 789 if (resource != null) { 790 final String markerId = IMarker.PROBLEM; 791 final String message = String.format("%1$s: %2$s", MESSAGE, chain); 792 final int lineNumber = 1; 793 final int severity = IMarker.SEVERITY_ERROR; 794 795 // check if there's a similar marker already, since aapt is launched twice 796 boolean markerAlreadyExists = false; 797 try { 798 IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO); 799 800 for (IMarker marker : markers) { 801 int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); 802 if (tmpLine != lineNumber) { 803 break; 804 } 805 806 int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); 807 if (tmpSeverity != severity) { 808 break; 809 } 810 811 String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); 812 if (tmpMsg == null || tmpMsg.equals(message) == false) { 813 break; 814 } 815 816 // if we're here, all the marker attributes are equals, we found it 817 // and exit 818 markerAlreadyExists = true; 819 break; 820 } 821 822 } catch (CoreException e) { 823 // if we couldn't get the markers, then we just mark the file again 824 // (since markerAlreadyExists is initialized to false, we do nothing) 825 } 826 827 if (!markerAlreadyExists) { 828 runLater(new Runnable() { 829 @Override 830 public void run() { 831 try { 832 sRefreshing = true; 833 834 // Adding a resource will force a refresh on the file; 835 // ignore these updates 836 BaseProjectHelper.markResource(resource, markerId, message, lineNumber, 837 severity); 838 } finally { 839 sRefreshing = false; 840 } 841 } 842 }); 843 } 844 } 845 } 846 847 // FIXME: Find more standard Eclipse way to do this. 848 // We need to run marker registration/deletion "later", because when the include 849 // scanning is running it's in the middle of resource notification, so the IDE 850 // throws an exception runLater(Runnable runnable)851 private static void runLater(Runnable runnable) { 852 Display display = Display.findDisplay(Thread.currentThread()); 853 if (display != null) { 854 display.asyncExec(runnable); 855 } else { 856 AdtPlugin.log(IStatus.WARNING, "Could not find display"); 857 } 858 } 859 860 /** 861 * Finds the project resource for the given layout path 862 * 863 * @param from the resource name 864 * @return the {@link IResource}, or null if not found 865 */ findResource(String from)866 private IResource findResource(String from) { 867 final IResource resource = mProject.findMember(WS_LAYOUTS + WS_SEP + from + '.' + EXT_XML); 868 return resource; 869 } 870 871 /** 872 * Creates a blank, project-less {@link IncludeFinder} <b>for use by unit tests 873 * only</b> 874 */ 875 @VisibleForTesting create()876 /* package */ static IncludeFinder create() { 877 IncludeFinder finder = new IncludeFinder(null); 878 finder.mIncludes = new HashMap<String, List<String>>(); 879 finder.mIncludedBy = new HashMap<String, List<String>>(); 880 return finder; 881 } 882 883 /** A reference to a particular file in the project */ 884 public static class Reference { 885 /** The unique id referencing the file, such as (for res/layout-land/main.xml) 886 * "layout-land/main") */ 887 private final String mId; 888 889 /** The project containing the file */ 890 private final IProject mProject; 891 892 /** The resource name of the file, such as (for res/layout/main.xml) "main" */ 893 private String mName; 894 895 /** Creates a new include reference */ Reference(IProject project, String id)896 private Reference(IProject project, String id) { 897 super(); 898 mProject = project; 899 mId = id; 900 } 901 902 /** 903 * Returns the id identifying the given file within the project 904 * 905 * @return the id identifying the given file within the project 906 */ getId()907 public String getId() { 908 return mId; 909 } 910 911 /** 912 * Returns the {@link IFile} in the project for the given file. May return null if 913 * there is an error in locating the file or if the file no longer exists. 914 * 915 * @return the project file, or null 916 */ getFile()917 public IFile getFile() { 918 String reference = mId; 919 if (!reference.contains(WS_SEP)) { 920 reference = FD_RES_LAYOUT + WS_SEP + reference; 921 } 922 923 String projectPath = SdkConstants.FD_RESOURCES + WS_SEP + reference + '.' + EXT_XML; 924 IResource member = mProject.findMember(projectPath); 925 if (member instanceof IFile) { 926 return (IFile) member; 927 } 928 929 return null; 930 } 931 932 /** 933 * Returns a description of this reference, suitable to be shown to the user 934 * 935 * @return a display name for the reference 936 */ getDisplayName()937 public String getDisplayName() { 938 // The ID is deliberately kept in a pretty user-readable format but we could 939 // consider prepending layout/ on ids that don't have it (to make the display 940 // more uniform) or ripping out all layout[-constraint] prefixes out and 941 // instead prepending @ etc. 942 return mId; 943 } 944 945 /** 946 * Returns the name of the reference, suitable for resource lookup. For example, 947 * for "res/layout/main.xml", as well as for "res/layout-land/main.xml", this 948 * would be "main". 949 * 950 * @return the resource name of the reference 951 */ getName()952 public String getName() { 953 if (mName == null) { 954 mName = mId; 955 int index = mName.lastIndexOf(WS_SEP); 956 if (index != -1) { 957 mName = mName.substring(index + 1); 958 } 959 } 960 961 return mName; 962 } 963 964 @Override hashCode()965 public int hashCode() { 966 final int prime = 31; 967 int result = 1; 968 result = prime * result + ((mId == null) ? 0 : mId.hashCode()); 969 return result; 970 } 971 972 @Override equals(Object obj)973 public boolean equals(Object obj) { 974 if (this == obj) 975 return true; 976 if (obj == null) 977 return false; 978 if (getClass() != obj.getClass()) 979 return false; 980 Reference other = (Reference) obj; 981 if (mId == null) { 982 if (other.mId != null) 983 return false; 984 } else if (!mId.equals(other.mId)) 985 return false; 986 return true; 987 } 988 989 @Override toString()990 public String toString() { 991 return "Reference [getId()=" + getId() //$NON-NLS-1$ 992 + ", getDisplayName()=" + getDisplayName() //$NON-NLS-1$ 993 + ", getName()=" + getName() //$NON-NLS-1$ 994 + ", getFile()=" + getFile() + "]"; //$NON-NLS-1$ 995 } 996 997 /** 998 * Creates a reference to the given file 999 * 1000 * @param file the file to create a reference for 1001 * @return a reference to the given file 1002 */ create(IFile file)1003 public static Reference create(IFile file) { 1004 return new Reference(file.getProject(), getMapKey(file)); 1005 } 1006 1007 /** 1008 * Returns the resource name of this layout, such as {@code @layout/foo}. 1009 * 1010 * @return the resource name 1011 */ getResourceName()1012 public String getResourceName() { 1013 return '@' + FD_RES_LAYOUT + '/' + getName(); 1014 } 1015 } 1016 1017 /** 1018 * Returns a collection of layouts (expressed as resource names, such as 1019 * {@code @layout/foo} which would be invalid includes in the given layout 1020 * (because it would introduce a cycle) 1021 * 1022 * @param layout the layout file to check for cyclic dependencies from 1023 * @return a collection of layout resources which cannot be included from 1024 * the given layout, never null 1025 */ getInvalidIncludes(IFile layout)1026 public Collection<String> getInvalidIncludes(IFile layout) { 1027 IProject project = layout.getProject(); 1028 Reference self = Reference.create(layout); 1029 1030 // Add anyone who transitively can reach this file via includes. 1031 LinkedList<Reference> queue = new LinkedList<Reference>(); 1032 List<Reference> invalid = new ArrayList<Reference>(); 1033 queue.add(self); 1034 invalid.add(self); 1035 Set<String> seen = new HashSet<String>(); 1036 seen.add(self.getId()); 1037 while (!queue.isEmpty()) { 1038 Reference reference = queue.removeFirst(); 1039 String refId = reference.getId(); 1040 1041 // Look up both configuration specific includes as well as includes in the 1042 // base versions 1043 List<String> included = getIncludedBy(refId); 1044 if (refId.indexOf('/') != -1) { 1045 List<String> baseIncluded = getIncludedBy(reference.getName()); 1046 if (included == null) { 1047 included = baseIncluded; 1048 } else if (baseIncluded != null) { 1049 included = new ArrayList<String>(included); 1050 included.addAll(baseIncluded); 1051 } 1052 } 1053 1054 if (included != null && included.size() > 0) { 1055 for (String id : included) { 1056 if (!seen.contains(id)) { 1057 seen.add(id); 1058 Reference ref = new Reference(project, id); 1059 invalid.add(ref); 1060 queue.addLast(ref); 1061 } 1062 } 1063 } 1064 } 1065 1066 List<String> result = new ArrayList<String>(); 1067 for (Reference reference : invalid) { 1068 result.add(reference.getResourceName()); 1069 } 1070 1071 return result; 1072 } 1073 } 1074