ProcessBindable.java revision 2611838bffef5a009ca71e3e9e59a93f29b098ed
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 */
16
17package android.databinding.annotationprocessor;
18
19import android.databinding.Bindable;
20import android.databinding.BindingBuildInfo;
21import android.databinding.tool.CompilerChef.BindableHolder;
22import android.databinding.tool.util.GenerationalClassUtil;
23import android.databinding.tool.util.L;
24import android.databinding.tool.util.Preconditions;
25
26import java.io.Serializable;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.List;
32import java.util.Set;
33
34import javax.annotation.processing.ProcessingEnvironment;
35import javax.annotation.processing.RoundEnvironment;
36import javax.annotation.processing.SupportedSourceVersion;
37import javax.lang.model.SourceVersion;
38import javax.lang.model.element.Element;
39import javax.lang.model.element.ElementKind;
40import javax.lang.model.element.ExecutableElement;
41import javax.lang.model.element.Name;
42import javax.lang.model.element.TypeElement;
43import javax.lang.model.element.VariableElement;
44import javax.lang.model.type.TypeKind;
45import javax.lang.model.util.Types;
46
47// binding app info and library info are necessary to trigger this.
48@SupportedSourceVersion(SourceVersion.RELEASE_7)
49public class ProcessBindable extends ProcessDataBinding.ProcessingStep implements BindableHolder {
50    private static final String INTERMEDIATE_FILE_EXT = "-br.bin";
51    Intermediate mProperties;
52    HashMap<String, HashSet<String>> mLayoutVariables = new HashMap<String, HashSet<String>>();
53
54    @Override
55    public boolean onHandleStep(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv,
56            BindingBuildInfo buildInfo) {
57        if (mProperties == null) {
58            mProperties = new IntermediateV1(buildInfo.modulePackage());
59            mergeLayoutVariables();
60            mLayoutVariables.clear();
61            TypeElement observableType = processingEnv.getElementUtils().
62                    getTypeElement("android.databinding.Observable");
63            Types typeUtils = processingEnv.getTypeUtils();
64            for (Element element : AnnotationUtil
65                    .getElementsAnnotatedWith(roundEnv, Bindable.class)) {
66                Element enclosingElement = element.getEnclosingElement();
67                ElementKind kind = enclosingElement.getKind();
68                if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) {
69                    L.e("Bindable must be on a member field or method. The enclosing type is %s",
70                            enclosingElement.getKind());
71                }
72                TypeElement enclosing = (TypeElement) enclosingElement;
73                if (!typeUtils.isAssignable(enclosing.asType(), observableType.asType())) {
74                    L.e("Bindable must be on a member in an Observable class. %s is not Observable",
75                            enclosingElement.getSimpleName());
76                }
77                String name = getPropertyName(element);
78                if (name != null) {
79                    Preconditions
80                            .checkNotNull(mProperties, "Must receive app / library info before "
81                                    + "Bindable fields.");
82                    mProperties.addProperty(enclosing.getQualifiedName().toString(), name);
83                }
84            }
85            GenerationalClassUtil.writeIntermediateFile(processingEnv,
86                    mProperties.getPackage(),
87                    createIntermediateFileName(mProperties.getPackage()), mProperties);
88            generateBRClasses(!buildInfo.isLibrary(), mProperties.getPackage());
89        }
90        return false;
91    }
92
93    @Override
94    public void addVariable(String variableName, String containingClassName) {
95        HashSet<String> variableNames = mLayoutVariables.get(containingClassName);
96        if (variableNames == null) {
97            variableNames = new HashSet<String>();
98            mLayoutVariables.put(containingClassName, variableNames);
99        }
100        variableNames.add(variableName);
101    }
102
103    @Override
104    public void onProcessingOver(RoundEnvironment roundEnvironment,
105            ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) {
106    }
107
108    private String createIntermediateFileName(String appPkg) {
109        return appPkg + INTERMEDIATE_FILE_EXT;
110    }
111
112    private void generateBRClasses(boolean useFinalFields, String pkg) {
113        L.d("************* Generating BR file %s. use final: %s", pkg, useFinalFields);
114        HashSet<String> properties = new HashSet<String>();
115        mProperties.captureProperties(properties);
116        List<Intermediate> previousIntermediates = loadPreviousBRFiles();
117        for (Intermediate intermediate : previousIntermediates) {
118            intermediate.captureProperties(properties);
119        }
120        writeBRClass(useFinalFields, pkg, properties);
121        if (useFinalFields) {
122            // generate BR for all previous packages
123            for (Intermediate intermediate : previousIntermediates) {
124                writeBRClass(true, intermediate.getPackage(),
125                        properties);
126            }
127        }
128    }
129
130    private void writeBRClass(boolean useFinalFields, String pkg, HashSet<String> properties) {
131        ArrayList<String> sortedProperties = new ArrayList<String>();
132        sortedProperties.addAll(properties);
133        Collections.sort(sortedProperties);
134        StringBuilder out = new StringBuilder();
135        String modifier = "public static " + (useFinalFields ? "final" : "") + " int ";
136        out.append("package " + pkg + ";\n\n" +
137                        "public class BR {\n" +
138                        "    " + modifier + "_all = 0;\n"
139        );
140        int id = 0;
141        for (String property : sortedProperties) {
142            id++;
143            out.append("    " + modifier + property + " = " + id + ";\n");
144        }
145        out.append("}\n");
146
147        getWriter().writeToFile(pkg + ".BR", out.toString() );
148    }
149
150    private String getPropertyName(Element element) {
151        switch (element.getKind()) {
152            case FIELD:
153                return stripPrefixFromField((VariableElement) element);
154            case METHOD:
155                return stripPrefixFromMethod((ExecutableElement) element);
156            default:
157                L.e("@Bindable is not allowed on %s", element.getKind());
158                return null;
159        }
160    }
161
162    private static String stripPrefixFromField(VariableElement element) {
163        Name name = element.getSimpleName();
164        if (name.length() >= 2) {
165            char firstChar = name.charAt(0);
166            char secondChar = name.charAt(1);
167            if (name.length() > 2 && firstChar == 'm' && secondChar == '_') {
168                char thirdChar = name.charAt(2);
169                if (Character.isJavaIdentifierStart(thirdChar)) {
170                    return "" + Character.toLowerCase(thirdChar) +
171                            name.subSequence(3, name.length());
172                }
173            } else if ((firstChar == 'm' && Character.isUpperCase(secondChar)) ||
174                    (firstChar == '_' && Character.isJavaIdentifierStart(secondChar))) {
175                return "" + Character.toLowerCase(secondChar) + name.subSequence(2, name.length());
176            }
177        }
178        return name.toString();
179    }
180
181    private String stripPrefixFromMethod(ExecutableElement element) {
182        Name name = element.getSimpleName();
183        CharSequence propertyName;
184        if (isGetter(element) || isSetter(element)) {
185            propertyName = name.subSequence(3, name.length());
186        } else if (isBooleanGetter(element)) {
187            propertyName = name.subSequence(2, name.length());
188        } else {
189            L.e("@Bindable associated with method must follow JavaBeans convention %s", element);
190            return null;
191        }
192        char firstChar = propertyName.charAt(0);
193        return "" + Character.toLowerCase(firstChar) +
194                propertyName.subSequence(1, propertyName.length());
195    }
196
197    private void mergeLayoutVariables() {
198        for (String containingClass : mLayoutVariables.keySet()) {
199            for (String variable : mLayoutVariables.get(containingClass)) {
200                mProperties.addProperty(containingClass, variable);
201            }
202        }
203    }
204
205    private static boolean prefixes(CharSequence sequence, String prefix) {
206        boolean prefixes = false;
207        if (sequence.length() > prefix.length()) {
208            int count = prefix.length();
209            prefixes = true;
210            for (int i = 0; i < count; i++) {
211                if (sequence.charAt(i) != prefix.charAt(i)) {
212                    prefixes = false;
213                    break;
214                }
215            }
216        }
217        return prefixes;
218    }
219
220    private static boolean isGetter(ExecutableElement element) {
221        Name name = element.getSimpleName();
222        return prefixes(name, "get") &&
223                Character.isJavaIdentifierStart(name.charAt(3)) &&
224                element.getParameters().isEmpty() &&
225                element.getReturnType().getKind() != TypeKind.VOID;
226    }
227
228    private static boolean isSetter(ExecutableElement element) {
229        Name name = element.getSimpleName();
230        return prefixes(name, "set") &&
231                Character.isJavaIdentifierStart(name.charAt(3)) &&
232                element.getParameters().size() == 1 &&
233                element.getReturnType().getKind() == TypeKind.VOID;
234    }
235
236    private static boolean isBooleanGetter(ExecutableElement element) {
237        Name name = element.getSimpleName();
238        return prefixes(name, "is") &&
239                Character.isJavaIdentifierStart(name.charAt(2)) &&
240                element.getParameters().isEmpty() &&
241                element.getReturnType().getKind() == TypeKind.BOOLEAN;
242    }
243
244    private List<Intermediate> loadPreviousBRFiles() {
245        return GenerationalClassUtil
246                .loadObjects(getClass().getClassLoader(),
247                        new GenerationalClassUtil.ExtensionFilter(INTERMEDIATE_FILE_EXT));
248    }
249
250    private interface Intermediate extends Serializable {
251
252        void captureProperties(Set<String> properties);
253
254        void addProperty(String className, String propertyName);
255
256        boolean hasValues();
257
258        String getPackage();
259    }
260
261    private static class IntermediateV1 implements Serializable, Intermediate {
262
263        private static final long serialVersionUID = 2L;
264
265        private String mPackage;
266        private final HashMap<String, HashSet<String>> mProperties = new HashMap<String, HashSet<String>>();
267
268        public IntermediateV1(String aPackage) {
269            mPackage = aPackage;
270        }
271
272        @Override
273        public void captureProperties(Set<String> properties) {
274            for (HashSet<String> propertySet : mProperties.values()) {
275                properties.addAll(propertySet);
276            }
277        }
278
279        @Override
280        public void addProperty(String className, String propertyName) {
281            HashSet<String> properties = mProperties.get(className);
282            if (properties == null) {
283                properties = new HashSet<String>();
284                mProperties.put(className, properties);
285            }
286            properties.add(propertyName);
287        }
288
289        @Override
290        public boolean hasValues() {
291            return !mProperties.isEmpty();
292        }
293
294        @Override
295        public String getPackage() {
296            return mPackage;
297        }
298    }
299}
300