ConvertibleMultiValuesMap.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.ConversionService;
import io.micronaut.core.convert.ConversionServiceAware;
import io.micronaut.core.type.Argument;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * An implementation of {@link ConvertibleMultiValues} that uses a backing {@link LinkedHashMap}.
 *
 * @param <V> The generic value
 * @author Graeme Rocher
 * @since 1.0
 */
public class ConvertibleMultiValuesMap<V> implements ConvertibleMultiValues<V>, ConversionServiceAware {
    @SuppressWarnings("java:S1845")
    public static final ConvertibleMultiValues EMPTY = new ConvertibleMultiValuesMap(Collections.emptyMap()) {
        @Override
        public void setConversionService(ConversionService conversionService) {
            // not needed
        }
    };

    protected final Map<CharSequence, List<V>> values;
    private ConversionService conversionService;

    /**
     * Construct an empty {@link ConvertibleValuesMap}.
     */
    public ConvertibleMultiValuesMap() {
        this(new LinkedHashMap<>(), ConversionService.SHARED);
    }

    /**
     * Construct a {@link ConvertibleValuesMap} from the given map.
     *
     * @param values The map
     */
    public ConvertibleMultiValuesMap(Map<CharSequence, List<V>> values) {
        this(values, ConversionService.SHARED);
    }

    /**
     * Construct a {@link ConvertibleValuesMap} from the given map and conversion service.
     *
     * @param values            The map
     * @param conversionService The conversion service
     */
    public ConvertibleMultiValuesMap(Map<CharSequence, List<V>> values, ConversionService conversionService) {
        this.values = wrapValues(values);
        this.conversionService = conversionService;
    }

    @Override
    public <T> Optional<T> get(CharSequence name, ArgumentConversionContext<T> conversionContext) {
        List<V> values = getAll(name);
        if (!values.isEmpty()) {
            boolean hasSingleEntry = values.size() == 1;
            if (hasSingleEntry) {
                final V v = values.iterator().next();
                if (conversionContext.getArgument().getType().isInstance(v)) {
                    return Optional.of((T) v);
                } else {
                    return conversionService.convert(v, conversionContext);
                }
            } else {
                return conversionService.convert(values, conversionContext);
            }
        } else {
            Argument<T> argument = conversionContext.getArgument();
            if (Map.class.isAssignableFrom(argument.getType())) {
                Argument valueType = argument.getTypeVariable("V").orElse(Argument.OBJECT_ARGUMENT);
                Map map = subMap(name.toString(), valueType);
                if (map.isEmpty()) {
                    return Optional.empty();
                } else {
                    return Optional.of((T) map);
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public List<V> getAll(CharSequence name) {
        List<V> value = values.get(name);
        if (value != null) {
            return Collections.unmodifiableList(value);
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    public V get(CharSequence name) {
        List<V> all = getAll(name);
        if (all.isEmpty()) {
            return null;
        }
        return all.get(0);
    }

    @Override
    public Set<String> names() {
        return values.keySet().stream().map(CharSequence::toString).collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public Collection<List<V>> values() {
        return Collections.unmodifiableCollection(values.values());
    }

    /**
     * Wraps the values (by default in an unmodifiable map).
     * @param values The values
     * @return The wrapped values.
     */
    protected Map<CharSequence, List<V>> wrapValues(Map<CharSequence, List<V>> values) {
        return Collections.unmodifiableMap(values);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ConvertibleMultiValuesMap<?> that = (ConvertibleMultiValuesMap<?>) o;
        return values.equals(that.values);
    }

    @Override
    public int hashCode() {
        return Objects.hash(values);
    }

    @Override
    public ConversionService getConversionService() {
        return conversionService;
    }

    @Override
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }
}