blob: d27dfff3252696bd9872b91c405224a2915260bf [file] [log] [blame]
/*
* Copyright (C) 2013 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.res2;
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.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.ResourceType;
import com.google.common.base.Splitter;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
public abstract class AbstractResourceRepository {
private final boolean mFramework;
private class RepositoryMerger implements MergeConsumer<ResourceItem> {
@Override
public void start() throws ConsumerException {
}
@Override
public void end() throws ConsumerException {
}
@Override
public void addItem(@NonNull ResourceItem item) throws ConsumerException {
if (item.isTouched()) {
AbstractResourceRepository.this.addItem(item);
}
}
@Override
public void removeItem(@NonNull ResourceItem removedItem, @Nullable ResourceItem replacedBy)
throws ConsumerException {
AbstractResourceRepository.this.removeItem(removedItem);
}
@Override
public boolean ignoreItemInMerge(ResourceItem item) {
// we never ignore any item.
return false;
}
}
public AbstractResourceRepository(boolean isFramework) {
mFramework = isFramework;
}
public boolean isFramework() {
return mFramework;
}
@NonNull
public MergeConsumer<ResourceItem> createMergeConsumer() {
return new RepositoryMerger();
}
@NonNull
protected abstract Map<ResourceType, ListMultimap<String, ResourceItem>> getMap();
@Nullable
protected abstract ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create);
@NonNull
protected ListMultimap<String, ResourceItem> getMap(ResourceType type) {
//noinspection ConstantConditions
return getMap(type, true); // Won't return null if create is false
}
@NonNull
public Map<ResourceType, ListMultimap<String, ResourceItem>> getItems() {
return getMap();
}
/** Lock used to protect map access */
protected static final Object ITEM_MAP_LOCK = new Object();
// TODO: Rename to getResourceItemList?
@Nullable
public List<ResourceItem> getResourceItem(@NonNull ResourceType resourceType,
@NonNull String resourceName) {
synchronized (ITEM_MAP_LOCK) {
ListMultimap<String, ResourceItem> map = getMap(resourceType, false);
if (map != null) {
return map.get(resourceName);
}
}
return null;
}
@NonNull
public Collection<String> getItemsOfType(@NonNull ResourceType type) {
synchronized (ITEM_MAP_LOCK) {
Multimap<String, ResourceItem> map = getMap(type, false);
if (map == null) {
return Collections.emptyList();
}
return Collections.unmodifiableCollection(map.keySet());
}
}
/**
* 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;
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 resourceType the type of resource to look up
* @param resourceName the name of the resource
* @return true if the resource is known
*/
public boolean hasResourceItem(@NonNull ResourceType resourceType,
@NonNull String resourceName) {
synchronized (ITEM_MAP_LOCK) {
ListMultimap<String, ResourceItem> map = getMap(resourceType, false);
if (map != null) {
List<ResourceItem> itemList = map.get(resourceName);
return itemList != null && !itemList.isEmpty();
}
}
return false;
}
/**
* Returns whether the repository has resources of a given {@link ResourceType}.
* @param resourceType the type of resource to check.
* @return true if the repository contains resources of the given type, false otherwise.
*/
public boolean hasResourcesOfType(@NonNull ResourceType resourceType) {
synchronized (ITEM_MAP_LOCK) {
ListMultimap<String, ResourceItem> map = getMap(resourceType, false);
return map != null && !map.isEmpty();
}
}
@NonNull
public List<ResourceType> getAvailableResourceTypes() {
synchronized (ITEM_MAP_LOCK) {
return Lists.newArrayList(getMap().keySet());
}
}
/**
* 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
* @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) {
synchronized (ITEM_MAP_LOCK) {
ListMultimap<String, ResourceItem> typeItems = getMap(type, false);
if (typeItems == null) {
return null;
}
List<ResourceItem> matchingItems = typeItems.get(name);
if (matchingItems == null || matchingItems.isEmpty()) {
return null;
}
ResourceItem match = (ResourceItem) config.findMatchingConfigurable(matchingItems);
if (match != null) {
return match.getSource();
}
}
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) {
Map<ResourceType, Map<String, ResourceValue>> map = Maps.newEnumMap(ResourceType.class);
synchronized (ITEM_MAP_LOCK) {
Map<ResourceType, ListMultimap<String, ResourceItem>> itemMap = getMap();
for (ResourceType key : ResourceType.values()) {
// get the local results and put them in the map
map.put(key, getConfiguredResources(itemMap, key, referenceConfig));
}
}
return map;
}
/**
* 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
public Map<String, ResourceValue> getConfiguredResources(
@NonNull ResourceType type,
@NonNull FolderConfiguration referenceConfig) {
return getConfiguredResources(getMap(), type, referenceConfig);
}
@NonNull
public Map<String, ResourceValue> getConfiguredResources(
@NonNull Map<ResourceType, ListMultimap<String, ResourceItem>> itemMap,
@NonNull ResourceType type,
@NonNull FolderConfiguration referenceConfig) {
// get the resource item for the given type
ListMultimap<String, ResourceItem> items = itemMap.get(type);
if (items == null) {
return Maps.newHashMap();
}
Set<String> keys = items.keySet();
// create the map
Map<String, ResourceValue> map = Maps.newHashMapWithExpectedSize(keys.size());
for (String key : keys) {
List<ResourceItem> keyItems = items.get(key);
// look for the best match for the given configuration
// the match has to be of type ResourceFile since that's what the input list contains
ResourceItem match = (ResourceItem) referenceConfig.findMatchingConfigurable(keyItems);
if (match != null) {
ResourceValue value = match.getResourceValue(mFramework);
if (value != null) {
map.put(match.getName(), value);
}
}
}
return map;
}
@Nullable
public ResourceValue getConfiguredValue(
@NonNull ResourceType type,
@NonNull String name,
@NonNull FolderConfiguration referenceConfig) {
// get the resource item for the given type
ListMultimap<String, ResourceItem> items = getMap(type, false);
if (items == null) {
return null;
}
List<ResourceItem> keyItems = items.get(name);
if (keyItems == null) {
return null;
}
// look for the best match for the given configuration
// the match has to be of type ResourceFile since that's what the input list contains
ResourceItem match = (ResourceItem) referenceConfig.findMatchingConfigurable(keyItems);
return match != null ? match.getResourceValue(mFramework) : null;
}
private void addItem(@NonNull ResourceItem item) {
synchronized (ITEM_MAP_LOCK) {
ListMultimap<String, ResourceItem> map = getMap(item.getType());
if (!map.containsValue(item)) {
map.put(item.getName(), item);
}
}
}
private void removeItem(@NonNull ResourceItem removedItem) {
synchronized (ITEM_MAP_LOCK) {
Multimap<String, ResourceItem> map = getMap(removedItem.getType(), false);
if (map != null) {
map.remove(removedItem.getName(), removedItem);
}
}
}
/**
* Returns the sorted list of languages used in the resources.
*/
@NonNull
public SortedSet<String> getLanguages() {
SortedSet<String> set = new TreeSet<String>();
// As an optimization we could just look for values since that's typically where
// the languages are defined -- not on layouts, menus, etc -- especially if there
// are no translations for it
Set<String> qualifiers = Sets.newHashSet();
synchronized (ITEM_MAP_LOCK) {
for (ListMultimap<String, ResourceItem> map : getMap().values()) {
for (ResourceItem item : map.values()) {
ResourceFile source = item.getSource();
if (source != null) {
qualifiers.add(source.getQualifiers());
}
}
}
}
Splitter splitter = Splitter.on('-');
for (String s : qualifiers) {
for (String qualifier : splitter.split(s)) {
if (qualifier.length() == 2 && Character.isLetter(qualifier.charAt(0))
&& Character.isLetter(qualifier.charAt(1))) {
set.add(qualifier);
}
}
}
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) {
SortedSet<String> set = new TreeSet<String>();
// As an optimization we could just look for values since that's typically where
// the languages are defined -- not on layouts, menus, etc -- especially if there
// are no translations for it
Set<String> qualifiers = Sets.newHashSet();
synchronized (ITEM_MAP_LOCK) {
for (ListMultimap<String, ResourceItem> map : getMap().values()) {
for (ResourceItem item : map.values()) {
ResourceFile source = item.getSource();
if (source != null) {
qualifiers.add(source.getQualifiers());
}
}
}
}
Splitter splitter = Splitter.on('-');
for (String s : qualifiers) {
boolean rightLanguage = false;
for (String qualifier : splitter.split(s)) {
if (currentLanguage.equals(qualifier)) {
rightLanguage = true;
} else if (rightLanguage
&& qualifier.length() == 3
&& qualifier.charAt(0) == 'r'
&& Character.isUpperCase(qualifier.charAt(1))
&& Character.isUpperCase(qualifier.charAt(2))) {
set.add(qualifier.substring(1));
}
}
}
return set;
}
public void clear() {
getMap().clear();
}
}