ResourceBundle.java revision af146d6a8c0efcf5682d14047c06866a5548f78f
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 android.databinding.tool.processing.ErrorMessages;
17import android.databinding.tool.processing.Scope;
18import android.databinding.tool.processing.ScopedException;
19import android.databinding.tool.processing.scopes.FileScopeProvider;
20import android.databinding.tool.processing.scopes.LocationScopeProvider;
21import android.databinding.tool.util.L;
22import android.databinding.tool.util.ParserHelper;
23import android.databinding.tool.util.Preconditions;
24
25import java.io.File;
26import java.io.InputStream;
27import java.io.Serializable;
28import java.io.StringWriter;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.HashMap;
32import java.util.HashSet;
33import java.util.List;
34import java.util.Map;
35import java.util.Set;
36
37import javax.xml.bind.JAXBContext;
38import javax.xml.bind.JAXBException;
39import javax.xml.bind.Marshaller;
40import javax.xml.bind.Unmarshaller;
41import javax.xml.bind.annotation.XmlAccessType;
42import javax.xml.bind.annotation.XmlAccessorType;
43import javax.xml.bind.annotation.XmlAttribute;
44import javax.xml.bind.annotation.XmlElement;
45import javax.xml.bind.annotation.XmlElementWrapper;
46import javax.xml.bind.annotation.XmlRootElement;
47
48/**
49 * This is a serializable class that can keep the result of parsing layout files.
50 */
51public class ResourceBundle implements Serializable {
52    private static final String[] ANDROID_VIEW_PACKAGE_VIEWS = new String[]
53            {"View", "ViewGroup", "ViewStub", "TextureView", "SurfaceView"};
54    private String mAppPackage;
55
56    private HashMap<String, List<LayoutFileBundle>> mLayoutBundles
57            = new HashMap<String, List<LayoutFileBundle>>();
58
59    private List<File> mRemovedFiles = new ArrayList<File>();
60
61    public ResourceBundle(String appPackage) {
62        mAppPackage = appPackage;
63    }
64
65    public void addLayoutBundle(LayoutFileBundle bundle) {
66        if (bundle.mFileName == null) {
67            L.e("File bundle must have a name. %s does not have one.", bundle);
68            return;
69        }
70        if (!mLayoutBundles.containsKey(bundle.mFileName)) {
71            mLayoutBundles.put(bundle.mFileName, new ArrayList<LayoutFileBundle>());
72        }
73        final List<LayoutFileBundle> bundles = mLayoutBundles.get(bundle.mFileName);
74        for (LayoutFileBundle existing : bundles) {
75            if (existing.equals(bundle)) {
76                L.d("skipping layout bundle %s because it already exists.", bundle);
77                return;
78            }
79        }
80        L.d("adding bundle %s", bundle);
81        bundles.add(bundle);
82    }
83
84    public HashMap<String, List<LayoutFileBundle>> getLayoutBundles() {
85        return mLayoutBundles;
86    }
87
88    public String getAppPackage() {
89        return mAppPackage;
90    }
91
92    public void validateMultiResLayouts() {
93        for (List<LayoutFileBundle> layoutFileBundles : mLayoutBundles.values()) {
94            for (LayoutFileBundle layoutFileBundle : layoutFileBundles) {
95                List<BindingTargetBundle> unboundIncludes = new ArrayList<BindingTargetBundle>();
96                for (BindingTargetBundle target : layoutFileBundle.getBindingTargetBundles()) {
97                    if (target.isBinder()) {
98                        List<LayoutFileBundle> boundTo =
99                                mLayoutBundles.get(target.getIncludedLayout());
100                        if (boundTo == null || boundTo.isEmpty()) {
101                            L.d("There is no binding for %s, reverting to plain layout",
102                                    target.getIncludedLayout());
103                            if (target.getId() == null) {
104                                unboundIncludes.add(target);
105                            } else {
106                                target.setIncludedLayout(null);
107                                target.setInterfaceType("android.view.View");
108                                target.mViewName = "android.view.View";
109                            }
110                        } else {
111                            String binding = boundTo.get(0).getFullBindingClass();
112                            target.setInterfaceType(binding);
113                        }
114                    }
115                }
116                layoutFileBundle.getBindingTargetBundles().removeAll(unboundIncludes);
117            }
118        }
119
120        for (Map.Entry<String, List<LayoutFileBundle>> bundles : mLayoutBundles.entrySet()) {
121            if (bundles.getValue().size() < 2) {
122                continue;
123            }
124
125            // validate all ids are in correct view types
126            // and all variables have the same name
127            for (LayoutFileBundle bundle : bundles.getValue()) {
128                bundle.mHasVariations = true;
129            }
130            String bindingClass = validateAndGetSharedClassName(bundles.getValue());
131            Map<String, NameTypeLocation> variableTypes = validateAndMergeNameTypeLocations(
132                    bundles.getValue(), ErrorMessages.MULTI_CONFIG_VARIABLE_TYPE_MISMATCH,
133                    new ValidateAndFilterCallback() {
134                        @Override
135                        public List<? extends NameTypeLocation> get(LayoutFileBundle bundle) {
136                            return bundle.mVariables;
137                        }
138                    });
139
140            Map<String, NameTypeLocation> importTypes = validateAndMergeNameTypeLocations(
141                    bundles.getValue(), ErrorMessages.MULTI_CONFIG_IMPORT_TYPE_MISMATCH,
142                    new ValidateAndFilterCallback() {
143                        @Override
144                        public List<NameTypeLocation> get(LayoutFileBundle bundle) {
145                            return bundle.mImports;
146                        }
147                    });
148
149            for (LayoutFileBundle bundle : bundles.getValue()) {
150                // now add missing ones to each to ensure they can be referenced
151                L.d("checking for missing variables in %s / %s", bundle.mFileName,
152                        bundle.mConfigName);
153                for (Map.Entry<String, NameTypeLocation> variable : variableTypes.entrySet()) {
154                    if (!NameTypeLocation.contains(bundle.mVariables, variable.getKey())) {
155                        NameTypeLocation orig = variable.getValue();
156                        bundle.addVariable(orig.name, orig.type, orig.location, false);
157                        L.d("adding missing variable %s to %s / %s", variable.getKey(),
158                                bundle.mFileName, bundle.mConfigName);
159                    }
160                }
161                for (Map.Entry<String, NameTypeLocation> userImport : importTypes.entrySet()) {
162                    if (!NameTypeLocation.contains(bundle.mImports, userImport.getKey())) {
163                        bundle.mImports.add(userImport.getValue());
164                        L.d("adding missing import %s to %s / %s", userImport.getKey(),
165                                bundle.mFileName, bundle.mConfigName);
166                    }
167                }
168            }
169
170            Set<String> includeBindingIds = new HashSet<String>();
171            Set<String> viewBindingIds = new HashSet<String>();
172            Map<String, String> viewTypes = new HashMap<String, String>();
173            Map<String, String> includes = new HashMap<String, String>();
174            L.d("validating ids for %s", bundles.getKey());
175            Set<String> conflictingIds = new HashSet<String>();
176            for (LayoutFileBundle bundle : bundles.getValue()) {
177                try {
178                    Scope.enter(bundle);
179                    for (BindingTargetBundle target : bundle.mBindingTargetBundles) {
180                        try {
181                            Scope.enter(target);
182                            L.d("checking %s %s %s", target.getId(), target.getFullClassName(),
183                                    target.isBinder());
184                            if (target.mId != null) {
185                                if (target.isBinder()) {
186                                    if (viewBindingIds.contains(target.mId)) {
187                                        L.d("%s is conflicting", target.mId);
188                                        conflictingIds.add(target.mId);
189                                        continue;
190                                    }
191                                    includeBindingIds.add(target.mId);
192                                } else {
193                                    if (includeBindingIds.contains(target.mId)) {
194                                        L.d("%s is conflicting", target.mId);
195                                        conflictingIds.add(target.mId);
196                                        continue;
197                                    }
198                                    viewBindingIds.add(target.mId);
199                                }
200                                String existingType = viewTypes.get(target.mId);
201                                if (existingType == null) {
202                                    L.d("assigning %s as %s", target.getId(),
203                                            target.getFullClassName());
204                                            viewTypes.put(target.mId, target.getFullClassName());
205                                    if (target.isBinder()) {
206                                        includes.put(target.mId, target.getIncludedLayout());
207                                    }
208                                } else if (!existingType.equals(target.getFullClassName())) {
209                                    if (target.isBinder()) {
210                                        L.d("overriding %s as base binder", target.getId());
211                                        viewTypes.put(target.mId,
212                                                "android.databinding.ViewDataBinding");
213                                        includes.put(target.mId, target.getIncludedLayout());
214                                    } else {
215                                        L.d("overriding %s as base view", target.getId());
216                                        viewTypes.put(target.mId, "android.view.View");
217                                    }
218                                }
219                            }
220                        } catch (ScopedException ex) {
221                            Scope.defer(ex);
222                        } finally {
223                            Scope.exit();
224                        }
225                    }
226                } finally {
227                    Scope.exit();
228                }
229            }
230
231            if (!conflictingIds.isEmpty()) {
232                for (LayoutFileBundle bundle : bundles.getValue()) {
233                    for (BindingTargetBundle target : bundle.mBindingTargetBundles) {
234                        if (conflictingIds.contains(target.mId)) {
235                            Scope.registerError(String.format(
236                                            ErrorMessages.MULTI_CONFIG_ID_USED_AS_IMPORT,
237                                            target.mId), bundle, target);
238                        }
239                    }
240                }
241            }
242
243            for (LayoutFileBundle bundle : bundles.getValue()) {
244                try {
245                    Scope.enter(bundle);
246                    for (Map.Entry<String, String> viewType : viewTypes.entrySet()) {
247                        BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey());
248                        if (target == null) {
249                            String include = includes.get(viewType.getKey());
250                            if (include == null) {
251                                bundle.createBindingTarget(viewType.getKey(), viewType.getValue(),
252                                        false, null, null, null);
253                            } else {
254                                BindingTargetBundle bindingTargetBundle = bundle
255                                        .createBindingTarget(
256                                                viewType.getKey(), null, false, null, null, null);
257                                bindingTargetBundle
258                                        .setIncludedLayout(includes.get(viewType.getKey()));
259                                bindingTargetBundle.setInterfaceType(viewType.getValue());
260                            }
261                        } else {
262                            L.d("setting interface type on %s (%s) as %s", target.mId,
263                                    target.getFullClassName(), viewType.getValue());
264                            target.setInterfaceType(viewType.getValue());
265                        }
266                    }
267                } catch (ScopedException ex) {
268                    Scope.defer(ex);
269                } finally {
270                    Scope.exit();
271                }
272            }
273        }
274        // assign class names to each
275        for (Map.Entry<String, List<LayoutFileBundle>> entry : mLayoutBundles.entrySet()) {
276            for (LayoutFileBundle bundle : entry.getValue()) {
277                final String configName;
278                if (bundle.hasVariations()) {
279                    // append configuration specifiers.
280                    final String parentFileName = bundle.mDirectory;
281                    L.d("parent file for %s is %s", bundle.getFileName(), parentFileName);
282                    if ("layout".equals(parentFileName)) {
283                        configName = "";
284                    } else {
285                        configName = ParserHelper.toClassName(parentFileName.substring("layout-".length()));
286                    }
287                } else {
288                    configName = "";
289                }
290                bundle.mConfigName = configName;
291            }
292        }
293    }
294
295    /**
296     * Receives a list of bundles which are representations of the same layout file in different
297     * configurations.
298     * @param bundles
299     * @return The map for variables and their types
300     */
301    private Map<String, NameTypeLocation> validateAndMergeNameTypeLocations(
302            List<LayoutFileBundle> bundles, String errorMessage,
303            ValidateAndFilterCallback callback) {
304        Map<String, NameTypeLocation> result = new HashMap<String, NameTypeLocation>();
305        Set<String> mismatched = new HashSet<String>();
306        for (LayoutFileBundle bundle : bundles) {
307            for (NameTypeLocation item : callback.get(bundle)) {
308                NameTypeLocation existing = result.get(item.name);
309                if (existing != null && !existing.type.equals(item.type)) {
310                    mismatched.add(item.name);
311                    continue;
312                }
313                result.put(item.name, item);
314            }
315        }
316        if (mismatched.isEmpty()) {
317            return result;
318        }
319        // create exceptions. We could get more clever and find the outlier but for now, listing
320        // each file w/ locations seems enough
321        for (String mismatch : mismatched) {
322            for (LayoutFileBundle bundle : bundles) {
323                NameTypeLocation found = null;
324                for (NameTypeLocation item : callback.get(bundle)) {
325                    if (mismatch.equals(item.name)) {
326                        found = item;
327                        break;
328                    }
329                }
330                if (found == null) {
331                    // variable is not defined in this layout, continue
332                    continue;
333                }
334                Scope.registerError(String.format(
335                                errorMessage, found.name, found.type,
336                                bundle.mDirectory + "/" + bundle.getFileName()), bundle,
337                        found.location.createScope());
338            }
339        }
340        return result;
341    }
342
343    /**
344     * Receives a list of bundles which are representations of the same layout file in different
345     * configurations.
346     * @param bundles
347     * @return The shared class name for these bundles
348     */
349    private String validateAndGetSharedClassName(List<LayoutFileBundle> bundles) {
350        String sharedClassName = null;
351        boolean hasMismatch = false;
352        for (LayoutFileBundle bundle : bundles) {
353            bundle.mHasVariations = true;
354            String fullBindingClass = bundle.getFullBindingClass();
355            if (sharedClassName == null) {
356                sharedClassName = fullBindingClass;
357            } else if (!sharedClassName.equals(fullBindingClass)) {
358                hasMismatch = true;
359                break;
360            }
361        }
362        if (!hasMismatch) {
363            return sharedClassName;
364        }
365        // generate proper exceptions for each
366        for (LayoutFileBundle bundle : bundles) {
367            Scope.registerError(String.format(ErrorMessages.MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH,
368                    bundle.getFullBindingClass(), bundle.mDirectory + "/" + bundle.getFileName()),
369                    bundle, bundle.getClassNameLocationProvider());
370        }
371        return sharedClassName;
372    }
373
374    public void addRemovedFile(File file) {
375        mRemovedFiles.add(file);
376    }
377
378    public List<File> getRemovedFiles() {
379        return mRemovedFiles;
380    }
381
382    @XmlAccessorType(XmlAccessType.NONE)
383    @XmlRootElement(name="Layout")
384    public static class LayoutFileBundle implements Serializable, FileScopeProvider {
385        @XmlAttribute(name="layout", required = true)
386        public String mFileName;
387        @XmlAttribute(name="modulePackage", required = true)
388        public String mModulePackage;
389        @XmlAttribute(name="absoluteFilePath", required = true)
390        public String mAbsoluteFilePath;
391        private String mConfigName;
392
393        // The binding class as given by the user
394        @XmlAttribute(name="bindingClass", required = false)
395        public String mBindingClass;
396
397        // The location of the name of the generated class, optional
398        @XmlElement(name = "ClassNameLocation", required = false)
399        private Location mClassNameLocation;
400        // The full package and class name as determined from mBindingClass and mModulePackage
401        private String mFullBindingClass;
402
403        // The simple binding class name as determined from mBindingClass and mModulePackage
404        private String mBindingClassName;
405
406        // The package of the binding class as determined from mBindingClass and mModulePackage
407        private String mBindingPackage;
408
409        @XmlAttribute(name="directory", required = true)
410        public String mDirectory;
411        public boolean mHasVariations;
412
413        @XmlElement(name="Variables")
414        public List<VariableDeclaration> mVariables = new ArrayList<VariableDeclaration>();
415
416        @XmlElement(name="Imports")
417        public List<NameTypeLocation> mImports = new ArrayList<NameTypeLocation>();
418
419        @XmlElementWrapper(name="Targets")
420        @XmlElement(name="Target")
421        public List<BindingTargetBundle> mBindingTargetBundles = new ArrayList<BindingTargetBundle>();
422
423        @XmlAttribute(name="isMerge", required = true)
424        private boolean mIsMerge;
425
426        private LocationScopeProvider mClassNameLocationProvider;
427
428        // for XML binding
429        public LayoutFileBundle() {
430        }
431
432        /**
433         * Updates configuration fields from the given bundle but does not change variables,
434         * binding expressions etc.
435         */
436        public void inheritConfigurationFrom(LayoutFileBundle other) {
437            mFileName = other.mFileName;
438            mModulePackage = other.mModulePackage;
439            mBindingClass = other.mBindingClass;
440            mFullBindingClass = other.mFullBindingClass;
441            mBindingClassName = other.mBindingClassName;
442            mBindingPackage = other.mBindingPackage;
443            mHasVariations = other.mHasVariations;
444            mIsMerge = other.mIsMerge;
445        }
446
447        public LayoutFileBundle(File file, String fileName, String directory,
448                String modulePackage, boolean isMerge) {
449            mFileName = fileName;
450            mDirectory = directory;
451            mModulePackage = modulePackage;
452            mIsMerge = isMerge;
453            mAbsoluteFilePath = file.getAbsolutePath();
454        }
455
456        public LocationScopeProvider getClassNameLocationProvider() {
457            if (mClassNameLocationProvider == null && mClassNameLocation != null
458                    && mClassNameLocation.isValid()) {
459                mClassNameLocationProvider = mClassNameLocation.createScope();
460            }
461            return mClassNameLocationProvider;
462        }
463
464        public void addVariable(String name, String type, Location location, boolean declared) {
465            Preconditions.check(!NameTypeLocation.contains(mVariables, name),
466                    "Cannot use same variable name twice. %s in %s", name, location);
467            mVariables.add(new VariableDeclaration(name, type, location, declared));
468        }
469
470        public void addImport(String alias, String type, Location location) {
471            Preconditions.check(!NameTypeLocation.contains(mImports, alias),
472                    "Cannot import same alias twice. %s in %s", alias, location);
473            mImports.add(new NameTypeLocation(alias, type, location));
474        }
475
476        public BindingTargetBundle createBindingTarget(String id, String viewName,
477                boolean used, String tag, String originalTag, Location location) {
478            BindingTargetBundle target = new BindingTargetBundle(id, viewName, used, tag,
479                    originalTag, location);
480            mBindingTargetBundles.add(target);
481            return target;
482        }
483
484        public boolean isEmpty() {
485            return mVariables.isEmpty() && mImports.isEmpty() && mBindingTargetBundles.isEmpty();
486        }
487
488        public BindingTargetBundle getBindingTargetById(String key) {
489            for (BindingTargetBundle target : mBindingTargetBundles) {
490                if (key.equals(target.mId)) {
491                    return target;
492                }
493            }
494            return null;
495        }
496
497        public String getFileName() {
498            return mFileName;
499        }
500
501        public String getConfigName() {
502            return mConfigName;
503        }
504
505        public String getDirectory() {
506            return mDirectory;
507        }
508
509        public boolean hasVariations() {
510            return mHasVariations;
511        }
512
513        public List<VariableDeclaration> getVariables() {
514            return mVariables;
515        }
516
517        public List<NameTypeLocation> getImports() {
518            return mImports;
519        }
520
521        public boolean isMerge() {
522            return mIsMerge;
523        }
524
525        public String getBindingClassName() {
526            if (mBindingClassName == null) {
527                String fullClass = getFullBindingClass();
528                int dotIndex = fullClass.lastIndexOf('.');
529                mBindingClassName = fullClass.substring(dotIndex + 1);
530            }
531            return mBindingClassName;
532        }
533
534        public void setBindingClass(String bindingClass, Location location) {
535            mBindingClass = bindingClass;
536            mClassNameLocation = location;
537        }
538
539        public String getBindingClassPackage() {
540            if (mBindingPackage == null) {
541                String fullClass = getFullBindingClass();
542                int dotIndex = fullClass.lastIndexOf('.');
543                mBindingPackage = fullClass.substring(0, dotIndex);
544            }
545            return mBindingPackage;
546        }
547
548        private String getFullBindingClass() {
549            if (mFullBindingClass == null) {
550                if (mBindingClass == null) {
551                    mFullBindingClass = getModulePackage() + ".databinding." +
552                            ParserHelper.toClassName(getFileName()) + "Binding";
553                } else if (mBindingClass.startsWith(".")) {
554                    mFullBindingClass = getModulePackage() + mBindingClass;
555                } else if (mBindingClass.indexOf('.') < 0) {
556                    mFullBindingClass = getModulePackage() + ".databinding." + mBindingClass;
557                } else {
558                    mFullBindingClass = mBindingClass;
559                }
560            }
561            return mFullBindingClass;
562        }
563
564        public List<BindingTargetBundle> getBindingTargetBundles() {
565            return mBindingTargetBundles;
566        }
567
568        @Override
569        public boolean equals(Object o) {
570            if (this == o) {
571                return true;
572            }
573            if (o == null || getClass() != o.getClass()) {
574                return false;
575            }
576
577            LayoutFileBundle bundle = (LayoutFileBundle) o;
578
579            if (mConfigName != null ? !mConfigName.equals(bundle.mConfigName)
580                    : bundle.mConfigName != null) {
581                return false;
582            }
583            if (mDirectory != null ? !mDirectory.equals(bundle.mDirectory)
584                    : bundle.mDirectory != null) {
585                return false;
586            }
587            return !(mFileName != null ? !mFileName.equals(bundle.mFileName)
588                    : bundle.mFileName != null);
589
590        }
591
592        @Override
593        public int hashCode() {
594            int result = mFileName != null ? mFileName.hashCode() : 0;
595            result = 31 * result + (mConfigName != null ? mConfigName.hashCode() : 0);
596            result = 31 * result + (mDirectory != null ? mDirectory.hashCode() : 0);
597            return result;
598        }
599
600        @Override
601        public String toString() {
602            return "LayoutFileBundle{" +
603                    "mHasVariations=" + mHasVariations +
604                    ", mDirectory='" + mDirectory + '\'' +
605                    ", mConfigName='" + mConfigName + '\'' +
606                    ", mModulePackage='" + mModulePackage + '\'' +
607                    ", mFileName='" + mFileName + '\'' +
608                    '}';
609        }
610
611        public String getModulePackage() {
612            return mModulePackage;
613        }
614
615        public String getAbsoluteFilePath() {
616            return mAbsoluteFilePath;
617        }
618
619        @Override
620        public String provideScopeFilePath() {
621            return mAbsoluteFilePath;
622        }
623
624        private static Marshaller sMarshaller;
625        private static Unmarshaller sUmarshaller;
626
627        public String toXML() throws JAXBException {
628            StringWriter writer = new StringWriter();
629            getMarshaller().marshal(this, writer);
630            return writer.getBuffer().toString();
631        }
632
633        public static LayoutFileBundle fromXML(InputStream inputStream) throws JAXBException {
634            return (LayoutFileBundle) getUnmarshaller().unmarshal(inputStream);
635        }
636
637        private static Marshaller getMarshaller() throws JAXBException {
638            if (sMarshaller == null) {
639                JAXBContext context = JAXBContext
640                        .newInstance(ResourceBundle.LayoutFileBundle.class);
641                sMarshaller = context.createMarshaller();
642            }
643            return sMarshaller;
644        }
645
646        private static Unmarshaller getUnmarshaller() throws JAXBException {
647            if (sUmarshaller == null) {
648                JAXBContext context = JAXBContext
649                        .newInstance(ResourceBundle.LayoutFileBundle.class);
650                sUmarshaller = context.createUnmarshaller();
651            }
652            return sUmarshaller;
653        }
654    }
655
656    @XmlAccessorType(XmlAccessType.NONE)
657    public static class NameTypeLocation {
658        @XmlAttribute(name="type", required = true)
659        public String type;
660
661        @XmlAttribute(name="name", required = true)
662        public String name;
663
664        @XmlElement(name="location", required = false)
665        public Location location;
666
667        public NameTypeLocation() {
668        }
669
670        public NameTypeLocation(String name, String type, Location location) {
671            this.type = type;
672            this.name = name;
673            this.location = location;
674        }
675
676        @Override
677        public String toString() {
678            return "{" +
679                    "type='" + type + '\'' +
680                    ", name='" + name + '\'' +
681                    ", location=" + location +
682                    '}';
683        }
684
685        @Override
686        public boolean equals(Object o) {
687            if (this == o) {
688                return true;
689            }
690            if (o == null || getClass() != o.getClass()) {
691                return false;
692            }
693
694            NameTypeLocation that = (NameTypeLocation) o;
695
696            if (location != null ? !location.equals(that.location) : that.location != null) {
697                return false;
698            }
699            if (!name.equals(that.name)) {
700                return false;
701            }
702            return type.equals(that.type);
703
704        }
705
706        @Override
707        public int hashCode() {
708            int result = type.hashCode();
709            result = 31 * result + name.hashCode();
710            result = 31 * result + (location != null ? location.hashCode() : 0);
711            return result;
712        }
713
714        public static boolean contains(List<? extends NameTypeLocation> list, String name) {
715            for (NameTypeLocation ntl : list) {
716                if (name.equals(ntl.name)) {
717                    return true;
718                }
719            }
720            return false;
721        }
722    }
723
724    @XmlAccessorType(XmlAccessType.NONE)
725    public static class VariableDeclaration extends NameTypeLocation {
726        @XmlAttribute(name="declared", required = false)
727        public boolean declared;
728
729        public VariableDeclaration() {
730
731        }
732
733        public VariableDeclaration(String name, String type, Location location, boolean declared) {
734            super(name, type, location);
735            this.declared = declared;
736        }
737    }
738
739    public static class MarshalledMapType {
740        public List<NameTypeLocation> entries;
741    }
742
743    @XmlAccessorType(XmlAccessType.NONE)
744    public static class BindingTargetBundle implements Serializable, LocationScopeProvider {
745        // public for XML serialization
746
747        @XmlAttribute(name="id")
748        public String mId;
749        @XmlAttribute(name="tag", required = true)
750        public String mTag;
751        @XmlAttribute(name="originalTag")
752        public String mOriginalTag;
753        @XmlAttribute(name="view", required = false)
754        public String mViewName;
755        private String mFullClassName;
756        public boolean mUsed = true;
757        @XmlElementWrapper(name="Expressions")
758        @XmlElement(name="Expression")
759        public List<BindingBundle> mBindingBundleList = new ArrayList<BindingBundle>();
760        @XmlAttribute(name="include")
761        public String mIncludedLayout;
762        @XmlElement(name="location")
763        public Location mLocation;
764        private String mInterfaceType;
765
766        // For XML serialization
767        public BindingTargetBundle() {}
768
769        public BindingTargetBundle(String id, String viewName, boolean used,
770                String tag, String originalTag, Location location) {
771            mId = id;
772            mViewName = viewName;
773            mUsed = used;
774            mTag = tag;
775            mOriginalTag = originalTag;
776            mLocation = location;
777        }
778
779        public void addBinding(String name, String expr, boolean isTwoWay, Location location,
780                Location valueLocation) {
781            mBindingBundleList.add(new BindingBundle(name, expr, isTwoWay, location, valueLocation));
782        }
783
784        public void setIncludedLayout(String includedLayout) {
785            mIncludedLayout = includedLayout;
786        }
787
788        public String getIncludedLayout() {
789            return mIncludedLayout;
790        }
791
792        public boolean isBinder() {
793            return mIncludedLayout != null;
794        }
795
796        public void setInterfaceType(String interfaceType) {
797            mInterfaceType = interfaceType;
798        }
799
800        public void setLocation(Location location) {
801            mLocation = location;
802        }
803
804        public Location getLocation() {
805            return mLocation;
806        }
807
808        public String getId() {
809            return mId;
810        }
811
812        public String getTag() {
813            return mTag;
814        }
815
816        public String getOriginalTag() {
817            return mOriginalTag;
818        }
819
820        public String getFullClassName() {
821            if (mFullClassName == null) {
822                if (isBinder()) {
823                    mFullClassName = mInterfaceType;
824                } else if (mViewName.indexOf('.') == -1) {
825                    if (Arrays.asList(ANDROID_VIEW_PACKAGE_VIEWS).contains(mViewName)) {
826                        mFullClassName = "android.view." + mViewName;
827                    } else if("WebView".equals(mViewName)) {
828                        mFullClassName = "android.webkit." + mViewName;
829                    } else {
830                        mFullClassName = "android.widget." + mViewName;
831                    }
832                } else {
833                    mFullClassName = mViewName;
834                }
835            }
836            if (mFullClassName == null) {
837                L.e("Unexpected full class name = null. view = %s, interface = %s, layout = %s",
838                        mViewName, mInterfaceType, mIncludedLayout);
839            }
840            return mFullClassName;
841        }
842
843        public boolean isUsed() {
844            return mUsed;
845        }
846
847        public List<BindingBundle> getBindingBundleList() {
848            return mBindingBundleList;
849        }
850
851        public String getInterfaceType() {
852            return mInterfaceType;
853        }
854
855        @Override
856        public List<Location> provideScopeLocation() {
857            return mLocation == null ? null : Arrays.asList(mLocation);
858        }
859
860        @XmlAccessorType(XmlAccessType.NONE)
861        public static class BindingBundle implements Serializable {
862
863            private String mName;
864            private String mExpr;
865            private Location mLocation;
866            private Location mValueLocation;
867            private boolean mIsTwoWay;
868
869            public BindingBundle() {}
870
871            public BindingBundle(String name, String expr, boolean isTwoWay, Location location,
872                    Location valueLocation) {
873                mName = name;
874                mExpr = expr;
875                mLocation = location;
876                mIsTwoWay = isTwoWay;
877                mValueLocation = valueLocation;
878            }
879
880            @XmlAttribute(name="attribute", required=true)
881            public String getName() {
882                return mName;
883            }
884
885            @XmlAttribute(name="text", required=true)
886            public String getExpr() {
887                return mExpr;
888            }
889
890            public void setName(String name) {
891                mName = name;
892            }
893
894            public void setExpr(String expr) {
895                mExpr = expr;
896            }
897
898            public void setTwoWay(boolean isTwoWay) {
899                mIsTwoWay = isTwoWay;
900            }
901
902            @XmlElement(name="Location")
903            public Location getLocation() {
904                return mLocation;
905            }
906
907            public void setLocation(Location location) {
908                mLocation = location;
909            }
910
911            @XmlElement(name="ValueLocation")
912            public Location getValueLocation() {
913                return mValueLocation;
914            }
915
916            @XmlElement(name="TwoWay")
917            public boolean isTwoWay() {
918                return mIsTwoWay;
919            }
920
921            public void setValueLocation(Location valueLocation) {
922                mValueLocation = valueLocation;
923            }
924        }
925    }
926
927    /**
928     * Just an inner callback class to process imports and variables w/ the same code.
929     */
930    private interface ValidateAndFilterCallback {
931        List<? extends NameTypeLocation> get(LayoutFileBundle bundle);
932    }
933}
934