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