/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.common.resources; import static com.android.SdkConstants.ATTR_REF_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.PREFIX_THEME_REF; import static com.android.SdkConstants.RESOURCE_CLZ_ATTR; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.configuration.Configurable; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.LocaleQualifier; import com.android.io.IAbstractFile; import com.android.io.IAbstractFolder; import com.android.io.IAbstractResource; import com.android.resources.FolderTypeRelationship; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * Base class for resource repository. * * A repository is both a file representation of a resource folder and a representation * of the generated resources, organized by type. * * {@link #getResourceFolder(IAbstractFolder)} and {@link #getSourceFiles(ResourceType, String, FolderConfiguration)} * give access to the folders and files of the resource folder. * * {@link #getResourceItemsOfType(ResourceType)} gives access to the resources directly. * */ public abstract class ResourceRepository { private final IAbstractFolder mResourceFolder; protected Map<ResourceFolderType, List<ResourceFolder>> mFolderMap = new EnumMap<ResourceFolderType, List<ResourceFolder>>(ResourceFolderType.class); protected Map<ResourceType, Map<String, ResourceItem>> mResourceMap = new EnumMap<ResourceType, Map<String, ResourceItem>>( ResourceType.class); private Map<Map<String, ResourceItem>, Collection<ResourceItem>> mReadOnlyListMap = new IdentityHashMap<Map<String, ResourceItem>, Collection<ResourceItem>>(); private final boolean mFrameworkRepository; private boolean mCleared = true; private boolean mInitializing = false; /** * Makes a resource repository * @param resFolder the resource folder of the repository. * @param isFrameworkRepository whether the repository is for framework resources. */ protected ResourceRepository(@NonNull IAbstractFolder resFolder, boolean isFrameworkRepository) { mResourceFolder = resFolder; mFrameworkRepository = isFrameworkRepository; } public IAbstractFolder getResFolder() { return mResourceFolder; } public boolean isFrameworkRepository() { return mFrameworkRepository; } public synchronized void clear() { mCleared = true; mFolderMap = new EnumMap<ResourceFolderType, List<ResourceFolder>>( ResourceFolderType.class); mResourceMap = new EnumMap<ResourceType, Map<String, ResourceItem>>( ResourceType.class); mReadOnlyListMap = new IdentityHashMap<Map<String, ResourceItem>, Collection<ResourceItem>>(); } /** * Ensures that the repository has been initialized again after a call to * {@link ResourceRepository#clear()} * * @return true if the repository was just re-initialized. */ public synchronized boolean ensureInitialized() { if (mCleared && !mInitializing) { ScanningContext context = new ScanningContext(this); mInitializing = true; IAbstractResource[] resources = mResourceFolder.listMembers(); for (IAbstractResource res : resources) { if (res instanceof IAbstractFolder) { IAbstractFolder folder = (IAbstractFolder)res; ResourceFolder resFolder = processFolder(folder); if (resFolder != null) { // now we process the content of the folder IAbstractResource[] files = folder.listMembers(); for (IAbstractResource fileRes : files) { if (fileRes instanceof IAbstractFile) { IAbstractFile file = (IAbstractFile)fileRes; resFolder.processFile(file, ResourceDeltaKind.ADDED, context); } } } } } mInitializing = false; mCleared = false; return true; } return false; } /** * Adds a Folder Configuration to the project. * @param type The resource type. * @param config The resource configuration. * @param folder The workspace folder object. * @return the {@link ResourceFolder} object associated to this folder. */ private ResourceFolder add( @NonNull ResourceFolderType type, @NonNull FolderConfiguration config, @NonNull IAbstractFolder folder) { // get the list for the resource type List<ResourceFolder> list = mFolderMap.get(type); if (list == null) { list = new ArrayList<ResourceFolder>(); ResourceFolder cf = new ResourceFolder(type, config, folder, this); list.add(cf); mFolderMap.put(type, list); return cf; } // look for an already existing folder configuration. for (ResourceFolder cFolder : list) { if (cFolder.mConfiguration.equals(config)) { // config already exist. Nothing to be done really, besides making sure // the IAbstractFolder object is up to date. cFolder.mFolder = folder; return cFolder; } } // If we arrive here, this means we didn't find a matching configuration. // So we add one. ResourceFolder cf = new ResourceFolder(type, config, folder, this); list.add(cf); return cf; } /** * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}. * @param type The type of the folder * @param removedFolder the IAbstractFolder object. * @param context the scanning context * @return the {@link ResourceFolder} that was removed, or null if no matches were found. */ @Nullable public ResourceFolder removeFolder( @NonNull ResourceFolderType type, @NonNull IAbstractFolder removedFolder, @Nullable ScanningContext context) { ensureInitialized(); // get the list of folders for the resource type. List<ResourceFolder> list = mFolderMap.get(type); if (list != null) { int count = list.size(); for (int i = 0 ; i < count ; i++) { ResourceFolder resFolder = list.get(i); IAbstractFolder folder = resFolder.getFolder(); if (removedFolder.equals(folder)) { // we found the matching ResourceFolder. we need to remove it. list.remove(i); // remove its content resFolder.dispose(context); return resFolder; } } } return null; } /** * Returns true if this resource repository contains a resource of the given * name. * * @param url the resource URL * @return true if the resource is known */ public boolean hasResourceItem(@NonNull String url) { // Handle theme references if (url.startsWith(PREFIX_THEME_REF)) { String remainder = url.substring(PREFIX_THEME_REF.length()); if (url.startsWith(ATTR_REF_PREFIX)) { url = PREFIX_RESOURCE_REF + url.substring(PREFIX_THEME_REF.length()); return hasResourceItem(url); } int colon = url.indexOf(':'); if (colon != -1) { // Convert from ?android:progressBarStyleBig to ?android:attr/progressBarStyleBig if (remainder.indexOf('/', colon) == -1) { remainder = remainder.substring(0, colon) + RESOURCE_CLZ_ATTR + '/' + remainder.substring(colon); } url = PREFIX_RESOURCE_REF + remainder; return hasResourceItem(url); } else { int slash = url.indexOf('/'); if (slash == -1) { url = PREFIX_RESOURCE_REF + RESOURCE_CLZ_ATTR + '/' + remainder; return hasResourceItem(url); } } } if (!url.startsWith(PREFIX_RESOURCE_REF)) { return false; } assert url.startsWith("@") || url.startsWith("?") : url; ensureInitialized(); int typeEnd = url.indexOf('/', 1); if (typeEnd != -1) { int nameBegin = typeEnd + 1; // Skip @ and @+ int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$ int colon = url.lastIndexOf(':', typeEnd); if (colon != -1) { typeBegin = colon + 1; } String typeName = url.substring(typeBegin, typeEnd); ResourceType type = ResourceType.getEnum(typeName); if (type != null) { String name = url.substring(nameBegin); return hasResourceItem(type, name); } } return false; } /** * Returns true if this resource repository contains a resource of the given * name. * * @param type the type of resource to look up * @param name the name of the resource * @return true if the resource is known */ public boolean hasResourceItem(@NonNull ResourceType type, @NonNull String name) { ensureInitialized(); Map<String, ResourceItem> map = mResourceMap.get(type); if (map != null) { ResourceItem resourceItem = map.get(name); if (resourceItem != null) { return true; } } return false; } /** * Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none * exist, it creates one. * * @param type the resource type * @param name the name of the resource. * @return A resource item matching the type and name. */ @NonNull public ResourceItem getResourceItem(@NonNull ResourceType type, @NonNull String name) { ensureInitialized(); // looking for an existing ResourceItem with this type and name ResourceItem item = findDeclaredResourceItem(type, name); // create one if there isn't one already, or if the existing one is inlined, since // clearly we need a non inlined one (the inline one is removed too) if (item == null || item.isDeclaredInline()) { ResourceItem oldItem = item != null && item.isDeclaredInline() ? item : null; item = createResourceItem(name); Map<String, ResourceItem> map = mResourceMap.get(type); if (map == null) { if (isFrameworkRepository()) { // Pick initial size for the maps. Also change the load factor to 1.0 // to avoid rehashing the whole table when we (as expected) get near // the known rough size of each resource type map. int size; switch (type) { // Based on counts in API 16. Going back to API 10, the counts // are roughly 25-50% smaller (e.g. compared to the top 5 types below // the fractions are 1107 vs 1734, 831 vs 1508, 895 vs 1255, // 733 vs 1064 and 171 vs 783. case PUBLIC: size = 1734; break; case DRAWABLE: size = 1508; break; case STRING: size = 1255; break; case ATTR: size = 1064; break; case STYLE: size = 783; break; case ID: size = 347; break; case DECLARE_STYLEABLE: size = 210; break; case LAYOUT: size = 187; break; case COLOR: size = 120; break; case ANIM: size = 95; break; case DIMEN: size = 81; break; case BOOL: size = 54; break; case INTEGER: size = 52; break; case ARRAY: size = 51; break; case PLURALS: size = 20; break; case XML: size = 14; break; case INTERPOLATOR : size = 13; break; case ANIMATOR: size = 8; break; case RAW: size = 4; break; case MENU: size = 2; break; case MIPMAP: size = 2; break; case FRACTION: size = 1; break; default: size = 2; } map = new HashMap<String, ResourceItem>(size, 1.0f); } else { map = new HashMap<String, ResourceItem>(); } mResourceMap.put(type, map); } map.put(item.getName(), item); if (oldItem != null) { map.remove(oldItem.getName()); } } return item; } /** * Creates a resource item with the given name. * @param name the name of the resource * @return a new ResourceItem (or child class) instance. */ @NonNull protected abstract ResourceItem createResourceItem(@NonNull String name); /** * Processes a folder and adds it to the list of existing folders. * @param folder the folder to process * @return the ResourceFolder created from this folder, or null if the process failed. */ @Nullable public ResourceFolder processFolder(@NonNull IAbstractFolder folder) { ensureInitialized(); // split the name of the folder in segments. String[] folderSegments = folder.getName().split(SdkConstants.RES_QUALIFIER_SEP); // get the enum for the resource type. ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]); if (type != null) { // get the folder configuration. FolderConfiguration config = FolderConfiguration.getConfig(folderSegments); if (config != null) { return add(type, config, folder); } } return null; } /** * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}. * @param type The {@link ResourceFolderType} */ @Nullable public List<ResourceFolder> getFolders(@NonNull ResourceFolderType type) { ensureInitialized(); return mFolderMap.get(type); } @NonNull public List<ResourceType> getAvailableResourceTypes() { ensureInitialized(); List<ResourceType> list = new ArrayList<ResourceType>(); // For each key, we check if there's a single ResourceType match. // If not, we look for the actual content to give us the resource type. for (ResourceFolderType folderType : mFolderMap.keySet()) { List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType); if (types.size() == 1) { // before we add it we check if it's not already present, since a ResourceType // could be created from multiple folders, even for the folders that only create // one type of resource (drawable for instance, can be created from drawable/ and // values/) if (list.contains(types.get(0)) == false) { list.add(types.get(0)); } } else { // there isn't a single resource type out of this folder, so we look for all // content. List<ResourceFolder> folders = mFolderMap.get(folderType); if (folders != null) { for (ResourceFolder folder : folders) { Collection<ResourceType> folderContent = folder.getResourceTypes(); // then we add them, but only if they aren't already in the list. for (ResourceType folderResType : folderContent) { if (list.contains(folderResType) == false) { list.add(folderResType); } } } } } } return list; } /** * Returns a list of {@link ResourceItem} matching a given {@link ResourceType}. * @param type the type of the resource items to return * @return a non null collection of resource items */ @NonNull public Collection<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) { ensureInitialized(); Map<String, ResourceItem> map = mResourceMap.get(type); if (map == null) { return Collections.emptyList(); } Collection<ResourceItem> roList = mReadOnlyListMap.get(map); if (roList == null) { roList = Collections.unmodifiableCollection(map.values()); mReadOnlyListMap.put(map, roList); } return roList; } /** * Returns whether the repository has resources of a given {@link ResourceType}. * @param type the type of resource to check. * @return true if the repository contains resources of the given type, false otherwise. */ public boolean hasResourcesOfType(@NonNull ResourceType type) { ensureInitialized(); Map<String, ResourceItem> items = mResourceMap.get(type); return (items != null && !items.isEmpty()); } /** * Returns the {@link ResourceFolder} associated with a {@link IAbstractFolder}. * @param folder The {@link IAbstractFolder} object. * @return the {@link ResourceFolder} or null if it was not found. */ @Nullable public ResourceFolder getResourceFolder(@NonNull IAbstractFolder folder) { ensureInitialized(); Collection<List<ResourceFolder>> values = mFolderMap.values(); for (List<ResourceFolder> list : values) { for (ResourceFolder resFolder : list) { IAbstractFolder wrapper = resFolder.getFolder(); if (wrapper.equals(folder)) { return resFolder; } } } return null; } /** * Returns the {@link ResourceFile} matching the given name, * {@link ResourceFolderType} and configuration. * <p/> * This only works with files generating one resource named after the file * (for instance, layouts, bitmap based drawable, xml, anims). * * @param name the resource name or file name * @param type the folder type search for * @param config the folder configuration to match for * @return the matching file or <code>null</code> if no match was found. */ @Nullable public ResourceFile getMatchingFile( @NonNull String name, @NonNull ResourceFolderType type, @NonNull FolderConfiguration config) { List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(type); for (ResourceType t : types) { if (t == ResourceType.ID) { continue; } ResourceFile match = getMatchingFile(name, t, config); if (match != null) { return match; } } return null; } /** * Returns the {@link ResourceFile} matching the given name, * {@link ResourceType} and configuration. * <p/> * This only works with files generating one resource named after the file * (for instance, layouts, bitmap based drawable, xml, anims). * * @param name the resource name or file name * @param type the folder type search for * @param config the folder configuration to match for * @return the matching file or <code>null</code> if no match was found. */ @Nullable public ResourceFile getMatchingFile( @NonNull String name, @NonNull ResourceType type, @NonNull FolderConfiguration config) { ensureInitialized(); String resourceName = name; int dot = resourceName.indexOf('.'); if (dot != -1) { resourceName = resourceName.substring(0, dot); } Map<String, ResourceItem> items = mResourceMap.get(type); if (items != null) { ResourceItem item = items.get(resourceName); if (item != null) { List<ResourceFile> files = item.getSourceFileList(); if (files != null) { if (files.size() > 1) { ResourceValue value = item.getResourceValue(type, config, isFrameworkRepository()); if (value != null) { String v = value.getValue(); if (v != null) { ResourceUrl url = ResourceUrl.parse(v); if (url != null) { return getMatchingFile(url.name, url.type, config); } else { // Looks like the resource value is pointing to a file // It's most likely one of the source files for this // resource item, so check those first for (ResourceFile f : files) { if (v.equals(f.getFile().getOsLocation())) { // Found the file return f; } } // No; look up the resource file from the full path File file = new File(v); if (file.exists()) { ResourceFile f = findResourceFile(file); if (f != null) { return f; } } } } } } else if (files.size() == 1) { // Single file: see if it matches ResourceFile matchingFile = files.get(0); if (matchingFile.getFolder().getConfiguration().isMatchFor(config)) { return matchingFile; } } } } } return null; } /** * Looks up the {@link ResourceFile} for the given {@link File}, if possible * * @param file the file * @return the corresponding {@link ResourceFile}, or null if not a known {@link ResourceFile} */ @Nullable protected ResourceFile findResourceFile(@NonNull File file) { // Look up the right resource file for this path String parentName = file.getParentFile().getName(); IAbstractFolder folder = getResFolder().getFolder(parentName); if (folder != null) { ResourceFolder resourceFolder = getResourceFolder(folder); if (resourceFolder == null) { FolderConfiguration configForFolder = FolderConfiguration .getConfigForFolder(parentName); if (configForFolder != null) { ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName); if (folderType != null) { resourceFolder = add(folderType, configForFolder, folder); } } } if (resourceFolder != null) { ResourceFile resourceFile = resourceFolder.getFile(file.getName()); if (resourceFile != null) { return resourceFile; } } } return null; } /** * Returns the list of source files for a given resource. * Optionally, if a {@link FolderConfiguration} is given, then only the best * match for this config is returned. * * @param type the type of the resource. * @param name the name of the resource. * @param referenceConfig an optional config for which only the best match will be returned. * * @return a list of files generating this resource or null if it was not found. */ @Nullable public List<ResourceFile> getSourceFiles(@NonNull ResourceType type, @NonNull String name, @Nullable FolderConfiguration referenceConfig) { ensureInitialized(); Collection<ResourceItem> items = getResourceItemsOfType(type); for (ResourceItem item : items) { if (name.equals(item.getName())) { if (referenceConfig != null) { Configurable match = referenceConfig.findMatchingConfigurable( item.getSourceFileList()); if (match instanceof ResourceFile) { return Collections.singletonList((ResourceFile) match); } return null; } return item.getSourceFileList(); } } return null; } /** * Returns the resources values matching a given {@link FolderConfiguration}. * * @param referenceConfig the configuration that each value must match. * @return a map with guaranteed to contain an entry for each {@link ResourceType} */ @NonNull public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources( @NonNull FolderConfiguration referenceConfig) { ensureInitialized(); return doGetConfiguredResources(referenceConfig); } /** * Returns the resources values matching a given {@link FolderConfiguration} for the current * project. * * @param referenceConfig the configuration that each value must match. * @return a map with guaranteed to contain an entry for each {@link ResourceType} */ @NonNull protected final Map<ResourceType, Map<String, ResourceValue>> doGetConfiguredResources( @NonNull FolderConfiguration referenceConfig) { ensureInitialized(); Map<ResourceType, Map<String, ResourceValue>> map = new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); for (ResourceType key : ResourceType.values()) { // get the local results and put them in the map map.put(key, getConfiguredResource(key, referenceConfig)); } return map; } /** * Returns the sorted list of languages used in the resources. */ @NonNull public SortedSet<String> getLanguages() { ensureInitialized(); SortedSet<String> set = new TreeSet<String>(); Collection<List<ResourceFolder>> folderList = mFolderMap.values(); for (List<ResourceFolder> folderSubList : folderList) { for (ResourceFolder folder : folderSubList) { FolderConfiguration config = folder.getConfiguration(); LocaleQualifier locale = config.getLocaleQualifier(); if (locale != null) { set.add(locale.getLanguage()); } } } return set; } /** * Returns the sorted list of regions used in the resources with the given language. * @param currentLanguage the current language the region must be associated with. */ @NonNull public SortedSet<String> getRegions(@NonNull String currentLanguage) { ensureInitialized(); SortedSet<String> set = new TreeSet<String>(); Collection<List<ResourceFolder>> folderList = mFolderMap.values(); for (List<ResourceFolder> folderSubList : folderList) { for (ResourceFolder folder : folderSubList) { FolderConfiguration config = folder.getConfiguration(); // get the language LocaleQualifier locale = config.getLocaleQualifier(); if (locale != null && currentLanguage.equals(locale.getLanguage()) && locale.getRegion() != null) { set.add(locale.getRegion()); } } } return set; } /** * Loads the resources. */ public void loadResources() { clear(); ensureInitialized(); } protected void removeFile(@NonNull Collection<ResourceType> types, @NonNull ResourceFile file) { ensureInitialized(); for (ResourceType type : types) { removeFile(type, file); } } protected void removeFile(@NonNull ResourceType type, @NonNull ResourceFile file) { Map<String, ResourceItem> map = mResourceMap.get(type); if (map != null) { Collection<ResourceItem> values = map.values(); List<ResourceItem> toDelete = null; for (ResourceItem item : values) { item.removeFile(file); if (item.hasNoSourceFile()) { if (toDelete == null) { toDelete = new ArrayList<ResourceItem>(values.size()); } toDelete.add(item); } } if (toDelete != null) { for (ResourceItem item : toDelete) { map.remove(item.getName()); } } } } /** * Returns a map of (resource name, resource value) for the given {@link ResourceType}. * <p/>The values returned are taken from the resource files best matching a given * {@link FolderConfiguration}. * @param type the type of the resources. * @param referenceConfig the configuration to best match. */ @NonNull private Map<String, ResourceValue> getConfiguredResource(@NonNull ResourceType type, @NonNull FolderConfiguration referenceConfig) { // get the resource item for the given type Map<String, ResourceItem> items = mResourceMap.get(type); if (items == null) { return new HashMap<String, ResourceValue>(); } // create the map HashMap<String, ResourceValue> map = new HashMap<String, ResourceValue>(items.size()); for (ResourceItem item : items.values()) { ResourceValue value = item.getResourceValue(type, referenceConfig, isFrameworkRepository()); if (value != null) { map.put(item.getName(), value); } } return map; } /** * Cleans up the repository of resource items that have no source file anymore. */ public void postUpdateCleanUp() { // Since removed files/folders remove source files from existing ResourceItem, loop through // all resource items and remove the ones that have no source files. Collection<Map<String, ResourceItem>> maps = mResourceMap.values(); for (Map<String, ResourceItem> map : maps) { Set<String> keySet = map.keySet(); Iterator<String> iterator = keySet.iterator(); while (iterator.hasNext()) { String name = iterator.next(); ResourceItem resourceItem = map.get(name); if (resourceItem.hasNoSourceFile()) { iterator.remove(); } } } } /** * Looks up an existing {@link ResourceItem} by {@link ResourceType} and name. This * ignores inline resources. * @param type the Resource Type. * @param name the Resource name. * @return the existing ResourceItem or null if no match was found. */ @Nullable private ResourceItem findDeclaredResourceItem(@NonNull ResourceType type, @NonNull String name) { Map<String, ResourceItem> map = mResourceMap.get(type); if (map != null) { ResourceItem resourceItem = map.get(name); if (resourceItem != null && !resourceItem.isDeclaredInline()) { return resourceItem; } } return null; } }