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
;
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
;
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:
70 * yar.PrePut=com.example.MyCallbacks:prePutCallback1,com.example.MyCallbacks:prePutCallback2
74 * Note that it is possible to have a line which refers to all kinds by
75 * omitting the kind name:
78 * .PreDelete=com.example.MyCallbacks:preDeleteCallback1
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.
89 * Processor Options:<ul>
90 * <li>debug - turns on debug statements</li>
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() {
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
) {
157 this.callbackContextClass
= callbackContextClass
;
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();
192 public SourceVersion
getSupportedSourceVersion() {
193 return SourceVersion
.latestSupported();
197 public boolean process(Set
<?
extends TypeElement
> annotations
, RoundEnvironment roundEnv
) {
199 return processImpl(annotations
, roundEnv
);
200 } catch (Exception e
) {
201 error(Throwables
.getStackTraceAsString(e
), null);
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.");
214 generateConfigFiles();
217 processAnnotations(annotations
, roundEnv
);
222 private void loadCallbacksConfigWriter() {
224 FileObject existingFile
= processingEnv
.getFiler().getResource(
225 StandardLocation
.CLASS_OUTPUT
, "", CALLBACKS_CONFIG_FILE
);
226 InputStream inputStream
= null;
227 if (existingFile
!= null) {
229 inputStream
= existingFile
.openInputStream();
230 } catch (IOException e
) {
233 callbacksConfigWriter
= new DatastoreCallbacksConfigWriter(inputStream
);
234 if (inputStream
!= null) {
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() {
254 OutputStream outputStream
= null;
256 outputStream
= getConfigOutputStream();
257 callbacksConfigWriter
.store(outputStream
);
258 log("Wrote config: " + callbacksConfigWriter
);
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
);
305 for (AnnotationValue av
: (List
<AnnotationValue
>) value
) {
306 addKind(av
.getValue().toString(), annotatedMethod
, kinds
);
316 * Perform validation on the provided kind and, if it passes, add it to the
319 private void addKind(String kind
, Element annotatedMethod
, Set
<String
> kinds
) {
321 if (kind
.isEmpty()) {
322 error("A callback cannot be associated with an empty kind.", annotatedMethod
);
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;
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
;
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
);
382 private boolean isSubTypeOfOneOf(TypeMirror typeMirror
, Class
<?
>... classes
) {
383 for (Class
<?
> cls
: classes
) {
384 if (processingEnv
.getTypeUtils().isSubtype(typeMirror
, getTypeMirror(cls
))) {
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()) {
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
>() {
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();