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