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