App Engine Java SDK version 1.7.0
[gae.git] / java / src / main / com / google / appengine / api / datastore / DatastoreCallbacksImpl.java
blobd41200afeb6851918dc4646383c1b5cb15385c8b
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;
17 import java.util.Map;
18 import java.util.Properties;
20 /**
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";
29 /**
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.
34 enum CallbackType {
35 /**
36 * @see PrePut
38 PrePut(PutContext.class),
40 /**
41 * @see PostPut
43 PostPut(PutContext.class),
45 /**
46 * @see PreDelete
48 PreDelete(DeleteContext.class),
50 /**
51 * @see PostDelete
53 PostDelete(DeleteContext.class),
55 /**
56 * @see PreGet
58 PreGet(PreGetContext.class),
60 /**
61 * @see PostLoad
63 PostLoad(PostLoadContext.class),
65 /**
66 * @see PreQuery
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;
76 try {
77 this.annotationType = (Class<? extends Annotation>) Class.forName(
78 getClass().getPackage().getName() + "." + this.name());
79 } catch (ClassNotFoundException e) {
80 throw new IllegalArgumentException(e);
85 /**
86 * Interface that we use to wrap a reflective call to the method that
87 * actually implements the callback.
89 interface Callback {
90 void run(CallbackContext<?> context);
93 /**
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 =
100 Maps.newHashMap();
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();
139 try {
140 props.loadFromXML(inputStream);
141 } catch (IOException e) {
142 throw new InvalidCallbacksConfigException(
143 "Unable to read datastore callbacks config file.", e);
145 return props;
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;
161 try {
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) {
184 return;
186 if (kind.isEmpty()) {
187 noKindCallbacksByType.put(callbackType, callback);
188 } else {
189 callbacksByTypeAndKind.get(callbackType).put(kind, callback);
193 @Override
194 public void executePrePutCallbacks(PutContext context) {
195 executeCallbacks(CallbackType.PrePut, context);
198 @Override
199 public void executePostPutCallbacks(PutContext context) {
200 executeCallbacks(CallbackType.PostPut, context);
203 @Override
204 public void executePreDeleteCallbacks(DeleteContext context) {
205 executeCallbacks(CallbackType.PreDelete, context);
208 @Override
209 public void executePostDeleteCallbacks(DeleteContext context) {
210 executeCallbacks(CallbackType.PostDelete, context);
213 @Override
214 public void executePreGetCallbacks(PreGetContext context) {
215 executeCallbacks(CallbackType.PreGet, context);
218 @Override
219 public void executePostLoadCallbacks(PostLoadContext context) {
220 executeCallbacks(CallbackType.PostLoad, context);
223 @Override
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
241 * annotated method.
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
249 * exist.
251 private Callback newCallback(CallbackType callbackType, String className, String methodName,
252 Class<? extends CallbackContext<?>> contextClass, boolean ignoreMissingMethods) {
253 try {
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 "
260 + "annotation %s.",
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);
276 return null;
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) {
292 Constructor<?> ctor;
293 try {
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);
301 try {
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() {
312 @Override
313 public void run(CallbackContext<?> context) {
314 try {
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) {
332 super(msg);