15
15
package com .google .cloud .functions .invoker ;
16
16
17
17
import static java .nio .charset .StandardCharsets .UTF_8 ;
18
+ import static java .util .stream .Collectors .toList ;
18
19
import static java .util .stream .Collectors .toMap ;
19
20
20
21
import com .google .cloud .functions .BackgroundFunction ;
21
22
import com .google .cloud .functions .Context ;
23
+ import com .google .cloud .functions .ExperimentalCloudEventsFunction ;
22
24
import com .google .cloud .functions .RawBackgroundFunction ;
23
25
import com .google .gson .Gson ;
24
26
import com .google .gson .GsonBuilder ;
@@ -54,20 +56,63 @@ private BackgroundFunctionExecutor(FunctionExecutor<?> functionExecutor) {
54
56
this .functionExecutor = functionExecutor ;
55
57
}
56
58
59
+ private enum FunctionKind {
60
+ BACKGROUND (BackgroundFunction .class ),
61
+ RAW_BACKGROUND (RawBackgroundFunction .class ),
62
+ CLOUD_EVENTS (ExperimentalCloudEventsFunction .class );
63
+
64
+ static final List <FunctionKind > VALUES = Arrays .asList (values ());
65
+
66
+ final Class <?> functionClass ;
67
+
68
+ FunctionKind (Class <?> functionClass ) {
69
+ this .functionClass = functionClass ;
70
+ }
71
+
72
+ /** Returns the {@link FunctionKind} that the given class implements, if any. */
73
+ static Optional <FunctionKind > forClass (Class <?> functionClass ) {
74
+ return VALUES .stream ().filter (v -> v .functionClass .isAssignableFrom (functionClass )).findFirst ();
75
+ }
76
+ }
77
+
57
78
/**
58
- * Makes a {@link HttpFunctionExecutor} for the given class.
79
+ * Optionally makes a {@link BackgroundFunctionExecutor} for the given class, if it implements one
80
+ * of {@link BackgroundFunction}, {@link RawBackgroundFunction}, or
81
+ * {@link ExperimentalCloudEventsFunction}. Otherwise returns {@link Optional#empty()}.
82
+ *
83
+ * @param functionClass the class of a possible background function implementation.
84
+ * @throws RuntimeException if the given class does implement one of the required interfaces, but we are
85
+ * unable to construct an instance using its no-arg constructor.
86
+ */
87
+ public static Optional <BackgroundFunctionExecutor > maybeForClass (Class <?> functionClass ) {
88
+ Optional <FunctionKind > maybeFunctionKind = FunctionKind .forClass (functionClass );
89
+ if (!maybeFunctionKind .isPresent ()) {
90
+ return Optional .empty ();
91
+ }
92
+ return Optional .of (forClass (functionClass , maybeFunctionKind .get ()));
93
+ }
94
+
95
+ /**
96
+ * Makes a {@link BackgroundFunctionExecutor} for the given class.
59
97
*
60
98
* @throws RuntimeException if either the class does not implement one of
61
- * {@link BackgroundFunction} or {@link RawBackgroundFunction},
62
- * or we are unable to construct an instance using its no-arg constructor.
99
+ * {@link BackgroundFunction}, {@link RawBackgroundFunction}, or
100
+ * {@link ExperimentalCloudEventsFunction}; or we are unable to construct an instance using its no-arg
101
+ * constructor.
63
102
*/
64
103
public static BackgroundFunctionExecutor forClass (Class <?> functionClass ) {
65
- if (!BackgroundFunction .class .isAssignableFrom (functionClass )
66
- && !RawBackgroundFunction .class .isAssignableFrom (functionClass )) {
104
+ Optional <FunctionKind > maybeFunctionKind = FunctionKind .forClass (functionClass );
105
+ if (!maybeFunctionKind .isPresent ()) {
106
+ List <String > classNames =
107
+ FunctionKind .VALUES .stream ().map (v -> v .functionClass .getName ()).collect (toList ());
67
108
throw new RuntimeException (
68
- "Class " + functionClass .getName () + " implements neither " + BackgroundFunction . class
69
- . getName () + " nor " + RawBackgroundFunction . class . getName ( ));
109
+ "Class " + functionClass .getName () + " must implement one of these interfaces: "
110
+ + String . join ( ", " , classNames ));
70
111
}
112
+ return forClass (functionClass , maybeFunctionKind .get ());
113
+ }
114
+
115
+ private static BackgroundFunctionExecutor forClass (Class <?> functionClass , FunctionKind functionKind ) {
71
116
Object instance ;
72
117
try {
73
118
instance = functionClass .getConstructor ().newInstance ();
@@ -76,23 +121,31 @@ public static BackgroundFunctionExecutor forClass(Class<?> functionClass) {
76
121
"Could not construct an instance of " + functionClass .getName () + ": " + e , e );
77
122
}
78
123
FunctionExecutor <?> executor ;
79
- if (instance instanceof RawBackgroundFunction ) {
80
- executor = new RawFunctionExecutor ((RawBackgroundFunction ) instance );
81
- } else {
82
- BackgroundFunction <?> backgroundFunction = (BackgroundFunction <?>) instance ;
83
- @ SuppressWarnings ("unchecked" )
84
- Class <? extends BackgroundFunction <?>> c =
85
- (Class <? extends BackgroundFunction <?>>) backgroundFunction .getClass ();
86
- Optional <Type > maybeTargetType = backgroundFunctionTypeArgument (c );
87
- if (!maybeTargetType .isPresent ()) {
88
- // This is probably because the user implemented just BackgroundFunction rather than
89
- // BackgroundFunction<T>.
90
- throw new RuntimeException (
91
- "Could not determine the payload type for BackgroundFunction of type "
92
- + instance .getClass ().getName ()
93
- + "; must implement BackgroundFunction<T> for some T" );
94
- }
95
- executor = new TypedFunctionExecutor <>(maybeTargetType .get (), backgroundFunction );
124
+ switch (functionKind ) {
125
+ case RAW_BACKGROUND :
126
+ executor = new RawFunctionExecutor ((RawBackgroundFunction ) instance );
127
+ break ;
128
+ case BACKGROUND :
129
+ BackgroundFunction <?> backgroundFunction = (BackgroundFunction <?>) instance ;
130
+ @ SuppressWarnings ("unchecked" )
131
+ Class <? extends BackgroundFunction <?>> c =
132
+ (Class <? extends BackgroundFunction <?>>) backgroundFunction .getClass ();
133
+ Optional <Type > maybeTargetType = backgroundFunctionTypeArgument (c );
134
+ if (!maybeTargetType .isPresent ()) {
135
+ // This is probably because the user implemented just BackgroundFunction rather than
136
+ // BackgroundFunction<T>.
137
+ throw new RuntimeException (
138
+ "Could not determine the payload type for BackgroundFunction of type "
139
+ + instance .getClass ().getName ()
140
+ + "; must implement BackgroundFunction<T> for some T" );
141
+ }
142
+ executor = new TypedFunctionExecutor <>(maybeTargetType .get (), backgroundFunction );
143
+ break ;
144
+ case CLOUD_EVENTS :
145
+ executor = new CloudEventFunctionExecutor ((ExperimentalCloudEventsFunction ) instance );
146
+ break ;
147
+ default : // can't happen, we've listed all the FunctionKind values already.
148
+ throw new AssertionError (functionKind );
96
149
}
97
150
return new BackgroundFunctionExecutor (executor );
98
151
}
@@ -177,12 +230,9 @@ final ClassLoader functionClassLoader() {
177
230
return functionClass .getClassLoader ();
178
231
}
179
232
180
- abstract void serviceLegacyEvent (HttpServletRequest req )
181
- throws Exception ;
233
+ abstract void serviceLegacyEvent (Event legacyEvent ) throws Exception ;
182
234
183
235
abstract void serviceCloudEvent (CloudEvent cloudEvent ) throws Exception ;
184
-
185
- abstract Class <CloudEventDataT > cloudEventDataType ();
186
236
}
187
237
188
238
private static class RawFunctionExecutor extends FunctionExecutor <Map <?, ?>> {
@@ -194,9 +244,8 @@ private static class RawFunctionExecutor extends FunctionExecutor<Map<?, ?>> {
194
244
}
195
245
196
246
@ Override
197
- void serviceLegacyEvent (HttpServletRequest req ) throws Exception {
198
- Event event = parseLegacyEvent (req );
199
- function .accept (new Gson ().toJson (event .getData ()), event .getContext ());
247
+ void serviceLegacyEvent (Event legacyEvent ) throws Exception {
248
+ function .accept (new Gson ().toJson (legacyEvent .getData ()), legacyEvent .getContext ());
200
249
}
201
250
202
251
@ Override
@@ -205,15 +254,6 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
205
254
String jsonData = cloudEvent .getData () == null ? "{}" : new String (cloudEvent .getData (), UTF_8 );
206
255
function .accept (jsonData , context );
207
256
}
208
-
209
- @ Override
210
- Class <Map <?, ?>> cloudEventDataType () {
211
- // This messing about with casts and @SuppressWarnings allows us to limit the use of the raw
212
- // Map type to just here.
213
- @ SuppressWarnings ("unchecked" )
214
- Class <Map <?, ?>> c = (Class <Map <?, ?>>) (Class <?>) Map .class ;
215
- return c ;
216
- }
217
257
}
218
258
219
259
private static class TypedFunctionExecutor <T > extends FunctionExecutor <T > {
@@ -233,10 +273,9 @@ static <T> TypedFunctionExecutor<T> of(Type type, BackgroundFunction<?> instance
233
273
}
234
274
235
275
@ Override
236
- void serviceLegacyEvent (HttpServletRequest req ) throws Exception {
237
- Event event = parseLegacyEvent (req );
238
- T payload = new Gson ().fromJson (event .getData (), type );
239
- function .accept (payload , event .getContext ());
276
+ void serviceLegacyEvent (Event legacyEvent ) throws Exception {
277
+ T payload = new Gson ().fromJson (legacyEvent .getData (), type );
278
+ function .accept (payload , legacyEvent .getContext ());
240
279
}
241
280
242
281
@ Override
@@ -250,27 +289,33 @@ void serviceCloudEvent(CloudEvent cloudEvent) throws Exception {
250
289
throw new IllegalStateException ("Event has no \" data\" component" );
251
290
}
252
291
}
292
+ }
293
+
294
+ private static class CloudEventFunctionExecutor extends FunctionExecutor <Void >{
295
+ private final ExperimentalCloudEventsFunction function ;
296
+
297
+ CloudEventFunctionExecutor (ExperimentalCloudEventsFunction function ) {
298
+ super (function .getClass ());
299
+ this .function = function ;
300
+ }
253
301
254
302
@ Override
255
- Class <T > cloudEventDataType () {
256
- if (!(type instanceof Class <?>)) {
257
- throw new IllegalStateException (
258
- "CloudEvents SDK currently does not permit deserializing types other than classes:"
259
- + " cannot deserialize " + type );
260
- }
261
- @ SuppressWarnings ("unchecked" )
262
- Class <T > c = (Class <T >) type ;
263
- return c ;
303
+ void serviceLegacyEvent (Event legacyEvent ) throws Exception {
304
+ throw new UnsupportedOperationException (
305
+ "Conversion from legacy events to CloudEvents not yet implemented" );
306
+ }
307
+
308
+ @ Override
309
+ void serviceCloudEvent (CloudEvent cloudEvent ) throws Exception {
310
+ function .accept (cloudEvent );
264
311
}
265
312
}
266
313
267
314
/** Executes the user's background function. This can handle all HTTP methods. */
268
315
@ Override
269
316
public void service (HttpServletRequest req , HttpServletResponse res ) throws IOException {
270
317
String contentType = req .getContentType ();
271
- ClassLoader oldContextLoader = Thread .currentThread ().getContextClassLoader ();
272
318
try {
273
- Thread .currentThread ().setContextClassLoader (functionExecutor .functionClassLoader ());
274
319
if ((contentType != null && contentType .startsWith ("application/cloudevents+json" ))
275
320
|| req .getHeader ("ce-specversion" ) != null ) {
276
321
serviceCloudEvent (req );
@@ -281,8 +326,6 @@ public void service(HttpServletRequest req, HttpServletResponse res) throws IOEx
281
326
} catch (Throwable t ) {
282
327
res .setStatus (HttpServletResponse .SC_INTERNAL_SERVER_ERROR );
283
328
logger .log (Level .WARNING , "Failed to execute " + functionExecutor .functionName (), t );
284
- } finally {
285
- Thread .currentThread ().setContextClassLoader (oldContextLoader );
286
329
}
287
330
}
288
331
@@ -306,10 +349,32 @@ private <CloudEventT> void serviceCloudEvent(HttpServletRequest req) throws Exce
306
349
() -> headers .getOrDefault ("ce-specversion" , listOfNull ).get (0 ),
307
350
unusedSpecVersion -> CloudEventsServletBinaryMessageReader .from (req , body ),
308
351
UnknownEncodingMessageReader ::new );
309
- executor .serviceCloudEvent (reader .toEvent ());
352
+ // It's important not to set the context ClassLoader earlier, because MessageUtils will use
353
+ // ServiceLoader.load(EventFormat.class) to find a handler to deserialize a binary CloudEvent
354
+ // and if it finds something from the function ClassLoader then that something will implement
355
+ // the EventFormat interface as defined by that ClassLoader rather than ours. Then ServiceLoader.load
356
+ // will throw ServiceConfigurationError. At this point we're still running with the default
357
+ // context ClassLoader, which is the system ClassLoader that has loaded the code here.
358
+ runWithContextClassLoader (() -> executor .serviceCloudEvent (reader .toEvent ()));
310
359
}
311
360
312
361
private void serviceLegacyEvent (HttpServletRequest req ) throws Exception {
313
- functionExecutor .serviceLegacyEvent (req );
362
+ Event event = parseLegacyEvent (req );
363
+ runWithContextClassLoader (() -> functionExecutor .serviceLegacyEvent (event ));
364
+ }
365
+
366
+ private void runWithContextClassLoader (ContextClassLoaderTask task ) throws Exception {
367
+ ClassLoader oldLoader = Thread .currentThread ().getContextClassLoader ();
368
+ try {
369
+ Thread .currentThread ().setContextClassLoader (functionExecutor .functionClassLoader ());
370
+ task .run ();
371
+ } finally {
372
+ Thread .currentThread ().setContextClassLoader (oldLoader );
373
+ }
374
+ }
375
+
376
+ @ FunctionalInterface
377
+ private interface ContextClassLoaderTask {
378
+ void run () throws Exception ;
314
379
}
315
380
}
0 commit comments