ProcessBindable.java revision 1b9940e612fc73202837fbe9db2f9035f307b5d1
1package com.android.databinding.annotationprocessor;
2
3import com.android.databinding.reflection.ModelAnalyzer;
4import com.android.databinding.reflection.ReflectionAnalyzer;
5
6import android.binding.Bindable;
7
8import java.io.File;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.ObjectInputStream;
12import java.io.ObjectOutputStream;
13import java.io.Serializable;
14import java.io.Writer;
15import java.net.URL;
16import java.util.ArrayList;
17import java.util.Collections;
18import java.util.Enumeration;
19import java.util.HashMap;
20import java.util.HashSet;
21import java.util.List;
22import java.util.Set;
23
24import javax.annotation.processing.AbstractProcessor;
25import javax.annotation.processing.ProcessingEnvironment;
26import javax.annotation.processing.RoundEnvironment;
27import javax.annotation.processing.SupportedAnnotationTypes;
28import javax.annotation.processing.SupportedSourceVersion;
29import javax.lang.model.SourceVersion;
30import javax.lang.model.element.Element;
31import javax.lang.model.element.ElementKind;
32import javax.lang.model.element.ExecutableElement;
33import javax.lang.model.element.Name;
34import javax.lang.model.element.PackageElement;
35import javax.lang.model.element.TypeElement;
36import javax.lang.model.element.VariableElement;
37import javax.lang.model.type.TypeKind;
38import javax.lang.model.util.Elements;
39import javax.lang.model.util.Types;
40import javax.tools.Diagnostic;
41import javax.tools.FileObject;
42import javax.tools.JavaFileObject;
43import javax.tools.StandardLocation;
44
45@SupportedAnnotationTypes({"android.binding.Bindable"})
46@SupportedSourceVersion(SourceVersion.RELEASE_7)
47public class ProcessBindable extends AbstractProcessor {
48
49    private boolean mFileGenerated;
50
51    public ProcessBindable() {
52    }
53
54    @Override
55    public synchronized void init(ProcessingEnvironment processingEnv) {
56        super.init(processingEnv);
57        ReflectionAnalyzer.setProcessingEnvironment(processingEnv);
58    }
59
60    @Override
61    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
62        if (mFileGenerated) {
63            return false;
64        }
65        Intermediate properties = readIntermediateFile();
66        for (Element element : roundEnv.getElementsAnnotatedWith(Bindable.class)) {
67            TypeElement enclosing = (TypeElement) element.getEnclosingElement();
68            properties.cleanProperties(enclosing.getQualifiedName().toString());
69        }
70        for (Element element : roundEnv.getElementsAnnotatedWith(Bindable.class)) {
71            TypeElement enclosing = (TypeElement) element.getEnclosingElement();
72            String name = getPropertyName(element);
73            if (name != null) {
74                properties.addProperty(enclosing.getQualifiedName().toString(), name);
75            }
76        }
77        writeIntermediateFile(properties);
78        generateBR(properties);
79        mFileGenerated = true;
80        return true;
81    }
82
83    private void generateBR(Intermediate intermediate) {
84        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
85                "************* Generating BR file from Bindable attributes");
86        HashSet<String> properties = new HashSet<>();
87        intermediate.captureProperties(properties);
88        mergeClassPathResources(properties);
89        try {
90            ArrayList<String> sortedProperties = new ArrayList<String>();
91            sortedProperties.addAll(properties);
92            Collections.sort(sortedProperties);
93
94            JavaFileObject fileObject = processingEnv.getFiler()
95                    .createSourceFile("android.binding.BR");
96            Writer writer = fileObject.openWriter();
97            writer.write("package android.binding;\n\n" +
98                            "public final class BR {\n" +
99                            "    public static final int _all = 0;\n"
100            );
101            int id = 0;
102            for (String property : sortedProperties) {
103                id++;
104                writer.write("    public static final int " + property + " = " + id + ";\n");
105            }
106            writer.write("    public static int getId(String key) {\n");
107            writer.write("        switch(key) {\n");
108            id = 0;
109            for (String property : sortedProperties) {
110                id++;
111                writer.write("            case \"" + property + "\": return " + id + ";\n");
112            }
113            writer.write("        }\n");
114            writer.write("        return -1;\n");
115            writer.write("    }");
116            writer.write("}\n");
117
118            writer.close();
119        } catch (IOException e) {
120            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
121                    "Could not generate BR file " + e.getLocalizedMessage());
122        }
123    }
124
125    private String getPropertyName(Element element) {
126        switch (element.getKind()) {
127            case FIELD:
128                return stripPrefixFromField((VariableElement) element);
129            case METHOD:
130                return stripPrefixFromMethod((ExecutableElement) element);
131            default:
132                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
133                        "@Bindable is not allowed on " + element.getKind(), element);
134                return null;
135        }
136    }
137
138    private static String stripPrefixFromField(VariableElement element) {
139        Name name = element.getSimpleName();
140        if (name.length() >= 2) {
141            char firstChar = name.charAt(0);
142            char secondChar = name.charAt(1);
143            if (name.length() > 2 && firstChar == 'm' && secondChar == '_') {
144                char thirdChar = name.charAt(2);
145                if (Character.isJavaIdentifierStart(thirdChar)) {
146                    return "" + Character.toLowerCase(thirdChar) +
147                            name.subSequence(3, name.length());
148                }
149            } else if ((firstChar == 'm' && Character.isUpperCase(secondChar)) ||
150                    (firstChar == '_' && Character.isJavaIdentifierStart(secondChar))) {
151                return "" + Character.toLowerCase(secondChar) + name.subSequence(2, name.length());
152            }
153        }
154        return name.toString();
155    }
156
157    private String stripPrefixFromMethod(ExecutableElement element) {
158        Name name = element.getSimpleName();
159        CharSequence propertyName;
160        if (isGetter(element) || isSetter(element)) {
161            propertyName = name.subSequence(3, name.length());
162        } else if (isBooleanGetter(element)) {
163            propertyName = name.subSequence(2, name.length());
164        } else {
165            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
166                    "@Bindable associated with method must follow JavaBeans convention", element);
167            return null;
168        }
169        char firstChar = propertyName.charAt(0);
170        return "" + Character.toLowerCase(firstChar) +
171                propertyName.subSequence(1, propertyName.length());
172    }
173
174    private static boolean prefixes(CharSequence sequence, String prefix) {
175        boolean prefixes = false;
176        if (sequence.length() > prefix.length()) {
177            int count = prefix.length();
178            prefixes = true;
179            for (int i = 0; i < count; i++) {
180                if (sequence.charAt(i) != prefix.charAt(i)) {
181                    prefixes = false;
182                    break;
183                }
184            }
185        }
186        return prefixes;
187    }
188
189    private static boolean isGetter(ExecutableElement element) {
190        Name name = element.getSimpleName();
191        return prefixes(name, "get") &&
192                Character.isJavaIdentifierStart(name.charAt(3)) &&
193                element.getParameters().isEmpty() &&
194                element.getReturnType().getKind() != TypeKind.VOID;
195    }
196
197    private static boolean isSetter(ExecutableElement element) {
198        Name name = element.getSimpleName();
199        return prefixes(name, "set") &&
200                Character.isJavaIdentifierStart(name.charAt(3)) &&
201                element.getParameters().size() == 1 &&
202                element.getReturnType().getKind() == TypeKind.VOID;
203    }
204
205    private static boolean isBooleanGetter(ExecutableElement element) {
206        Name name = element.getSimpleName();
207        return prefixes(name, "is") &&
208                Character.isJavaIdentifierStart(name.charAt(2)) &&
209                element.getParameters().isEmpty() &&
210                element.getReturnType().getKind() == TypeKind.BOOLEAN;
211    }
212
213    private Intermediate readIntermediateFile() {
214        Intermediate properties = null;
215        ObjectInputStream in = null;
216        try {
217            FileObject intermediate = processingEnv.getFiler()
218                    .getResource(StandardLocation.CLASS_OUTPUT,
219                            ProcessBindable.class.getPackage().getName(), "binding_properties.bin");
220            if (new File(intermediate.getName()).exists()) {
221                in = new ObjectInputStream(intermediate.openInputStream());
222                properties = (Intermediate) in.readObject();
223            }
224        } catch (IOException e) {
225            System.err.println("Could not read Binding properties intermediate file: " +
226                    e.getLocalizedMessage());
227        } catch (ClassNotFoundException e) {
228            System.err.println("Could not read Binding properties intermediate file: " +
229                    e.getLocalizedMessage());
230        } finally {
231            try {
232                if (in != null) {
233                    in.close();
234                }
235            } catch (IOException e) {
236                e.printStackTrace();
237            }
238        }
239        if (properties == null) {
240            properties = new IntermediateV1();
241        }
242        return properties;
243    }
244
245    private void mergeClassPathResources(HashSet<String> intermediateProperties) {
246        try {
247            String resourcePath = ProcessBindable.class.getPackage().getName()
248                    .replace('.', '/') + "/binding_properties.bin";
249            Enumeration<URL> resources = getClass().getClassLoader()
250                    .getResources(resourcePath);
251            while (resources.hasMoreElements()) {
252                URL url = resources.nextElement();
253                System.out.println("Merging binding adapters from " + url);
254                InputStream inputStream = null;
255                try {
256                    inputStream = url.openStream();
257                    ObjectInputStream in = new ObjectInputStream(inputStream);
258                    Intermediate properties = (Intermediate) in.readObject();
259                    if (properties != null) {
260                        properties.captureProperties(intermediateProperties);
261                    }
262                } catch (IOException e) {
263                    System.err.println("Could not merge in Bindables from " + url + ": " +
264                            e.getLocalizedMessage());
265                } catch (ClassNotFoundException e) {
266                    System.err.println("Could not read Binding properties intermediate file: " +
267                            e.getLocalizedMessage());
268                } finally {
269                    try {
270                        inputStream.close();
271                    } catch (IOException e2) {
272                        System.err.println("Error closing intermediate Bindables store: " +
273                                e2.getLocalizedMessage());
274                    }
275                }
276            }
277        } catch (IOException e) {
278            System.err.println("Could not read Binding properties intermediate file: " +
279                    e.getLocalizedMessage());
280        }
281    }
282
283    private void writeIntermediateFile(Intermediate properties) {
284        try {
285            FileObject intermediate = processingEnv.getFiler().createResource(
286                    StandardLocation.CLASS_OUTPUT, ProcessBindable.class.getPackage().getName(),
287                    "binding_properties.bin");
288            ObjectOutputStream out = new ObjectOutputStream(intermediate.openOutputStream());
289            out.writeObject(properties);
290            out.close();
291        } catch (IOException e) {
292            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
293                    "Could not write to intermediate file: " + e.getLocalizedMessage());
294        }
295    }
296
297    private interface Intermediate {
298        void captureProperties(Set<String> properties);
299
300        void cleanProperties(String className);
301
302        void addProperty(String className, String propertyName);
303    }
304
305    private static class IntermediateV1 implements Serializable, Intermediate {
306        private static final long serialVersionUID = 1L;
307
308        private final HashMap<String, HashSet<String>> mProperties = new HashMap<>();
309
310        @Override
311        public void captureProperties(Set<String> properties) {
312            for (HashSet<String> propertySet : mProperties.values()) {
313                properties.addAll(propertySet);
314            }
315        }
316
317        @Override
318        public void cleanProperties(String className) {
319            mProperties.remove(className);
320        }
321
322        @Override
323        public void addProperty(String className, String propertyName) {
324            HashSet<String> properties = mProperties.get(className);
325            if (properties == null) {
326                properties = new HashSet<>();
327                mProperties.put(className, properties);
328            }
329            properties.add(propertyName);
330        }
331    }
332}
333