1import static java.util.stream.Collectors.toList;
2
3import java.util.ArrayList;
4import java.util.Arrays;
5import java.util.Collection;
6import java.util.Collections;
7import java.util.List;
8import java.util.Objects;
9import java.util.function.Function;
10import software.amazon.awssdk.annotations.Generated;
11import software.amazon.awssdk.annotations.SdkInternalApi;
12import software.amazon.awssdk.core.SdkPojo;
13import software.amazon.awssdk.core.SdkResponse;
14import software.amazon.awssdk.core.exception.SdkClientException;
15import software.amazon.awssdk.core.exception.SdkServiceException;
16import software.amazon.awssdk.core.waiters.WaiterAcceptor;
17import software.amazon.awssdk.core.waiters.WaiterState;
18import software.amazon.awssdk.utils.ToString;
19
20/**
21 * Contains classes used at runtime by the code generator classes for waiter acceptors generated from JMESPath
22 * expressions.
23 */
24@Generated("software.amazon.awssdk:codegen")
25@SdkInternalApi
26public final class WaitersRuntime {
27    /**
28     * The default acceptors that should be matched *last* in the list of acceptors used by the SDK client waiters.
29     */
30    public static final List<WaiterAcceptor<Object>> DEFAULT_ACCEPTORS = Collections.unmodifiableList(defaultAcceptors());
31
32    private WaitersRuntime() {
33    }
34
35    private static List<WaiterAcceptor<Object>> defaultAcceptors() {
36        return Collections.singletonList(retryOnUnmatchedResponseWaiter());
37    }
38
39    private static WaiterAcceptor<Object> retryOnUnmatchedResponseWaiter() {
40        return WaiterAcceptor.retryOnResponseAcceptor(r -> true);
41    }
42
43    /**
44     * An intermediate value for JMESPath expressions, encapsulating the different data types supported by JMESPath and the
45     * operations on that data.
46     */
47    public static final class Value {
48        /**
49         * A null value.
50         */
51        private static final Value NULL_VALUE = new Value(null);
52
53        /**
54         * The type associated with this value.
55         */
56        private final Type type;
57
58        /**
59         * Whether this value is a "projection" value. Projection values are LIST values where certain operations are performed
60         * on each element of the list, instead of on the entire list.
61         */
62        private final boolean isProjection;
63
64        /**
65         * The value if this is a {@link Type#POJO} (or null otherwise).
66         */
67        private SdkPojo pojoValue;
68
69        /**
70         * The value if this is an {@link Type#INTEGER} (or null otherwise).
71         */
72        private Integer integerValue;
73
74        /**
75         * The value if this is an {@link Type#STRING} (or null otherwise).
76         */
77        private String stringValue;
78
79        /**
80         * The value if this is an {@link Type#LIST} (or null otherwise).
81         */
82        private List<Object> listValue;
83
84        /**
85         * The value if this is an {@link Type#BOOLEAN} (or null otherwise).
86         */
87        private Boolean booleanValue;
88
89        /**
90         * Create a LIST value, specifying whether this is a projection. This is private and is usually invoked by
91         * {@link #newProjection(Collection)}.
92         */
93        private Value(Collection<?> value, boolean projection) {
94            this.type = Type.LIST;
95            this.listValue = new ArrayList<>(value);
96            this.isProjection = projection;
97        }
98
99        /**
100         * Create a non-projection value, where the value type is determined reflectively.
101         */
102        public Value(Object value) {
103            this.isProjection = false;
104
105            if (value == null) {
106                this.type = Type.NULL;
107            } else if (value instanceof SdkPojo) {
108                this.type = Type.POJO;
109                this.pojoValue = (SdkPojo) value;
110            } else if (value instanceof String) {
111                this.type = Type.STRING;
112                this.stringValue = (String) value;
113            } else if (value instanceof Integer) {
114                this.type = Type.INTEGER;
115                this.integerValue = (Integer) value;
116            } else if (value instanceof Collection) {
117                this.type = Type.LIST;
118                this.listValue = new ArrayList<>(((Collection<?>) value));
119            } else if (value instanceof Boolean) {
120                this.type = Type.BOOLEAN;
121                this.booleanValue = (Boolean) value;
122            } else {
123                throw new IllegalArgumentException("Unsupported value type: " + value.getClass());
124            }
125        }
126
127        /**
128         * Create a {@link Type#LIST} with a {@link #isProjection} of true.
129         */
130        private static Value newProjection(Collection<?> values) {
131            return new Value(values, true);
132        }
133
134        /**
135         * Retrieve the actual value that this represents (this will be the same value passed to the constructor).
136         */
137        public Object value() {
138            switch (type) {
139                case NULL: return null;
140                case POJO: return pojoValue;
141                case INTEGER: return integerValue;
142                case STRING: return stringValue;
143                case BOOLEAN: return booleanValue;
144                case LIST: return listValue;
145                default: throw new IllegalStateException();
146            }
147        }
148
149        /**
150         * Retrieve the actual value that this represents, as a list.
151         */
152        public List<Object> values() {
153            if (type == Type.NULL) {
154                return Collections.emptyList();
155            }
156
157            if (type == Type.LIST) {
158                return listValue;
159            }
160
161            return Collections.singletonList(value());
162        }
163
164        /**
165         * Convert this value to a new constant value, discarding the current value.
166         */
167        public Value constant(Value value) {
168            return value;
169        }
170
171        /**
172         * Convert this value to a new constant value, discarding the current value.
173         */
174        public Value constant(Object constant) {
175            return new Value(constant);
176        }
177
178        /**
179         * Execute a wildcard expression on this value: https://jmespath.org/specification.html#wildcard-expressions
180         */
181        public Value wildcard() {
182            if (type == Type.NULL) {
183                return NULL_VALUE;
184            }
185
186            if (type != Type.POJO) {
187                throw new IllegalArgumentException("Cannot flatten a " + type);
188            }
189
190            return Value.newProjection(pojoValue.sdkFields().stream()
191                                                .map(f -> f.getValueOrDefault(pojoValue))
192                                                .filter(Objects::nonNull)
193                                                .collect(toList()));
194        }
195
196        /**
197         * Execute a flattening expression on this value: https://jmespath.org/specification.html#flatten-operator
198         */
199        public Value flatten() {
200            if (type == Type.NULL) {
201                return NULL_VALUE;
202            }
203
204            if (type != Type.LIST) {
205                throw new IllegalArgumentException("Cannot flatten a " + type);
206            }
207
208            List<Object> result = new ArrayList<>();
209            for (Object listEntry : listValue) {
210                Value listValue = new Value(listEntry);
211                if (listValue.type != Type.LIST) {
212                    result.add(listEntry);
213                } else {
214                    result.addAll(listValue.listValue);
215                }
216            }
217
218            return Value.newProjection(result);
219        }
220
221        /**
222         * Retrieve an identifier from this value: https://jmespath.org/specification.html#identifiers
223         */
224        public Value field(String fieldName) {
225            if (isProjection) {
226                return project(v -> v.field(fieldName));
227            }
228
229            if (type == Type.NULL) {
230                return NULL_VALUE;
231            }
232
233            if (type == Type.POJO) {
234                return pojoValue.sdkFields()
235                                .stream()
236                                .filter(f -> f.memberName().equals(fieldName))
237                                .map(f -> f.getValueOrDefault(pojoValue))
238                                .map(Value::new)
239                                .findAny()
240                                .orElseThrow(() -> new IllegalArgumentException("No such field: " + fieldName));
241            }
242
243            throw new IllegalArgumentException("Cannot get a field from a " + type);
244        }
245
246        /**
247         * Filter this value: https://jmespath.org/specification.html#filter-expressions
248         */
249        public Value filter(Function<Value, Value> predicate) {
250            if (isProjection) {
251                return project(f -> f.filter(predicate));
252            }
253
254            if (type == Type.NULL) {
255                return NULL_VALUE;
256            }
257
258            if (type != Type.LIST) {
259                throw new IllegalArgumentException("Unsupported type for filter function: " + type);
260            }
261
262            List<Object> results = new ArrayList<>();
263            listValue.forEach(entry -> {
264                Value entryValue = new Value(entry);
265                Value predicateResult = predicate.apply(entryValue);
266                if (predicateResult.isTrue()) {
267                    results.add(entry);
268                }
269            });
270            return new Value(results);
271        }
272
273        /**
274         * Execute the length function, with this value as the first parameter: https://jmespath.org/specification.html#length
275         */
276        public Value length() {
277            if (type == Type.NULL) {
278                return NULL_VALUE;
279            }
280
281            if (type == Type.STRING) {
282                return new Value(stringValue.length());
283            }
284
285            if (type == Type.POJO) {
286                return new Value(pojoValue.sdkFields().size());
287            }
288
289            if (type == Type.LIST) {
290                return new Value(Math.toIntExact(listValue.size()));
291            }
292
293            throw new IllegalArgumentException("Unsupported type for length function: " + type);
294        }
295
296        /**
297         * Execute the contains function, with this value as the first parameter: https://jmespath.org/specification.html#contains
298         */
299        public Value contains(Value rhs) {
300            if (type == Type.NULL) {
301                return NULL_VALUE;
302            }
303
304            if (type == Type.STRING) {
305                if (rhs.type != Type.STRING) {
306                    // Unclear from the spec whether we can check for a boolean in a string, for example...
307                    return new Value(false);
308                }
309
310                return new Value(stringValue.contains(rhs.stringValue));
311            }
312
313            if (type == Type.LIST) {
314                return new Value(listValue.stream().anyMatch(v -> Objects.equals(v, rhs.value())));
315            }
316
317            throw new IllegalArgumentException("Unsupported type for contains function: " + type);
318        }
319
320        /**
321         * Compare this value to another value, using the specified comparison operator:
322         * https://jmespath.org/specification.html#comparison-operators
323         */
324        public Value compare(String comparison, Value rhs) {
325            if (type != rhs.type) {
326                return new Value(false);
327            }
328
329            if (type == Type.INTEGER) {
330                switch (comparison) {
331                    case "<": return new Value(integerValue < rhs.integerValue);
332                    case "<=": return new Value(integerValue <= rhs.integerValue);
333                    case ">": return new Value(integerValue > rhs.integerValue);
334                    case ">=": return new Value(integerValue >= rhs.integerValue);
335                    case "==": return new Value(Objects.equals(integerValue, rhs.integerValue));
336                    case "!=": return new Value(!Objects.equals(integerValue, rhs.integerValue));
337                    default: throw new IllegalArgumentException("Unsupported comparison: " + comparison);
338                }
339            }
340
341            if (type == Type.NULL || type == Type.STRING || type == Type.BOOLEAN) {
342                switch (comparison) {
343                    case "<":
344                    case "<=":
345                    case ">":
346                    case ">=":
347                        return NULL_VALUE; // Invalid comparison, spec says to treat as null.
348                    case "==": return new Value(Objects.equals(value(), rhs.value()));
349                    case "!=": return new Value(!Objects.equals(value(), rhs.value()));
350                    default: throw new IllegalArgumentException("Unsupported comparison: " + comparison);
351                }
352            }
353
354            throw new IllegalArgumentException("Unsupported type in comparison: " + type);
355        }
356
357        /**
358         * Perform a multi-select list expression on this value: https://jmespath.org/specification.html#multiselect-list
359         */
360        @SafeVarargs
361        public final Value multiSelectList(Function<Value, Value>... functions) {
362            if (isProjection) {
363                return project(v -> v.multiSelectList(functions));
364            }
365            if (type == Type.NULL) {
366                return NULL_VALUE;
367            }
368
369            List<Object> result = new ArrayList<>();
370            for (Function<Value, Value> function : functions) {
371                result.add(function.apply(this).value());
372            }
373            return new Value(result);
374        }
375
376        /**
377         * Perform an OR comparison between this value and another one: https://jmespath.org/specification.html#or-expressions
378         */
379        public Value or(Value rhs) {
380            if (isTrue()) {
381                return this;
382            } else {
383                return rhs.isTrue() ? rhs : NULL_VALUE;
384            }
385        }
386
387        /**
388         * Perform an AND comparison between this value and another one: https://jmespath.org/specification.html#or-expressions
389         */
390        public Value and(Value rhs) {
391            return isTrue() ? rhs : this;
392        }
393
394        /**
395         * Perform a NOT conversion on this value: https://jmespath.org/specification.html#not-expressions
396         */
397        public Value not() {
398            return new Value(!isTrue());
399        }
400
401        /**
402         * Returns true is this value is "true-like" (or false otherwise): https://jmespath.org/specification.html#or-expressions
403         */
404        private boolean isTrue() {
405            switch (type) {
406                case POJO:
407                    return !pojoValue.sdkFields().isEmpty();
408                case LIST:
409                    return !listValue.isEmpty();
410                case STRING:
411                    return !stringValue.isEmpty();
412                case BOOLEAN:
413                    return booleanValue;
414                default:
415                    return false;
416            }
417        }
418
419        /**
420         * Project the provided function across all values in this list. Assumes this is a LIST and isProjection is true.
421         */
422        private Value project(Function<Value, Value> functionToApply) {
423            return new Value(listValue.stream()
424                                      .map(Value::new)
425                                      .map(functionToApply)
426                                      .map(Value::value)
427                                      .collect(toList()),
428                             true);
429        }
430
431        /**
432         * The JMESPath type of this value.
433         */
434        private enum Type {
435            POJO,
436            LIST,
437            BOOLEAN,
438            STRING,
439            INTEGER,
440            NULL
441        }
442
443        @Override
444        public boolean equals(Object o) {
445            if (this == o) {
446                return true;
447            }
448            if (o == null || getClass() != o.getClass()) {
449                return false;
450            }
451
452            Value value = (Value) o;
453
454            return type == value.type && Objects.equals(value(), value.value());
455        }
456
457        @Override
458        public int hashCode() {
459            Object value = value();
460
461            int result = type.hashCode();
462            result = 31 * result + (value != null ? value.hashCode() : 0);
463            return result;
464        }
465
466        @Override
467        public String toString() {
468            return ToString.builder("Value")
469                           .add("type", type)
470                           .add("value", value())
471                           .build();
472        }
473    }
474
475    /**
476     * A {@link WaiterAcceptor} implementation that checks for a specific HTTP response status, regardless of whether it's
477     * reported by a response or an exception.
478     */
479    public static final class ResponseStatusAcceptor implements WaiterAcceptor<SdkResponse> {
480        private final int statusCode;
481        private final WaiterState waiterState;
482
483        public ResponseStatusAcceptor(int statusCode, WaiterState waiterState) {
484            this.statusCode = statusCode;
485            this.waiterState = waiterState;
486        }
487
488        @Override
489        public WaiterState waiterState() {
490            return waiterState;
491        }
492
493        @Override
494        public boolean matches(SdkResponse response) {
495            return response.sdkHttpResponse() != null &&
496                   response.sdkHttpResponse().statusCode() == statusCode;
497        }
498
499        @Override
500        public boolean matches(Throwable throwable) {
501            if (throwable instanceof SdkServiceException) {
502                return ((SdkServiceException) throwable).statusCode() == statusCode;
503            }
504
505            return false;
506        }
507    }
508}
509