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