Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / compilation / DatastoreCallbacksProcessor.java
blob69e24b19f3feac22938b1351a62281ddcfe508d5
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 package com.google.appengine.tools.compilation;
4 import com.google.appengine.api.datastore.DeleteContext;
5 import com.google.appengine.api.datastore.CallbackContext;
6 import com.google.appengine.api.datastore.PostLoadContext;
7 import com.google.appengine.api.datastore.PreGetContext;
8 import com.google.appengine.api.datastore.PreQueryContext;
9 import com.google.appengine.api.datastore.PostDelete;
10 import com.google.appengine.api.datastore.PostLoad;
11 import com.google.appengine.api.datastore.PostPut;
12 import com.google.appengine.api.datastore.PreDelete;
13 import com.google.appengine.api.datastore.PreGet;
14 import com.google.appengine.api.datastore.PrePut;
15 import com.google.appengine.api.datastore.PreQuery;
16 import com.google.appengine.api.datastore.PutContext;
17 import com.google.common.annotations.VisibleForTesting;
18 import com.google.common.base.Predicate;
19 import com.google.common.base.Throwables;
20 import com.google.common.collect.ImmutableMap;
21 import com.google.common.collect.Iterables;
22 import com.google.common.collect.Sets;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.lang.annotation.Annotation;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
33 import javax.annotation.processing.AbstractProcessor;
34 import javax.annotation.processing.Filer;
35 import javax.annotation.processing.RoundEnvironment;
36 import javax.annotation.processing.SupportedAnnotationTypes;
37 import javax.annotation.processing.SupportedOptions;
38 import javax.annotation.processing.SupportedSourceVersion;
39 import javax.lang.model.SourceVersion;
40 import javax.lang.model.element.AnnotationMirror;
41 import javax.lang.model.element.AnnotationValue;
42 import javax.lang.model.element.Element;
43 import javax.lang.model.element.ExecutableElement;
44 import javax.lang.model.element.Modifier;
45 import javax.lang.model.element.PackageElement;
46 import javax.lang.model.element.TypeElement;
47 import javax.lang.model.type.DeclaredType;
48 import javax.lang.model.type.TypeKind;
49 import javax.lang.model.type.TypeMirror;
50 import javax.lang.model.util.ElementFilter;
51 import javax.tools.Diagnostic.Kind;
52 import javax.tools.FileObject;
53 import javax.tools.StandardLocation;
55 /**
56 * Processes datastore callback annotations (
57 * {@link PrePut}, {@link PostPut}, {@link PreDelete}, {@link PostDelete},
58 * {@link PreGet}, {@link com.google.appengine.api.datastore.PostLoad}, {@link PreQuery}) and
59 * generates a config file that the Datastore API can load at runtime. Each
60 * line of the config file is of the format: <br>
61 * kind.callback_type=comma-separated list of methods<br>
62 * where 'kind' is the kind of the entity to which the config on that line
63 * applies, 'callback_type' is one of PrePut, PostPut, PreDelete, PostDelete,
64 * and each entry in the comma-separated list of methods is a colon-delimited
65 * fully-qualified classname:method name tuple. So for example, if the dev
66 * wants a method named 'prePutCallback1' belonging to class
67 * 'com.example.MyCallbacks and a method named 'prePutCallback2'
68 * belonging to the same class to be invoked before any entity of kind 'yar' is
69 * put, the config file will look like this:
70 * <blockquote>
71 * <pre>
72 * yar.PrePut=com.example.MyCallbacks:prePutCallback1,com.example.MyCallbacks:prePutCallback2
73 * </pre>
74 * </blockquote>
76 * Note that it is possible to have a line which refers to all kinds by
77 * omitting the kind name:
78 * <blockquote>
79 * <pre>
80 * .PreDelete=com.example.MyCallbacks:preDeleteCallback1
81 * </pre>
82 * </blockquote>
84 * <p>
85 * Each type of callback has its own signature requirements for the methods it
86 * annotates. If any of these signature requirements are violated the processor
87 * will produce an error. See the javadoc for the annotations for more
88 * information about the callback-specific signature requirements.
90 * <p>
91 * Processor Options:<ul>
92 * <li>debug - turns on debug statements</li>
93 * </ul>
96 @SupportedAnnotationTypes({
97 "com.google.appengine.api.datastore.PrePut",
98 "com.google.appengine.api.datastore.PostPut",
99 "com.google.appengine.api.datastore.PreDelete",
100 "com.google.appengine.api.datastore.PostDelete",
101 "com.google.appengine.api.datastore.PreGet",
102 "com.google.appengine.api.datastore.PostLoad",
103 "com.google.appengine.api.datastore.PreQuery"})
104 @SupportedSourceVersion(SourceVersion.RELEASE_6)
105 @SupportedOptions({"debug"})
106 public class DatastoreCallbacksProcessor extends AbstractProcessor {
108 private static final String CALLBACKS_CONFIG_FILE =
109 "META-INF" + File.separator + "datastorecallbacks.xml";
111 private final OutputStream configOutputStream;
113 public DatastoreCallbacksProcessor() {
114 this(null);
117 @VisibleForTesting
118 DatastoreCallbacksProcessor(OutputStream configOutputStream) {
119 this.configOutputStream = configOutputStream;
123 * Internal interface describing an object that knows how to perform
124 * callback-specific verifications.
126 interface CallbackVerifier {
127 void verify(ExecutableElement annotatedMethod);
131 * Abstract {@link CallbackVerifier} implementation containing state and
132 * functionality that is common to all types of callbacks.
134 abstract class BaseCallbackVerifier implements CallbackVerifier {
135 final Class<? extends Annotation> callbackType;
137 BaseCallbackVerifier(Class<? extends Annotation> callbackType) {
138 this.callbackType = callbackType;
141 void verifySingleParamIsOfProperType(ExecutableElement annotatedMethod,
142 Class<? extends CallbackContext<?>> expectedType) {
143 if (annotatedMethod.getParameters().size() != 1 ||
144 !annotatedMethod.getParameters().get(0).asType().equals(getTypeMirror(expectedType))) {
145 error(String.format("%s method must have a single argument of type '%s'.",
146 callbackType.getSimpleName(), expectedType.getName()), annotatedMethod);
152 * Verifier for callbacks that take a single argument that extends
153 * {@link CallbackContext}.
155 class SingleParamCallbackVerifier extends BaseCallbackVerifier {
156 private final Class<? extends CallbackContext<?>> callbackContextClass;
158 SingleParamCallbackVerifier(Class<? extends Annotation> callbackType,
159 Class<? extends CallbackContext<?>> callbackContextClass) {
160 super(callbackType);
161 this.callbackContextClass = callbackContextClass;
164 @Override
165 public void verify(ExecutableElement annotatedMethod) {
166 verifySingleParamIsOfProperType(annotatedMethod, callbackContextClass);
171 * Keeps track of the callbacks we encounter and writes them out in the
172 * appropriate format once processing is complete.
174 private DatastoreCallbacksConfigWriter callbacksConfigWriter;
177 * Used to avoid performing class-level validation more than once.
179 private final Set<Element> verifiedClasses = Sets.newHashSet();
181 final Map<Class<? extends Annotation>, CallbackVerifier> callbackVerifiers =
182 new ImmutableMap.Builder<Class<? extends Annotation>, CallbackVerifier>()
183 .put(PrePut.class, new SingleParamCallbackVerifier(PrePut.class, PutContext.class))
184 .put(PostPut.class, new SingleParamCallbackVerifier(PostPut.class, PutContext.class))
185 .put(PreDelete.class, new SingleParamCallbackVerifier(
186 PreDelete.class, DeleteContext.class))
187 .put(PostDelete.class, new SingleParamCallbackVerifier(
188 PostDelete.class, DeleteContext.class))
189 .put(PreGet.class, new SingleParamCallbackVerifier(PreGet.class, PreGetContext.class))
190 .put(PostLoad.class, new SingleParamCallbackVerifier(
191 PostLoad.class, PostLoadContext.class))
192 .put(PreQuery.class, new SingleParamCallbackVerifier(
193 PreQuery.class, PreQueryContext.class)).build();
195 @Override
196 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
197 try {
198 return processImpl(annotations, roundEnv);
199 } catch (Exception e) {
200 error(Throwables.getStackTraceAsString(e), null);
201 return true;
205 private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
206 if (callbacksConfigWriter == null) {
207 loadCallbacksConfigWriter();
209 if (roundEnv.processingOver()) {
210 if (roundEnv.errorRaised()) {
211 log("Not writing config file due to errors.");
212 } else {
213 generateConfigFiles();
215 } else {
216 processAnnotations(annotations, roundEnv);
218 return true;
221 private void loadCallbacksConfigWriter() {
222 try {
223 FileObject existingFile = processingEnv.getFiler().getResource(
224 StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
225 InputStream inputStream = null;
226 if (existingFile != null) {
227 try {
228 inputStream = existingFile.openInputStream();
229 } catch (IOException e) {
232 callbacksConfigWriter = new DatastoreCallbacksConfigWriter(inputStream);
233 if (inputStream != null) {
234 inputStream.close();
236 } catch (IOException e) {
237 throw new RuntimeException(String.format("Unable to read %s", CALLBACKS_CONFIG_FILE), e);
241 OutputStream getConfigOutputStream() throws IOException {
242 if (configOutputStream != null) {
243 return configOutputStream;
245 Filer filer = processingEnv.getFiler();
246 FileObject fileObject =
247 filer.createResource(StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
248 return fileObject.openOutputStream();
251 private void generateConfigFiles() {
252 try {
253 OutputStream outputStream = null;
254 try {
255 outputStream = getConfigOutputStream();
256 callbacksConfigWriter.store(outputStream);
257 log("Wrote config: " + callbacksConfigWriter);
258 } finally {
259 if (outputStream != null) {
260 outputStream.close();
263 } catch (IOException e) {
264 throw new RuntimeException(String.format("Unable to create %s", CALLBACKS_CONFIG_FILE), e);
268 private void processAnnotations(
269 Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
270 for (TypeElement annotationElement : annotations) {
271 Set<? extends Element> annotatedMethods =
272 roundEnv.getElementsAnnotatedWith(annotationElement);
273 for (Element annotatedMethod : annotatedMethods) {
274 String enclosingClass =
275 getBinaryName(((TypeElement) annotatedMethod.getEnclosingElement()));
276 String method = annotatedMethod.getSimpleName().toString();
277 Set<String> kinds = verifyCallback((ExecutableElement) annotatedMethod, annotationElement,
278 enclosingClass, method);
279 if (!roundEnv.errorRaised()) {
280 callbacksConfigWriter.addCallback(kinds, annotationElement.getSimpleName().toString(),
281 enclosingClass, method);
288 * Extract the value of the kind attribute from the given annotation mirror.
289 * Return {@code null} if we encountered errors while validating the kinds.
290 * An empty set indicates that the callback applies to entities of all kinds.
292 @SuppressWarnings("unchecked")
293 private Set<String> extractKindsFromCallbackAnnotation(Element annotatedMethod,
294 AnnotationMirror annotationMirror) {
295 Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValueMap =
296 processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
297 Set<String> kinds = Sets.newLinkedHashSet();
298 for (ExecutableElement annotationParamName : annotationValueMap.keySet()) {
299 if (annotationParamName.getSimpleName().toString().equals("kinds")) {
300 Object value = annotationValueMap.get(annotationParamName).getValue();
301 if (value instanceof String) {
302 addKind((String) value, annotatedMethod, kinds);
303 } else {
304 for (AnnotationValue av : (List<AnnotationValue>) value) {
305 addKind(av.getValue().toString(), annotatedMethod, kinds);
308 break;
311 return kinds;
315 * Perform validation on the provided kind and, if it passes, add it to the
316 * given {@link Set}.
318 private void addKind(String kind, Element annotatedMethod, Set<String> kinds) {
319 kind = kind.trim();
320 if (kind.isEmpty()) {
321 error("A callback cannot be associated with an empty kind.", annotatedMethod);
322 } else {
323 kinds.add(kind);
327 * Verifies constraints on the callback method and class.
329 private Set<String> verifyCallback(ExecutableElement annotatedMethod,
330 TypeElement annotationElement, String cls, String method) {
331 Element classElement = annotatedMethod.getEnclosingElement();
332 if (verifiedClasses.add(classElement)) {
333 boolean hasNoArgConstructor = false;
334 for (ExecutableElement ctor : ElementFilter.constructorsIn(
335 classElement.getEnclosedElements())) {
336 if (ctor.getParameters().isEmpty()) {
337 hasNoArgConstructor = true;
338 break;
341 if (!hasNoArgConstructor) {
342 error("A class with a callback method must have a no-arg constructor.", classElement);
346 if (callbacksConfigWriter.hasCallback(cls, method)) {
347 error("Method can only have one callback annotation.", annotatedMethod);
349 if (annotatedMethod.getModifiers().contains(Modifier.STATIC)) {
350 error("Callback method must not be static.", annotatedMethod);
353 if (!annotatedMethod.getReturnType().getKind().equals(TypeKind.VOID)) {
354 error("Return type of callback method must be void.", annotatedMethod);
357 for (TypeMirror typeMirror : annotatedMethod.getThrownTypes()) {
358 if (!isSubTypeOfOneOf(typeMirror, RuntimeException.class, Error.class)) {
359 error("Callback methods cannot throw checked exceptions.", annotatedMethod);
363 AnnotationMirror annotationMirror = getAnnotationMirror(annotatedMethod, annotationElement);
364 Set<String> kinds = extractKindsFromCallbackAnnotation(annotatedMethod, annotationMirror);
366 CallbackVerifier verifier;
367 try {
368 verifier = callbackVerifiers.get(
369 Class.forName(annotationElement.getQualifiedName().toString()));
370 } catch (ClassNotFoundException e) {
371 throw new RuntimeException(e);
373 if (verifier == null) {
374 throw new RuntimeException(
375 "No verifier registered for " + annotationElement.getQualifiedName());
377 verifier.verify(annotatedMethod);
378 return kinds;
381 private boolean isSubTypeOfOneOf(TypeMirror typeMirror, Class<?>... classes) {
382 for (Class<?> cls : classes) {
383 if (processingEnv.getTypeUtils().isSubtype(typeMirror, getTypeMirror(cls))) {
384 return true;
387 return false;
391 * Returns the binary name of a reference type. For example,
392 * {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
394 private String getBinaryName(TypeElement element) {
395 return getBinaryNameImpl(element, element.getSimpleName().toString());
398 private String getBinaryNameImpl(Element element, String className) {
399 Element enclosingElement = element.getEnclosingElement();
401 if (enclosingElement instanceof PackageElement) {
402 PackageElement pkg = (PackageElement) enclosingElement;
403 if (pkg.isUnnamed()) {
404 return className;
406 return pkg.getQualifiedName() + "." + className;
409 return getBinaryNameImpl(enclosingElement, enclosingElement.getSimpleName() + "$" + className);
412 private AnnotationMirror getAnnotationMirror(Element annotatedMethod,
413 final TypeElement annotationElement) {
414 return Iterables.find(annotatedMethod.getAnnotationMirrors(),
415 new Predicate<AnnotationMirror>() {
416 @Override
417 public boolean apply(AnnotationMirror mirror) {
418 log("mirror: " + mirror);
419 DeclaredType type = mirror.getAnnotationType();
420 TypeElement typeElement = (TypeElement) type.asElement();
421 return typeElement.getQualifiedName().contentEquals(
422 annotationElement.getQualifiedName());
427 private void log(String msg) {
428 if (processingEnv.getOptions().containsKey("debug")) {
429 processingEnv.getMessager().printMessage(Kind.NOTE, "Datastore Callbacks: " + msg);
433 private void error(String msg, Element element) {
434 processingEnv.getMessager().printMessage(
435 Kind.ERROR, "Datastore Callbacks: " + msg, element);
438 private TypeMirror getTypeMirror(Class<?> cls) {
439 return processingEnv.getElementUtils().getTypeElement(cls.getName()).asType();