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 for (List<ResourceFolder> list : mFolderMap.values()) { 359 for (ResourceFolder resFolder : list) { 360 IAbstractFolder wrapper = resFolder.getFolder(); 361 if (wrapper.equals(folder)) { 362 return resFolder; 363 } 364 } 365 } 366 367 return null; 368 } 369 370 /** 371 * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and 372 * configuration. 373 * <p/>This only works with files generating one resource named after the file (for instance, 374 * layouts, bitmap based drawable, xml, anims). 375 * @return the matching file or <code>null</code> if no match was found. 376 */ 377 public ResourceFile getMatchingFile(String name, ResourceFolderType type, 378 FolderConfiguration config) { 379 // get the folders for the given type 380 List<ResourceFolder> folders = mFolderMap.get(type); 381 382 // look for folders containing a file with the given name. 383 ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>(folders.size()); 384 385 // remove the folders that do not have a file with the given name. 386 for (int i = 0 ; i < folders.size(); i++) { 387 ResourceFolder folder = folders.get(i); 388 389 if (folder.hasFile(name) == true) { 390 matchingFolders.add(folder); 391 } 392 } 393 394 // from those, get the folder with a config matching the given reference configuration. 395 Configurable match = config.findMatchingConfigurable(matchingFolders); 396 397 // do we have a matching folder? 398 if (match instanceof ResourceFolder) { 399 // get the ResourceFile from the filename 400 return ((ResourceFolder)match).getFile(name); 401 } 402 403 return null; 404 } 405 406 /** 407 * Returns the list of source files for a given resource. 408 * Optionally, if a {@link FolderConfiguration} is given, then only the best 409 * match for this config is returned. 410 * 411 * @param type the type of the resource. 412 * @param name the name of the resource. 413 * @param referenceConfig an optional config for which only the best match will be returned. 414 * 415 * @return a list of files generating this resource or null if it was not found. 416 */ 417 public List<ResourceFile> getSourceFiles(ResourceType type, String name, 418 FolderConfiguration referenceConfig) { 419 420 Collection<ResourceItem> items = getResourceItemsOfType(type); 421 422 for (ResourceItem item : items) { 423 if (name.equals(item.getName())) { 424 if (referenceConfig != null) { 425 Configurable match = referenceConfig.findMatchingConfigurable( 426 item.getSourceFileList()); 427 428 if (match instanceof ResourceFile) { 429 return Collections.singletonList((ResourceFile) match); 430 } 431 432 return null; 433 } 434 return item.getSourceFileList(); 435 } 436 } 437 438 return null; 439 } 440 441 /** 442 * Returns the resources values matching a given {@link FolderConfiguration}. 443 * 444 * @param referenceConfig the configuration that each value must match. 445 * @return a map with guaranteed to contain an entry for each {@link ResourceType} 446 */ 447 public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources( 448 FolderConfiguration referenceConfig) { 449 return doGetConfiguredResources(referenceConfig); 450 } 451 452 /** 453 * Returns the resources values matching a given {@link FolderConfiguration} for the current 454 * project. 455 * 456 * @param referenceConfig the configuration that each value must match. 457 * @return a map with guaranteed to contain an entry for each {@link ResourceType} 458 */ 459 protected final Map<ResourceType, Map<String, ResourceValue>> doGetConfiguredResources( 460 FolderConfiguration referenceConfig) { 461 462 Map<ResourceType, Map<String, ResourceValue>> map = 463 new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); 464 465 for (ResourceType key : ResourceType.values()) { 466 // get the local results and put them in the map 467 map.put(key, getConfiguredResource(key, referenceConfig)); 468 } 469 470 return map; 471 } 472 473 /** 474 * Returns the sorted list of languages used in the resources. 475 */ 476 public SortedSet<String> getLanguages() { 477 SortedSet<String> set = new TreeSet<String>(); 478 479 Collection<List<ResourceFolder>> folderList = mFolderMap.values(); 480 for (List<ResourceFolder> folderSubList : folderList) { 481 for (ResourceFolder folder : folderSubList) { 482 FolderConfiguration config = folder.getConfiguration(); 483 LanguageQualifier lang = config.getLanguageQualifier(); 484 if (lang != null) { 485 set.add(lang.getShortDisplayValue()); 486 } 487 } 488 } 489 490 return set; 491 } 492 493 /** 494 * Returns the sorted list of regions used in the resources with the given language. 495 * @param currentLanguage the current language the region must be associated with. 496 */ 497 public SortedSet<String> getRegions(String currentLanguage) { 498 SortedSet<String> set = new TreeSet<String>(); 499 500 Collection<List<ResourceFolder>> folderList = mFolderMap.values(); 501 for (List<ResourceFolder> folderSubList : folderList) { 502 for (ResourceFolder folder : folderSubList) { 503 FolderConfiguration config = folder.getConfiguration(); 504 505 // get the language 506 LanguageQualifier lang = config.getLanguageQualifier(); 507 if (lang != null && lang.getShortDisplayValue().equals(currentLanguage)) { 508 RegionQualifier region = config.getRegionQualifier(); 509 if (region != null) { 510 set.add(region.getShortDisplayValue()); 511 } 512 } 513 } 514 } 515 516 return set; 517 } 518 519 /** 520 * Loads the resources from a resource folder. 521 * <p/> 522 * 523 * @param rootFolder The folder to read the resources from. This is the top level 524 * resource folder (res/) 525 * @throws IOException 526 */ 527 public void loadResources(IAbstractFolder rootFolder) 528 throws IOException { 529 ScanningContext context = new ScanningContext(this); 530 531 IAbstractResource[] files = rootFolder.listMembers(); 532 for (IAbstractResource file : files) { 533 if (file instanceof IAbstractFolder) { 534 IAbstractFolder folder = (IAbstractFolder) file; 535 ResourceFolder resFolder = processFolder(folder); 536 537 if (resFolder != null) { 538 // now we process the content of the folder 539 IAbstractResource[] children = folder.listMembers(); 540 541 for (IAbstractResource childRes : children) { 542 if (childRes instanceof IAbstractFile) { 543 resFolder.processFile((IAbstractFile) childRes, 544 ResourceDeltaKind.ADDED, context); 545 } 546 } 547 } 548 } 549 } 550 } 551 552 553 protected void removeFile(Collection<ResourceType> types, ResourceFile file) { 554 for (ResourceType type : types) { 555 removeFile(type, file); 556 } 557 } 558 559 protected void removeFile(ResourceType type, ResourceFile file) { 560 List<ResourceItem> list = mResourceMap.get(type); 561 if (list != null) { 562 for (int i = 0 ; i < list.size(); i++) { 563 ResourceItem item = list.get(i); 564 item.removeFile(file); 565 } 566 } 567 } 568 569 /** 570 * Returns a map of (resource name, resource value) for the given {@link ResourceType}. 571 * <p/>The values returned are taken from the resource files best matching a given 572 * {@link FolderConfiguration}. 573 * @param type the type of the resources. 574 * @param referenceConfig the configuration to best match. 575 */ 576 private Map<String, ResourceValue> getConfiguredResource(ResourceType type, 577 FolderConfiguration referenceConfig) { 578 579 // get the resource item for the given type 580 List<ResourceItem> items = mResourceMap.get(type); 581 if (items == null) { 582 return new HashMap<String, ResourceValue>(); 583 } 584 585 // create the map 586 HashMap<String, ResourceValue> map = new HashMap<String, ResourceValue>(items.size()); 587 588 for (ResourceItem item : items) { 589 ResourceValue value = item.getResourceValue(type, referenceConfig, 590 isFrameworkRepository()); 591 if (value != null) { 592 map.put(item.getName(), value); 593 } 594 } 595 596 return map; 597 } 598 599 600 /** 601 * Called after a resource change event, when the resource delta has been processed. 602 */ 603 protected void postUpdate() { 604 // Since removed files/folders remove source files from existing ResourceItem, loop through 605 // all resource items and remove the ones that have no source files. 606 607 Collection<List<ResourceItem>> lists = mResourceMap.values(); 608 for (List<ResourceItem> list : lists) { 609 for (int i = 0 ; i < list.size() ;) { 610 if (list.get(i).hasNoSourceFile()) { 611 list.remove(i); 612 } else { 613 i++; 614 } 615 } 616 } 617 } 618 619 /** 620 * Looks up an existing {@link ResourceItem} by {@link ResourceType} and name. This 621 * ignores inline resources. 622 * @param type the Resource Type. 623 * @param name the Resource name. 624 * @return the existing ResourceItem or null if no match was found. 625 */ 626 private ResourceItem findDeclaredResourceItem(ResourceType type, String name) { 627 List<ResourceItem> list = mResourceMap.get(type); 628 629 if (list != null) { 630 for (ResourceItem item : list) { 631 // ignore inline 632 if (name.equals(item.getName()) && item.isDeclaredInline() == false) { 633 return item; 634 } 635 } 636 } 637 638 return null; 639 } 640 } 641