MapKit.java

package space.sunqian.common.collect;

import space.sunqian.annotations.Immutable;
import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.annotations.OutParam;
import space.sunqian.common.Fs;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Utilities for {@link Map}.
 *
 * @author sunqian
 */
public class MapKit {

    /**
     * Returns whether the given map is null or empty.
     *
     * @param map the given map
     * @return whether the given map is null or empty
     */
    public static boolean isEmpty(@Nullable Map<?, ?> map) {
        if (map == null) {
            return true;
        }
        return map.isEmpty();
    }

    /**
     * Returns whether the given map is not null and empty.
     *
     * @param map the given map
     * @return whether the given map is not null and empty
     */
    public static boolean isNotEmpty(@Nullable Map<?, ?> map) {
        return !isEmpty(map);
    }

    /**
     * Returns a new immutable map of which content is added from the given array.
     * <p>
     * Every two elements of the array form a key-value pair, that means, the {@code array[0]} and {@code array[1]} will
     * be the first key-value pair, the {@code array[2]} and {@code array[3]} will be the second key-value pair, and so
     * on. If the length of the array is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     * <p>
     * The behavior of this method is equivalent to:
     * <pre>{@code
     *  return Collections.unmodifiableMap(linkedHashMap(array));
     *  }</pre>
     *
     * @param array the given array
     * @param <K>   the key type
     * @param <V>   the value type
     * @return a new {@link HashMap} initialing with the given array
     */
    public static <K, V> @Nonnull @Immutable Map<K, V> map(Object @Nonnull ... array) {
        return Collections.unmodifiableMap(linkedHashMap(array));
    }

    /**
     * Returns a new {@link HashMap} initialing with the given array.
     * <p>
     * Every two elements of the array form a key-value pair, that means, the {@code array[0]} and {@code array[1]} will
     * be the first key-value pair, the {@code array[2]} and {@code array[3]} will be the second key-value pair, and so
     * on. If the length of the array is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param array the given array
     * @param <K>   the key type
     * @param <V>   the value type
     * @return a new {@link HashMap} initialing with the given array
     */
    public static <K, V> @Nonnull HashMap<K, V> hashMap(Object @Nonnull ... array) {
        return putAll(new HashMap<>(), array);
    }

    /**
     * Returns a new {@link LinkedHashMap} initialing with the given array
     * <p>
     * Every two elements of the array form a key-value pair, that means, the {@code array[0]} and {@code array[1]} will
     * be the first key-value pair, the {@code array[2]} and {@code array[3]} will be the second key-value pair, and so
     * on. If the length of the array is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param array the given array
     * @param <K>   the key type
     * @param <V>   the value type
     * @return a new {@link HashMap} initialing with the given array
     */
    public static <K, V> @Nonnull LinkedHashMap<K, V> linkedHashMap(Object @Nonnull ... array) {
        return putAll(new LinkedHashMap<>(), array);
    }

    /**
     * Returns a new immutable map of which content is added from the given iterable.
     * <p>
     * Every two elements of the iterable form a key-value pair, that means, the {@code it[0]} and {@code it[1]} will be
     * the first key-value pair, the {@code it[2]} and {@code it[3]} will be the second key-value pair, and so on. If
     * the length of the iterable is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put. This is the iterable version of {@link #map(Object...)}.
     *
     * @param it  the given iterable
     * @param <K> the key type
     * @param <V> the value type
     * @return a new {@link HashMap} initialing with the given iterable
     */
    public static <K, V> @Nonnull @Immutable Map<K, V> toMap(@Nonnull Iterable<?> it) {
        Object[] array = CollectKit.toArray(it);
        return map(array);
    }

    /**
     * Returns a new immutable map of which entries are put from the given map, and all key-value pairs are mapped from
     * the old type to the new type during the put operation. The behavior is equivalent to:
     * <pre>{@code
     * return Collections.unmodifiableMap(
     *     map.entrySet().stream().collect(Collectors.toMap(
     *         entry -> keyMapper.apply(entry.getKey()),
     *         entry -> valueMapper.apply(entry.getValue()),
     *         (v1, v2) -> v2,
     *         LinkedHashMap::new
     *     ))
     * );
     * }</pre>
     *
     * @param map         the given map
     * @param keyMapper   the key mapper
     * @param valueMapper the value mapper
     * @param <KO>        the old key type
     * @param <VO>        the old value type
     * @param <KN>        the new key type
     * @param <VN>        the new value type
     * @return a new immutable map of which entries are put from the given map
     */
    public static <KO, VO, KN, VN> @Nonnull @Immutable Map<KN, VN> toMap(
        @Nonnull Map<KO, VO> map,
        @Nonnull Function<? super KO, ? extends KN> keyMapper,
        @Nonnull Function<? super VO, ? extends VN> valueMapper
    ) {
        return Collections.unmodifiableMap(
            map.entrySet().stream().collect(Collectors.toMap(
                entry -> keyMapper.apply(entry.getKey()),
                entry -> valueMapper.apply(entry.getValue()),
                (v1, v2) -> v2,
                LinkedHashMap::new
            ))
        );
    }

    /**
     * Returns a new {@link HashMap} initialing with the given iterable.
     * <p>
     * Every two elements of the iterable form a key-value pair, that means, the {@code it[0]} and {@code it[1]} will be
     * the first key-value pair, the {@code it[2]} and {@code it[3]} will be the second key-value pair, and so on. If
     * the length of the iterable is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param it  the given iterable
     * @param <K> the key type
     * @param <V> the value type
     * @return a new {@link HashMap} initialing with the given iterable
     */
    public static <K, V> @Nonnull HashMap<K, V> toHashMap(@Nonnull Iterable<?> it) {
        Object[] array = CollectKit.toArray(it);
        return hashMap(array);
    }

    /**
     * Returns a new {@link LinkedHashMap} initialing with the given iterable.
     * <p>
     * Every two elements of the iterable form a key-value pair, that means, the {@code it[0]} and {@code it[1]} will be
     * the first key-value pair, the {@code it[2]} and {@code it[3]} will be the second key-value pair, and so on. If
     * the length of the iterable is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param it  the given iterable
     * @param <K> the key type
     * @param <V> the value type
     * @return a new {@link HashMap} initialing with the given iterable
     */
    public static <K, V> @Nonnull LinkedHashMap<K, V> toLinkedHashMap(@Nonnull Iterable<?> it) {
        Object[] array = CollectKit.toArray(it);
        return linkedHashMap(array);
    }

    /**
     * Puts all elements from the given array into the given map and returns the given map.
     * <p>
     * Every two elements of the array form a key-value pair, that means, the {@code array[0]} and {@code array[1]} will
     * be the first key-value pair, the {@code array[2]} and {@code array[3]} will be the second key-value pair, and so
     * on. If the length of the array is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param map   the given map
     * @param array the given array
     * @param <K>   the key type
     * @param <V>   the value type
     * @param <M>   the type of the given map
     * @return the given map
     */
    public static <K, V, M extends Map<K, V>> @Nonnull M putAll(
        @Nonnull @OutParam M map,
        Object @Nonnull ... array
    ) {
        int end = array.length / 2 * 2;
        int i = 0;
        while (i < end) {
            K key = Fs.as(array[i++]);
            V value = Fs.as(array[i++]);
            map.put(key, value);
        }
        if (end < array.length) {
            map.put(Fs.as(array[end]), null);
        }
        return map;
    }

    /**
     * Puts all elements from the given iterable into the given map and returns the given map.
     * <p>
     * Every two elements of the iterable form a key-value pair, that means, the {@code it[0]} and {@code it[1]} will be
     * the first key-value pair, the {@code it[2]} and {@code it[3]} will be the second key-value pair, and so on. If
     * the length of the iterable is odd and the last key cannot match the value, then the last pair will be the
     * key-{@code null} pair to put.
     *
     * @param map the given map
     * @param it  the given iterable
     * @param <K> the key type
     * @param <V> the value type
     * @param <M> the type of the given map
     * @return the given map
     */
    public static <K, V, M extends Map<K, V>> @Nonnull M putAll(
        @Nonnull @OutParam M map,
        @Nonnull Iterable<?> it
    ) {
        Iterator<?> iterator = it.iterator();
        while (iterator.hasNext()) {
            K key = Fs.as(iterator.next());
            V value = iterator.hasNext() ? Fs.as(iterator.next()) : null;
            map.put(key, value);
        }
        return map;
    }

    /**
     * Resolves the final value in a chain. For example, a map contains: [{@code A -> B, B -> C, C -> D}],
     * {@code resolveChain(map, A, stack)} will return D.
     * <p>
     * This method gets the value with the given key by {@link Map#get(Object)}, and then uses that value as the next
     * key to get the next value. This loop continues until the get method returns null when a value is used as the key,
     * and that value will be returned. If the given key doesn't map a value, or the loop is infinite, it will return
     * null.
     *
     * @param map   the given map
     * @param key   the given key
     * @param stack the stack to check infinite loop
     * @param <T>   the value type
     * @return the final value in a chain
     */
    public static <T> @Nullable T resolveChain(@Nonnull Map<?, T> map, T key, @Nonnull Set<T> stack) {
        if (!map.containsKey(key)) {
            return null;
        }
        stack.add(key);
        T nextKey = map.get(key);
        while (true) {
            if (stack.contains(nextKey)) {
                return null;
            }
            if (!map.containsKey(nextKey)) {
                return nextKey;
            }
            stack.add(nextKey);
            nextKey = map.get(nextKey);
        }
    }

    /**
     * Returns a new immutable {@link Map.Entry} with the given key and value.
     *
     * @param key   the given key
     * @param value the given value
     * @param <K>   the key type
     * @param <V>   the value type
     * @return a new immutable {@link Map.Entry} with the given key and value
     */
    public static <K, V> Map.@Nonnull Entry<K, V> entry(K key, V value) {
        return new Map.Entry<K, V>() {
            @Override
            public K getKey() {
                return key;
            }

            @Override
            public V getValue() {
                return value;
            }

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

    private MapKit() {
    }
}