Home | History | Annotate | Download | only in resources
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
      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.common.resources;
     18 
     19 import com.android.AndroidConstants;
     20 import com.android.ide.common.rendering.api.ResourceValue;
     21 import com.android.ide.common.resources.configuration.Configurable;
     22 import com.android.ide.common.resources.configuration.FolderConfiguration;
     23 import com.android.ide.common.resources.configuration.LanguageQualifier;
     24 import com.android.ide.common.resources.configuration.RegionQualifier;
     25 import com.android.io.IAbstractFile;
     26 import com.android.io.IAbstractFolder;
     27 import com.android.io.IAbstractResource;
     28 import com.android.resources.FolderTypeRelationship;
     29 import com.android.resources.ResourceFolderType;
     30 import com.android.resources.ResourceType;
     31 
     32 import java.io.IOException;
     33 import java.util.ArrayList;
     34 import java.util.Collection;
     35 import java.util.Collections;
     36 import java.util.EnumMap;
     37 import java.util.HashMap;
     38 import java.util.IdentityHashMap;
     39 import java.util.List;
     40 import java.util.Map;
     41 import java.util.SortedSet;
     42 import java.util.TreeSet;
     43 
     44 /**
     45  * Base class for resource repository.
     46  *
     47  * A repository is both a file representation of a resource folder and a representation
     48  * of the generated resources, organized by type.
     49  *
     50  * {@link #getResourceFolder(IAbstractFolder)} and {@link #getSourceFiles(ResourceType, String, FolderConfiguration)}
     51  * give access to the folders and files of the resource folder.
     52  *
     53  * {@link #getResources(ResourceType)} gives access to the resources directly.
     54  *
     55  */
     56 public abstract class ResourceRepository {
     57 
     58     protected final Map<ResourceFolderType, List<ResourceFolder>> mFolderMap =
     59         new EnumMap<ResourceFolderType, List<ResourceFolder>>(ResourceFolderType.class);
     60 
     61     protected final Map<ResourceType, List<ResourceItem>> mResourceMap =
     62         new EnumMap<ResourceType, List<ResourceItem>>(ResourceType.class);
     63 
     64     private final Map<List<ResourceItem>, List<ResourceItem>> mReadOnlyListMap =
     65         new IdentityHashMap<List<ResourceItem>, List<ResourceItem>>();
     66 
     67     private final boolean mFrameworkRepository;
     68 
     69     protected final IntArrayWrapper mWrapper = new IntArrayWrapper(null);
     70 
     71     /**
     72      * Makes a resource repository
     73      * @param isFrameworkRepository whether the repository is for framework resources.
     74      */
     75     protected ResourceRepository(boolean isFrameworkRepository) {
     76         mFrameworkRepository = isFrameworkRepository;
     77     }
     78 
     79     public boolean isFrameworkRepository() {
     80         return mFrameworkRepository;
     81     }
     82 
     83     /**
     84      * Adds a Folder Configuration to the project.
     85      * @param type The resource type.
     86      * @param config The resource configuration.
     87      * @param folder The workspace folder object.
     88      * @return the {@link ResourceFolder} object associated to this folder.
     89      */
     90     private ResourceFolder add(ResourceFolderType type, FolderConfiguration config,
     91             IAbstractFolder folder) {
     92         // get the list for the resource type
     93         List<ResourceFolder> list = mFolderMap.get(type);
     94 
     95         if (list == null) {
     96             list = new ArrayList<ResourceFolder>();
     97 
     98             ResourceFolder cf = new ResourceFolder(type, config, folder, this);
     99             list.add(cf);
    100 
    101             mFolderMap.put(type, list);
    102 
    103             return cf;
    104         }
    105 
    106         // look for an already existing folder configuration.
    107         for (ResourceFolder cFolder : list) {
    108             if (cFolder.mConfiguration.equals(config)) {
    109                 // config already exist. Nothing to be done really, besides making sure
    110                 // the IAbstractFolder object is up to date.
    111                 cFolder.mFolder = folder;
    112                 return cFolder;
    113             }
    114         }
    115 
    116         // If we arrive here, this means we didn't find a matching configuration.
    117         // So we add one.
    118         ResourceFolder cf = new ResourceFolder(type, config, folder, this);
    119         list.add(cf);
    120 
    121         return cf;
    122     }
    123 
    124     /**
    125      * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}.
    126      * @param type The type of the folder
    127      * @param removedFolder the IAbstractFolder object.
    128      * @return the {@link ResourceFolder} that was removed, or null if no matches were found.
    129      */
    130     public ResourceFolder removeFolder(ResourceFolderType type, IAbstractFolder removedFolder,
    131             ScanningContext context) {
    132         // get the list of folders for the resource type.
    133         List<ResourceFolder> list = mFolderMap.get(type);
    134 
    135         if (list != null) {
    136             int count = list.size();
    137             for (int i = 0 ; i < count ; i++) {
    138                 ResourceFolder resFolder = list.get(i);
    139                 IAbstractFolder folder = resFolder.getFolder();
    140                 if (removedFolder.equals(folder)) {
    141                     // we found the matching ResourceFolder. we need to remove it.
    142                     list.remove(i);
    143 
    144                     // remove its content
    145                     resFolder.dispose(context);
    146 
    147                     return resFolder;
    148                 }
    149             }
    150         }
    151 
    152         return null;
    153     }
    154 
    155     /**
    156      * Returns true if this resource repository contains a resource of the given
    157      * name.
    158      *
    159      * @param url the resource URL
    160      * @return true if the resource is known
    161      */
    162     public boolean hasResourceItem(String url) {
    163         assert url.startsWith("@") : url;
    164 
    165         int typeEnd = url.indexOf('/', 1);
    166         if (typeEnd != -1) {
    167             int nameBegin = typeEnd + 1;
    168 
    169             // Skip @ and @+
    170             int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$
    171 
    172             int colon = url.lastIndexOf(':', typeEnd);
    173             if (colon != -1) {
    174                 typeBegin = colon + 1;
    175             }
    176             String typeName = url.substring(typeBegin, typeEnd);
    177             ResourceType type = ResourceType.getEnum(typeName);
    178             if (type != null) {
    179                 String name = url.substring(nameBegin);
    180                 return hasResourceItem(type, name);
    181             }
    182         }
    183 
    184         return false;
    185     }
    186 
    187     /**
    188      * Returns true if this resource repository contains a resource of the given
    189      * name.
    190      *
    191      * @param type the type of resource to look up
    192      * @param name the name of the resource
    193      * @return true if the resource is known
    194      */
    195     public boolean hasResourceItem(ResourceType type, String name) {
    196         List<ResourceItem> list = mResourceMap.get(type);
    197 
    198         if (list != null) {
    199             for (ResourceItem item : list) {
    200                 if (name.equals(item.getName())) {
    201                     return true;
    202                 }
    203             }
    204         }
    205 
    206         return false;
    207     }
    208 
    209     /**
    210      * Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none
    211      * exist, it creates one.
    212      *
    213      * @param type the resource type
    214      * @param name the name of the resource.
    215      * @return A resource item matching the type and name.
    216      */
    217     protected ResourceItem getResourceItem(ResourceType type, String name) {
    218         // looking for an existing ResourceItem with this type and name
    219         ResourceItem item = findDeclaredResourceItem(type, name);
    220 
    221         // create one if there isn't one already, or if the existing one is inlined, since
    222         // clearly we need a non inlined one (the inline one is removed too)
    223         if (item == null || item.isDeclaredInline()) {
    224             ResourceItem oldItem = item != null && item.isDeclaredInline() ? item : null;
    225 
    226             item = createResourceItem(name);
    227 
    228             List<ResourceItem> list = mResourceMap.get(type);
    229             if (list == null) {
    230                 list = new ArrayList<ResourceItem>();
    231                 mResourceMap.put(type, list);
    232             }
    233 
    234             list.add(item);
    235 
    236             if (oldItem != null) {
    237                 list.remove(oldItem);
    238             }
    239         }
    240 
    241         return item;
    242     }
    243 
    244     /**
    245      * Creates a resource item with the given name.
    246      * @param name the name of the resource
    247      * @return a new ResourceItem (or child class) instance.
    248      */
    249     protected abstract ResourceItem createResourceItem(String name);
    250 
    251     /**
    252      * Processes a folder and adds it to the list of existing folders.
    253      * @param folder the folder to process
    254      * @return the ResourceFolder created from this folder, or null if the process failed.
    255      */
    256     public ResourceFolder processFolder(IAbstractFolder folder) {
    257         // split the name of the folder in segments.
    258         String[] folderSegments = folder.getName().split(AndroidConstants.RES_QUALIFIER_SEP);
    259 
    260         // get the enum for the resource type.
    261         ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]);
    262 
    263         if (type != null) {
    264             // get the folder configuration.
    265             FolderConfiguration config = FolderConfiguration.getConfig(folderSegments);
    266 
    267             if (config != null) {
    268                 return add(type, config, folder);
    269             }
    270         }
    271 
    272         return null;
    273     }
    274 
    275     /**
    276      * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}.
    277      * @param type The {@link ResourceFolderType}
    278      */
    279     public List<ResourceFolder> getFolders(ResourceFolderType type) {
    280         return mFolderMap.get(type);
    281     }
    282 
    283     public List<ResourceType> getAvailableResourceTypes() {
    284         List<ResourceType> list = new ArrayList<ResourceType>();
    285 
    286         // For each key, we check if there's a single ResourceType match.
    287         // If not, we look for the actual content to give us the resource type.
    288 
    289         for (ResourceFolderType folderType : mFolderMap.keySet()) {
    290             List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType);
    291             if (types.size() == 1) {
    292                 // before we add it we check if it's not already present, since a ResourceType
    293                 // could be created from multiple folders, even for the folders that only create
    294                 // one type of resource (drawable for instance, can be created from drawable/ and
    295                 // values/)
    296                 if (list.contains(types.get(0)) == false) {
    297                     list.add(types.get(0));
    298                 }
    299             } else {
    300                 // there isn't a single resource type out of this folder, so we look for all
    301                 // content.
    302                 List<ResourceFolder> folders = mFolderMap.get(folderType);
    303                 if (folders != null) {
    304                     for (ResourceFolder folder : folders) {
    305                         Collection<ResourceType> folderContent = folder.getResourceTypes();
    306 
    307                         // then we add them, but only if they aren't already in the list.
    308                         for (ResourceType folderResType : folderContent) {
    309                             if (list.contains(folderResType) == false) {
    310                                 list.add(folderResType);
    311                             }
    312                         }
    313                     }
    314                 }
    315             }
    316         }
    317 
    318         return list;
    319     }
    320 
    321     /**
    322      * Returns a list of {@link ResourceItem} matching a given {@link ResourceType}.
    323      * @param type the type of the resource items to return
    324      * @return a non null collection of resource items
    325      */
    326     public Collection<ResourceItem> getResourceItemsOfType(ResourceType type) {
    327         List<ResourceItem> list = mResourceMap.get(type);
    328 
    329         if (list == null) {
    330             return Collections.emptyList();
    331         }
    332 
    333         List<ResourceItem> roList = mReadOnlyListMap.get(list);
    334         if (roList == null) {
    335             roList = Collections.unmodifiableList(list);
    336             mReadOnlyListMap.put(list, roList);
    337         }
    338 
    339         return roList;
    340     }
    341 
    342     /**
    343      * Returns whether the repository has resources of a given {@link ResourceType}.
    344      * @param type the type of resource to check.
    345      * @return true if the repository contains resources of the given type, false otherwise.
    346      */
    347     public boolean hasResourcesOfType(ResourceType type) {
    348         List<ResourceItem> items = mResourceMap.get(type);
    349         return (items != null && items.size() > 0);
    350     }
    351 
    352     /**
    353      * Returns the {@link ResourceFolder} associated with a {@link IAbstractFolder}.
    354      * @param folder The {@link IAbstractFolder} object.
    355      * @return the {@link ResourceFolder} or null if it was not found.
    356      */
    357     public ResourceFolder getResourceFolder(IAbstractFolder folder) {
    358         Collection<List<ResourceFolder>> values = mFolderMap.values();
    359 
    360         if (values.isEmpty()) { // This shouldn't be necessary, but has been observed
    361             try {
    362                 loadResources(folder.getParentFolder());
    363             } catch (IOException e) {
    364                 e.printStackTrace();
    365             }
    366         }
    367 
    368         for (List<ResourceFolder> list : values) {
    369             for (ResourceFolder resFolder : list) {
    370                 IAbstractFolder wrapper = resFolder.getFolder();
    371                 if (wrapper.equals(folder)) {
    372                     return resFolder;
    373                 }
    374             }
    375         }
    376 
    377         return null;
    378     }
    379 
    380     /**
    381      * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and
    382      * configuration.
    383      * <p/>This only works with files generating one resource named after the file (for instance,
    384      * layouts, bitmap based drawable, xml, anims).
    385      * @return the matching file or <code>null</code> if no match was found.
    386      */
    387     public ResourceFile getMatchingFile(String name, ResourceFolderType type,
    388             FolderConfiguration config) {
    389         // get the folders for the given type
    390         List<ResourceFolder> folders = mFolderMap.get(type);
    391 
    392         // look for folders containing a file with the given name.
    393         ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>(folders.size());
    394 
    395         // remove the folders that do not have a file with the given name.
    396         for (int i = 0 ; i < folders.size(); i++) {
    397             ResourceFolder folder = folders.get(i);
    398 
    399             if (folder.hasFile(name) == true) {
    400                 matchingFolders.add(folder);
    401             }
    402         }
    403 
    404         // from those, get the folder with a config matching the given reference configuration.
    405         Configurable match = config.findMatchingConfigurable(matchingFolders);
    406 
    407         // do we have a matching folder?
    408         if (match instanceof ResourceFolder) {
    409             // get the ResourceFile from the filename
    410             return ((ResourceFolder)match).getFile(name);
    411         }
    412 
    413         return null;
    414     }
    415 
    416     /**
    417      * Returns the list of source files for a given resource.
    418      * Optionally, if a {@link FolderConfiguration} is given, then only the best
    419      * match for this config is returned.
    420      *
    421      * @param type the type of the resource.
    422      * @param name the name of the resource.
    423      * @param referenceConfig an optional config for which only the best match will be returned.
    424      *
    425      * @return a list of files generating this resource or null if it was not found.
    426      */
    427     public List<ResourceFile> getSourceFiles(ResourceType type, String name,
    428             FolderConfiguration referenceConfig) {
    429 
    430         Collection<ResourceItem> items = getResourceItemsOfType(type);
    431 
    432         for (ResourceItem item : items) {
    433             if (name.equals(item.getName())) {
    434                 if (referenceConfig != null) {
    435                     Configurable match = referenceConfig.findMatchingConfigurable(
    436                             item.getSourceFileList());
    437 
    438                     if (match instanceof ResourceFile) {
    439                         return Collections.singletonList((ResourceFile) match);
    440                     }
    441 
    442                     return null;
    443                 }
    444                 return item.getSourceFileList();
    445             }
    446         }
    447 
    448         return null;
    449     }
    450 
    451     /**
    452      * Returns the resources values matching a given {@link FolderConfiguration}.
    453      *
    454      * @param referenceConfig the configuration that each value must match.
    455      * @return a map with guaranteed to contain an entry for each {@link ResourceType}
    456      */
    457     public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources(
    458             FolderConfiguration referenceConfig) {
    459         return doGetConfiguredResources(referenceConfig);
    460     }
    461 
    462     /**
    463      * Returns the resources values matching a given {@link FolderConfiguration} for the current
    464      * project.
    465      *
    466      * @param referenceConfig the configuration that each value must match.
    467      * @return a map with guaranteed to contain an entry for each {@link ResourceType}
    468      */
    469     protected final Map<ResourceType, Map<String, ResourceValue>> doGetConfiguredResources(
    470             FolderConfiguration referenceConfig) {
    471 
    472         Map<ResourceType, Map<String, ResourceValue>> map =
    473             new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class);
    474 
    475         for (ResourceType key : ResourceType.values()) {
    476             // get the local results and put them in the map
    477             map.put(key, getConfiguredResource(key, referenceConfig));
    478         }
    479 
    480         return map;
    481     }
    482 
    483     /**
    484      * Returns the sorted list of languages used in the resources.
    485      */
    486     public SortedSet<String> getLanguages() {
    487         SortedSet<String> set = new TreeSet<String>();
    488 
    489         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
    490         for (List<ResourceFolder> folderSubList : folderList) {
    491             for (ResourceFolder folder : folderSubList) {
    492                 FolderConfiguration config = folder.getConfiguration();
    493                 LanguageQualifier lang = config.getLanguageQualifier();
    494                 if (lang != null) {
    495                     set.add(lang.getShortDisplayValue());
    496                 }
    497             }
    498         }
    499 
    500         return set;
    501     }
    502 
    503     /**
    504      * Returns the sorted list of regions used in the resources with the given language.
    505      * @param currentLanguage the current language the region must be associated with.
    506      */
    507     public SortedSet<String> getRegions(String currentLanguage) {
    508         SortedSet<String> set = new TreeSet<String>();
    509 
    510         Collection<List<ResourceFolder>> folderList = mFolderMap.values();
    511         for (List<ResourceFolder> folderSubList : folderList) {
    512             for (ResourceFolder folder : folderSubList) {
    513                 FolderConfiguration config = folder.getConfiguration();
    514 
    515                 // get the language
    516                 LanguageQualifier lang = config.getLanguageQualifier();
    517                 if (lang != null && lang.getShortDisplayValue().equals(currentLanguage)) {
    518                     RegionQualifier region = config.getRegionQualifier();
    519                     if (region != null) {
    520                         set.add(region.getShortDisplayValue());
    521                     }
    522                 }
    523             }
    524         }
    525 
    526         return set;
    527     }
    528 
    529     /**
    530      * Loads the resources from a resource folder.
    531      * <p/>
    532      *
    533      * @param rootFolder The folder to read the resources from. This is the top level
    534      * resource folder (res/)
    535      * @throws IOException
    536      */
    537     public void loadResources(IAbstractFolder rootFolder)
    538             throws IOException {
    539         ScanningContext context = new ScanningContext(this);
    540 
    541         IAbstractResource[] files = rootFolder.listMembers();
    542         for (IAbstractResource file : files) {
    543             if (file instanceof IAbstractFolder) {
    544                 IAbstractFolder folder = (IAbstractFolder) file;
    545                 ResourceFolder resFolder = processFolder(folder);
    546 
    547                 if (resFolder != null) {
    548                     // now we process the content of the folder
    549                     IAbstractResource[] children = folder.listMembers();
    550 
    551                     for (IAbstractResource childRes : children) {
    552                         if (childRes instanceof IAbstractFile) {
    553                             resFolder.processFile((IAbstractFile) childRes,
    554                                     ResourceDeltaKind.ADDED, context);
    555                         }
    556                     }
    557                 }
    558             }
    559         }
    560     }
    561 
    562 
    563     protected void removeFile(Collection<ResourceType> types, ResourceFile file) {
    564         for (ResourceType type : types) {
    565             removeFile(type, file);
    566         }
    567     }
    568 
    569     protected void removeFile(ResourceType type, ResourceFile file) {
    570         List<ResourceItem> list = mResourceMap.get(type);
    571         if (list != null) {
    572             for (int i = 0 ; i < list.size(); i++) {
    573                 ResourceItem item = list.get(i);
    574                 item.removeFile(file);
    575             }
    576         }
    577     }
    578 
    579     /**
    580      * Returns a map of (resource name, resource value) for the given {@link ResourceType}.
    581      * <p/>The values returned are taken from the resource files best matching a given
    582      * {@link FolderConfiguration}.
    583      * @param type the type of the resources.
    584      * @param referenceConfig the configuration to best match.
    585      */
    586     private Map<String, ResourceValue> getConfiguredResource(ResourceType type,
    587             FolderConfiguration referenceConfig) {
    588 
    589         // get the resource item for the given type
    590         List<ResourceItem> items = mResourceMap.get(type);
    591         if (items == null) {
    592             return new HashMap<String, ResourceValue>();
    593         }
    594 
    595         // create the map
    596         HashMap<String, ResourceValue> map = new HashMap<String, ResourceValue>(items.size());
    597 
    598         for (ResourceItem item : items) {
    599             ResourceValue value = item.getResourceValue(type, referenceConfig,
    600                     isFrameworkRepository());
    601             if (value != null) {
    602                 map.put(item.getName(), value);
    603             }
    604         }
    605 
    606         return map;
    607     }
    608 
    609 
    610     /**
    611      * Cleans up the repository of resource items that have no source file anymore.
    612      */
    613     public void postUpdateCleanUp() {
    614         // Since removed files/folders remove source files from existing ResourceItem, loop through
    615         // all resource items and remove the ones that have no source files.
    616 
    617         Collection<List<ResourceItem>> lists = mResourceMap.values();
    618         for (List<ResourceItem> list : lists) {
    619             for (int i = 0 ; i < list.size() ;) {
    620                 if (list.get(i).hasNoSourceFile()) {
    621                     list.remove(i);
    622                 } else {
    623                     i++;
    624                 }
    625             }
    626         }
    627     }
    628 
    629     /**
    630      * Looks up an existing {@link ResourceItem} by {@link ResourceType} and name. This
    631      * ignores inline resources.
    632      * @param type the Resource Type.
    633      * @param name the Resource name.
    634      * @return the existing ResourceItem or null if no match was found.
    635      */
    636     private ResourceItem findDeclaredResourceItem(ResourceType type, String name) {
    637         List<ResourceItem> list = mResourceMap.get(type);
    638 
    639         if (list != null) {
    640             for (ResourceItem item : list) {
    641                 // ignore inline
    642                 if (name.equals(item.getName()) && item.isDeclaredInline() == false) {
    643                     return item;
    644                 }
    645             }
    646         }
    647 
    648         return null;
    649     }
    650 }
    651