ResourceBundle.java revision c1560e6b00b398867da12fbdc5a1fcd1d50b801c
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * Unless required by applicable law or agreed to in writing, software 8 * distributed under the License is distributed on an "AS IS" BASIS, 9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 * See the License for the specific language governing permissions and 11 * limitations under the License. 12 */ 13 14package android.databinding.tool.store; 15 16import org.apache.commons.lang3.ArrayUtils; 17 18import android.databinding.tool.util.L; 19import android.databinding.tool.util.ParserHelper; 20import android.databinding.tool.util.Preconditions; 21 22import java.io.Serializable; 23import java.util.ArrayList; 24import java.util.HashMap; 25import java.util.HashSet; 26import java.util.List; 27import java.util.Map; 28import java.util.Set; 29 30import javax.xml.bind.annotation.XmlAccessType; 31import javax.xml.bind.annotation.XmlAccessorType; 32import javax.xml.bind.annotation.XmlAttribute; 33import javax.xml.bind.annotation.XmlElement; 34import javax.xml.bind.annotation.XmlElementWrapper; 35import javax.xml.bind.annotation.XmlRootElement; 36import javax.xml.bind.annotation.adapters.XmlAdapter; 37import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; 38 39/** 40 * This is a serializable class that can keep the result of parsing layout files. 41 */ 42public class ResourceBundle implements Serializable { 43 private static final String[] ANDROID_VIEW_PACKAGE_VIEWS = new String[] 44 {"View", "ViewGroup", "ViewStub", "TextureView", "SurfaceView"}; 45 private String mAppPackage; 46 47 private HashMap<String, List<LayoutFileBundle>> mLayoutBundles 48 = new HashMap<String, List<LayoutFileBundle>>(); 49 50 public ResourceBundle(String appPackage) { 51 mAppPackage = appPackage; 52 } 53 54 public void addLayoutBundle(LayoutFileBundle bundle) { 55 if (bundle.mFileName == null) { 56 L.e("File bundle must have a name. %s does not have one.", bundle); 57 return; 58 } 59 if (!mLayoutBundles.containsKey(bundle.mFileName)) { 60 mLayoutBundles.put(bundle.mFileName, new ArrayList<LayoutFileBundle>()); 61 } 62 final List<LayoutFileBundle> bundles = mLayoutBundles.get(bundle.mFileName); 63 for (LayoutFileBundle existing : bundles) { 64 if (existing.equals(bundle)) { 65 L.d("skipping layout bundle %s because it already exists.", bundle); 66 return; 67 } 68 } 69 L.d("adding bundle %s", bundle); 70 bundles.add(bundle); 71 } 72 73 public HashMap<String, List<LayoutFileBundle>> getLayoutBundles() { 74 return mLayoutBundles; 75 } 76 77 public String getAppPackage() { 78 return mAppPackage; 79 } 80 81 public void validateMultiResLayouts() { 82 for (List<LayoutFileBundle> layoutFileBundles : mLayoutBundles.values()) { 83 for (LayoutFileBundle layoutFileBundle : layoutFileBundles) { 84 for (BindingTargetBundle target : layoutFileBundle.getBindingTargetBundles()) { 85 if (target.isBinder()) { 86 List<LayoutFileBundle> boundTo = 87 mLayoutBundles.get(target.getIncludedLayout()); 88 if (boundTo == null || boundTo.isEmpty()) { 89 L.e("There is no binding for %s", target.getIncludedLayout()); 90 } else { 91 String binding = boundTo.get(0).getFullBindingClass(); 92 target.setInterfaceType(binding); 93 } 94 } 95 } 96 } 97 } 98 99 for (Map.Entry<String, List<LayoutFileBundle>> bundles : mLayoutBundles.entrySet()) { 100 if (bundles.getValue().size() < 2) { 101 continue; 102 } 103 // validate all ids are in correct view types 104 // and all variables have the same name 105 Map<String, NameTypeLocation> variableTypes = new HashMap<String, NameTypeLocation>(); 106 Map<String, NameTypeLocation> importTypes = new HashMap<String, NameTypeLocation>(); 107 String bindingClass = null; 108 109 for (LayoutFileBundle bundle : bundles.getValue()) { 110 bundle.mHasVariations = true; 111 if (bindingClass == null) { 112 bindingClass = bundle.getFullBindingClass(); 113 } else { 114 if (!bindingClass.equals(bundle.getFullBindingClass())) { 115 L.e("Binding class names must match. Layout file for %s have " + 116 "different binding class names %s and %s", 117 bundle.getFileName(), 118 bindingClass, bundle.getFullBindingClass()); 119 } 120 } 121 for (NameTypeLocation variable : bundle.mVariables) { 122 NameTypeLocation existing = variableTypes.get(variable.name); 123 if (existing != null && !existing.equals(variable)) { 124 L.e("inconsistent variable types for %s for layout %s", 125 variable, bundle.mFileName); 126 continue; 127 } 128 variableTypes.put(variable.name, variable); 129 } 130 for (NameTypeLocation userImport : bundle.mImports) { 131 NameTypeLocation existing = importTypes.get(userImport.name); 132 if (existing != null && !existing.equals(userImport)) { 133 L.e("inconsistent variable types for %s for layout %s", 134 userImport, bundle.mFileName); 135 continue; 136 } 137 importTypes.put(userImport.name, userImport); 138 } 139 } 140 141 for (LayoutFileBundle bundle : bundles.getValue()) { 142 // now add missing ones to each to ensure they can be referenced 143 L.d("checking for missing variables in %s / %s", bundle.mFileName, 144 bundle.mConfigName); 145 for (Map.Entry<String, NameTypeLocation> variable : variableTypes.entrySet()) { 146 if (!NameTypeLocation.contains(bundle.mVariables, variable.getKey())) { 147 bundle.mVariables.add(variable.getValue()); 148 L.d("adding missing variable %s to %s / %s", variable.getKey(), 149 bundle.mFileName, bundle.mConfigName); 150 } 151 } 152 for (Map.Entry<String, NameTypeLocation> userImport : importTypes.entrySet()) { 153 if (!NameTypeLocation.contains(bundle.mImports, userImport.getKey())) { 154 bundle.mImports.add(userImport.getValue()); 155 L.d("adding missing import %s to %s / %s", userImport.getKey(), 156 bundle.mFileName, bundle.mConfigName); 157 } 158 } 159 } 160 161 Set<String> includeBindingIds = new HashSet<String>(); 162 Set<String> viewBindingIds = new HashSet<String>(); 163 Map<String, String> viewTypes = new HashMap<String, String>(); 164 Map<String, String> includes = new HashMap<String, String>(); 165 L.d("validating ids for %s", bundles.getKey()); 166 for (LayoutFileBundle bundle : bundles.getValue()) { 167 for (BindingTargetBundle target : bundle.mBindingTargetBundles) { 168 L.d("checking %s %s %s", target.getId(), target.getFullClassName(), 169 target.isBinder()); 170 if (target.mId != null) { 171 if (target.isBinder()) { 172 if (viewBindingIds.contains(target.getFullClassName())) { 173 L.e("Cannot use the same id for a View and an include tag. Error " + 174 "in file %s / %s", bundle.mFileName, bundle.mConfigName); 175 } 176 includeBindingIds.add(target.getFullClassName()); 177 } else { 178 if (includeBindingIds.contains(target.getFullClassName())) { 179 L.e("Cannot use the same id for a View and an include tag. Error in" 180 + " file %s / %s", bundle.mFileName, bundle.mConfigName); 181 } 182 viewBindingIds.add(target.getFullClassName()); 183 } 184 String existingType = viewTypes.get(target.mId); 185 if (existingType == null) { 186 L.d("assigning %s as %s", target.getId(), target.getFullClassName()); 187 viewTypes.put(target.mId, target.getFullClassName()); 188 if (target.isBinder()) { 189 includes.put(target.mId, target.getIncludedLayout()); 190 } 191 } else if (!existingType.equals(target.getFullClassName())) { 192 if (target.isBinder()) { 193 L.d("overriding %s as base binder", target.getId()); 194 viewTypes.put(target.mId, 195 "android.databinding.ViewDataBinding"); 196 includes.put(target.mId, target.getIncludedLayout()); 197 } else { 198 L.d("overriding %s as base view", target.getId()); 199 viewTypes.put(target.mId, "android.view.View"); 200 } 201 } 202 } 203 } 204 } 205 206 for (LayoutFileBundle bundle : bundles.getValue()) { 207 for (Map.Entry<String, String> viewType : viewTypes.entrySet()) { 208 BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey()); 209 if (target == null) { 210 String include = includes.get(viewType.getKey()); 211 if (include == null) { 212 bundle.createBindingTarget(viewType.getKey(), viewType.getValue(), 213 false, null, null, null); 214 } else { 215 BindingTargetBundle bindingTargetBundle = bundle.createBindingTarget( 216 viewType.getKey(), null, false, null, null, null); 217 bindingTargetBundle.setIncludedLayout(includes.get(viewType.getKey())); 218 bindingTargetBundle.setInterfaceType(viewType.getValue()); 219 } 220 } else { 221 L.d("setting interface type on %s (%s) as %s", target.mId, 222 target.getFullClassName(), viewType.getValue()); 223 target.setInterfaceType(viewType.getValue()); 224 } 225 } 226 } 227 } 228 // assign class names to each 229 for (Map.Entry<String, List<LayoutFileBundle>> entry : mLayoutBundles.entrySet()) { 230 for (LayoutFileBundle bundle : entry.getValue()) { 231 final String configName; 232 if (bundle.hasVariations()) { 233 // append configuration specifiers. 234 final String parentFileName = bundle.mDirectory; 235 L.d("parent file for %s is %s", bundle.getFileName(), parentFileName); 236 if ("layout".equals(parentFileName)) { 237 configName = ""; 238 } else { 239 configName = ParserHelper.toClassName(parentFileName.substring("layout-".length())); 240 } 241 } else { 242 configName = ""; 243 } 244 bundle.mConfigName = configName; 245 } 246 } 247 } 248 249 @XmlAccessorType(XmlAccessType.NONE) 250 @XmlRootElement(name="Layout") 251 public static class LayoutFileBundle implements Serializable { 252 @XmlAttribute(name="layout", required = true) 253 public String mFileName; 254 @XmlAttribute(name="modulePackage", required = true) 255 public String mModulePackage; 256 private String mConfigName; 257 258 // The binding class as given by the user 259 @XmlAttribute(name="bindingClass", required = false) 260 public String mBindingClass; 261 262 // The full package and class name as determined from mBindingClass and mModulePackage 263 private String mFullBindingClass; 264 265 // The simple binding class name as determined from mBindingClass and mModulePackage 266 private String mBindingClassName; 267 268 // The package of the binding class as determined from mBindingClass and mModulePackage 269 private String mBindingPackage; 270 271 @XmlAttribute(name="directory", required = true) 272 public String mDirectory; 273 public boolean mHasVariations; 274 275 @XmlElement(name="Variables") 276 public List<NameTypeLocation> mVariables = new ArrayList<>(); 277 278 @XmlElement(name="Imports") 279 public List<NameTypeLocation> mImports = new ArrayList<>(); 280 281 @XmlElementWrapper(name="Targets") 282 @XmlElement(name="Target") 283 public List<BindingTargetBundle> mBindingTargetBundles = new ArrayList<BindingTargetBundle>(); 284 285 @XmlAttribute(name="isMerge", required = true) 286 private boolean mIsMerge; 287 288 // for XML binding 289 public LayoutFileBundle() { 290 } 291 292 public LayoutFileBundle(String fileName, String directory, String modulePackage, 293 boolean isMerge) { 294 mFileName = fileName; 295 mDirectory = directory; 296 mModulePackage = modulePackage; 297 mIsMerge = isMerge; 298 } 299 300 public void addVariable(String name, String type, Location location) { 301 Preconditions.check(!NameTypeLocation.contains(mVariables, name), 302 "Cannot use same variable name twice. %s in %s", name, location); 303 mVariables.add(new NameTypeLocation(name, type, location)); 304 } 305 306 public void addImport(String alias, String type, Location location) { 307 Preconditions.check(!NameTypeLocation.contains(mImports, alias), 308 "Cannot import same alias twice. %s in %s", alias, location); 309 mImports.add(new NameTypeLocation(alias, type, location)); 310 } 311 312 public BindingTargetBundle createBindingTarget(String id, String viewName, 313 boolean used, String tag, String originalTag, Location location) { 314 BindingTargetBundle target = new BindingTargetBundle(id, viewName, used, tag, 315 originalTag, location); 316 mBindingTargetBundles.add(target); 317 return target; 318 } 319 320 public boolean isEmpty() { 321 return mVariables.isEmpty() && mImports.isEmpty() && mBindingTargetBundles.isEmpty(); 322 } 323 324 public BindingTargetBundle getBindingTargetById(String key) { 325 for (BindingTargetBundle target : mBindingTargetBundles) { 326 if (key.equals(target.mId)) { 327 return target; 328 } 329 } 330 return null; 331 } 332 333 public String getFileName() { 334 return mFileName; 335 } 336 337 public String getConfigName() { 338 return mConfigName; 339 } 340 341 public String getDirectory() { 342 return mDirectory; 343 } 344 345 public boolean hasVariations() { 346 return mHasVariations; 347 } 348 349 public List<NameTypeLocation> getVariables() { 350 return mVariables; 351 } 352 353 public List<NameTypeLocation> getImports() { 354 return mImports; 355 } 356 357 public boolean isMerge() { 358 return mIsMerge; 359 } 360 361 public String getBindingClassName() { 362 if (mBindingClassName == null) { 363 String fullClass = getFullBindingClass(); 364 int dotIndex = fullClass.lastIndexOf('.'); 365 mBindingClassName = fullClass.substring(dotIndex + 1); 366 } 367 return mBindingClassName; 368 } 369 370 public void setBindingClass(String bindingClass) { 371 mBindingClass = bindingClass; 372 } 373 374 public String getBindingClassPackage() { 375 if (mBindingPackage == null) { 376 String fullClass = getFullBindingClass(); 377 int dotIndex = fullClass.lastIndexOf('.'); 378 mBindingPackage = fullClass.substring(0, dotIndex); 379 } 380 return mBindingPackage; 381 } 382 383 private String getFullBindingClass() { 384 if (mFullBindingClass == null) { 385 if (mBindingClass == null) { 386 mFullBindingClass = getModulePackage() + ".databinding." + 387 ParserHelper.toClassName(getFileName()) + "Binding"; 388 } else if (mBindingClass.startsWith(".")) { 389 mFullBindingClass = getModulePackage() + mBindingClass; 390 } else if (mBindingClass.indexOf('.') < 0) { 391 mFullBindingClass = getModulePackage() + ".databinding." + mBindingClass; 392 } else { 393 mFullBindingClass = mBindingClass; 394 } 395 } 396 return mFullBindingClass; 397 } 398 399 public List<BindingTargetBundle> getBindingTargetBundles() { 400 return mBindingTargetBundles; 401 } 402 403 @Override 404 public boolean equals(Object o) { 405 if (this == o) { 406 return true; 407 } 408 if (o == null || getClass() != o.getClass()) { 409 return false; 410 } 411 412 LayoutFileBundle bundle = (LayoutFileBundle) o; 413 414 if (mConfigName != null ? !mConfigName.equals(bundle.mConfigName) 415 : bundle.mConfigName != null) { 416 return false; 417 } 418 if (mDirectory != null ? !mDirectory.equals(bundle.mDirectory) 419 : bundle.mDirectory != null) { 420 return false; 421 } 422 if (mFileName != null ? !mFileName.equals(bundle.mFileName) 423 : bundle.mFileName != null) { 424 return false; 425 } 426 427 return true; 428 } 429 430 @Override 431 public int hashCode() { 432 int result = mFileName != null ? mFileName.hashCode() : 0; 433 result = 31 * result + (mConfigName != null ? mConfigName.hashCode() : 0); 434 result = 31 * result + (mDirectory != null ? mDirectory.hashCode() : 0); 435 return result; 436 } 437 438 @Override 439 public String toString() { 440 return "LayoutFileBundle{" + 441 "mHasVariations=" + mHasVariations + 442 ", mDirectory='" + mDirectory + '\'' + 443 ", mConfigName='" + mConfigName + '\'' + 444 ", mModulePackage='" + mModulePackage + '\'' + 445 ", mFileName='" + mFileName + '\'' + 446 '}'; 447 } 448 449 public String getModulePackage() { 450 return mModulePackage; 451 } 452 } 453 454 @XmlAccessorType(XmlAccessType.NONE) 455 public static class NameTypeLocation { 456 @XmlAttribute(name="type", required = true) 457 public String type; 458 459 @XmlAttribute(name="name", required = true) 460 public String name; 461 462 @XmlElement(name="location", required = false) 463 public Location location; 464 465 public NameTypeLocation() { 466 } 467 468 public NameTypeLocation(String name, String type, Location location) { 469 this.type = type; 470 this.name = name; 471 this.location = location; 472 } 473 474 @Override 475 public String toString() { 476 return "{" + 477 "type='" + type + '\'' + 478 ", name='" + name + '\'' + 479 ", location=" + location + 480 '}'; 481 } 482 483 @Override 484 public boolean equals(Object o) { 485 if (this == o) { 486 return true; 487 } 488 if (o == null || getClass() != o.getClass()) { 489 return false; 490 } 491 492 NameTypeLocation that = (NameTypeLocation) o; 493 494 if (location != null ? !location.equals(that.location) : that.location != null) { 495 return false; 496 } 497 if (!name.equals(that.name)) { 498 return false; 499 } 500 if (!type.equals(that.type)) { 501 return false; 502 } 503 504 return true; 505 } 506 507 @Override 508 public int hashCode() { 509 int result = type.hashCode(); 510 result = 31 * result + name.hashCode(); 511 result = 31 * result + (location != null ? location.hashCode() : 0); 512 return result; 513 } 514 515 public static boolean contains(List<NameTypeLocation> list, String name) { 516 for (NameTypeLocation ntl : list) { 517 if (name.equals(ntl.name)) { 518 return true; 519 } 520 } 521 return false; 522 } 523 } 524 525 public static class MarshalledMapType { 526 public List<NameTypeLocation> entries; 527 } 528 529 @XmlAccessorType(XmlAccessType.NONE) 530 public static class BindingTargetBundle implements Serializable { 531 // public for XML serialization 532 533 @XmlAttribute(name="id") 534 public String mId; 535 @XmlAttribute(name="tag", required = true) 536 public String mTag; 537 @XmlAttribute(name="originalTag") 538 public String mOriginalTag; 539 @XmlAttribute(name="view", required = false) 540 public String mViewName; 541 private String mFullClassName; 542 public boolean mUsed = true; 543 @XmlElementWrapper(name="Expressions") 544 @XmlElement(name="Expression") 545 public List<BindingBundle> mBindingBundleList = new ArrayList<BindingBundle>(); 546 @XmlAttribute(name="include") 547 public String mIncludedLayout; 548 @XmlElement(name="location") 549 public Location mLocation; 550 private String mInterfaceType; 551 552 // For XML serialization 553 public BindingTargetBundle() {} 554 555 public BindingTargetBundle(String id, String viewName, boolean used, 556 String tag, String originalTag, Location location) { 557 mId = id; 558 mViewName = viewName; 559 mUsed = used; 560 mTag = tag; 561 mOriginalTag = originalTag; 562 mLocation = location; 563 } 564 565 public void addBinding(String name, String expr) { 566 mBindingBundleList.add(new BindingBundle(name, expr)); 567 } 568 569 public void setIncludedLayout(String includedLayout) { 570 mIncludedLayout = includedLayout; 571 } 572 573 public String getIncludedLayout() { 574 return mIncludedLayout; 575 } 576 577 public boolean isBinder() { 578 return mIncludedLayout != null; 579 } 580 581 public void setInterfaceType(String interfaceType) { 582 mInterfaceType = interfaceType; 583 } 584 585 public void setLocation(Location location) { 586 mLocation = location; 587 } 588 589 public Location getLocation() { 590 return mLocation; 591 } 592 593 public String getId() { 594 return mId; 595 } 596 597 public String getTag() { 598 return mTag; 599 } 600 601 public String getOriginalTag() { 602 return mOriginalTag; 603 } 604 605 public String getFullClassName() { 606 if (mFullClassName == null) { 607 if (isBinder()) { 608 mFullClassName = mInterfaceType; 609 } else if (mViewName.indexOf('.') == -1) { 610 if (ArrayUtils.contains(ANDROID_VIEW_PACKAGE_VIEWS, mViewName)) { 611 mFullClassName = "android.view." + mViewName; 612 } else if("WebView".equals(mViewName)) { 613 mFullClassName = "android.webkit." + mViewName; 614 } else { 615 mFullClassName = "android.widget." + mViewName; 616 } 617 } else { 618 mFullClassName = mViewName; 619 } 620 } 621 if (mFullClassName == null) { 622 L.e("Unexpected full class name = null. view = %s, interface = %s, layout = %s", 623 mViewName, mInterfaceType, mIncludedLayout); 624 } 625 return mFullClassName; 626 } 627 628 public boolean isUsed() { 629 return mUsed; 630 } 631 632 public List<BindingBundle> getBindingBundleList() { 633 return mBindingBundleList; 634 } 635 636 public String getInterfaceType() { 637 return mInterfaceType; 638 } 639 640 @XmlAccessorType(XmlAccessType.NONE) 641 public static class BindingBundle implements Serializable { 642 643 private String mName; 644 private String mExpr; 645 646 public BindingBundle() {} 647 648 public BindingBundle(String name, String expr) { 649 mName = name; 650 mExpr = expr; 651 } 652 653 @XmlAttribute(name="attribute", required=true) 654 public String getName() { 655 return mName; 656 } 657 658 @XmlAttribute(name="text", required=true) 659 public String getExpr() { 660 return mExpr; 661 } 662 663 public void setName(String name) { 664 mName = name; 665 } 666 667 public void setExpr(String expr) { 668 mExpr = expr; 669 } 670 } 671 } 672} 673