1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / compilation / DatastoreCallbacksProcessor.java
blob07b638fa504a2c92da597172cdcc2d95a7e492e5
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.lang.model.SourceVersion;
38 import javax.lang.model.element.AnnotationMirror;
39 import javax.lang.model.element.AnnotationValue;
40 import javax.lang.model.element.Element;
41 import javax.lang.model.element.ExecutableElement;
42 import javax.lang.model.element.Modifier;
43 import javax.lang.model.element.PackageElement;
44 import javax.lang.model.element.TypeElement;
45 import javax.lang.model.type.DeclaredType;
46 import javax.lang.model.type.TypeKind;
47 import javax.lang.model.type.TypeMirror;
48 import javax.lang.model.util.ElementFilter;
49 import javax.tools.Diagnostic.Kind;
50 import javax.tools.FileObject;
51 import javax.tools.StandardLocation;
53 /**
54 * Processes datastore callback annotations (
55 * {@link PrePut}, {@link PostPut}, {@link PreDelete}, {@link PostDelete},
56 * {@link PreGet}, {@link com.google.appengine.api.datastore.PostLoad}, {@link PreQuery}) and
57 * generates a config file that the Datastore API can load at runtime. Each
58 * line of the config file is of the format: <br>
59 * kind.callback_type=comma-separated list of methods<br>
60 * where 'kind' is the kind of the entity to which the config on that line
61 * applies, 'callback_type' is one of PrePut, PostPut, PreDelete, PostDelete,
62 * and each entry in the comma-separated list of methods is a colon-delimited
63 * fully-qualified classname:method name tuple. So for example, if the dev
64 * wants a method named 'prePutCallback1' belonging to class
65 * 'com.example.MyCallbacks and a method named 'prePutCallback2'
66 * belonging to the same class to be invoked before any entity of kind 'yar' is
67 * put, the config file will look like this:
68 * <blockquote>
69 * <pre>
70 * yar.PrePut=com.example.MyCallbacks:prePutCallback1,com.example.MyCallbacks:prePutCallback2
71 * </pre>
72 * </blockquote>
74 * Note that it is possible to have a line which refers to all kinds by
75 * omitting the kind name:
76 * <blockquote>
77 * <pre>
78 * .PreDelete=com.example.MyCallbacks:preDeleteCallback1
79 * </pre>
80 * </blockquote>
82 * <p>
83 * Each type of callback has its own signature requirements for the methods it
84 * annotates. If any of these signature requirements are violated the processor
85 * will produce an error. See the javadoc for the annotations for more
86 * information about the callback-specific signature requirements.
88 * <p>
89 * Processor Options:<ul>
90 * <li>debug - turns on debug statements</li>
91 * </ul>
94 @SupportedAnnotationTypes({
95 "com.google.appengine.api.datastore.PrePut",
96 "com.google.appengine.api.datastore.PostPut",
97 "com.google.appengine.api.datastore.PreDelete",
98 "com.google.appengine.api.datastore.PostDelete",
99 "com.google.appengine.api.datastore.PreGet",
100 "com.google.appengine.api.datastore.PostLoad",
101 "com.google.appengine.api.datastore.PreQuery"})
102 @SupportedOptions({"debug"})
103 public class DatastoreCallbacksProcessor extends AbstractProcessor {
105 private static final String CALLBACKS_CONFIG_FILE = "META-INF/datastorecallbacks.xml";
107 private final OutputStream configOutputStream;
109 public DatastoreCallbacksProcessor() {
110 this(null);
113 @VisibleForTesting
114 DatastoreCallbacksProcessor(OutputStream configOutputStream) {
115 this.configOutputStream = configOutputStream;
119 * Internal interface describing an object that knows how to perform
120 * callback-specific verifications.
122 interface CallbackVerifier {
123 void verify(ExecutableElement annotatedMethod);
127 * Abstract {@link CallbackVerifier} implementation containing state and
128 * functionality that is common to all types of callbacks.
130 abstract class BaseCallbackVerifier implements CallbackVerifier {
131 final Class<? extends Annotation> callbackType;
133 BaseCallbackVerifier(Class<? extends Annotation> callbackType) {
134 this.callbackType = callbackType;
137 void verifySingleParamIsOfProperType(ExecutableElement annotatedMethod,
138 Class<? extends CallbackContext<?>> expectedType) {
139 if (annotatedMethod.getParameters().size() != 1 ||
140 !annotatedMethod.getParameters().get(0).asType().equals(getTypeMirror(expectedType))) {
141 error(String.format("%s method must have a single argument of type '%s'.",
142 callbackType.getSimpleName(), expectedType.getName()), annotatedMethod);
148 * Verifier for callbacks that take a single argument that extends
149 * {@link CallbackContext}.
151 class SingleParamCallbackVerifier extends BaseCallbackVerifier {
152 private final Class<? extends CallbackContext<?>> callbackContextClass;
154 SingleParamCallbackVerifier(Class<? extends Annotation> callbackType,
155 Class<? extends CallbackContext<?>> callbackContextClass) {
156 super(callbackType);
157 this.callbackContextClass = callbackContextClass;
160 @Override
161 public void verify(ExecutableElement annotatedMethod) {
162 verifySingleParamIsOfProperType(annotatedMethod, callbackContextClass);
167 * Keeps track of the callbacks we encounter and writes them out in the
168 * appropriate format once processing is complete.
170 private DatastoreCallbacksConfigWriter callbacksConfigWriter;
173 * Used to avoid performing class-level validation more than once.
175 private final Set<Element> verifiedClasses = Sets.newHashSet();
177 final Map<Class<? extends Annotation>, CallbackVerifier> callbackVerifiers =
178 new ImmutableMap.Builder<Class<? extends Annotation>, CallbackVerifier>()
179 .put(PrePut.class, new SingleParamCallbackVerifier(PrePut.class, PutContext.class))
180 .put(PostPut.class, new SingleParamCallbackVerifier(PostPut.class, PutContext.class))
181 .put(PreDelete.class, new SingleParamCallbackVerifier(
182 PreDelete.class, DeleteContext.class))
183 .put(PostDelete.class, new SingleParamCallbackVerifier(
184 PostDelete.class, DeleteContext.class))
185 .put(PreGet.class, new SingleParamCallbackVerifier(PreGet.class, PreGetContext.class))
186 .put(PostLoad.class, new SingleParamCallbackVerifier(
187 PostLoad.class, PostLoadContext.class))
188 .put(PreQuery.class, new SingleParamCallbackVerifier(
189 PreQuery.class, PreQueryContext.class)).build();
191 @Override
192 public SourceVersion getSupportedSourceVersion() {
193 return SourceVersion.latestSupported();
196 @Override
197 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
198 try {
199 return processImpl(annotations, roundEnv);
200 } catch (Exception e) {
201 error(Throwables.getStackTraceAsString(e), null);
202 return true;
206 private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
207 if (callbacksConfigWriter == null) {
208 loadCallbacksConfigWriter();
210 if (roundEnv.processingOver()) {
211 if (roundEnv.errorRaised()) {
212 log("Not writing config file due to errors.");
213 } else {
214 generateConfigFiles();
216 } else {
217 processAnnotations(annotations, roundEnv);
219 return true;
222 private void loadCallbacksConfigWriter() {
223 try {
224 FileObject existingFile = processingEnv.getFiler().getResource(
225 StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
226 InputStream inputStream = null;
227 if (existingFile != null) {
228 try {
229 inputStream = existingFile.openInputStream();
230 } catch (IOException e) {
233 callbacksConfigWriter = new DatastoreCallbacksConfigWriter(inputStream);
234 if (inputStream != null) {
235 inputStream.close();
237 } catch (IOException e) {
238 throw new RuntimeException(String.format("Unable to read %s", CALLBACKS_CONFIG_FILE), e);
242 OutputStream getConfigOutputStream() throws IOException {
243 if (configOutputStream != null) {
244 return configOutputStream;
246 Filer filer = processingEnv.getFiler();
247 FileObject fileObject =
248 filer.createResource(StandardLocation.CLASS_OUTPUT, "", CALLBACKS_CONFIG_FILE);
249 return fileObject.openOutputStream();
252 private void generateConfigFiles() {
253 try {
254 OutputStream outputStream = null;
255 try {
256 outputStream = getConfigOutputStream();
257 callbacksConfigWriter.store(outputStream);
258 log("Wrote config: " + callbacksConfigWriter);
259 } finally {
260 if (outputStream != null) {
261 outputStream.close();
264 } catch (IOException e) {
265 throw new RuntimeException(String.format("Unable to create %s", CALLBACKS_CONFIG_FILE), e);
269 private void processAnnotations(
270 Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
271 for (TypeElement annotationElement : annotations) {
272 Set<? extends Element> annotatedMethods =
273 roundEnv.getElementsAnnotatedWith(annotationElement);
274 for (Element annotatedMethod : annotatedMethods) {
275 String enclosingClass =
276 getBinaryName(((TypeElement) annotatedMethod.getEnclosingElement()));
277 String method = annotatedMethod.getSimpleName().toString();
278 Set<String> kinds = verifyCallback((ExecutableElement) annotatedMethod, annotationElement,
279 enclosingClass, method);
280 if (!roundEnv.errorRaised()) {
281 callbacksConfigWriter.addCallback(kinds, annotationElement.getSimpleName().toString(),
282 enclosingClass, method);
289 * Extract the value of the kind attribute from the given annotation mirror.
290 * Return {@code null} if we encountered errors while validating the kinds.
291 * An empty set indicates that the callback applies to entities of all kinds.
293 @SuppressWarnings("unchecked")
294 private Set<String> extractKindsFromCallbackAnnotation(Element annotatedMethod,
295 AnnotationMirror annotationMirror) {
296 Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValueMap =
297 processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror);
298 Set<String> kinds = Sets.newLinkedHashSet();
299 for (ExecutableElement annotationParamName : annotationValueMap.keySet()) {
300 if (annotationParamName.getSimpleName().toString().equals("kinds")) {
301 Object value = annotationValueMap.get(annotationParamName).getValue();
302 if (value instanceof String) {
303 addKind((String) value, annotatedMethod, kinds);
304 } else {
305 for (AnnotationValue av : (List<AnnotationValue>) value) {
306 addKind(av.getValue().toString(), annotatedMethod, kinds);
309 break;
312 return kinds;
316 * Perform validation on the provided kind and, if it passes, add it to the
317 * given {@link Set}.
319 private void addKind(String kind, Element annotatedMethod, Set<String> kinds) {
320 kind = kind.trim();
321 if (kind.isEmpty()) {
322 error("A callback cannot be associated with an empty kind.", annotatedMethod);
323 } else {
324 kinds.add(kind);
328 * Verifies constraints on the callback method and class.
330 private Set<String> verifyCallback(ExecutableElement annotatedMethod,
331 TypeElement annotationElement, String cls, String method) {
332 Element classElement = annotatedMethod.getEnclosingElement();
333 if (verifiedClasses.add(classElement)) {
334 boolean hasNoArgConstructor = false;
335 for (ExecutableElement ctor : ElementFilter.constructorsIn(
336 classElement.getEnclosedElements())) {
337 if (ctor.getParameters().isEmpty()) {
338 hasNoArgConstructor = true;
339 break;
342 if (!hasNoArgConstructor) {
343 error("A class with a callback method must have a no-arg constructor.", classElement);
347 if (callbacksConfigWriter.hasCallback(cls, method)) {
348 error("Method can only have one callback annotation.", annotatedMethod);
350 if (annotatedMethod.getModifiers().contains(Modifier.STATIC)) {
351 error("Callback method must not be static.", annotatedMethod);
354 if (!annotatedMethod.getReturnType().getKind().equals(TypeKind.VOID)) {
355 error("Return type of callback method must be void.", annotatedMethod);
358 for (TypeMirror typeMirror : annotatedMethod.getThrownTypes()) {
359 if (!isSubTypeOfOneOf(typeMirror, RuntimeException.class, Error.class)) {
360 error("Callback methods cannot throw checked exceptions.", annotatedMethod);
364 AnnotationMirror annotationMirror = getAnnotationMirror(annotatedMethod, annotationElement);
365 Set<String> kinds = extractKindsFromCallbackAnnotation(annotatedMethod, annotationMirror);
367 CallbackVerifier verifier;
368 try {
369 verifier = callbackVerifiers.get(
370 Class.forName(annotationElement.getQualifiedName().toString()));
371 } catch (ClassNotFoundException e) {
372 throw new RuntimeException(e);
374 if (verifier == null) {
375 throw new RuntimeException(
376 "No verifier registered for " + annotationElement.getQualifiedName());
378 verifier.verify(annotatedMethod);
379 return kinds;
382 private boolean isSubTypeOfOneOf(TypeMirror typeMirror, Class<?>... classes) {
383 for (Class<?> cls : classes) {
384 if (processingEnv.getTypeUtils().isSubtype(typeMirror, getTypeMirror(cls))) {
385 return true;
388 return false;
392 * Returns the binary name of a reference type. For example,
393 * {@code com.google.Foo$Bar}, instead of {@code com.google.Foo.Bar}.
395 private String getBinaryName(TypeElement element) {
396 return getBinaryNameImpl(element, element.getSimpleName().toString());
399 private String getBinaryNameImpl(Element element, String className) {
400 Element enclosingElement = element.getEnclosingElement();
402 if (enclosingElement instanceof PackageElement) {
403 PackageElement pkg = (PackageElement) enclosingElement;
404 if (pkg.isUnnamed()) {
405 return className;
407 return pkg.getQualifiedName() + "." + className;
410 return getBinaryNameImpl(enclosingElement, enclosingElement.getSimpleName() + "$" + className);
413 private AnnotationMirror getAnnotationMirror(Element annotatedMethod,
414 final TypeElement annotationElement) {
415 return Iterables.find(annotatedMethod.getAnnotationMirrors(),
416 new Predicate<AnnotationMirror>() {
417 @Override
418 public boolean apply(AnnotationMirror mirror) {
419 log("mirror: " + mirror);
420 DeclaredType type = mirror.getAnnotationType();
421 TypeElement typeElement = (TypeElement) type.asElement();
422 return typeElement.getQualifiedName().contentEquals(
423 annotationElement.getQualifiedName());
428 private void log(String msg) {
429 if (processingEnv.getOptions().containsKey("debug")) {
430 processingEnv.getMessager().printMessage(Kind.NOTE, "Datastore Callbacks: " + msg);
434 private void error(String msg, Element element) {
435 processingEnv.getMessager().printMessage(
436 Kind.ERROR, "Datastore Callbacks: " + msg, element);
439 private TypeMirror getTypeMirror(Class<?> cls) {
440 return processingEnv.getElementUtils().getTypeElement(cls.getName()).asType();