Home | History | Annotate | Download | only in gle2
      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 &lt;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