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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 624 public static void start() { 625 assert sListener == null; 626 sListener = new ResourceListener(); 627 ResourceManager.getInstance().addListener(sListener); 628 } 629 630 public static void stop() { 631 assert sListener != null; 632 ResourceManager.getInstance().addListener(sListener); 633 } 634 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 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 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 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 public void fileChanged(IProject project, ResourceFile file, int eventType) { 686 if (sRefreshing) { 687 return; 688 } 689 690 if ((eventType & (CHANGED | ADDED | REMOVED | CONTENT)) == 0) { 691 return; 692 } 693 694 IncludeFinder finder = get(project); 695 if (finder != null) { 696 if (finder.updateFileIncludes(file, true)) { 697 finder.saveSettings(); 698 } 699 } 700 } 701 702 public void folderChanged(IProject project, ResourceFolder folder, int eventType) { 703 // We only care about layout resource files 704 } 705 } 706 707 // ----- Cycle detection ----- 708 709 private void detectCycles(String from) { 710 // Perform DFS on the include graph and look for a cycle; if we find one, produce 711 // a chain of includes on the way back to show to the user 712 if (mIncludes.size() > 0) { 713 Set<String> visiting = new HashSet<String>(mIncludes.size()); 714 String chain = dfs(from, visiting); 715 if (chain != null) { 716 addError(from, chain); 717 } else { 718 // Is there an existing error for us to clean up? 719 removeErrors(from); 720 } 721 } 722 } 723 724 /** Format to chain include cycles in: a=>b=>c=>d etc */ 725 private final String CHAIN_FORMAT = "%1$s=>%2$s"; //$NON-NLS-1$ 726 727 private String dfs(String from, Set<String> visiting) { 728 visiting.add(from); 729 730 List<String> includes = mIncludes.get(from); 731 if (includes != null && includes.size() > 0) { 732 for (String include : includes) { 733 if (visiting.contains(include)) { 734 return String.format(CHAIN_FORMAT, from, include); 735 } 736 String chain = dfs(include, visiting); 737 if (chain != null) { 738 return String.format(CHAIN_FORMAT, from, chain); 739 } 740 } 741 } 742 743 visiting.remove(from); 744 745 return null; 746 } 747 748 private void removeErrors(String from) { 749 final IResource resource = findResource(from); 750 if (resource != null) { 751 try { 752 final String markerId = IMarker.PROBLEM; 753 754 IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO); 755 756 for (final IMarker marker : markers) { 757 String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); 758 if (tmpMsg == null || tmpMsg.startsWith(MESSAGE)) { 759 // Remove 760 runLater(new Runnable() { 761 public void run() { 762 try { 763 sRefreshing = true; 764 marker.delete(); 765 } catch (CoreException e) { 766 AdtPlugin.log(e, "Can't delete problem marker"); 767 } finally { 768 sRefreshing = false; 769 } 770 } 771 }); 772 } 773 } 774 } catch (CoreException e) { 775 // if we couldn't get the markers, then we just mark the file again 776 // (since markerAlreadyExists is initialized to false, we do nothing) 777 } 778 } 779 } 780 781 /** Error message for cycles */ 782 private static final String MESSAGE = "Found cyclical <include> chain"; 783 784 private void addError(String from, String chain) { 785 final IResource resource = findResource(from); 786 if (resource != null) { 787 final String markerId = IMarker.PROBLEM; 788 final String message = String.format("%1$s: %2$s", MESSAGE, chain); 789 final int lineNumber = 1; 790 final int severity = IMarker.SEVERITY_ERROR; 791 792 // check if there's a similar marker already, since aapt is launched twice 793 boolean markerAlreadyExists = false; 794 try { 795 IMarker[] markers = resource.findMarkers(markerId, true, IResource.DEPTH_ZERO); 796 797 for (IMarker marker : markers) { 798 int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); 799 if (tmpLine != lineNumber) { 800 break; 801 } 802 803 int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); 804 if (tmpSeverity != severity) { 805 break; 806 } 807 808 String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); 809 if (tmpMsg == null || tmpMsg.equals(message) == false) { 810 break; 811 } 812 813 // if we're here, all the marker attributes are equals, we found it 814 // and exit 815 markerAlreadyExists = true; 816 break; 817 } 818 819 } catch (CoreException e) { 820 // if we couldn't get the markers, then we just mark the file again 821 // (since markerAlreadyExists is initialized to false, we do nothing) 822 } 823 824 if (!markerAlreadyExists) { 825 runLater(new Runnable() { 826 public void run() { 827 try { 828 sRefreshing = true; 829 830 // Adding a resource will force a refresh on the file; 831 // ignore these updates 832 BaseProjectHelper.markResource(resource, markerId, message, lineNumber, 833 severity); 834 } finally { 835 sRefreshing = false; 836 } 837 } 838 }); 839 } 840 } 841 } 842 843 // FIXME: Find more standard Eclipse way to do this. 844 // We need to run marker registration/deletion "later", because when the include 845 // scanning is running it's in the middle of resource notification, so the IDE 846 // throws an exception 847 private static void runLater(Runnable runnable) { 848 Display display = Display.findDisplay(Thread.currentThread()); 849 if (display != null) { 850 display.asyncExec(runnable); 851 } else { 852 AdtPlugin.log(IStatus.WARNING, "Could not find display"); 853 } 854 } 855 856 /** 857 * Finds the project resource for the given layout path 858 * 859 * @param from the resource name 860 * @return the {@link IResource}, or null if not found 861 */ 862 private IResource findResource(String from) { 863 final IResource resource = mProject.findMember(WS_LAYOUTS + WS_SEP + from + '.' + EXT_XML); 864 return resource; 865 } 866 867 /** 868 * Creates a blank, project-less {@link IncludeFinder} <b>for use by unit tests 869 * only</b> 870 */ 871 @VisibleForTesting 872 /* package */ static IncludeFinder create() { 873 IncludeFinder finder = new IncludeFinder(null); 874 finder.mIncludes = new HashMap<String, List<String>>(); 875 finder.mIncludedBy = new HashMap<String, List<String>>(); 876 return finder; 877 } 878 879 /** A reference to a particular file in the project */ 880 public static class Reference { 881 /** The unique id referencing the file, such as (for res/layout-land/main.xml) 882 * "layout-land/main") */ 883 private final String mId; 884 885 /** The project containing the file */ 886 private final IProject mProject; 887 888 /** The resource name of the file, such as (for res/layout/main.xml) "main" */ 889 private String mName; 890 891 /** Creates a new include reference */ 892 private Reference(IProject project, String id) { 893 super(); 894 mProject = project; 895 mId = id; 896 } 897 898 /** 899 * Returns the id identifying the given file within the project 900 * 901 * @return the id identifying the given file within the project 902 */ 903 public String getId() { 904 return mId; 905 } 906 907 /** 908 * Returns the {@link IFile} in the project for the given file. May return null if 909 * there is an error in locating the file or if the file no longer exists. 910 * 911 * @return the project file, or null 912 */ 913 public IFile getFile() { 914 String reference = mId; 915 if (!reference.contains(WS_SEP)) { 916 reference = FD_RES_LAYOUT + WS_SEP + reference; 917 } 918 919 String projectPath = SdkConstants.FD_RESOURCES + WS_SEP + reference + '.' + EXT_XML; 920 IResource member = mProject.findMember(projectPath); 921 if (member instanceof IFile) { 922 return (IFile) member; 923 } 924 925 return null; 926 } 927 928 /** 929 * Returns a description of this reference, suitable to be shown to the user 930 * 931 * @return a display name for the reference 932 */ 933 public String getDisplayName() { 934 // The ID is deliberately kept in a pretty user-readable format but we could 935 // consider prepending layout/ on ids that don't have it (to make the display 936 // more uniform) or ripping out all layout[-constraint] prefixes out and 937 // instead prepending @ etc. 938 return mId; 939 } 940 941 /** 942 * Returns the name of the reference, suitable for resource lookup. For example, 943 * for "res/layout/main.xml", as well as for "res/layout-land/main.xml", this 944 * would be "main". 945 * 946 * @return the resource name of the reference 947 */ 948 public String getName() { 949 if (mName == null) { 950 mName = mId; 951 int index = mName.lastIndexOf(WS_SEP); 952 if (index != -1) { 953 mName = mName.substring(index + 1); 954 } 955 } 956 957 return mName; 958 } 959 960 @Override 961 public int hashCode() { 962 final int prime = 31; 963 int result = 1; 964 result = prime * result + ((mId == null) ? 0 : mId.hashCode()); 965 return result; 966 } 967 968 @Override 969 public boolean equals(Object obj) { 970 if (this == obj) 971 return true; 972 if (obj == null) 973 return false; 974 if (getClass() != obj.getClass()) 975 return false; 976 Reference other = (Reference) obj; 977 if (mId == null) { 978 if (other.mId != null) 979 return false; 980 } else if (!mId.equals(other.mId)) 981 return false; 982 return true; 983 } 984 985 @Override 986 public String toString() { 987 return "Reference [getId()=" + getId() //$NON-NLS-1$ 988 + ", getDisplayName()=" + getDisplayName() //$NON-NLS-1$ 989 + ", getName()=" + getName() //$NON-NLS-1$ 990 + ", getFile()=" + getFile() + "]"; //$NON-NLS-1$ 991 } 992 993 /** 994 * Creates a reference to the given file 995 * 996 * @param file the file to create a reference for 997 * @return a reference to the given file 998 */ 999 public static Reference create(IFile file) { 1000 return new Reference(file.getProject(), getMapKey(file)); 1001 } 1002 1003 /** 1004 * Returns the resource name of this layout, such as {@code @layout/foo}. 1005 * 1006 * @return the resource name 1007 */ 1008 public String getResourceName() { 1009 return '@' + FD_RES_LAYOUT + '/' + getName(); 1010 } 1011 } 1012 1013 /** 1014 * Returns a collection of layouts (expressed as resource names, such as 1015 * {@code @layout/foo} which would be invalid includes in the given layout 1016 * (because it would introduce a cycle) 1017 * 1018 * @param layout the layout file to check for cyclic dependencies from 1019 * @return a collection of layout resources which cannot be included from 1020 * the given layout, never null 1021 */ 1022 public Collection<String> getInvalidIncludes(IFile layout) { 1023 IProject project = layout.getProject(); 1024 Reference self = Reference.create(layout); 1025 1026 // Add anyone who transitively can reach this file via includes. 1027 LinkedList<Reference> queue = new LinkedList<Reference>(); 1028 List<Reference> invalid = new ArrayList<Reference>(); 1029 queue.add(self); 1030 invalid.add(self); 1031 Set<String> seen = new HashSet<String>(); 1032 seen.add(self.getId()); 1033 while (!queue.isEmpty()) { 1034 Reference reference = queue.removeFirst(); 1035 String refId = reference.getId(); 1036 1037 // Look up both configuration specific includes as well as includes in the 1038 // base versions 1039 List<String> included = getIncludedBy(refId); 1040 if (refId.indexOf('/') != -1) { 1041 List<String> baseIncluded = getIncludedBy(reference.getName()); 1042 if (included == null) { 1043 included = baseIncluded; 1044 } else if (baseIncluded != null) { 1045 included = new ArrayList<String>(included); 1046 included.addAll(baseIncluded); 1047 } 1048 } 1049 1050 if (included != null && included.size() > 0) { 1051 for (String id : included) { 1052 if (!seen.contains(id)) { 1053 seen.add(id); 1054 Reference ref = new Reference(project, id); 1055 invalid.add(ref); 1056 queue.addLast(ref); 1057 } 1058 } 1059 } 1060 } 1061 1062 List<String> result = new ArrayList<String>(); 1063 for (Reference reference : invalid) { 1064 result.add(reference.getResourceName()); 1065 } 1066 1067 return result; 1068 } 1069 } 1070