Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / compilation / DatastoreCallbacksProcessor.java
bloba455aac9c2b24d5ce31b1743fe655d8a18d02d7e
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.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.lang.annotation.Annotation;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
32 import javax.annotation.processing.AbstractProcessor;
33 import javax.annotation.processing.Filer;
34 import javax.annotation.processing.RoundEnvironment;
35 import javax.annotation.processing.SupportedAnnotationTypes;
36 import javax.annotation.processing.SupportedOptions;
37 import javax.annotation.processing.SupportedSourceVersion;
38 import javax.lang.model.SourceVersion;
39 import javax.lang.model.element.AnnotationMirror;
40 import javax.lang.model.element.AnnotationValue;
41 import javax.lang.model.element.Element;
42 import javax.lang.model.element.ExecutableElement;
43 import javax.lang.model.element.Modifier;
44 import javax.lang.model.element.PackageElement;
45 import javax.lang.model.element.TypeElement;
46 import javax.lang.model.type.DeclaredType;
47 import javax.lang.model.type.TypeKind;
48 import javax.lang.model.type.TypeMirror;
49 import javax.lang.model.util.ElementFilter;
50 import javax.tools.Diagnostic.Kind;
51 import javax.tools.FileObject;
52 import javax.tools.StandardLocation;
54 /**
55 * Processes datastore callback annotations (
56 * {@link PrePut}, {@link PostPut}, {@link PreDelete}, {@link PostDelete},
57 * {@link PreGet}, {@link com.google.appengine.api.datastore.PostLoad}, {@link PreQuery}) and
58 * generates a config file that the Datastore API can load at runtime. Each
59 * line of the config file is of the format: <br>
60 * kind.callback_type=comma-separated list of methods<br>
61 * where 'kind' is the kind of the entity to which the config on that line
62 * applies, 'callback_type' is one of PrePut, PostPut, PreDelete, PostDelete,
63 * and each entry in the comma-separated list of methods is a colon-delimited
64 * fully-qualified classname:method name tuple. So for example, if the dev
65 * wants a method named 'prePutCallback1' belonging to class
66 * 'com.example.MyCallbacks and a method named 'prePutCallback2'
67 * belonging to the same class to be invoked before any entity of kind 'yar' is
68 * put, the config file will look like this:
69 * <blockquote>
70 * <pre>
71 * yar.PrePut=com.example.MyCallbacks:prePutCallback1,com.example.MyCallbacks:prePutCallback2
72 * </pre>
73 * </blockquote>
75 * Note that it is possible to have a line which refers to all kinds by
76 * omitting the kind name:
77 * <blockquote>
78 * <pre>
79 * .PreDelete=com.example.MyCallbacks:preDeleteCallback1
80 * </pre>
81 * </blockquote>
83 * <p>
84 * Each type of callback has its own signature requirements for the methods it
85 * annotates. If any of these signature requirements are violated the processor
86 * will produce an error. See the javadoc for the annotations for more
87 * information about the callback-specific signature requirements.
89 * <p>
90 * Processor Options:<ul>
91 * <li>debug - turns on debug statements</li>
92 * </ul>
95 @SupportedAnnotationTypes({
96 "com.google.appengine.api.datastore.PrePut",
97 "com.google.appengine.api.datastore.PostPut",
98 "com.google.appengine.api.datastore.PreDelete",
99 "com.google.appengine.api.datastore.PostDelete",
100 "com.google.appengine.api.datastore.PreGet",
101 "com.google.appengine.api.datastore.PostLoad",
102 "com.google.appengine.api.datastore.PreQuery"})
103 @SupportedSourceVersion(SourceVersion.RELEASE_6)
104 @SupportedOptions({"debug"})
105 public class DatastoreCallbacksProcessor extends AbstractProcessor {
107 private static final String CALLBACKS_CONFIG_FILE = "META-INF/datastorecallbacks.xml";
109 private final OutputStream configOutputStream;
111 public DatastoreCallbacksProcessor() {
112 this(null);
115 @VisibleForTesting
116 DatastoreCallbacksProcessor(OutputStream configOutputStream) {
117 this.configOutputStream = configOutputStream;
121 * Internal interface describing an object that knows how to perform
122 * callback-specific verifications.
124 interface CallbackVerifier {
125 void verify(ExecutableElement annotatedMethod);
129 * Abstract {@link CallbackVerifier} implementation containing state and
130 * functionality that is common to all types of callbacks.
132 abstract class BaseCallbackVerifier implements CallbackVerifier {
133 final Class<? extends Annotation> callbackType;
135 BaseCallbackVerifier(Class<? extends Annotation> callbackType) {
136 this.callbackType = callbackType;
139 void verifySingleParamIsOfProperType(ExecutableElement annotatedMethod,
140 Class<? extends CallbackContext<?>> expectedType) {
141 if (annotatedMethod.getParameters().size() != 1 ||
142 !annotatedMethod.getParameters().get(0).asType().equals(getTypeMirror(expectedType))) {
143 error(String.format("%s method must have a single argument of type '%s'.",
144 callbackType.getSimpleName(), expectedType.getName()), annotatedMethod);
150 * Verifier for callbacks that take a single argument that extends
151 * {@link CallbackContext}.
153 class SingleParamCallbackVerifier extends BaseCallbackVerifier {
154 private final Class<? extends CallbackContext<?>> callbackContextClass;
156 SingleParamCallbackVerifier(Class<? extends Annotation> callbackType,
157 Class<? extends CallbackContext<?>> callbackContextClass) {
158 super(callbackType);
159 this.callbackContextClass = callbackContextClass;
162 @Override
163 public void verify(ExecutableElement annotatedMethod) {
164 verifySingleParamIsOfProperType(annotatedMethod, callbackContextClass);
169 * Keeps track of the callbacks we encounter and writes them out in the
170 * appropriate format once processing is complete.
172 private DatastoreCallbacksConfigWriter callbacksConfigWriter;
175 * Used to avoid performing class-level validation more than once.
177 private final Set<Element> verifiedClasses = Sets.newHashSet();
179 final Map<Class<? extends Annotation>, CallbackVerifier> callbackVerifiers =
180 new ImmutableMap.Builder<Class<? extends Annotation>, CallbackVerifier>()
181 .put(PrePut.class, new SingleParamCallbackVerifier(PrePut.class, PutContext.class))
182 .put(PostPut.class, new SingleParamCallbackVerifier(PostPut.class, PutContext.class))
183 .put(PreDelete.class, new SingleParamCallbackVerifier(
184 PreDelete.class, DeleteContext.class))
185 .put(PostDelete.class, new SingleParamCallbackVerifier(
186 PostDelete.class, DeleteContext.class))
187 .put(PreGet.class, new SingleParamCallbackVerifier(PreGet.class, PreGetContext.class))
188 .put(PostLoad.class, new SingleParamCallbackVerifier(
189 PostLoad.class, PostLoadContext.class))
190 .put(PreQuery.class, new SingleParamCallbackVerifier(
191 PreQuery.class, PreQueryContext.class)).build();
193 @Override
194 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
195 try {
196 return processImpl(annotations, roundEnv);
197 } catch (Exception e) {
198 error(Throwables.getStackTraceAsString(e), null);
199 return true;
203 private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
204 if (callbacksConfigWriter == null) {
205 loadCallbacksConfigWriter();
207 if (roundEnv.processingOver()) {
208 if (roundEnv.errorRaised()) {
209 log("Not writing config file due to errors.");
210 } else {
211 generateConfigFiles();
213 } else {
214 processAnnotations(annotations, roundEnv);
216 return true;
219 private void loadCallbacksConfigWriter() {
220 try {
221 FileObject existingFile = processingEnv.getFiler().getResource(
222 StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
223 InputStream inputStream = null;
224 if (existingFile != null) {
225 try {
226 inputStream = existingFile.openInputStream();
227 } catch (IOException e) {
230 callbacksConfigWriter = new DatastoreCallbacksConfigWriter(inputStream);
231 if (inputStream != null) {
232 inputStream.close();
234 } catch (IOException e) {
235 throw new RuntimeException(String.format("Unable to read %s", CALLBACKS_CONFIG_FILE), e);
239 OutputStream getConfigOutputStream() throws IOException {
240 if (configOutputStream != null) {
241 return configOutputStream;
243 Filer filer = processingEnv.getFiler();
244 FileObject fileObject =
245 filer.createResource(StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
246 return fileObject.openOutputStream();
249 private void generateConfigFiles() {
250 try {
251 OutputStream outputStream = null;
252 try {
253 outputStream = getConfigOutputStream();
254 callbacksConfigWriter.store(outputStream);
255 log("Wrote config: " + callbacksConfigWriter);
256 } finally {
257 if (outputStream != null) {
258 outputStream.close();
261 } catch (IOException e) {
262 throw new RuntimeException(String.format("Unable to create %s", CALLBACKS_CONFIG_FILE), e);
266 private void processAnnotations(
267 Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
268 for (TypeElement annotationElement : annotations) {
269 Set<? extends Element> annotatedMethods =
270 roundEnv.getElementsAnnotatedWith(annotationElement);
271 for (Element annotatedMethod : annotatedMethods) {
272 String enclosingClass =
273 getBinaryName(((TypeElement) annotatedMethod.getEnclosingElement()));
274 String method = annotatedMethod.getSimpleName().toString();
275 Set<String> kinds = verifyCallback((ExecutableElement) annotatedMethod, annotationElement,
276 enclosingClass, method);
277 if (!roundEnv.errorRaised()) {
278 callbacksConfigWriter.addCallback(kinds, annotationElement.getSimpleName().toString(),
279 enclosingClass, method);
286 * Extract the value of the kind attribute from the given annotation mirror.
287 * Return {@code null} if we encountered errors while validating the kinds.
288 * An empty set indicates that the callback applies to entities of all kinds.
290 @SuppressWarnings("unchecked")
291 private Set<String> extractKindsFromCallbackAnnotation(Element annotatedMethod,
292 AnnotationMirror annotationMirror) {
293 Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValueMap =
294 processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
295 Set<String> kinds = Sets.newLinkedHashSet();
296 for (ExecutableElement annotationParamName : annotationValueMap.keySet()) {
297 if (annotationParamName.getSimpleName().toString().equals("kinds")) {
298 Object value = annotationValueMap.get(annotationParamName).getValue();
299 if (value instanceof String) {
300 addKind((String) value, annotatedMethod, kinds);
301 } else {
302 for (AnnotationValue av : (List<AnnotationValue>) value) {
303 addKind(av.getValue().toString(), annotatedMethod, kinds);
306 break;
309 return kinds;
313 * Perform validation on the provided kind and, if it passes, add it to the
314 * given {@link Set}.
316 private void addKind(String kind, Element annotatedMethod, Set<String> kinds) {
317 kind = kind.trim();
318 if (kind.isEmpty()) {
319 error("A callback cannot be associated with an empty kind.", annotatedMethod);
320 } else {
321 kinds.add(kind);
325 * Verifies constraints on the callback method and class.
327 private Set<String> verifyCallback(ExecutableElement annotatedMethod,
328 TypeElement annotationElement, String cls, String method) {
329 Element classElement = annotatedMethod.getEnclosingElement();
330 if (verifiedClasses.add(classElement)) {
331 boolean hasNoArgConstructor = false;
332 for (ExecutableElement ctor : ElementFilter.constructorsIn(
333 classElement.getEnclosedElements())) {
334 if (ctor.getParameters().isEmpty()) {
335 hasNoArgConstructor = true;
336 break;
339 if (!hasNoArgConstructor) {
340 error("A class with a callback method must have a no-arg constructor.", classElement);
344 if (callbacksConfigWriter.hasCallback(cls, method)) {
345 error("Method can only have one callback annotation.", annotatedMethod);
347 if (annotatedMethod.getModifiers().contains(Modifier.STATIC)) {
348 error("Callback method must not be static.", annotatedMethod);
351 if (!annotatedMethod.getReturnType().getKind().equals(TypeKind.VOID)) {
352 error("Return type of callback method must be void.", annotatedMethod);
355 for (TypeMirror typeMirror : annotatedMethod.getThrownTypes()) {
356 if (!isSubTypeOfOneOf(typeMirror, RuntimeException.class, Error.class)) {
357 error("Callback methods cannot throw checked exceptions.", annotatedMethod);
361 AnnotationMirror annotationMirror = getAnnotationMirror(annotatedMethod, annotationElement);
362 Set<String> kinds = extractKindsFromCallbackAnnotation(annotatedMethod, annotationMirror);
364 CallbackVerifier verifier;
365 try {
366 verifier = callbackVerifiers.get(
367 Class.forName(annotationElement.getQualifiedName().toString()));
368 } catch (ClassNotFoundException e) {
369 throw new RuntimeException(e);
371 if (verifier == null) {
372 throw new RuntimeException(
373 "No verifier registered for " + annotationElement.getQualifiedName());
375 verifier.verify(annotatedMethod);
376 return kinds;
379 private boolean isSubTypeOfOneOf(TypeMirror typeMirror, Class<?>... classes) {
380 for (Class<?> cls : classes) {
381 if (processingEnv.getTypeUtils().isSubtype(typeMirror, getTypeMirror(cls))) {
382 return true;
385 return false;
389 * Returns the binary name of a reference type. For example,
390 * {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
392 private String getBinaryName(TypeElement element) {
393 return getBinaryNameImpl(element, element.getSimpleName().toString());
396 private String getBinaryNameImpl(Element element, String className) {
397 Element enclosingElement = element.getEnclosingElement();
399 if (enclosingElement instanceof PackageElement) {
400 PackageElement pkg = (PackageElement) enclosingElement;
401 if (pkg.isUnnamed()) {
402 return className;
404 return pkg.getQualifiedName() + "." + className;
407 return getBinaryNameImpl(enclosingElement, enclosingElement.getSimpleName() + "$" + className);
410 private AnnotationMirror getAnnotationMirror(Element annotatedMethod,
411 final TypeElement annotationElement) {
412 return Iterables.find(annotatedMethod.getAnnotationMirrors(),
413 new Predicate<AnnotationMirror>() {
414 @Override
415 public boolean apply(AnnotationMirror mirror) {
416 log("mirror: " + mirror);
417 DeclaredType type = mirror.getAnnotationType();
418 TypeElement typeElement = (TypeElement) type.asElement();
419 return typeElement.getQualifiedName().contentEquals(
420 annotationElement.getQualifiedName());
425 private void log(String msg) {
426 if (processingEnv.getOptions().containsKey("debug")) {
427 processingEnv.getMessager().printMessage(Kind.NOTE, "Datastore Callbacks: " + msg);
431 private void error(String msg, Element element) {
432 processingEnv.getMessager().printMessage(
433 Kind.ERROR, "Datastore Callbacks: " + msg, element);
436 private TypeMirror getTypeMirror(Class<?> cls) {
437 return processingEnv.getElementUtils().getTypeElement(cls.getName()).asType();