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