Parameters.java

package org.greenbytes.http.sfv;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * Represents the Parameters of an Item or an Inner List.
 * 
 * @see <a href=
 *      "https://www.rfc-editor.org/rfc/rfc9651.html#param">Section
 *      3.1.2 of RFC 9651</a>
 */
public class Parameters implements Map<String, Item<?>> {

    private final Map<String, Item<?>> delegate;

    /** Empty parameters instance. */
    protected static final Parameters EMPTY = new Parameters(Collections.emptyMap());

    private Parameters(Map<String, Object> value) {
        this.delegate = Collections.unmodifiableMap(checkAndTransformMap(value));
    }

    /**
     * Creates an unmodifiable {@link Parameters} instance representing the
     * specified {@code Map<String, Item>} value.
     * <p>
     * Note that the {@link Map} implementation that is used here needs to
     * iterate predictably based on insertion order, such as
     * {@link java.util.LinkedHashMap}.
     *
     * @param value
     *            a {@code Map<String, Item>} value
     * @return a {@link Parameters} representing {@code value}.
     */
    public static Parameters of(Map<String, Object> value) {
        return new Parameters(value);
    }

    /**
     * Creates an unmodifiable {@link Parameters} instance representing
     * the specified {@linkplain Object}s.
     * @param obs (needs to be an even-number of {@linkplain Object}s)
     * @return a {@link Parameters} representing {@code obs}.
     */
    public static Parameters valueOf(Object... obs) {
        if (obs.length == 1 && obs[0] instanceof Map) {
            throw new IllegalArgumentException("requires even number of arguments, got: " +
                    obs[0].getClass().getName() + " - did you mean to use 'of()'?");
        }
        if (obs.length % 2 != 0) {
            throw new IllegalArgumentException("requires even number of arguments, got: " + obs.length);
        } else {
            Map<String, Object> map = new LinkedHashMap<>();
            for (int i = 0; i < obs.length; i += 2) {
                String key = obs[i].toString();
                if (map.containsKey(key)) {
                    throw new IllegalArgumentException("key " + key + " already exists");
                }
                map.put(key, obs[i + 1]);
            }
            return of(map);
        }
    }

    /**
     * Serialize this parameter to a {@linkplain StringBuilder}
     * @param sb to serialize to
     * @return updated {@linkplain StringBuilder}
     */
    public StringBuilder serializeTo(StringBuilder sb) {
        for (Map.Entry<String, Item<?>> e : delegate.entrySet()) {
            sb.append(';').append(e.getKey());
            if (!(e.getValue().get().equals(Boolean.TRUE))) {
                sb.append('=');
                e.getValue().serializeTo(sb);
            }
        }
        return sb;
    }

    /**
     * Serialize this parameter.
     * @return serialization
     */
    public String serialize() {
        return serializeTo(new StringBuilder()).toString();
    }

    /**
     * Serialize debug information to an existing {@link StringBuilder}.
     *
     * @param sb
     *            where to serialize to
     * @param indentLevel how much to indent
     * @param classFormatter to format the classname when desires (can be a function that returns an empty string)
     * @return the {@link StringBuilder} so calls can be chained.
     */
    public StringBuilder serializeToForDebug(StringBuilder sb, int indentLevel, Function<Class, String> classFormatter) {
        if (!delegate.isEmpty()) {
            String indent = indentLevel != 0 ? String.format("%" + indentLevel + "s", "") : "";
            String classn = classFormatter.apply(this.getClass());
            sb.append(indent).append(serialize()).append(classn).append("\n");
            for (Map.Entry<String, Item<?>> e : delegate.entrySet()) {
                sb.append("  " + indent).append(e.getKey()).append(" -> ");
                e.getValue().serializeToForDebug(sb, 0, classFormatter);
            }
            return sb;
        } else {
            return sb;
        }
    }

    private static Map<String, Item<?>> checkAndTransformMap(Map<String, Object> map) {
        Map<String, Item<?>> result = new LinkedHashMap<>(
                Objects.requireNonNull(map, "Map must not be null").size());
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = Utils.checkKey(entry.getKey());
            Item<?> value = Utils.asBareItem(entry.getValue());
            if (!value.params().isEmpty()) {
                throw new IllegalArgumentException("Parameter value for '" + key + "' must be bare item (no parameters)");
            }
            result.put(entry.getKey(), value);
        }
        return result;
    }

    // delegate methods, autogenerated

    public void clear() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item<?> compute(String key,
                           BiFunction<? super String, ? super Item<?>, ? extends Item<?>> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item<?> computeIfAbsent(String key,
                                   Function<? super String, ? extends Item<?>> mappingFunction) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item<?> computeIfPresent(String key,
                                    BiFunction<? super String, ? super Item<?>, ? extends Item<?>> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    public boolean containsKey(Object key) {
        return delegate.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return delegate.containsValue(value);
    }

    public Set<Entry<String, Item<?>>> entrySet() {
        return delegate.entrySet();
    }

    @Override
    public void forEach(BiConsumer<? super String, ? super Item<?>> action) {
        delegate.forEach(action);
    }

    public Item<?> get(Object key) {
        return delegate.get(key);
    }

    @Override
    public Item<?> getOrDefault(Object key, Item<?> defaultValue) {
        return delegate.getOrDefault(key, defaultValue);
    }

    public boolean isEmpty() {
        return delegate.isEmpty();
    }

    public Set<String> keySet() {
        return delegate.keySet();
    }

    @Override
    public Item<?> merge(String key, Item<?> value,
            BiFunction<? super Item<?>, ? super Item<?>, ? extends Item<?>> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    public Item<?> put(String key, Item<?> value) {
        throw new UnsupportedOperationException();
    }

    public void putAll(Map<? extends String, ? extends Item<?>> m) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item<?> putIfAbsent(String key, Item<?> value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean remove(Object key, Object value) {
        throw new UnsupportedOperationException();
    }

    public Item<?> remove(Object key) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean replace(String key, Item<?> oldValue, Item<?> newValue) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Item<?> replace(String key, Item<?> value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void replaceAll(BiFunction<? super String, ? super Item<?>, ? extends Item<?>> function) {
        throw new UnsupportedOperationException();
    }

    public int size() {
        return delegate.size();
    }

    public Collection<Item<?>> values() {
        return delegate.values();
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Parameters)) {
            return false;
        } else {
            Parameters that = (Parameters) o;
            return Objects.equals(delegate, that.delegate);
        }
    }

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