ConvertibleValues.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.annotation.Nullable;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.ConversionServiceProvider;
import io.micronaut.core.reflect.GenericTypeUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.value.ValueResolver;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

/**
 * An interface for classes that represent a map-like structure of values that can be converted.
 *
 * @param <V> The generic value
 * @author Graeme Rocher
 * @since 1.0
 */
public interface ConvertibleValues<V> extends ValueResolver<CharSequence>, Iterable<Map.Entry<String, V>>, ConversionServiceProvider {

    ConvertibleValues EMPTY = new ConvertibleValuesMap<>(Collections.emptyMap());

    /**
     * @return The names of the values
     */
    Set<String> names();

    /**
     * @return The values
     */
    Collection<V> values();

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

    /**
     * @return The concrete type of the value
     */
    @SuppressWarnings("unchecked")
    default Class<V> getValueType() {
        Optional<Class<?>> type = GenericTypeUtils.resolveInterfaceTypeArgument(getClass(), ConvertibleValues.class);
        return (Class<V>) type.orElse(Object.class);
    }

    /**
     * Whether the given key is contained within these values.
     *
     * @param name The key name
     * @return True if it is
     */
    default boolean contains(String name) {
        return get(name, Argument.OBJECT_ARGUMENT).isPresent();
    }

    /**
     * Get a raw value without any conversion.
     *
     * @param name The key name
     * @return True if it is
     * @since 2.0.0
     */
    default @Nullable V getValue(CharSequence name) {
        return (V) get(name, Argument.OBJECT_ARGUMENT).orElse(null);
    }

    /**
     * Performs the given action for each value. 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 forEach(BiConsumer<String, V> action) {
        Objects.requireNonNull(action, "Consumer cannot be null");

        Collection<String> headerNames = names();
        for (String headerName : headerNames) {
            Optional<V> vOptional = this.get(headerName, getValueType());
            vOptional.ifPresent(v -> action.accept(headerName, v));
        }
    }

    /**
     * Return this {@link ConvertibleValues} as a map for the given key type and value type. The map represents a copy of the data held by this instance.
     *
     * @return The values
     */
    default Map<String, V> asMap() {
        Map<String, V> newMap = new LinkedHashMap<>();
        for (Map.Entry<String, V> entry : this) {
            String key = entry.getKey();
            newMap.put(key, entry.getValue());
        }
        return newMap;
    }

    /**
     * Return this {@link ConvertibleValues} as a map for the given key type and value type. If any entry cannot be
     * converted to the target key/value type then the entry is simply excluded, hence the size of the map returned
     * may not match the size of this {@link ConvertibleValues}.
     *
     * @param keyType   The key type
     * @param valueType The value type
     * @param <KT>      The key type
     * @param <VT>      The value type
     * @return The values with the key converted to the given key type and the value to the given value type.
     */
    default <KT, VT> Map<KT, VT> asMap(Class<KT> keyType, Class<VT> valueType) {
        Map<KT, VT> newMap = new LinkedHashMap<>();
        for (Map.Entry<String, V> entry : this) {
            String key = entry.getKey();
            Optional<KT> convertedKey = getConversionService().convert(key, keyType);
            if (convertedKey.isPresent()) {
                Optional<VT> convertedValue = getConversionService().convert(entry.getValue(), valueType);
                convertedValue.ifPresent(vt -> newMap.put(convertedKey.get(), vt));
            }
        }
        return newMap;
    }

    /**
     * Return this {@link ConvertibleValues} as a {@link Properties} object returning only keys and values that
     * can be represented as a string.
     *
     * @return The values with the key converted to the given key type and the value to the given value type.
     * @since 1.0.3
     */
    default Properties asProperties() {
        Properties props = new Properties();

        for (Map.Entry<String, V> entry : this) {
            String key = entry.getKey();
            V value = entry.getValue();
            if (value instanceof CharSequence || value instanceof Number) {
                props.setProperty(key, value.toString());
            }
        }
        return props;
    }

    /**
     * Returns a submap for all the keys with the given prefix.
     *
     * @param prefix    The prefix
     * @param valueType The value type
     * @return The submap
     */
    @SuppressWarnings("unchecked")
    default Map<String, V> subMap(String prefix, Class<V> valueType) {
        return subMap(prefix, Argument.of(valueType));
    }

    /**
     * Returns a submap for all the keys with the given prefix.
     *
     * @param prefix    The prefix
     * @param valueType The value type
     * @return The submap
     */
    @SuppressWarnings("unchecked")
    default Map<String, V> subMap(String prefix, Argument<V> valueType) {
        return subMap(prefix, ConversionContext.of(valueType));
    }

    /**
     * Returns a submap for all the keys with the given prefix.
     *
     * @param prefix    The prefix
     * @param valueType The value type
     * @return The submap
     */
    @SuppressWarnings("unchecked")
    default Map<String, V> subMap(String prefix, ArgumentConversionContext<V> valueType) {
        // special handling for maps for resolving sub keys
        String finalPrefix = prefix + '.';
        return names().stream()
            .filter(name -> name.startsWith(finalPrefix))
            .collect(Collectors.toMap(name -> name.substring(finalPrefix.length()), name -> get(name, valueType).orElse(null)));
    }

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

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

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

                    @Override
                    public V getValue() {
                        return get(name, getValueType()).orElse(null);
                    }

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

    /**
     * Creates a new {@link ConvertibleValues} for the values.
     *
     * @param values A map of values
     * @param <T>    The target generic type
     * @return The values
     */
    static <T> ConvertibleValues<T> of(Map<? extends CharSequence, T> values) {
        return of(values, ConversionService.SHARED);
    }

    /**
     * Creates a new {@link ConvertibleValues} for the values.
     *
     * @param values A map of values
     * @param conversionService The conversion service
     * @param <T>    The target generic type
     * @return The values
     * @since 4.0.0
     */
    static <T> ConvertibleValues<T> of(Map<? extends CharSequence, T> values, ConversionService conversionService) {
        if (values == null) {
            return ConvertibleValuesMap.empty();
        } else {
            return new ConvertibleValuesMap<>(values, conversionService);
        }
    }

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

    @Override
    default ConversionService getConversionService() {
        return ConversionService.SHARED;
    }
}