1 // Copyright 2011 Google Inc. All Rights Reserved.
2 package com
.google
.appengine
.api
.datastore
;
4 import com
.google
.common
.base
.Splitter
;
5 import com
.google
.common
.base
.Throwables
;
6 import com
.google
.common
.collect
.LinkedHashMultimap
;
7 import com
.google
.common
.collect
.Maps
;
8 import com
.google
.common
.collect
.Multimap
;
10 import java
.io
.IOException
;
11 import java
.io
.InputStream
;
12 import java
.lang
.annotation
.Annotation
;
13 import java
.lang
.reflect
.Constructor
;
14 import java
.lang
.reflect
.InvocationTargetException
;
15 import java
.lang
.reflect
.Method
;
16 import java
.util
.List
;
18 import java
.util
.Properties
;
21 * {@link DatastoreCallbacks} implementation that knows how to parse a datastore
22 * callbacks config file.
25 class DatastoreCallbacksImpl
implements DatastoreCallbacks
{
27 static final String FORMAT_VERSION_PROPERTY
= "DatastoreCallbacksFormatVersion";
30 * The types of the callbacks we support. There must be one enum value per
31 * callback annotation, and each value must be the simple name of one of
32 * these annotation classes.
38 PrePut(PutContext
.class),
43 PostPut(PutContext
.class),
48 PreDelete(DeleteContext
.class),
53 PostDelete(DeleteContext
.class),
58 PreGet(PreGetContext
.class),
63 PostLoad(PostLoadContext
.class),
68 PreQuery(PreQueryContext
.class);
70 final Class
<?
extends CallbackContext
<?
>> contextClass
;
71 final Class
<?
extends Annotation
> annotationType
;
73 @SuppressWarnings("unchecked")
74 CallbackType(Class
<?
extends CallbackContext
<?
>> contextClass
) {
75 this.contextClass
= contextClass
;
77 this.annotationType
= (Class
<?
extends Annotation
>) Class
.forName(
78 getClass().getPackage().getName() + "." + this.name());
79 } catch (ClassNotFoundException e
) {
80 throw new IllegalArgumentException(e
);
86 * Interface that we use to wrap a reflective call to the method that
87 * actually implements the callback.
90 void run(CallbackContext
<?
> context
);
94 * Key is the kind (possibly the empty string), value is a {@link Map} where
95 * the key is the {@link CallbackType} and the value is a {@link List} of
96 * {@link Callback Callbacks}. Given a kind and a {@link CallbackType} we
97 * can quickly navigate to a list of {@link Callback Callbacks} to run.
99 private final Map
<CallbackType
, Multimap
<String
, Callback
>> callbacksByTypeAndKind
=
101 private final Multimap
<CallbackType
, Callback
> noKindCallbacksByType
=
102 LinkedHashMultimap
.create();
105 * Constructs DatastoreCallbacksImpl from a config in the appropriate format.
107 * @param inputStream Provides the config.
108 * @param ignoreMissingMethods If {@code true}, methods that are referenced
109 * in the config that do not exist will be ignored. If {@code false},
110 * methods that are referenced in the config that do not exist will generate
111 * an {@link InvalidCallbacksConfigException}.
113 DatastoreCallbacksImpl(InputStream inputStream
, boolean ignoreMissingMethods
) {
114 if (inputStream
== null) {
115 throw new NullPointerException("inputStream must not be null");
117 Properties props
= loadProperties(inputStream
);
119 if (!"1".equals(props
.get(FORMAT_VERSION_PROPERTY
))) {
120 throw new IllegalArgumentException("Unsupported version for datastore callbacks config: " +
121 props
.get(FORMAT_VERSION_PROPERTY
));
123 for (CallbackType callbackType
: CallbackType
.values()) {
124 callbacksByTypeAndKind
.put(callbackType
, LinkedHashMultimap
.<String
, Callback
>create());
126 for (String key
: props
.stringPropertyNames()) {
127 if (!key
.equals(FORMAT_VERSION_PROPERTY
)) {
128 processCallbackWithKey(key
, props
, ignoreMissingMethods
);
134 * Loads a {@link Properties} object from the contents of the provided
135 * {@link InputStream}.
137 private Properties
loadProperties(InputStream inputStream
) {
138 Properties props
= new Properties();
140 props
.loadFromXML(inputStream
);
141 } catch (IOException e
) {
142 throw new InvalidCallbacksConfigException(
143 "Unable to read datastore callbacks config file.", e
);
149 * Processes the callback in the provided {@link Properties} identified by the
150 * provided {@code key}.
152 private void processCallbackWithKey(String key
, Properties props
, boolean ignoreMissingMethods
) {
154 String
[] kindCallbackTypePair
= key
.split("\\.(?!.*\\.)");
155 if (kindCallbackTypePair
.length
!= 2) {
156 throw new InvalidCallbacksConfigException(String
.format(
157 "Could not extract kind and callback type from '%s'", key
));
159 String kind
= kindCallbackTypePair
[0];
160 CallbackType callbackType
;
162 callbackType
= CallbackType
.valueOf(kindCallbackTypePair
[1]);
163 } catch (IllegalArgumentException iae
) {
164 throw new InvalidCallbacksConfigException(String
.format(
165 "Received unknown callback type %s", kindCallbackTypePair
[1]));
167 String value
= props
.getProperty(key
);
169 for (String method
: Splitter
.on(',').trimResults().split(value
)) {
170 String
[] classMethodPair
= method
.split(":");
171 if (classMethodPair
.length
!= 2) {
172 throw new InvalidCallbacksConfigException(String
.format(
173 "Could not extract fully-qualified classname and method from '%s'", method
));
175 addCallback(callbackType
, kind
, classMethodPair
[0], classMethodPair
[1], ignoreMissingMethods
);
179 private void addCallback(CallbackType callbackType
, String kind
, String className
,
180 String methodName
, boolean ignoreMissingMethods
) {
181 Callback callback
= newCallback(
182 callbackType
, className
, methodName
, callbackType
.contextClass
, ignoreMissingMethods
);
183 if (callback
== null) {
186 if (kind
.isEmpty()) {
187 noKindCallbacksByType
.put(callbackType
, callback
);
189 callbacksByTypeAndKind
.get(callbackType
).put(kind
, callback
);
194 public void executePrePutCallbacks(PutContext context
) {
195 executeCallbacks(CallbackType
.PrePut
, context
);
199 public void executePostPutCallbacks(PutContext context
) {
200 executeCallbacks(CallbackType
.PostPut
, context
);
204 public void executePreDeleteCallbacks(DeleteContext context
) {
205 executeCallbacks(CallbackType
.PreDelete
, context
);
209 public void executePostDeleteCallbacks(DeleteContext context
) {
210 executeCallbacks(CallbackType
.PostDelete
, context
);
214 public void executePreGetCallbacks(PreGetContext context
) {
215 executeCallbacks(CallbackType
.PreGet
, context
);
219 public void executePostLoadCallbacks(PostLoadContext context
) {
220 executeCallbacks(CallbackType
.PostLoad
, context
);
224 public void executePreQueryCallbacks(PreQueryContext context
) {
225 executeCallbacks(CallbackType
.PreQuery
, context
);
228 private <T
> void executeCallbacks(CallbackType callbackType
, BaseCallbackContext
<T
> context
) {
229 context
.executeCallbacks(
230 callbacksByTypeAndKind
.get(callbackType
), noKindCallbacksByType
.get(callbackType
));
234 * Instantiates a callback of the appropriate type that, when executed,
235 * invokes the method with the given name on the given object.
237 * @param className Fully-qualified name of the class with a method that
238 * was annotated as a callback.
239 * @param methodName The name of the annotated method.
240 * @param contextClass The type of the single argument expected by the
242 * @param ignoreMissingMethods If {@code true}, methods that are referenced
243 * in the config that do not exist will be ignored. If {@code false},
244 * methods that are referenced in the config that do not exist will generate
245 * an {@link InvalidCallbacksConfigException}.
247 * @return A {@link Callback} of the appropriate concrete type, or {@code null}
248 * if {@code ignoreMissingMethods} is {@code true} and the method does not
251 private Callback
newCallback(CallbackType callbackType
, String className
, String methodName
,
252 Class
<?
extends CallbackContext
<?
>> contextClass
, boolean ignoreMissingMethods
) {
254 Class
<?
> cls
= loadClass(className
);
255 Method m
= cls
.getDeclaredMethod(methodName
, contextClass
);
256 m
.setAccessible(true);
257 if (m
.getAnnotation(callbackType
.annotationType
) == null) {
258 throw new InvalidCallbacksConfigException(String
.format(
259 "Unable to initialize datastore callbacks because method %s.%s(%s) is missing "
261 cls
.getName(), methodName
, contextClass
.getName(), callbackType
));
263 Object callbackImplementor
= newInstance(cls
);
264 return allocateCallback(callbackImplementor
, m
);
265 } catch (ClassNotFoundException e
) {
266 if (!ignoreMissingMethods
) {
267 throw new InvalidCallbacksConfigException(
268 "Unable to initialize datastore callbacks due to missing class.", e
);
270 } catch (NoSuchMethodException e
) {
271 if (!ignoreMissingMethods
) {
272 throw new InvalidCallbacksConfigException(
273 "Unable to initialize datastore callbacks because of reference to missing method.", e
);
280 * Loads the class identified by the provided fully-qualified classname using
281 * the current thread's context classloader.
283 private static Class
<?
> loadClass(String className
) throws ClassNotFoundException
{
284 return Thread
.currentThread().getContextClassLoader().loadClass(className
);
288 * Constructs and returns an instance of the given class by locating and
289 * invoking its no-arg constructor.
291 private static Object
newInstance(Class
<?
> cls
) {
294 ctor
= cls
.getDeclaredConstructor();
295 } catch (NoSuchMethodException e
) {
296 throw new InvalidCallbacksConfigException(String
.format(
297 "Unable to initialize datastore callbacks because class %s does not have a no-arg "
298 + "constructor.", cls
.getName()), e
);
300 ctor
.setAccessible(true);
302 return ctor
.newInstance();
303 } catch (Exception e
) {
304 throw new InvalidCallbacksConfigException(String
.format(
305 "Unable to initialize datastore callbacks due to exception received while constructing "
306 + "an instance of %s", cls
.getName()), e
);
310 private Callback
allocateCallback(final Object callbackImplementor
, final Method callbackMethod
) {
311 return new Callback() {
313 public void run(CallbackContext
<?
> context
) {
315 callbackMethod
.invoke(callbackImplementor
, context
);
316 } catch (IllegalAccessException e
) {
317 throw new RuntimeException(e
);
318 } catch (InvocationTargetException e
) {
319 Throwables
.propagateIfPossible(e
.getCause());
320 throw new RuntimeException("Callback method threw a checked exception.", e
.getCause());
326 static class InvalidCallbacksConfigException
extends RuntimeException
{
327 InvalidCallbacksConfigException(String msg
, Throwable throwable
) {
328 super(msg
, throwable
);
331 InvalidCallbacksConfigException(String msg
) {