SetterStore.java revision 9784c9aaedeb863018f5fcaa0a598e8e2f8ed2f3
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.databinding.tool.store;
17
18import org.apache.commons.lang3.StringUtils;
19
20import android.databinding.tool.reflection.ModelAnalyzer;
21import android.databinding.tool.reflection.ModelClass;
22import android.databinding.tool.reflection.ModelMethod;
23import android.databinding.tool.util.GenerationalClassUtil;
24import android.databinding.tool.util.L;
25import android.databinding.tool.util.Preconditions;
26
27import java.io.IOException;
28import java.io.Serializable;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Collections;
32import java.util.Comparator;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39import java.util.TreeMap;
40
41import javax.annotation.processing.ProcessingEnvironment;
42import javax.lang.model.element.ExecutableElement;
43import javax.lang.model.element.Modifier;
44import javax.lang.model.element.TypeElement;
45import javax.lang.model.element.VariableElement;
46import javax.lang.model.type.ArrayType;
47import javax.lang.model.type.DeclaredType;
48import javax.lang.model.type.TypeKind;
49import javax.lang.model.type.TypeMirror;
50
51public class SetterStore {
52    private static SetterStore sStore;
53
54    private final IntermediateV1 mStore;
55    private final ModelAnalyzer mClassAnalyzer;
56    private HashMap<String, List<String>> mInstanceAdapters;
57
58    private Comparator<MultiAttributeSetter> COMPARE_MULTI_ATTRIBUTE_SETTERS =
59            new Comparator<MultiAttributeSetter>() {
60                @Override
61                public int compare(MultiAttributeSetter o1, MultiAttributeSetter o2) {
62                    if (o1.attributes.length != o2.attributes.length) {
63                        return o2.attributes.length - o1.attributes.length;
64                    }
65                    ModelClass view1 = mClassAnalyzer.findClass(o1.mKey.viewType, null).erasure();
66                    ModelClass view2 = mClassAnalyzer.findClass(o2.mKey.viewType, null).erasure();
67                    if (!view1.equals(view2)) {
68                        if (view1.isAssignableFrom(view2)) {
69                            return 1;
70                        } else {
71                            return -1;
72                        }
73                    }
74                    if (!o1.mKey.attributeIndices.keySet()
75                            .equals(o2.mKey.attributeIndices.keySet())) {
76                        // order by attribute name
77                        Iterator<String> o1Keys = o1.mKey.attributeIndices.keySet().iterator();
78                        Iterator<String> o2Keys = o2.mKey.attributeIndices.keySet().iterator();
79                        while (o1Keys.hasNext()) {
80                            String key1 = o1Keys.next();
81                            String key2 = o2Keys.next();
82                            int compare = key1.compareTo(key2);
83                            if (compare != 0) {
84                                return compare;
85                            }
86                        }
87                        Preconditions.check(false,
88                                "The sets don't match! That means the keys shouldn't match also");
89                    }
90                    // Same view type. Same attributes
91                    for (String attribute : o1.mKey.attributeIndices.keySet()) {
92                        final int index1 = o1.mKey.attributeIndices.get(attribute);
93                        final int index2 = o2.mKey.attributeIndices.get(attribute);
94                        ModelClass type1 = mClassAnalyzer
95                                .findClass(o1.mKey.parameterTypes[index1], null);
96                        ModelClass type2 = mClassAnalyzer
97                                .findClass(o2.mKey.parameterTypes[index2], null);
98                        if (type1.equals(type2)) {
99                            continue;
100                        }
101                        if (o1.mCasts[index1] != null) {
102                            if (o2.mCasts[index2] == null) {
103                                return 1; // o2 is better
104                            } else {
105                                continue; // both are casts
106                            }
107                        } else if (o2.mCasts[index2] != null) {
108                            return -1; // o1 is better
109                        }
110                        if (o1.mConverters[index1] != null) {
111                            if (o2.mConverters[index2] == null) {
112                                return 1; // o2 is better
113                            } else {
114                                continue; // both are conversions
115                            }
116                        } else if (o2.mConverters[index2] != null) {
117                            return -1; // o1 is better
118                        }
119
120                        if (type1.isPrimitive()) {
121                            if (type2.isPrimitive()) {
122                                int type1ConversionLevel = ModelMethod
123                                        .getImplicitConversionLevel(type1);
124                                int type2ConversionLevel = ModelMethod
125                                        .getImplicitConversionLevel(type2);
126                                return type2ConversionLevel - type1ConversionLevel;
127                            } else {
128                                // type1 is primitive and has higher priority
129                                return -1;
130                            }
131                        } else if (type2.isPrimitive()) {
132                            return 1;
133                        }
134                        if (type1.isAssignableFrom(type2)) {
135                            return 1;
136                        } else if (type2.isAssignableFrom(type1)) {
137                            return -1;
138                        }
139                    }
140                    // hmmm... same view type, same attributes, same parameter types... ?
141                    return 0;
142                }
143            };
144
145    private SetterStore(ModelAnalyzer modelAnalyzer, IntermediateV1 store) {
146        mClassAnalyzer = modelAnalyzer;
147        mStore = store;
148    }
149
150    public static SetterStore get(ModelAnalyzer modelAnalyzer) {
151        if (sStore == null) {
152            sStore = load(modelAnalyzer);
153        }
154        return sStore;
155    }
156
157    private static SetterStore load(ModelAnalyzer modelAnalyzer) {
158        IntermediateV1 store = new IntermediateV1();
159        List<Intermediate> previousStores = GenerationalClassUtil
160                .loadObjects(GenerationalClassUtil.ExtensionFilter.SETTER_STORE);
161        for (Intermediate intermediate : previousStores) {
162            merge(store, intermediate);
163        }
164        return new SetterStore(modelAnalyzer, store);
165    }
166
167    public void addRenamedMethod(String attribute, String declaringClass, String method,
168            TypeElement declaredOn) {
169        attribute = stripNamespace(attribute);
170        HashMap<String, MethodDescription> renamed = mStore.renamedMethods.get(attribute);
171        if (renamed == null) {
172            renamed = new HashMap<String, MethodDescription>();
173            mStore.renamedMethods.put(attribute, renamed);
174        }
175        MethodDescription methodDescription = new MethodDescription(
176                declaredOn.getQualifiedName().toString(), method);
177        L.d("STORE addmethod desc %s", methodDescription);
178        renamed.put(declaringClass, methodDescription);
179    }
180
181    public void addBindingAdapter(ProcessingEnvironment processingEnv, String attribute,
182            ExecutableElement bindingMethod, boolean takesComponent) {
183        attribute = stripNamespace(attribute);
184        L.d("STORE addBindingAdapter %s %s", attribute, bindingMethod);
185        HashMap<AccessorKey, MethodDescription> adapters = mStore.adapterMethods.get(attribute);
186
187        if (adapters == null) {
188            adapters = new HashMap<AccessorKey, MethodDescription>();
189            mStore.adapterMethods.put(attribute, adapters);
190        }
191        List<? extends VariableElement> parameters = bindingMethod.getParameters();
192        final int viewIndex = takesComponent ? 1 : 0;
193        TypeMirror viewType = eraseType(processingEnv, parameters.get(viewIndex).asType());
194        String view = getQualifiedName(viewType);
195        TypeMirror parameterType = eraseType(processingEnv, parameters.get(viewIndex + 1).asType());
196        String value = getQualifiedName(parameterType);
197
198        AccessorKey key = new AccessorKey(view, value);
199        if (adapters.containsKey(key)) {
200            throw new IllegalArgumentException("Already exists!");
201        }
202
203        adapters.put(key, new MethodDescription(bindingMethod, 1, takesComponent));
204    }
205
206    private static TypeMirror eraseType(ProcessingEnvironment processingEnv,
207            TypeMirror typeMirror) {
208        if (hasTypeVar(typeMirror)) {
209            return processingEnv.getTypeUtils().erasure(typeMirror);
210        } else {
211            return typeMirror;
212        }
213    }
214
215    private static ModelClass eraseType(ModelClass modelClass) {
216        if (hasTypeVar(modelClass)) {
217            return modelClass.erasure();
218        } else {
219            return modelClass;
220        }
221    }
222
223    private static boolean hasTypeVar(TypeMirror typeMirror) {
224        TypeKind kind = typeMirror.getKind();
225        if (kind == TypeKind.TYPEVAR) {
226            return true;
227        } else if (kind == TypeKind.ARRAY) {
228            return hasTypeVar(((ArrayType) typeMirror).getComponentType());
229        } else if (kind == TypeKind.DECLARED) {
230            DeclaredType declaredType = (DeclaredType) typeMirror;
231            List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
232            if (typeArguments == null || typeArguments.isEmpty()) {
233                return false;
234            }
235            for (TypeMirror arg : typeArguments) {
236                if (hasTypeVar(arg)) {
237                    return true;
238                }
239            }
240            return false;
241        } else {
242            return false;
243        }
244    }
245
246    private static boolean hasTypeVar(ModelClass type) {
247        if (type.isTypeVar()) {
248            return true;
249        } else if (type.isArray()) {
250            return hasTypeVar(type.getComponentType());
251        } else {
252            List<ModelClass> typeArguments = type.getTypeArguments();
253            if (typeArguments == null) {
254                return false;
255            }
256            for (ModelClass arg : typeArguments) {
257                if (hasTypeVar(arg)) {
258                    return true;
259                }
260            }
261            return false;
262        }
263    }
264
265    public void addBindingAdapter(ProcessingEnvironment processingEnv, String[] attributes,
266            ExecutableElement bindingMethod, boolean takesComponent, boolean requireAll) {
267        L.d("STORE add multi-value BindingAdapter %d %s", attributes.length, bindingMethod);
268        MultiValueAdapterKey key = new MultiValueAdapterKey(processingEnv, bindingMethod,
269                attributes, takesComponent, requireAll);
270        MethodDescription methodDescription = new MethodDescription(bindingMethod,
271                attributes.length, takesComponent);
272        mStore.multiValueAdapters.put(key, methodDescription);
273    }
274
275    private static String[] stripAttributes(String[] attributes) {
276        String[] strippedAttributes = new String[attributes.length];
277        for (int i = 0; i < attributes.length; i++) {
278            strippedAttributes[i] = stripNamespace(attributes[i]);
279        }
280        return strippedAttributes;
281    }
282
283    public void addUntaggableTypes(String[] typeNames, TypeElement declaredOn) {
284        L.d("STORE addUntaggableTypes %s %s", Arrays.toString(typeNames), declaredOn);
285        String declaredType = declaredOn.getQualifiedName().toString();
286        for (String type : typeNames) {
287            mStore.untaggableTypes.put(type, declaredType);
288        }
289    }
290
291    private static String getQualifiedName(TypeMirror type) {
292        final TypeKind kind = type.getKind();
293        if (kind == TypeKind.ARRAY) {
294            return getQualifiedName(((ArrayType) type).getComponentType()) + "[]";
295        } else if (kind == TypeKind.DECLARED && isIncompleteType(type)) {
296            DeclaredType declaredType = (DeclaredType) type;
297            return declaredType.asElement().toString();
298        } else {
299            return type.toString();
300        }
301    }
302
303    private static boolean isIncompleteType(TypeMirror type) {
304        final TypeKind kind = type.getKind();
305        if (kind == TypeKind.TYPEVAR || kind == TypeKind.WILDCARD) {
306            return true;
307        } else if (kind == TypeKind.DECLARED) {
308            DeclaredType declaredType = (DeclaredType) type;
309            List<? extends TypeMirror> typeArgs = declaredType.getTypeArguments();
310            if (typeArgs == null) {
311                return false;
312            }
313            for (TypeMirror arg : typeArgs) {
314                if (isIncompleteType(arg)) {
315                    return true;
316                }
317            }
318        }
319        return false;
320    }
321
322    public void addConversionMethod(ExecutableElement conversionMethod) {
323        L.d("STORE addConversionMethod %s", conversionMethod);
324        List<? extends VariableElement> parameters = conversionMethod.getParameters();
325        String fromType = getQualifiedName(parameters.get(0).asType());
326        String toType = getQualifiedName(conversionMethod.getReturnType());
327        MethodDescription methodDescription = new MethodDescription(conversionMethod, 1, false);
328        HashMap<String, MethodDescription> convertTo = mStore.conversionMethods.get(fromType);
329        if (convertTo == null) {
330            convertTo = new HashMap<String, MethodDescription>();
331            mStore.conversionMethods.put(fromType, convertTo);
332        }
333        convertTo.put(toType, methodDescription);
334    }
335
336    public void clear(Set<String> classes) {
337        ArrayList<AccessorKey> removedAccessorKeys = new ArrayList<AccessorKey>();
338        for (HashMap<AccessorKey, MethodDescription> adapters : mStore.adapterMethods.values()) {
339            for (AccessorKey key : adapters.keySet()) {
340                MethodDescription description = adapters.get(key);
341                if (classes.contains(description.type)) {
342                    removedAccessorKeys.add(key);
343                }
344            }
345            removeFromMap(adapters, removedAccessorKeys);
346        }
347
348        ArrayList<String> removedRenamed = new ArrayList<String>();
349        for (HashMap<String, MethodDescription> renamed : mStore.renamedMethods.values()) {
350            for (String key : renamed.keySet()) {
351                if (classes.contains(renamed.get(key).type)) {
352                    removedRenamed.add(key);
353                }
354            }
355            removeFromMap(renamed, removedRenamed);
356        }
357
358        ArrayList<String> removedConversions = new ArrayList<String>();
359        for (HashMap<String, MethodDescription> convertTos : mStore.conversionMethods.values()) {
360            for (String toType : convertTos.keySet()) {
361                MethodDescription methodDescription = convertTos.get(toType);
362                if (classes.contains(methodDescription.type)) {
363                    removedConversions.add(toType);
364                }
365            }
366            removeFromMap(convertTos, removedConversions);
367        }
368
369        ArrayList<String> removedUntaggable = new ArrayList<String>();
370        for (String typeName : mStore.untaggableTypes.keySet()) {
371            if (classes.contains(mStore.untaggableTypes.get(typeName))) {
372                removedUntaggable.add(typeName);
373            }
374        }
375        removeFromMap(mStore.untaggableTypes, removedUntaggable);
376    }
377
378    private static <K, V> void removeFromMap(Map<K, V> map, List<K> keys) {
379        for (K key : keys) {
380            map.remove(key);
381        }
382        keys.clear();
383    }
384
385    public void write(String projectPackage, ProcessingEnvironment processingEnvironment)
386            throws IOException {
387        GenerationalClassUtil.writeIntermediateFile(processingEnvironment,
388                projectPackage, projectPackage +
389                        GenerationalClassUtil.ExtensionFilter.SETTER_STORE.getExtension(), mStore);
390    }
391
392    private static String stripNamespace(String attribute) {
393        if (!attribute.startsWith("android:")) {
394            int colon = attribute.indexOf(':');
395            if (colon >= 0) {
396                attribute = attribute.substring(colon + 1);
397            }
398        }
399        return attribute;
400    }
401
402    public List<MultiAttributeSetter> getMultiAttributeSetterCalls(String[] attributes,
403            ModelClass viewType, ModelClass[] valueType) {
404        attributes = stripAttributes(attributes);
405        final ArrayList<MultiAttributeSetter> calls = new ArrayList<MultiAttributeSetter>();
406        if (viewType != null && viewType.isGeneric()) {
407            List<ModelClass> viewGenerics = viewType.getTypeArguments();
408            for (int i = 0; i < valueType.length; i++) {
409                valueType[i] = eraseType(valueType[i], viewGenerics);
410            }
411            viewType = viewType.erasure();
412        }
413        ArrayList<MultiAttributeSetter> matching = getMatchingMultiAttributeSetters(attributes,
414                viewType, valueType);
415        Collections.sort(matching, COMPARE_MULTI_ATTRIBUTE_SETTERS);
416        while (!matching.isEmpty()) {
417            MultiAttributeSetter bestMatch = matching.get(0);
418            calls.add(bestMatch);
419            removeConsumedAttributes(matching, bestMatch.attributes);
420        }
421        return calls;
422    }
423
424    private static String simpleName(String className) {
425        int dotIndex = className.lastIndexOf('.');
426        if (dotIndex < 0) {
427            return className;
428        } else {
429            return className.substring(dotIndex + 1);
430        }
431    }
432
433    public Map<String, List<String>> getComponentBindingAdapters() {
434        ensureInstanceAdapters();
435        return mInstanceAdapters;
436    }
437
438    private String getBindingAdapterCall(String className) {
439        ensureInstanceAdapters();
440        final String simpleName = simpleName(className);
441        List<String> adapters = mInstanceAdapters.get(simpleName);
442        if (adapters.size() == 1) {
443            return "get" + simpleName + "()";
444        } else {
445            int index = adapters.indexOf(className) + 1;
446            return "get" + simpleName + index + "()";
447        }
448    }
449
450    private void ensureInstanceAdapters() {
451        if (mInstanceAdapters == null) {
452            HashSet<String> adapters = new HashSet<String>();
453            for (HashMap<AccessorKey, MethodDescription> methods : mStore.adapterMethods.values()) {
454                for (MethodDescription method : methods.values()) {
455                    if (!method.isStatic) {
456                        adapters.add(method.type);
457                    }
458                }
459            }
460            for (MethodDescription method : mStore.multiValueAdapters.values()) {
461                if (!method.isStatic) {
462                    adapters.add(method.type);
463                }
464            }
465            mInstanceAdapters = new HashMap<String, List<String>>();
466            for (String adapter : adapters) {
467                final String simpleName = simpleName(adapter);
468                List<String> list = mInstanceAdapters.get(simpleName);
469                if (list == null) {
470                    list = new ArrayList<String>();
471                    mInstanceAdapters.put(simpleName, list);
472                }
473                list.add(adapter);
474            }
475            for (List<String> list : mInstanceAdapters.values()) {
476                if (list.size() > 1) {
477                    Collections.sort(list);
478                }
479            }
480        }
481    }
482
483    // Removes all MultiAttributeSetters that require any of the values in attributes
484    private static void removeConsumedAttributes(ArrayList<MultiAttributeSetter> matching,
485            String[] attributes) {
486        for (int i = matching.size() - 1; i >= 0; i--) {
487            final MultiAttributeSetter setter = matching.get(i);
488            boolean found = false;
489            for (String attribute : attributes) {
490                if (isInArray(attribute, setter.attributes)) {
491                    found = true;
492                    break;
493                }
494            }
495            if (found) {
496                matching.remove(i);
497            }
498        }
499    }
500
501    // Linear search through the String array for a specific value.
502    private static boolean isInArray(String str, String[] array) {
503        for (String value : array) {
504            if (value.equals(str)) {
505                return true;
506            }
507        }
508        return false;
509    }
510
511    private ArrayList<MultiAttributeSetter> getMatchingMultiAttributeSetters(String[] attributes,
512            ModelClass viewType, ModelClass[] valueType) {
513        final ArrayList<MultiAttributeSetter> setters = new ArrayList<MultiAttributeSetter>();
514        for (MultiValueAdapterKey adapter : mStore.multiValueAdapters.keySet()) {
515            if (adapter.requireAll && adapter.attributes.length > attributes.length) {
516                continue;
517            }
518            ModelClass viewClass = mClassAnalyzer.findClass(adapter.viewType, null);
519            if (viewClass.isGeneric()) {
520                viewClass = viewClass.erasure();
521            }
522            if (!viewClass.isAssignableFrom(viewType)) {
523                continue;
524            }
525            final MethodDescription method = mStore.multiValueAdapters.get(adapter);
526            final MultiAttributeSetter setter = createMultiAttributeSetter(method, attributes,
527                    valueType, adapter);
528            if (setter != null) {
529                setters.add(setter);
530            }
531        }
532        return setters;
533    }
534
535    private MultiAttributeSetter createMultiAttributeSetter(MethodDescription method,
536            String[] allAttributes, ModelClass[] attributeValues, MultiValueAdapterKey adapter) {
537        int matchingAttributes = 0;
538        String[] casts = new String[adapter.attributes.length];
539        MethodDescription[] conversions = new MethodDescription[adapter.attributes.length];
540        boolean[] supplied = new boolean[adapter.attributes.length];
541
542        for (int i = 0; i < allAttributes.length; i++) {
543            Integer index = adapter.attributeIndices.get(allAttributes[i]);
544            if (index != null) {
545                supplied[index] = true;
546                matchingAttributes++;
547                final String parameterTypeStr = adapter.parameterTypes[index];
548                final ModelClass parameterType = eraseType(
549                        mClassAnalyzer.findClass(parameterTypeStr, null));
550                final ModelClass attributeType = attributeValues[i];
551                if (!parameterType.isAssignableFrom(attributeType)) {
552                    if (ModelMethod.isBoxingConversion(parameterType, attributeType)) {
553                        // automatic boxing is ok
554                        continue;
555                    } else if (ModelMethod.isImplicitConversion(attributeType, parameterType)) {
556                        // implicit conversion is ok
557                        continue;
558                    }
559                    // Look for a converter
560                    conversions[index] = getConversionMethod(attributeType, parameterType, null);
561                    if (conversions[index] == null) {
562                        if (attributeType.isObject()) {
563                            // Cast is allowed also
564                            casts[index] = parameterTypeStr;
565                        } else {
566                            // Parameter type mismatch
567                            return null;
568                        }
569                    }
570                }
571            }
572        }
573
574        if ((adapter.requireAll && matchingAttributes != adapter.attributes.length) ||
575                matchingAttributes == 0) {
576            return null;
577        } else {
578            return new MultiAttributeSetter(adapter, supplied, method, conversions, casts);
579        }
580    }
581
582    public SetterCall getSetterCall(String attribute, ModelClass viewType,
583            ModelClass valueType, Map<String, String> imports) {
584        attribute = stripNamespace(attribute);
585        SetterCall setterCall = null;
586        MethodDescription conversionMethod = null;
587        if (viewType != null) {
588            viewType = viewType.erasure();
589            HashMap<AccessorKey, MethodDescription> adapters = mStore.adapterMethods.get(attribute);
590            ModelMethod bestSetterMethod = getBestSetter(viewType, valueType, attribute, imports);
591            ModelClass bestViewType = null;
592            ModelClass bestValueType = null;
593            if (bestSetterMethod != null) {
594                bestViewType = bestSetterMethod.getDeclaringClass();
595                bestValueType = bestSetterMethod.getParameterTypes()[0];
596                setterCall = new ModelMethodSetter(bestSetterMethod);
597            }
598
599            if (adapters != null) {
600                for (AccessorKey key : adapters.keySet()) {
601                    try {
602                        ModelClass adapterViewType = mClassAnalyzer
603                                .findClass(key.viewType, imports).erasure();
604                        if (adapterViewType != null && adapterViewType.isAssignableFrom(viewType)) {
605                            try {
606                                L.d("setter parameter type is %s", key.valueType);
607                                final ModelClass adapterValueType = eraseType(mClassAnalyzer
608                                        .findClass(key.valueType, imports));
609                                L.d("setter %s takes type %s, compared to %s",
610                                        adapters.get(key).method, adapterValueType.toJavaCode(),
611                                        valueType.toJavaCode());
612                                boolean isBetterView = bestViewType == null ||
613                                        bestValueType.isAssignableFrom(adapterValueType);
614                                if (isBetterParameter(valueType, adapterValueType, bestValueType,
615                                        isBetterView, imports)) {
616                                    bestViewType = adapterViewType;
617                                    bestValueType = adapterValueType;
618                                    MethodDescription adapter = adapters.get(key);
619                                    setterCall = new AdapterSetter(adapter, adapterValueType);
620                                }
621
622                            } catch (Exception e) {
623                                L.e(e, "Unknown class: %s", key.valueType);
624                            }
625                        }
626                    } catch (Exception e) {
627                        L.e(e, "Unknown class: %s", key.viewType);
628                    }
629                }
630            }
631
632            conversionMethod = getConversionMethod(valueType, bestValueType, imports);
633            if (valueType.isObject() && setterCall != null && bestValueType.isNullable()) {
634                setterCall.setCast(bestValueType);
635            }
636        }
637        if (setterCall == null) {
638            if (viewType != null && !viewType.isViewDataBinding()) {
639                return null; // no setter found!!
640            }
641            setterCall = new DummySetter(getDefaultSetter(attribute));
642        }
643        setterCall.setConverter(conversionMethod);
644        return setterCall;
645    }
646
647    public boolean isUntaggable(String viewType) {
648        return mStore.untaggableTypes.containsKey(viewType);
649    }
650
651    private ModelMethod getBestSetter(ModelClass viewType, ModelClass argumentType,
652            String attribute, Map<String, String> imports) {
653        if (viewType.isGeneric()) {
654            argumentType = eraseType(argumentType, viewType.getTypeArguments());
655            viewType = viewType.erasure();
656        }
657        List<String> setterCandidates = new ArrayList<String>();
658        HashMap<String, MethodDescription> renamed = mStore.renamedMethods.get(attribute);
659        if (renamed != null) {
660            for (String className : renamed.keySet()) {
661                try {
662                    ModelClass renamedViewType = mClassAnalyzer.findClass(className, imports);
663                    if (renamedViewType.erasure().isAssignableFrom(viewType)) {
664                        setterCandidates.add(renamed.get(className).method);
665                        break;
666                    }
667                } catch (Exception e) {
668                    //printMessage(Diagnostic.Kind.NOTE, "Unknown class: " + className);
669                }
670            }
671        }
672        setterCandidates.add(getDefaultSetter(attribute));
673        setterCandidates.add(trimAttributeNamespace(attribute));
674
675        ModelMethod bestMethod = null;
676        ModelClass bestParameterType = null;
677        List<ModelClass> args = new ArrayList<ModelClass>();
678        args.add(argumentType);
679        for (String name : setterCandidates) {
680            ModelMethod[] methods = viewType.getMethods(name, 1);
681
682            for (ModelMethod method : methods) {
683                ModelClass[] parameterTypes = method.getParameterTypes();
684                ModelClass param = parameterTypes[0];
685                if (method.isVoid() &&
686                        isBetterParameter(argumentType, param, bestParameterType, true, imports)) {
687                    bestParameterType = param;
688                    bestMethod = method;
689                }
690            }
691        }
692        return bestMethod;
693    }
694
695    private static ModelClass eraseType(ModelClass type, List<ModelClass> typeParameters) {
696        List<ModelClass> typeArguments = type.getTypeArguments();
697        if (typeArguments == null || typeParameters == null) {
698            return type;
699        }
700        for (ModelClass arg : typeArguments) {
701            if (typeParameters.contains(arg)) {
702                return type.erasure();
703            }
704        }
705        return type;
706    }
707
708    private static String trimAttributeNamespace(String attribute) {
709        final int colonIndex = attribute.indexOf(':');
710        return colonIndex == -1 ? attribute : attribute.substring(colonIndex + 1);
711    }
712
713    private static String getDefaultSetter(String attribute) {
714        return "set" + StringUtils.capitalize(trimAttributeNamespace(attribute));
715    }
716
717    private boolean isBetterParameter(ModelClass argument, ModelClass parameter,
718            ModelClass oldParameter, boolean isBetterViewTypeMatch, Map<String, String> imports) {
719        // Right view type. Check the value
720        if (!isBetterViewTypeMatch && oldParameter.equals(argument)) {
721            return false;
722        } else if (argument.equals(parameter)) {
723            // Exact match
724            return true;
725        } else if (!isBetterViewTypeMatch &&
726                ModelMethod.isBoxingConversion(oldParameter, argument)) {
727            return false;
728        } else if (ModelMethod.isBoxingConversion(parameter, argument)) {
729            // Boxing/unboxing is second best
730            return true;
731        } else {
732            int oldConversionLevel = ModelMethod.getImplicitConversionLevel(oldParameter);
733            if (ModelMethod.isImplicitConversion(argument, parameter)) {
734                // Better implicit conversion
735                int conversionLevel = ModelMethod.getImplicitConversionLevel(parameter);
736                return oldConversionLevel < 0 || conversionLevel < oldConversionLevel;
737            } else if (oldConversionLevel >= 0) {
738                return false;
739            } else if (parameter.isAssignableFrom(argument)) {
740                // Right type, see if it is better than the current best match.
741                if (oldParameter == null) {
742                    return true;
743                } else {
744                    return oldParameter.isAssignableFrom(parameter);
745                }
746            } else {
747                MethodDescription conversionMethod = getConversionMethod(argument, parameter,
748                        imports);
749                if (conversionMethod != null) {
750                    return true;
751                }
752                if (getConversionMethod(argument, oldParameter, imports) != null) {
753                    return false;
754                }
755                return argument.isObject() && !parameter.isPrimitive();
756            }
757        }
758    }
759
760    private MethodDescription getConversionMethod(ModelClass from, ModelClass to,
761            Map<String, String> imports) {
762        if (from != null && to != null) {
763            if (to.isObject()) {
764                return null;
765            }
766            for (String fromClassName : mStore.conversionMethods.keySet()) {
767                try {
768                    ModelClass convertFrom = mClassAnalyzer.findClass(fromClassName, imports);
769                    if (canUseForConversion(from, convertFrom)) {
770                        HashMap<String, MethodDescription> conversion =
771                                mStore.conversionMethods.get(fromClassName);
772                        for (String toClassName : conversion.keySet()) {
773                            try {
774                                ModelClass convertTo = mClassAnalyzer.findClass(toClassName,
775                                        imports);
776                                if (canUseForConversion(convertTo, to)) {
777                                    return conversion.get(toClassName);
778                                }
779                            } catch (Exception e) {
780                                L.d(e, "Unknown class: %s", toClassName);
781                            }
782                        }
783                    }
784                } catch (Exception e) {
785                    L.d(e, "Unknown class: %s", fromClassName);
786                }
787            }
788        }
789        return null;
790    }
791
792    private boolean canUseForConversion(ModelClass from, ModelClass to) {
793        if (from.isIncomplete() || to.isIncomplete()) {
794            from = from.erasure();
795            to = to.erasure();
796        }
797        return from.equals(to) || ModelMethod.isBoxingConversion(from, to) ||
798                to.isAssignableFrom(from);
799    }
800
801    private static void merge(IntermediateV1 store, Intermediate dumpStore) {
802        IntermediateV1 intermediateV1 = (IntermediateV1) dumpStore.upgrade();
803        merge(store.adapterMethods, intermediateV1.adapterMethods);
804        merge(store.renamedMethods, intermediateV1.renamedMethods);
805        merge(store.conversionMethods, intermediateV1.conversionMethods);
806        store.multiValueAdapters.putAll(intermediateV1.multiValueAdapters);
807        store.untaggableTypes.putAll(intermediateV1.untaggableTypes);
808    }
809
810    private static <K, V> void merge(HashMap<K, HashMap<V, MethodDescription>> first,
811            HashMap<K, HashMap<V, MethodDescription>> second) {
812        for (K key : second.keySet()) {
813            HashMap<V, MethodDescription> firstVals = first.get(key);
814            HashMap<V, MethodDescription> secondVals = second.get(key);
815            if (firstVals == null) {
816                first.put(key, secondVals);
817            } else {
818                for (V key2 : secondVals.keySet()) {
819                    if (!firstVals.containsKey(key2)) {
820                        firstVals.put(key2, secondVals.get(key2));
821                    }
822                }
823            }
824        }
825    }
826
827    private static String createAdapterCall(MethodDescription adapter,
828            String componentExpression, String viewExpression, String... args) {
829        StringBuilder sb = new StringBuilder();
830
831        if (adapter.isStatic) {
832            sb.append(adapter.type);
833        } else {
834            final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance());
835            final String binderCall =  setterStore.getBindingAdapterCall(adapter.type);
836            sb.append(componentExpression).append('.').append(binderCall);
837        }
838        sb.append('.').append(adapter.method).append('(');
839        if (adapter.componentClass != null) {
840            if (!"DataBindingComponent".equals(adapter.componentClass)) {
841                sb.append('(').append(adapter.componentClass).append(") ");
842            }
843            sb.append(componentExpression).append(", ");
844        }
845        sb.append(viewExpression);
846        for (String arg: args) {
847            sb.append(", ").append(arg);
848        }
849        sb.append(')');
850        return sb.toString();
851    }
852
853    private static class MultiValueAdapterKey implements Serializable {
854        private static final long serialVersionUID = 1;
855
856        public final String viewType;
857
858        public final String[] attributes;
859
860        public final String[] parameterTypes;
861
862        public final boolean requireAll;
863
864        public final TreeMap<String, Integer> attributeIndices = new TreeMap<String, Integer>();
865
866        public MultiValueAdapterKey(ProcessingEnvironment processingEnv,
867                ExecutableElement method, String[] attributes, boolean takesComponent,
868                boolean requireAll) {
869            this.attributes = stripAttributes(attributes);
870            this.requireAll = requireAll;
871            List<? extends VariableElement> parameters = method.getParameters();
872            final int argStart = 1 + (takesComponent ? 1 : 0);
873            this.viewType = getQualifiedName(eraseType(processingEnv,
874                    parameters.get(argStart - 1).asType()));
875            this.parameterTypes = new String[parameters.size() - argStart];
876            for (int i = 0; i < attributes.length; i++) {
877                TypeMirror typeMirror = eraseType(processingEnv,
878                        parameters.get(i + argStart).asType());
879                this.parameterTypes[i] = getQualifiedName(typeMirror);
880                attributeIndices.put(this.attributes[i], i);
881            }
882        }
883
884        @Override
885        public boolean equals(Object obj) {
886            if (!(obj instanceof MultiValueAdapterKey)) {
887                return false;
888            }
889            final MultiValueAdapterKey that = (MultiValueAdapterKey) obj;
890            if (!this.viewType.equals(that.viewType) ||
891                    this.attributes.length != that.attributes.length ||
892                    !this.attributeIndices.keySet().equals(that.attributeIndices.keySet())) {
893                return false;
894            }
895
896            for (int i = 0; i < this.attributes.length; i++) {
897                final int thatIndex = that.attributeIndices.get(this.attributes[i]);
898                final String thisParameter = parameterTypes[i];
899                final String thatParameter = that.parameterTypes[thatIndex];
900                if (!thisParameter.equals(thatParameter)) {
901                    return false;
902                }
903            }
904            return true;
905        }
906
907        @Override
908        public int hashCode() {
909            return mergedHashCode(viewType, attributeIndices.keySet());
910        }
911    }
912
913    private static int mergedHashCode(Object... objects) {
914        return Arrays.hashCode(objects);
915    }
916
917    private static class MethodDescription implements Serializable {
918
919        private static final long serialVersionUID = 1;
920
921        public final String type;
922
923        public final String method;
924
925        public final boolean requiresOldValue;
926
927        public final boolean isStatic;
928
929        public final String componentClass;
930
931        public MethodDescription(String type, String method) {
932            this.type = type;
933            this.method = method;
934            this.requiresOldValue = false;
935            this.isStatic = true;
936            this.componentClass = null;
937            L.d("BINARY created method desc 1 %s %s", type, method );
938        }
939
940        public MethodDescription(ExecutableElement method, int numAttributes,
941                boolean takesComponent) {
942            TypeElement enclosingClass = (TypeElement) method.getEnclosingElement();
943            this.type = enclosingClass.getQualifiedName().toString();
944            this.method = method.getSimpleName().toString();
945            final int argStart = 1 + (takesComponent ? 1 : 0);
946            this.requiresOldValue = method.getParameters().size() - argStart == numAttributes * 2;
947            this.isStatic = method.getModifiers().contains(Modifier.STATIC);
948            this.componentClass = takesComponent
949                    ? getQualifiedName(method.getParameters().get(0).asType())
950                    : null;
951
952            L.d("BINARY created method desc 2 %s %s, %s", type, this.method, method);
953        }
954
955        @Override
956        public boolean equals(Object obj) {
957            if (obj instanceof MethodDescription) {
958                MethodDescription that = (MethodDescription) obj;
959                return that.type.equals(this.type) && that.method.equals(this.method);
960            } else {
961                return false;
962            }
963        }
964
965        @Override
966        public int hashCode() {
967            return mergedHashCode(type, method);
968        }
969
970        @Override
971        public String toString() {
972            return type + "." + method + "()";
973        }
974    }
975
976    private static class AccessorKey implements Serializable {
977
978        private static final long serialVersionUID = 1;
979
980        public final String viewType;
981
982        public final String valueType;
983
984        public AccessorKey(String viewType, String valueType) {
985            this.viewType = viewType;
986            this.valueType = valueType;
987        }
988
989        @Override
990        public int hashCode() {
991            return mergedHashCode(viewType, valueType);
992        }
993
994        @Override
995        public boolean equals(Object obj) {
996            if (obj instanceof AccessorKey) {
997                AccessorKey that = (AccessorKey) obj;
998                return viewType.equals(that.valueType) && valueType.equals(that.valueType);
999            } else {
1000                return false;
1001            }
1002        }
1003
1004        @Override
1005        public String toString() {
1006            return "AK(" + viewType + ", " + valueType + ")";
1007        }
1008    }
1009
1010    private interface Intermediate extends Serializable {
1011        Intermediate upgrade();
1012    }
1013
1014    private static class IntermediateV1 implements Serializable, Intermediate {
1015        private static final long serialVersionUID = 1;
1016        public final HashMap<String, HashMap<AccessorKey, MethodDescription>> adapterMethods =
1017                new HashMap<String, HashMap<AccessorKey, MethodDescription>>();
1018        public final HashMap<String, HashMap<String, MethodDescription>> renamedMethods =
1019                new HashMap<String, HashMap<String, MethodDescription>>();
1020        public final HashMap<String, HashMap<String, MethodDescription>> conversionMethods =
1021                new HashMap<String, HashMap<String, MethodDescription>>();
1022        public final HashMap<String, String> untaggableTypes = new HashMap<String, String>();
1023        public final HashMap<MultiValueAdapterKey, MethodDescription> multiValueAdapters =
1024                new HashMap<MultiValueAdapterKey, MethodDescription>();
1025
1026        public IntermediateV1() {
1027        }
1028
1029        @Override
1030        public Intermediate upgrade() {
1031            return this;
1032        }
1033    }
1034
1035    public static class DummySetter extends SetterCall {
1036        private String mMethodName;
1037
1038        public DummySetter(String methodName) {
1039            mMethodName = methodName;
1040        }
1041
1042        @Override
1043        public String toJavaInternal(String componentExpression, String viewExpression,
1044                String valueExpression) {
1045            return viewExpression + "." + mMethodName + "(" + valueExpression + ")";
1046        }
1047
1048        @Override
1049        public String toJavaInternal(String componentExpression, String viewExpression,
1050                String oldValue, String valueExpression) {
1051            return viewExpression + "." + mMethodName + "(" + valueExpression + ")";
1052        }
1053
1054        @Override
1055        public int getMinApi() {
1056            return 1;
1057        }
1058
1059        @Override
1060        public boolean requiresOldValue() {
1061            return false;
1062        }
1063
1064        @Override
1065        public ModelClass[] getParameterTypes() {
1066            return new ModelClass[] {
1067                    ModelAnalyzer.getInstance().findClass(Object.class)
1068            };
1069        }
1070
1071        @Override
1072        public String getBindingAdapterInstanceClass() {
1073            return null;
1074        }
1075    }
1076
1077    public static class AdapterSetter extends SetterCall {
1078        final MethodDescription mAdapter;
1079        final ModelClass mParameterType;
1080
1081        public AdapterSetter(MethodDescription adapter, ModelClass parameterType) {
1082            mAdapter = adapter;
1083            mParameterType = parameterType;
1084        }
1085
1086        @Override
1087        public String toJavaInternal(String componentExpression, String viewExpression,
1088                String valueExpression) {
1089            return createAdapterCall(mAdapter, componentExpression,
1090                    viewExpression, mCastString + valueExpression);
1091        }
1092
1093        @Override
1094        protected String toJavaInternal(String componentExpression, String viewExpression,
1095                String oldValue, String valueExpression) {
1096            return createAdapterCall(mAdapter, componentExpression,
1097                    viewExpression, mCastString + oldValue, mCastString + valueExpression);
1098        }
1099
1100        @Override
1101        public int getMinApi() {
1102            return 1;
1103        }
1104
1105        @Override
1106        public boolean requiresOldValue() {
1107            return mAdapter.requiresOldValue;
1108        }
1109
1110        @Override
1111        public ModelClass[] getParameterTypes() {
1112            return new ModelClass[] { mParameterType };
1113        }
1114
1115        @Override
1116        public String getBindingAdapterInstanceClass() {
1117            return mAdapter.isStatic ? null : mAdapter.type;
1118        }
1119    }
1120
1121    public static class ModelMethodSetter extends SetterCall {
1122        final ModelMethod mModelMethod;
1123
1124        public ModelMethodSetter(ModelMethod modelMethod) {
1125            mModelMethod = modelMethod;
1126        }
1127
1128        @Override
1129        public String toJavaInternal(String componentExpression, String viewExpression,
1130                String valueExpression) {
1131            return viewExpression + "." + mModelMethod.getName() + "(" + mCastString +
1132                    valueExpression + ")";
1133        }
1134
1135        @Override
1136        protected String toJavaInternal(String componentExpression, String viewExpression,
1137                String oldValue, String valueExpression) {
1138            return viewExpression + "." + mModelMethod.getName() + "(" +
1139                    mCastString + oldValue + ", " + mCastString + valueExpression + ")";
1140        }
1141
1142        @Override
1143        public int getMinApi() {
1144            return mModelMethod.getMinApi();
1145        }
1146
1147        @Override
1148        public boolean requiresOldValue() {
1149            return mModelMethod.getParameterTypes().length == 3;
1150        }
1151
1152        @Override
1153        public ModelClass[] getParameterTypes() {
1154            return new ModelClass[] { mModelMethod.getParameterTypes()[0] };
1155        }
1156
1157        @Override
1158        public String getBindingAdapterInstanceClass() {
1159            return null;
1160        }
1161    }
1162
1163    public interface BindingSetterCall {
1164        String toJava(String componentExpression, String viewExpression,
1165                String... valueExpressions);
1166
1167        int getMinApi();
1168
1169        boolean requiresOldValue();
1170
1171        ModelClass[] getParameterTypes();
1172
1173        String getBindingAdapterInstanceClass();
1174    }
1175
1176    public static abstract class SetterCall implements BindingSetterCall {
1177        private MethodDescription mConverter;
1178        protected String mCastString = "";
1179
1180        public SetterCall() {
1181        }
1182
1183        public void setConverter(MethodDescription converter) {
1184            mConverter = converter;
1185        }
1186
1187        protected abstract String toJavaInternal(String componentExpression, String viewExpression,
1188                String converted);
1189
1190        protected abstract String toJavaInternal(String componentExpression, String viewExpression,
1191                String oldValue, String converted);
1192
1193        @Override
1194        public final String toJava(String componentExpression, String viewExpression,
1195                String... valueExpression) {
1196            Preconditions.check(valueExpression.length == 2, "value expressions size must be 2");
1197            if (requiresOldValue()) {
1198                return toJavaInternal(componentExpression, viewExpression,
1199                        convertValue(valueExpression[0]), convertValue(valueExpression[1]));
1200            } else {
1201                return toJavaInternal(componentExpression, viewExpression,
1202                        convertValue(valueExpression[1]));
1203            }
1204        }
1205
1206        protected String convertValue(String valueExpression) {
1207            return mConverter == null ? valueExpression :
1208                    mConverter.type + "." + mConverter.method + "(" + valueExpression + ")";
1209        }
1210
1211        abstract public int getMinApi();
1212
1213        public void setCast(ModelClass castTo) {
1214            mCastString = "(" + castTo.toJavaCode() + ") ";
1215        }
1216    }
1217
1218    public static class MultiAttributeSetter implements BindingSetterCall {
1219        public final String[] attributes;
1220        private final MethodDescription mAdapter;
1221        private final MethodDescription[] mConverters;
1222        private final String[] mCasts;
1223        private final MultiValueAdapterKey mKey;
1224        private final boolean[] mSupplied;
1225
1226        public MultiAttributeSetter(MultiValueAdapterKey key, boolean[] supplied,
1227                MethodDescription adapter, MethodDescription[] converters, String[] casts) {
1228            Preconditions.check(converters != null &&
1229                    converters.length == key.attributes.length &&
1230                    casts != null && casts.length == key.attributes.length &&
1231                    supplied.length == key.attributes.length,
1232                    "invalid arguments to create multi attr setter");
1233            this.mAdapter = adapter;
1234            this.mConverters = converters;
1235            this.mCasts = casts;
1236            this.mKey = key;
1237            this.mSupplied = supplied;
1238            if (key.requireAll) {
1239                this.attributes = key.attributes;
1240            } else {
1241                int numSupplied = 0;
1242                for (int i = 0; i < mKey.attributes.length; i++) {
1243                    if (supplied[i]) {
1244                        numSupplied++;
1245                    }
1246                }
1247                if (numSupplied == key.attributes.length) {
1248                    this.attributes = key.attributes;
1249                } else {
1250                    this.attributes = new String[numSupplied];
1251                    int attrIndex = 0;
1252                    for (int i = 0; i < key.attributes.length; i++) {
1253                        if (supplied[i]) {
1254                            attributes[attrIndex++] = key.attributes[i];
1255                        }
1256                    }
1257                }
1258            }
1259        }
1260
1261        @Override
1262        public final String toJava(String componentExpression, String viewExpression,
1263                String[] valueExpressions) {
1264            Preconditions.check(valueExpressions.length == attributes.length * 2,
1265                    "MultiAttributeSetter needs %s items, received %s",
1266                    Arrays.toString(attributes), Arrays.toString(valueExpressions));
1267            final int numAttrs = mKey.attributes.length;
1268            String[] args = new String[numAttrs + (requiresOldValue() ? numAttrs : 0)];
1269
1270            final int startIndex = mAdapter.requiresOldValue ? 0 : numAttrs;
1271            int attrIndex = mAdapter.requiresOldValue ? 0 : attributes.length;
1272            final ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
1273            StringBuilder argBuilder = new StringBuilder();
1274            final int endIndex = numAttrs * 2;
1275            for (int i = startIndex; i < endIndex; i++) {
1276                argBuilder.setLength(0);
1277                if (!mSupplied[i % numAttrs]) {
1278                    final String paramType = mKey.parameterTypes[i % numAttrs];
1279                    final String defaultValue = modelAnalyzer.getDefaultValue(paramType);
1280                    argBuilder.append('(')
1281                            .append(paramType)
1282                            .append(')')
1283                            .append(defaultValue);
1284                } else {
1285                    if (mConverters[i % numAttrs] != null) {
1286                        final MethodDescription converter = mConverters[i % numAttrs];
1287                        argBuilder.append(converter.type)
1288                                .append('.')
1289                                .append(converter.method)
1290                                .append('(')
1291                                .append(valueExpressions[attrIndex])
1292                                .append(')');
1293                    } else {
1294                        if (mCasts[i % numAttrs] != null) {
1295                            argBuilder.append('(')
1296                                    .append(mCasts[i % numAttrs])
1297                                    .append(')');
1298                        }
1299                        argBuilder.append(valueExpressions[attrIndex]);
1300                    }
1301                    attrIndex++;
1302                }
1303                args[i - startIndex] = argBuilder.toString();
1304            }
1305            return createAdapterCall(mAdapter, componentExpression, viewExpression, args);
1306        }
1307
1308        @Override
1309        public int getMinApi() {
1310            return 1;
1311        }
1312
1313        @Override
1314        public boolean requiresOldValue() {
1315            return mAdapter.requiresOldValue;
1316        }
1317
1318        @Override
1319        public ModelClass[] getParameterTypes() {
1320            ModelClass[] parameters = new ModelClass[attributes.length];
1321            String[] paramTypeStrings = mKey.parameterTypes;
1322            ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
1323            int attrIndex = 0;
1324            for (int i = 0; i < mKey.attributes.length; i++) {
1325                if (mSupplied[i]) {
1326                    parameters[attrIndex++] = modelAnalyzer.findClass(paramTypeStrings[i], null);
1327                }
1328            }
1329            return parameters;
1330        }
1331
1332        @Override
1333        public String getBindingAdapterInstanceClass() {
1334            return mAdapter.isStatic ? null : mAdapter.type;
1335        }
1336
1337        @Override
1338        public String toString() {
1339            return "MultiAttributeSetter{" +
1340                    "attributes=" + Arrays.toString(attributes) +
1341                    ", mAdapter=" + mAdapter +
1342                    ", mConverters=" + Arrays.toString(mConverters) +
1343                    ", mCasts=" + Arrays.toString(mCasts) +
1344                    ", mKey=" + mKey +
1345                    '}';
1346        }
1347    }
1348}
1349