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

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

Go to download

The AWS Java SDK for the Amazon EC2 Container Registry holds the client classes that are used for communicating with the Amazon EC2 Container Registry Service

There is a newer version: 2.30.1
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.ecr.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();
        }
    }
}