• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;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 {
fileChanged(IProject project, ResourceFile file, int eventType)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 
folderChanged(IProject project, ResourceFolder folder, int eventType)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 
detectCycles(String from)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 
dfs(String from, Set<String> visiting)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 
removeErrors(String from)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 
addError(String from, String chain)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
runLater(Runnable runnable)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      */
findResource(String from)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
create()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 */
Reference(IProject project, String id)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          */
getId()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          */
getFile()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          */
getDisplayName()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          */
getName()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
hashCode()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
equals(Object obj)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
toString()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          */
create(IFile file)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          */
getResourceName()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      */
getInvalidIncludes(IFile layout)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