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