ConvertibleMultiValues.java

/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 io.micronaut.core.convert.value;

import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.reflect.GenericTypeUtils;
import io.micronaut.core.type.Argument;

import io.micronaut.core.annotation.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;

/**
 * Specialization of {@link ConvertibleValues} where each name has multiple possible values.
 *
 * @param <V> The generic value
 * @author Graeme Rocher
 * @since 1.0
 */
public interface ConvertibleMultiValues<V> extends ConvertibleValues<List<V>> {
    /**
     * Get all the values for the given name without applying conversion.
     *
     * @param name The header name
     * @return All the values
     */
    List<V> getAll(CharSequence name);

    /**
     * Get a value without applying any conversion.
     *
     * @param name The name of the value
     * @return The raw value or null
     * @see #getFirst(CharSequence)
     */
    @Nullable V get(CharSequence name);

    /**
     * @return Whether these values are empty
     */
    @Override
    default boolean isEmpty() {
        return this == ConvertibleMultiValuesMap.EMPTY || names().isEmpty();
    }

    /**
     * Performs the given action for each header. Note that in the case
     * where multiple values exist for the same header then the consumer will be invoked
     * multiple times for the same key.
     *
     * @param action The action to be performed for each entry
     * @throws NullPointerException if the specified action is null
     * @since 1.0
     */
    default void forEachValue(BiConsumer<String, V> action) {
        Objects.requireNonNull(action, "Consumer cannot be null");

        Collection<String> names = names();
        for (String headerName : names) {
            Collection<V> values = getAll(headerName);
            for (V value : values) {
                action.accept(headerName, value);
            }
        }
    }

    @Override
    default void forEach(BiConsumer<String, List<V>> action) {
        Objects.requireNonNull(action, "Consumer cannot be null");

        Collection<String> names = names();
        for (String headerName : names) {
            List<V> values = getAll(headerName);
            action.accept(headerName, values);
        }
    }

    @Override
    default Iterator<Map.Entry<String, List<V>>> iterator() {
        Iterator<String> headerNames = names().iterator();
        return new Iterator<>() {
            @Override
            public boolean hasNext() {
                return headerNames.hasNext();
            }

            @Override
            public Map.Entry<String, List<V>> next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }

                String name = headerNames.next();
                return new Map.Entry<>() {
                    @Override
                    public String getKey() {
                        return name;
                    }

                    @Override
                    public List<V> getValue() {
                        return getAll(name);
                    }

                    @Override
                    public List<V> setValue(List<V> value) {
                        throw new UnsupportedOperationException("Not mutable");
                    }
                };
            }
        };
    }

    /**
     * Get the first value of the given header.
     *
     * @param name The header name
     * @return The first value or null if it is present
     */
    default Optional<V> getFirst(CharSequence name) {
        Optional<Class<?>> type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleMultiValues.class);
        return (Optional<V>) getFirst(name, type.orElse(Object.class));
    }

    /**
     * Find a header and convert it to the given type.
     *
     * @param name         The name of the header
     * @param requiredType The required type
     * @param <T>          The generic type
     * @return If the header is presented and can be converted an optional of the value otherwise {@link Optional#empty()}
     */
    default <T> Optional<T> getFirst(CharSequence name, Class<T> requiredType) {
        return getFirst(name, Argument.of(requiredType));
    }

    /**
     * Find a header and convert it to the given type.
     *
     * @param name         The name of the header
     * @param requiredType The required type
     * @param <T>          The generic type
     * @return If the header is presented and can be converted an optional of the value otherwise {@link Optional#empty()}
     */
    default <T> Optional<T> getFirst(CharSequence name, Argument<T> requiredType) {
        V v = get(name);
        if (v != null) {
            return ConversionService.SHARED.convert(v, ConversionContext.of(requiredType));
        }
        return Optional.empty();
    }

    /**
     * Find a header and convert it to the given type.
     *
     * @param name              The name of the header
     * @param conversionContext The conversion context
     * @param <T>               The generic type
     * @return If the header is presented and can be converted an optional of the value otherwise {@link Optional#empty()}
     */
    default <T> Optional<T> getFirst(CharSequence name, ArgumentConversionContext<T> conversionContext) {
        V v = get(name);
        if (v != null) {
            return ConversionService.SHARED.convert(v, conversionContext);
        }
        return Optional.empty();
    }

    /**
     * Find a header and convert it to the given type.
     *
     * @param name         The name of the header
     * @param requiredType The required type
     * @param defaultValue The default value
     * @param <T>          The generic type
     * @return The first value of the default supplied value if it is present
     */
    default <T> T getFirst(CharSequence name, Class<T> requiredType, T defaultValue) {
        return getFirst(name, requiredType).orElse(defaultValue);
    }

    /**
     * Creates a new {@link io.micronaut.core.value.OptionalValues} for the given type and values.
     *
     * @param values A map of values
     * @param <T>    The target generic type
     * @return The values
     */
    static <T> ConvertibleMultiValues<T> of(Map<CharSequence, List<T>> values) {
        return new ConvertibleMultiValuesMap<>(values);
    }

    /**
     * An empty {@link ConvertibleValues}.
     *
     * @param <V> The generic type
     * @return The empty {@link ConvertibleValues}
     */
    @SuppressWarnings("unchecked")
    static <V> ConvertibleMultiValues<V> empty() {
        return ConvertibleMultiValuesMap.EMPTY;
    }
}