-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathStdCallbackContext.java
More file actions
355 lines (319 loc) · 15.1 KB
/
Copy pathStdCallbackContext.java
File metadata and controls
355 lines (319 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package software.amazon.cloudformation.proxy;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.concurrent.ThreadSafe;
/**
* StdCallbackContext provide a mechanism that automatically provides the
* memoization for retention and callback of request, responses, stabilize
* handles during handler invocations. During replay callbacks, this
* automatically replays responses for different calls along the call graph
* ensuring that we only execute the portions of the call graph that needs
* execution and dedupe calls as needed.
*
* This is not a sophisticated class that does request inspection based call
* result, it is primarily a function result memoization that is ensured that it
* is invoked once. Attempts to call the function multiple times with different
* arguments will yield the same result for the same call graph key for
* {@link StdCallbackContext#request(String, Function)} and
* {@link StdCallbackContext#response(String, BiFunction)}. For
* {@link StdCallbackContext#stabilize(String, CallChain.Callback)}, only when
* True is returned it is memoized.
*/
@ThreadSafe
@lombok.EqualsAndHashCode
@lombok.ToString
public class StdCallbackContext {
public static class Serializer extends JsonSerializer<Map<String, Object>> {
@Override
public void serialize(Map<String, Object> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
writeMap(value, gen, serializers);
}
@SuppressWarnings("unchecked")
private void writeObject(Object val, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (val == null) {
gen.writeNull();
return;
}
// Primitive
if (val instanceof String || val instanceof Number || val instanceof Boolean) {
gen.writeObject(val);
return;
}
// Encode object type information
gen.writeStartArray();
Class<?> type = val.getClass();
// write class name first
gen.writeString(type.getName());
// the write value next
if (val instanceof Collection<?>) {
writeCollection((Collection<?>) val, gen, serializers);
} else if (val instanceof Map<?, ?>) {
Map<?, ?> map = (Map<?, ?>) val;
writeMap(map, gen, serializers);
} else {
JsonSerializer<Object> serializer = serializers.findValueSerializer(type);
serializer.serialize(val, gen, serializers);
}
// end marker
gen.writeEndArray();
}
private void writeCollection(Collection<?> collection, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeStartArray();
for (Object each : collection) {
writeObject(each, gen, serializers);
}
gen.writeEndArray();
}
private void writeMap(Map<?, ?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
for (Map.Entry<?, ?> each : value.entrySet()) {
Object key = each.getKey();
if (!(key instanceof String)) {
throw new JsonGenerationException("Expected string key got " + key.getClass(), gen);
}
gen.writeFieldName((String) each.getKey());
writeObject(each.getValue(), gen, serializers);
}
gen.writeEndObject();
}
}
public static class Deserializer extends JsonDeserializer<Map<String, Object>> {
@Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return readMap(LinkedHashMap.class, p, ctxt);
}
@SuppressWarnings("unchecked")
private Map<String, Object> readMap(Class<?> type, JsonParser p, DeserializationContext ctxt) throws IOException {
if (!p.isExpectedStartObjectToken()) {
throw new JsonParseException(p, "Expected start of object for Map got " + p.currentToken());
}
try {
Map<String, Object> value = (Map<String, Object>) type.getDeclaredConstructor().newInstance();
JsonToken next = p.nextToken();
while (next != JsonToken.END_OBJECT) {
if (next != JsonToken.FIELD_NAME) {
throw new JsonParseException(p, "Key was not present " + next);
}
String key = p.currentName();
p.nextToken(); // position to next
Object val = readObject(p, ctxt);
value.put(key, val);
next = p.nextToken();
}
return value;
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new JsonMappingException(p, "Can not create empty map for class " + type + " @ " + p.getCurrentLocation(),
e);
}
}
private Object readObject(JsonParser p, DeserializationContext ctxt) throws IOException,
NoSuchMethodException,
InvocationTargetException {
Object val = null;
JsonToken next = p.currentToken();
switch (next) {
// Primitive Types
case VALUE_TRUE:
case VALUE_FALSE:
val = p.getValueAsBoolean();
break;
case VALUE_STRING:
val = p.getText();
break;
case VALUE_NUMBER_FLOAT:
case VALUE_NUMBER_INT:
val = p.getNumberValue();
break;
// Encoded Object information
case START_ARRAY:
val = readEncoded(p, ctxt);
break;
default:
throw new JsonParseException(p, "Object encoding not understood " + next);
}
return val;
}
private Object readEncoded(JsonParser p, DeserializationContext ctxt) throws IOException {
if (!p.isExpectedStartArrayToken()) {
throw new JsonParseException(p, "Expected array for encoded object got " + p.currentToken());
}
Object value = null;
JsonToken next = p.nextToken();
if (next != JsonToken.VALUE_STRING) {
throw new JsonParseException(p, "Encoded Class value not present " + next);
}
String typeName = p.getText();
p.nextToken(); // fwd to next
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
loader = getClass().getClassLoader();
}
try {
Class<?> type = loader.loadClass(typeName);
if (Collection.class.isAssignableFrom(type)) {
value = readCollection(type, p, ctxt);
} else if (Map.class.isAssignableFrom(type)) {
value = readMap(type, p, ctxt);
} else {
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(ctxt.constructType(type));
value = deser.deserialize(p, ctxt);
}
} catch (ClassNotFoundException e) {
throw new JsonParseException(p, "Type name encoded " + typeName + " could not be loaded", e);
}
if (p.nextToken() != JsonToken.END_ARRAY) {
throw new JsonParseException(p, "Encoded expected end of ARRAY marker " + p.currentToken());
}
return value;
}
@SuppressWarnings("unchecked")
private Object readCollection(Class<?> type, JsonParser p, DeserializationContext ctxt) throws IOException {
if (!p.isExpectedStartArrayToken()) {
throw new JsonParseException(p, "Expected array for encoded object got " + p.currentToken());
}
try {
Collection<Object> value = (Collection<Object>) type.getDeclaredConstructor().newInstance();
p.nextToken(); // move to next token
do {
Object val = readObject(p, ctxt);
value.add(val);
} while (p.nextToken() != JsonToken.END_ARRAY);
return value;
} catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw new IOException("Can not create empty constructor collection class " + type + " @ "
+ p.getCurrentLocation(), e);
}
}
}
/*
* Uses a LinkedHashMap to preserve the order of calls within a set of
* callGraphs. If things interleave in terms of entries then it means that the
* context was being used in different threads.
*/
@JsonDeserialize(using = Deserializer.class)
@JsonSerialize(using = Serializer.class)
private Map<String, Object> callGraphs = Collections.synchronizedMap(new LinkedHashMap<>(10));
@SuppressWarnings("unchecked")
public <M, R> Function<M, R> request(String callGraph, Function<M, R> func) {
return (m) -> (R) callGraphs.computeIfAbsent(callGraph + ".request", (ign) -> func.apply(m));
}
@SuppressWarnings("unchecked")
public <R> R evictRequestRecord(String callGraph) {
return (R) callGraphs.remove(callGraph + ".request");
}
@SuppressWarnings("unchecked")
public <R, C, RT> BiFunction<R, C, RT> response(String callGraph, BiFunction<R, C, RT> func) {
return (r, c) -> (RT) callGraphs.computeIfAbsent(callGraph + ".response", (ign) -> func.apply(r, c));
}
public Map<String, Object> callGraphs() {
return Collections.unmodifiableMap(callGraphs);
}
@SuppressWarnings("unchecked")
public <ResponseT> ResponseT response(String callGraph) {
return (ResponseT) callGraphs.get(callGraph + ".response");
}
@SuppressWarnings("unchecked")
public <RequestT> RequestT findFirstRequestByContains(String contains) {
return (RequestT) findFirst((key) -> key.contains(contains) && key.endsWith(".request"));
}
@SuppressWarnings("unchecked")
public <RequestT> List<RequestT> findAllRequestByContains(String contains) {
return (List<RequestT>) findAll((key) -> key.contains(contains) && key.endsWith(".request"));
}
@SuppressWarnings("unchecked")
public <ResponseT> ResponseT findFirstResponseByContains(String contains) {
return (ResponseT) findFirst((key) -> key.contains(contains) && key.endsWith(".response"));
}
@SuppressWarnings("unchecked")
public <ResponseT> List<ResponseT> findAllResponseByContains(String contains) {
return (List<ResponseT>) findAll((key) -> key.contains(contains) && key.endsWith(".response"));
}
Object findFirst(Predicate<String> contains) {
Objects.requireNonNull(contains);
return callGraphs.entrySet().stream().filter(e -> contains.test(e.getKey())).findFirst().map(Map.Entry::getValue)
.orElse(null);
}
List<Object> findAll(Predicate<String> contains) {
Objects.requireNonNull(contains);
return callGraphs.entrySet().stream().filter(e -> contains.test(e.getKey())).map(Map.Entry::getValue)
.collect(Collectors.toList());
}
<RequestT, ResponseT, ClientT, ModelT, CallbackT extends StdCallbackContext>
CallChain.Callback<RequestT, ResponseT, ClientT, ModelT, CallbackT, Boolean>
stabilize(String callGraph, CallChain.Callback<RequestT, ResponseT, ClientT, ModelT, CallbackT, Boolean> callback) {
return (request1, response1, client, model, context) -> {
final String key = callGraph + ".stabilize";
Boolean result = (Boolean) callGraphs.getOrDefault(key, Boolean.FALSE);
if (!result) {
//
// The StdCallbackContext can be shared. However the call to stabilize for a
// given content
// is usually confined to one thread. If for some reason we spread that across
// threads, the
// worst that can happen is a double compute for stabilize. This isn't the
// intended pattern.
// Why are we changing it from computeIfAbsent pattern? For the callback we send
// in the
// StdCallbackContext which can be used to add things into context. That will
// lead to
// ConcurrentModificationExceptions when the compute running added things into
// context when
// needed
//
result = callback.invoke(request1, response1, client, model, context);
if (result) {
callGraphs.put(key, Boolean.TRUE);
}
}
return result;
};
}
public int attempts(String callGraph) {
return (Integer) callGraphs.computeIfAbsent(callGraph + ".attempts", (ign) -> 1);
}
public void attempts(String callGraph, int attempts) {
callGraphs.put(callGraph + ".attempts", attempts);
}
@VisibleForTesting
void setCallGraphs(LinkedHashMap<String, Object> graphs) {
this.callGraphs = Collections.synchronizedMap(new LinkedHashMap<>(graphs));
}
}