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