All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.services.lambda.jmespath.internal.JmesPathRuntime Maven / Gradle / Ivy

Go to download

The AWS Java SDK for AWS Lambda module holds the client classes that are used for communicating with AWS Lambda Service

There is a newer version: 2.29.15
Show newest version
/*
 * Copyright 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.awssdk.services.lambda.jmespath.internal;

import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.SdkField;
import software.amazon.awssdk.core.SdkPojo;
import software.amazon.awssdk.utils.ToString;

@SdkInternalApi
public final class JmesPathRuntime {

    private JmesPathRuntime() {
    }

    /**
     * An intermediate value for JMESPath expressions, encapsulating the different data types supported by JMESPath and
     * the operations on that data.
     */
    public static final class Value {
        /**
         * A null value.
         */
        private static final Value NULL_VALUE = new Value(null);

        /**
         * The type associated with this value.
         */
        private final Type type;

        /**
         * Whether this value is a "projection" value. Projection values are LIST values where certain operations are
         * performed on each element of the list, instead of on the entire list.
         */
        private final boolean isProjection;

        /**
         * The value if this is a {@link Type#POJO} (or null otherwise).
         */
        private SdkPojo pojoValue;

        /**
         * The value if this is an {@link Type#INTEGER} (or null otherwise).
         */
        private Integer integerValue;

        /**
         * The value if this is an {@link Type#STRING} (or null otherwise).
         */
        private String stringValue;

        /**
         * The value if this is an {@link Type#LIST} (or null otherwise).
         */
        private List listValue;

        /**
         * The value if this is an {@link Type#MAP} (or null otherwise).
         */
        private Map mapValue;

        /**
         * The value if this is an {@link Type#BOOLEAN} (or null otherwise).
         */
        private Boolean booleanValue;

        /**
         * Create a LIST value, specifying whether this is a projection. This is private and is usually invoked by
         * {@link #newProjection(Collection)}.
         */
        private Value(Collection value, boolean projection) {
            this.type = Type.LIST;
            this.listValue = new ArrayList<>(value);
            this.isProjection = projection;
        }

        /**
         * Create a MAP value, specifying whether this is a projection. This is private and is usually invoked by
         * {@link #newProjection(Map)}.
         */
        private Value(Map value, boolean projection) {
            this.type = Type.MAP;
            this.mapValue = new HashMap<>(value);
            this.isProjection = projection;
        }

        /**
         * Create a non-projection value, where the value type is determined reflectively.
         */
        public Value(Object value) {
            this.isProjection = false;

            if (value == null) {
                this.type = Type.NULL;
            } else if (value instanceof SdkPojo) {
                this.type = Type.POJO;
                this.pojoValue = (SdkPojo) value;
            } else if (value instanceof String) {
                this.type = Type.STRING;
                this.stringValue = (String) value;
            } else if (value instanceof Integer) {
                this.type = Type.INTEGER;
                this.integerValue = (Integer) value;
            } else if (value instanceof Collection) {
                this.type = Type.LIST;
                this.listValue = new ArrayList<>(((Collection) value));
            } else if (value instanceof Map) {
                this.type = Type.MAP;
                this.mapValue = new HashMap<>((Map) value);
            } else if (value instanceof Boolean) {
                this.type = Type.BOOLEAN;
                this.booleanValue = (Boolean) value;
            } else {
                throw new IllegalArgumentException("Unsupported value type: " + value.getClass());
            }
        }

        /**
         * Create a {@link Type#LIST} with a {@link #isProjection} of true.
         */
        private static Value newProjection(Collection values) {
            return new Value(values, true);
        }

        /**
         * Create a {@link Type#MAP} with a {@link #isProjection} of true.
         */
        private static Value newProjection(Map values) {
            return new Value(values, true);
        }

        /**
         * Retrieve the actual value that this represents (this will be the same value passed to the constructor).
         */
        public Object value() {
            switch (type) {
            case NULL:
                return null;
            case POJO:
                return pojoValue;
            case INTEGER:
                return integerValue;
            case STRING:
                return stringValue;
            case BOOLEAN:
                return booleanValue;
            case LIST:
                return listValue;
            case MAP:
                return mapValue;
            default:
                throw new IllegalStateException();
            }
        }

        /**
         * Retrieve the actual value that this represents, as a list of object.
         */
        public List values() {
            if (type == Type.NULL) {
                return Collections.emptyList();
            }

            if (type == Type.LIST) {
                return listValue;
            }

            return Collections.singletonList(value());
        }

        /**
         * Retrieve the actual value that this represents, as a map of objects.
         */
        private Map mapValues() {
            if (type == Type.NULL) {
                return Collections.emptyMap();
            }

            if (type == Type.MAP) {
                return mapValue;
            }

            throw new IllegalStateException("Must be MAP type to get map values");
        }

        /**
         * Retrieve the actual value that this represents, as a Boolean. Note that only null, boolean and string types
         * are supported.
         */
        public Boolean booleanValue() {
            switch (type) {
            case NULL:
                return null;
            case STRING:
                return Boolean.parseBoolean(stringValue);
            case BOOLEAN:
                return booleanValue;
            default:
                throw new IllegalStateException(String.format("Cannot convert type %s to Boolean.", type));
            }
        }

        /**
         * Retrieve the actual value that this represents, as a String. Note that collection types are not supported.
         */
        public String stringValue() {
            switch (type) {
            case NULL:
                return null;
            case INTEGER:
                return integerValue.toString();
            case STRING:
                return stringValue;
            case BOOLEAN:
                return booleanValue.toString();
            default:
                throw new IllegalStateException(String.format("Cannot convert type %s to String.", type));
            }
        }

        /**
         * Retrieve the actual value that this represents, as a list of String. Note that if the contents of the list is
         * not String, an exception is thrown. If the value has a different type, the code makes a best effort to return
         * a single element list of String. See {@code stringValue}.
         */
        public List stringValues() {
            if (type == Type.NULL) {
                return Collections.emptyList();
            }

            if (type == Type.LIST) {
                List result = new ArrayList<>();
                for (Object listEntry : listValue) {
                    Value entryAsValue = new Value(listEntry);
                    if (entryAsValue.type == Type.NULL) {
                        continue;
                    }
                    if (entryAsValue.type != Type.STRING) {
                        throw new IllegalStateException("Expected list elements to be of type String, but were "
                                + entryAsValue.type);
                    }
                    result.add(entryAsValue.stringValue);
                }
                return result;
            }

            return Collections.singletonList(stringValue());
        }

        /**
         * Retrieve the actual value that this represents, as a map of Strings. Note that if the contents of the map are
         * not String, or if the value has a different type, an exception is thrown.
         */
        public Map stringValuesMap() {
            if (type == Type.NULL) {
                return Collections.emptyMap();
            }

            if (type == Type.MAP) {
                Map result = new HashMap<>();
                mapValue.forEach((key, value) -> {
                    Value keyAsValue = new Value(key);
                    Value entryAsValue = new Value(value);
                    if (keyAsValue.type != Type.NULL) {
                        if (!isStringType(keyAsValue) || !isStringType(entryAsValue)) {
                            throw new IllegalStateException("Keys and values must be String type");
                        }
                        result.put(keyAsValue.stringValue, entryAsValue.stringValue);
                    }
                });

                return result;
            }

            throw new IllegalArgumentException("Not of type MAP");
        }

        private boolean isStringType(Value value) {
            return value.type == Type.STRING;
        }

        /**
         * Convert this value to a new constant value, discarding the current value.
         */
        public Value constant(Value value) {
            return value;
        }

        /**
         * Convert this value to a new constant value, discarding the current value.
         */
        public Value constant(Object constant) {
            return new Value(constant);
        }

        /**
         * Execute a wildcard expression on this value: https://jmespath.org/specification.html#wildcard-expressions
         */
        public Value wildcard() {
            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type == Value.Type.LIST) {
                return newProjection(listValue);
            }

            if (type == Type.MAP) {
                return newProjection(mapValue);
            }

            if (type == Value.Type.POJO) {
                return newProjection(pojoValue.sdkFields().stream().map(f -> f.getValueOrDefault(pojoValue))
                        .filter(Objects::nonNull).collect(toList()));
            }

            throw new IllegalArgumentException("Cannot use a wildcard expression on a " + type);
        }

        /**
         * Execute a flattening expression on this value: https://jmespath.org/specification.html#flatten-operator
         */
        public Value flatten() {
            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type != Type.LIST) {
                throw new IllegalArgumentException("Cannot flatten a " + type);
            }

            List result = new ArrayList<>();
            for (Object listEntry : listValue) {
                Value listValue = new Value(listEntry);
                if (listValue.type != Type.LIST) {
                    result.add(listEntry);
                } else {
                    result.addAll(listValue.listValue);
                }
            }

            return Value.newProjection(result);
        }

        /**
         * Retrieve an identifier from this value: https://jmespath.org/specification.html#identifiers
         */
        public Value field(String fieldName) {
            if (isProjection) {
                return project(v -> v.field(fieldName));
            }

            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type == Type.POJO) {
                return pojoValue.sdkFields().stream().filter(f -> f.memberName().equals(fieldName))
                        .map(f -> f.getValueOrDefault(pojoValue)).map(Value::new).findAny()
                        .orElseThrow(() -> new IllegalArgumentException("No such field: " + fieldName));
            }

            throw new IllegalArgumentException("Cannot get a field from a " + type);
        }

        /**
         * Filter this value: https://jmespath.org/specification.html#filter-expressions
         */
        public Value filter(Function predicate) {
            if (isProjection) {
                return project(f -> f.filter(predicate));
            }

            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type == Type.LIST) {
                List results = new ArrayList<>();
                listValue.forEach(entry -> {
                    Value entryValue = new Value(entry);
                    Value predicateResult = predicate.apply(entryValue);
                    if (predicateResult.isTrue()) {
                        results.add(entry);
                    }
                });
                return new Value(results);
            }

            if (type == Type.MAP) {
                Map results = new HashMap<>();
                mapValue.forEach((key, entry) -> {
                    Value entryValue = new Value(entry);
                    Value predicateResult = predicate.apply(entryValue);
                    if (predicateResult.isTrue()) {
                        results.put(key, entry);
                    }
                });
                return new Value(results);
            }

            throw new IllegalArgumentException("Unsupported type for filter function: " + type);
        }

        /**
         * Execute the length function, with this value as the first parameter:
         * https://jmespath.org/specification.html#length
         */
        public Value length() {
            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type == Type.STRING) {
                return new Value(stringValue.length());
            }

            if (type == Type.POJO) {
                return new Value(pojoValue.sdkFields().size());
            }

            if (type == Type.LIST) {
                return new Value(Math.toIntExact(listValue.size()));
            }

            if (type == Type.MAP) {
                return new Value(Math.toIntExact(mapValue.size()));
            }

            throw new IllegalArgumentException("Unsupported type for length function: " + type);
        }

        public Value keys() {
            if (type == Type.NULL) {
                return new Value(Collections.emptyList(), false);
            }

            if (type == Type.POJO) {
                return new Value(pojoValue.sdkFields().stream().map(SdkField::memberName).collect(toList()));
            }

            if (type == Type.MAP) {
                return new Value(mapValue.keySet());
            }

            throw new IllegalArgumentException("Unsupported type for keys function: " + type);
        }

        /**
         * Execute the contains function, with this value as the first parameter:
         * https://jmespath.org/specification.html#contains
         */
        public Value contains(Value rhs) {
            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            if (type == Type.STRING) {
                if (rhs.type != Type.STRING) {
                    // Unclear from the spec whether we can check for a boolean in a string, for example...
                    return new Value(false);
                }

                return new Value(stringValue.contains(rhs.stringValue));
            }

            Object value = rhs.value();

            if (type == Type.LIST) {
                return new Value(listValue.stream().anyMatch(v -> Objects.equals(v, value)));
            }

            if (type == Type.MAP) {
                return new Value(mapValue.containsValue(value));
            }

            throw new IllegalArgumentException("Unsupported type for contains function: " + type);
        }

        /**
         * Compare this value to another value, using the specified comparison operator:
         * https://jmespath.org/specification.html#comparison-operators
         */
        public Value compare(String comparison, Value rhs) {
            if (type != rhs.type) {
                return new Value(false);
            }

            if (type == Type.INTEGER) {
                switch (comparison) {
                case "<":
                    return new Value(integerValue < rhs.integerValue);
                case "<=":
                    return new Value(integerValue <= rhs.integerValue);
                case ">":
                    return new Value(integerValue > rhs.integerValue);
                case ">=":
                    return new Value(integerValue >= rhs.integerValue);
                case "==":
                    return new Value(Objects.equals(integerValue, rhs.integerValue));
                case "!=":
                    return new Value(!Objects.equals(integerValue, rhs.integerValue));
                default:
                    throw new IllegalArgumentException("Unsupported comparison: " + comparison);
                }
            }

            if (type == Type.NULL || type == Type.STRING || type == Type.BOOLEAN) {
                switch (comparison) {
                case "<":
                case "<=":
                case ">":
                case ">=":
                    return NULL_VALUE; // Invalid comparison, spec says to treat as null.
                case "==":
                    return new Value(Objects.equals(value(), rhs.value()));
                case "!=":
                    return new Value(!Objects.equals(value(), rhs.value()));
                default:
                    throw new IllegalArgumentException("Unsupported comparison: " + comparison);
                }
            }

            throw new IllegalArgumentException("Unsupported type in comparison: " + type);
        }

        /**
         * Perform a multi-select list expression on this value:
         * https://jmespath.org/specification.html#multiselect-list
         */
        @SafeVarargs
        public final Value multiSelectList(Function... functions) {
            if (isProjection) {
                return project(v -> v.multiSelectList(functions));
            }
            if (type == Type.NULL) {
                return NULL_VALUE;
            }

            List result = new ArrayList<>();
            for (Function function : functions) {
                result.add(function.apply(this).value());
            }
            return new Value(result);
        }

        /**
         * Perform a multi-select hash expression on this value:
         * https://jmespath.org/specification.html#multiselect-hash
         */
        public final Value multiSelectHash(Map> selections) {
            if (isProjection) {
                return project(v -> v.multiSelectHash(selections));
            }
            if (type == Type.NULL) {
                return NULL_VALUE;
            }
            if (type != Type.MAP) {
                throw new IllegalArgumentException("Multi-select map operation is only supported for maps");
            }

            Map result = new HashMap<>();
            for (Map.Entry> entry : selections.entrySet()) {
                String key = entry.getKey();
                Function function = entry.getValue();
                Value selectedValue = function.apply(new Value(mapValue.get(key)));
                result.put(key, selectedValue.value());
            }

            return new Value(result);
        }

        /**
         * Perform an OR comparison between this value and another one:
         * https://jmespath.org/specification.html#or-expressions
         */
        public Value or(Value rhs) {
            if (isTrue()) {
                return this;
            } else {
                return rhs.isTrue() ? rhs : NULL_VALUE;
            }
        }

        /**
         * Perform an AND comparison between this value and another one:
         * https://jmespath.org/specification.html#or-expressions
         */
        public Value and(Value rhs) {
            return isTrue() ? rhs : this;
        }

        /**
         * Perform a NOT conversion on this value: https://jmespath.org/specification.html#not-expressions
         */
        public Value not() {
            return new Value(!isTrue());
        }

        /**
         * Returns true is this value is "true-like" (or false otherwise):
         * https://jmespath.org/specification.html#or-expressions
         */
        private boolean isTrue() {
            switch (type) {
            case POJO:
                return !pojoValue.sdkFields().isEmpty();
            case LIST:
                return !listValue.isEmpty();
            case MAP:
                return !mapValue.isEmpty();
            case STRING:
                return !stringValue.isEmpty();
            case BOOLEAN:
                return booleanValue;
            default:
                return false;
            }
        }

        /**
         * Project the provided function across all values in this list. Assumes this is a LIST or MAP and isProjection
         * is true.
         */
        private Value project(Function functionToApply) {
            if (type == Type.LIST) {
                return new Value(listValue.stream().map(Value::new).map(functionToApply).map(Value::value)
                        .filter(Objects::nonNull).collect(toList()), true);
            }

            if (type == Type.MAP) {
                return new Value(mapValue.values().stream().map(Value::new).map(functionToApply).map(Value::value)
                        .filter(Objects::nonNull).collect(toList()), true);
            }

            throw new IllegalArgumentException("Can only project on List or Map types");
        }

        /**
         * The JMESPath type of this value.
         */
        private enum Type {
            POJO,
            LIST,
            MAP,
            BOOLEAN,
            STRING,
            INTEGER,
            NULL
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Value value = (Value) o;

            return type == value.type && Objects.equals(value(), value.value());
        }

        @Override
        public int hashCode() {
            Object value = value();

            int result = type.hashCode();
            result = 31 * result + (value != null ? value.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return ToString.builder("Value").add("type", type).add("value", value()).build();
        }
    }
}