Utils.java

package org.greenbytes.http.sfv;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Common utility methods.
 */
public class Utils {

    private Utils() {
    }

    /**
     * Check for character to be a decimal digit.
     * @param c character to check
     * @return {@code true} if and only if a digit
     */
    protected static boolean isDigit(char c) {
        return c >= '0' && c <= '9';
    }

    /**
     * Check for character to be lowercase ASCII alpha character.
     * @param c character to check
     * @return {@code true} if and only if lowercase ASCII alpha
     */
    protected static boolean isLcAlpha(char c) {
        return (c >= 'a' && c <= 'z');
    }

    /**
     * Check for character to be lowercase or uppercase ASCII alpha character.
     * @param c character to check
     * @return {@code true} if and only if lowercase or uppercase ASCII alpha
     */
    protected static boolean isAlpha(char c) {
        return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
    }

    /**
     * Checks <a href="https://www.rfc-editor.org/rfc/rfc9651.html#dictionaries">key</a> for validity.
     * @param value to check
     * @return checked value
     * @throws IllegalArgumentException when invalid
     */
    protected static String checkKey(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("Key can not be null or empty");
        }
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            if ((i == 0 && (c != '*' && !isLcAlpha(c)))
                    || !(isLcAlpha(c) || isDigit(c) || c == '_' || c == '-' || c == '.' || c == '*')) {
                throw new IllegalArgumentException(
                        String.format("Invalid character in key at position %d: '%c' (0x%04x)", i, c, (int) c));
            }
        }
        return value;
    }

    /**
     * Checks all <a href="https://www.rfc-editor.org/rfc/rfc9651.html#dictionaries">keys</a>
     * in {@linkplain Map} for validity
     * @param value map to check
     * @return checked map
     * @throws IllegalArgumentException when invalid key found
     */
    protected static Map<String, ListElement<?>> checkKeys(Map<String, ListElement<?>> value) {
        for (String key : Objects.requireNonNull(value, "value must not be null").keySet()) {
            checkKey(key);
        }
        return value;
    }

    /**
     * Converts an {@linkplain Object} to an {@link Item} (on a best-effort basis).
     * @param o to convert
     * @return converted to {@linkplain Item}
     * @throws IllegalArgumentException when it can't be converted
     */
    protected static Item<?> asBareItem(Object o) {
        if (o instanceof Item) {
            if (o instanceof Parameterizable) {
                Parameterizable p = ((Parameterizable)o);
                if (!p.params().isEmpty()) {
                    throw new IllegalArgumentException("Can't map value " + o + " (" + o.getClass() + "): carries parameters.");
                }
            }
        }
        return asItem(o);
    }

    /**
     * Converts an {@linkplain Object} to an {@link Item} (on a best-effort basis).
     * <p>
     * Currently mapped:
     * <ul>
     *     <li>{@linkplain Item} → {@linkplain Item}</li>
     *     <li>{@linkplain Integer} → {@linkplain IntegerItem}</li>
     *     <li>{@linkplain Long} → {@linkplain IntegerItem}</li>
     *     <li>{@linkplain String} → {@linkplain StringItem} or {@linkplain DisplayStringItem}</li>
     *     <li>{@linkplain Boolean} → {@linkplain BooleanItem}</li>
     *     <li>{@code byte[]} → {@linkplain ByteSequenceItem}</li>
     *     <li>{@linkplain BigDecimal} → {@linkplain DecimalItem}</li>
     *     <li>{@linkplain Double} → {@linkplain DecimalItem}</li>
     *     <li>{@linkplain Float} → {@linkplain DecimalItem}</li>
     * </ul>
     * Same as {@linkplain #asBareItem(Object)}, but allowing {@linkplain Parameters}
     * @param o to convert
     * @return converted to {@linkplain Item}
     * @throws IllegalArgumentException when it can't be converted
     */
    protected static Item<?> asItem(Object o) {
        if (o instanceof Item) {
            return (Item<?>) o;
        } else if (o instanceof Integer) {
            return IntegerItem.of(((Integer) o).longValue());
        } else if (o instanceof Long) {
            return IntegerItem.of((Long) o);
        } else if (o instanceof String) {
            try {
                return StringItem.of((String) o);
            } catch (IllegalArgumentException ia) {
                return DisplayStringItem.valueOf((String) o);
            }
        } else if (o instanceof Boolean) {
            return BooleanItem.of((Boolean) o);
        } else if (o instanceof byte[]) {
            return ByteSequenceItem.valueOf((byte[]) o);
        } else if (o instanceof BigDecimal) {
            return DecimalItem.valueOf((BigDecimal)o);
        } else if (o instanceof Double) {
            return DecimalItem.valueOf(BigDecimal.valueOf((Double)o));
        } else if (o instanceof Float) {
            return DecimalItem.valueOf(BigDecimal.valueOf((Float)o));
        } else {
            throw new IllegalArgumentException("Can't map value " + o.toString() + " (" + o.getClass() + ")");
        }
    }

    /**
     * Converts an {@linkplain Object} to an {@linkplain List} of {@linkplain Item}s
     * (on a best-effort basis).
     * Same as {@linkplain #asItem(Object)}, but also allowing {@linkplain InnerList}
     * @param o to convert
     * @return convert to {@linkplain ListElement}
     * @throws IllegalArgumentException when it can't be converted
     */
    protected static ListElement<?> asListElement(Object o) {
        if (o instanceof InnerList) {
            return (InnerList) o;
        } else {
            return asItem(o);
        }
    }
}