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