All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.ide.common.resources.ResourceRepository Maven / Gradle / Ivy

/*
 * 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> mFolderMap =
        new EnumMap>(ResourceFolderType.class);

    protected Map> mResourceMap =
            new EnumMap>(
                    ResourceType.class);

    private Map, Collection> mReadOnlyListMap =
            new IdentityHashMap, Collection>();

    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.class);
        mResourceMap = new EnumMap>(
                ResourceType.class);

        mReadOnlyListMap =
            new IdentityHashMap, Collection>();
    }

    /**
     * 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 list = mFolderMap.get(type);

        if (list == null) {
            list = new ArrayList();

            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 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 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 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(size, 1.0f);
                } else {
                    map = new HashMap();
                }
                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 getFolders(@NonNull ResourceFolderType type) {
        ensureInitialized();

        return mFolderMap.get(type);
    }

    @NonNull
    public List getAvailableResourceTypes() {
        ensureInitialized();

        List list = new ArrayList();

        // 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 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 folders = mFolderMap.get(folderType);
                if (folders != null) {
                    for (ResourceFolder folder : folders) {
                        Collection 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 getResourceItemsOfType(@NonNull ResourceType type) {
        ensureInitialized();

        Map map = mResourceMap.get(type);

        if (map == null) {
            return Collections.emptyList();
        }

        Collection 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 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> values = mFolderMap.values();

        for (List 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.
     * 

* 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 null if no match was found. */ @Nullable public ResourceFile getMatchingFile( @NonNull String name, @NonNull ResourceFolderType type, @NonNull FolderConfiguration config) { List 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. *

* 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 null 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 items = mResourceMap.get(type); if (items != null) { ResourceItem item = items.get(resourceName); if (item != null) { List 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 getSourceFiles(@NonNull ResourceType type, @NonNull String name, @Nullable FolderConfiguration referenceConfig) { ensureInitialized(); Collection 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> 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> doGetConfiguredResources( @NonNull FolderConfiguration referenceConfig) { ensureInitialized(); Map> map = new EnumMap>(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 getLanguages() { ensureInitialized(); SortedSet set = new TreeSet(); Collection> folderList = mFolderMap.values(); for (List 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 getRegions(@NonNull String currentLanguage) { ensureInitialized(); SortedSet set = new TreeSet(); Collection> folderList = mFolderMap.values(); for (List 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 types, @NonNull ResourceFile file) { ensureInitialized(); for (ResourceType type : types) { removeFile(type, file); } } protected void removeFile(@NonNull ResourceType type, @NonNull ResourceFile file) { Map map = mResourceMap.get(type); if (map != null) { Collection values = map.values(); List toDelete = null; for (ResourceItem item : values) { item.removeFile(file); if (item.hasNoSourceFile()) { if (toDelete == null) { toDelete = new ArrayList(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}. *

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 getConfiguredResource(@NonNull ResourceType type, @NonNull FolderConfiguration referenceConfig) { // get the resource item for the given type Map items = mResourceMap.get(type); if (items == null) { return new HashMap(); } // create the map HashMap map = new HashMap(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> maps = mResourceMap.values(); for (Map map : maps) { Set keySet = map.keySet(); Iterator 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 map = mResourceMap.get(type); if (map != null) { ResourceItem resourceItem = map.get(name); if (resourceItem != null && !resourceItem.isDeclaredInline()) { return resourceItem; } } return null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy