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