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