Чудный код в классе MultiValueSet в серверах на базе OverWorld

Gaikotsu

яжпрограммист
Легенда
Победитель в номинации 2025
Часть системы
Знаток Lineage2
Орден Почета
Суровый спец
Наставник
Мыслитель
Арбитр
Авторитет форума
Оратор
Уважаемый собеседник
Легенда Истоков
Победитель в номинации 2024
Победитель в номинации 2023
Победитель в номинации 2022
Победитель в номинации 2021
Участник Новогоднего Фонда 2021
Эксперт
Знаток
Просветитель
Опора сообщества
Хранитель Пера
Куратор Данных
Медаль Благодарности
Старожил II степени
Старожил I степени
Победитель в номинации 2020
Победитель в номинации 2019
Клиент разработчик
Преподаватель
За веру и верность форуму
Победитель в номинации 2018
Медаль за активность на Форуме
За заслуги перед форумом
Web разработчик
Разработчик
За знание датапака
За знание ядра
Сообщения
2 414
Розыгрыши
0
Решения
41
Репутация
6 650
Реакции
3 625
Баллы
2 363
Хроники
  1. Chaotic Throne: High Five
  2. Homunculus
Исходники
Присутствуют
Сборка
OverWorld
Как-то раз, ковыряясь по одному делу в профайлере, отслеживающем работу запущенного сервера, обратил внимание на то что в нем сразу же после запуска и загрузки сервера наблюдается несколько миллионов перехваченных исключений вида IllegalArgumentException.
От увиденного я немного скажем так, удивился и стал копать - что это вобще за нафиг то такое и какого фига так много то?

Причина в итоге оказалась очень даже занятной.
В серверах на базе овера в commons есть такой класс как MultiValueSet и часто используемый наследный от него StatsSet.
По сути это банальный HashMap, с доп. методами позволяющими возвращать из него значения нужного типа. И используется он во многих парсерах данных тех же хмлок - предметы, скиллы, нпс и т.д. и т.п.
И как выяснилось, для некоторых типов значений там очень "творчески" сделана обработка ситуаций, когда запрашиваемого значения в мапе нет и надо вернуть значение, заданное по умолчанию.
Вот как один из примеров такого:
Java:
    public int[] getIntegerArray(final T key)
    {
        final Object val = get(key);

        if(val instanceof int[])
        {
            return (int[]) val;
        }
        if(val instanceof Number)
        {
            return new int[] { ((Number) val).intValue() };
        }
        if(val instanceof String)
        {
            final String[] vals = ((String) val).split(";");

            final int[] result = new int[vals.length];

            int i = 0;
            for(final String v : vals)
            {
                result[i++] = Integer.parseInt(v);
            }

            return result;
        }

        throw new IllegalArgumentException("Integer array required, but found: " + val + '!');
    }

    public int[] getIntegerArray(final T key, final int[] defaultArray)
    {
        try
        {
            return getIntegerArray(key);
        }
        catch(IllegalArgumentException e)
        {
            return defaultArray;
        }
    }
Да, вы правильно поняли - если к примеру значения в мапе нет или оно не подходит по формату - будет кинуто исключение IllegalArgumentException, далее оно будет перехвачено и будет возвращено дефолтное значение.
Ну и вот именно таких перехватов лично у меня в сервере при загрузке вот и набегало несколько миллионов.

не понимаю что мешало написать как-то типа так, чтобы было более адекватно и не требовало кидать исключения на такие ситуации - т.е. если значения нет, то сразу вернуть дефолтное и все
Java:
    public int[] getIntegerArray(T key)
    {
        return getIntegerArray(key, ArrayUtils.EMPTY_INT_ARRAY, defDelimiter);
    }

    public int[] getIntegerArray(T key, int[] def)
    {
        return getIntegerArray(key, def, defDelimiter);
    }

    public int[] getIntegerArray(T key, int[] def, String delimiter)
    {
        try
        {
            Object val = get(key);

            if (val == null)
                return def;

            if (val instanceof int[])
                return (int[]) val;

            if (val instanceof Number)
                return new int[] { ((Number) val).intValue() };

            if (val instanceof String)
            {
                String[] vals = ((String) val).split(delimiter);
                int[] result = new int[vals.length];
                int i = 0;

                for (String v : vals)
                    result[i++] = Integer.parseInt(v);

                return result;
            }

            throw new IllegalArgumentException("Integer array required, but found: <" + val + "> [" + key + "]");
        }
        catch (IllegalArgumentException e)
        {
            System.out.println(e.getLocalizedMessage());
            e.printStackTrace();
            return def;
        }
    }
В итоге после переписывания методов в MultiValueSet на более адекватные, количество перехватываемых и обрабатываемых исключений при загрузке сервера удалось сократить на порядки - вот чисто для примера текущая ситуация, сразу после полной загрузки сервера.
javaw_zcEMEil7fj.webp

Ну и за счет этого так же местами значительно ускорился запуск сервера, так как не приходится обрабатывать дофига кидаемых исключений. К примеру разбор хмлок скиллов, на которые как раз выпадала значительная часть этих миллионов исключений, ускорился как минимум раз 5-6. Если до правок данные скиллов разбирало около 30-60 секунд, то теперь это же делается за 5-6 секунд.

---
Данный косяк присутствует скорее всего в большинстве сборок на базе овера и даже по идее феникса, т.к. код данного класса вроде еще с тех времен тянется и мало кто в него заглядывает вобще.
 
Просто мысли в слух. А почему не отказаться от этого, и не переписать нормально? Знаний у тебя явно хватит.
 
Просто мысли в слух. А почему не отказаться от этого, и не переписать нормально? Знаний у тебя явно хватит.
Ну так я и переписал у себя.

Это все чисто информация для тех у кого сервера тоже на базе феникса/овера - с большой вероятностью у них данная проблема тоже в наличии и стоит тоже переписать код, чтобы хотя бы немного ускорить загрузку сервера.

З.Ы. косяк кстати еще в том что хоть и кидается исключение с сообщением, но при перехвате сообщение при этом не выводится, а просто тихо возвращает дефолтное значение. а в связи с этим можно словить не очевидный косяк в ситуациях когда значение задано, но задано в неправильном формате - например ожидается массив/список чисел, но вместо него например задана строка. В итоге в этой ситуации просто тихо вернет дефолтное значение, а потом сиди и гадай - почему что-то не так работает как ожидается.
 
Последнее редактирование:
Можно привести примеры где этот код используеться? Ну и интересно почему это все должно парситься подобным способом каждый раз... Я понимаю что XML такая, или скорее не предусмотрен другой подход (например можно уделить отдельный элемент скажем <intArray value="1,2,3,4" />), но какие данные там возвращаються?

Ну и в дополнение я бы сказал что вместо того что-бы просто напечатать результаты ошибки можно также (и полезнее) использовать логер, так как такиe вот ошибки могут просто уйти в никуда....
 
ну например все ноды "stat" и т.д. скилла при парсе загоняются в такую мапу и передаются в дальнейшем в конструктор класса скилла, где извлекаются и назначаются соответствующим переменным скилла
XML:
    <skill id="19013" levels="1" name="Очищение Клана" pts_name="s_pledge_clan_undebuf1">
        <stat name="icon" value="icon.skill19013" />
        <stat name="magic_type" value="special" />
        <stat name="magic_level" value="85" />
        <stat name="effect_point" value="100" />
        <stat name="hit_time" value="1" />
        <stat name="hit_cancel_time" value="0.5" />
        <stat name="cool_time" value="0.5" />
        <stat name="reuse_delay" value="1800" />
        <stat name="reuse_delay_type" value="shared" />
        <stat name="affect_range" value="1000" />
        <stat name="target" value="pledge" />
        <stat name="skill_type" value="effect" />
        <stat name="operate_type" value="A1" />
        <stat name="consume_fame" value="1500" />
        <stat name="consume_item" value="spirit_ore;300" />
        <stat name="undeletable" value="true" />
        <stat name="clan_unit" value="elite" />
        <stat name="next_action" value="none" />
        <stat name="allowed_on_olympiad" value="false" />
        <for>
            <effect name="i_dispel_by_category" params="debuff;100;10" />
        </for>
    </skill>
Java:
    protected Skill(StatsSet set)
    {
        _icon = set.getString("icon", StringUtils.EMPTY);
        _id = set.getInteger("skill_id", 0);
        _level = set.getInteger("level", 1);
        _subLevel = set.getInteger("sub_level", 0);
        _minLevel = set.getInteger("min_level", 1);
        _maxLevel = set.getInteger("max_level", 1);
        _name = set.getString("name");

        ...

ну а ошибки парса и т.д. в любом случае как минимум в лог консоли пишутся
 
Давно еще себе писал более оптимизированную реализацию. Правда под Java 21. Сразу уточняю, что операции вне контракта Map не делались атомарными даже в рамках конкурентных карт. Это осознанное архитектурное решение, а не упущение.

Java:
package ru.nts.commons.collections;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Обертка над Map, позволяющая безопасно работать с разными типами данных.
 * Поддерживает автоматическое преобразование типов при получении значений, а также
 * добавление значений, примитивных типов и Enum'ов.
 * Также реализует стандартные методы Map.
 * <br>
 * <b>ВАЖНО:</b> Даже при использовании потокобезопасной внутренней карты, консистентность данных при использовании кастомных методов не гарантируется.
 * Все манипуляции производятся с извлеченным на момент обращения значением и если другой поток изменит данные в этот момент, то это не будет учтено.
 * <br>
 * В случае, если используется стандартные методы Map, то потокобезопасность будет напрямую зависеть от типа внутренней карты.
 *
 *
 * @author Aristo
 */
@Slf4j
public class StatsSet implements Map<String, Object> {

    private final @NotNull Map<String, Object> internalMap;

    /**
     * Создает новый экземпляр {@link StatsSet}, используя фабрику для создания внутренней карты.
     *
     * @param mapFactory фабрика для создания внутренней карты
     */
    public StatsSet(@NotNull Supplier<Map<String, Object>> mapFactory) {
        this(mapFactory.get());
    }

    /**
     * Создает новый экземпляр {@link StatsSet} с заданной внутренней картой.
     *
     * @param map внутренняя карта
     */
    private StatsSet(@NotNull Map<String, Object> map) {
        internalMap = map;
    }

    /**
     * Создает новый экземпляр {@link StatsSet} и оборачивает заданную карту.
     *
     * @param map карта, которую нужно обернуть
     * @return новый экземпляр {@link StatsSet}
     */
    public static StatsSet wrap(@NotNull Map<String, Object> map) {
        return new StatsSet(map);
    }

    /**
     * Парсит строку параметров по заданным разделителям и создает {@link StatsSet}.
     * Например: (key1)(spliter2)(value1)(spliter1)(key2)(spliter2)(value2)(spliter1)(keyN)(spliter2)(valueN)
     *
     * @param params    строка параметров
     * @param splitter1 разделитель между парами ключ-значение
     * @param splitter2 разделитель между ключом и значением
     * @param mapFactory фабрика для создания внутренней карты
     * @return новый экземпляр {@link StatsSet} с данными из строки параметров
     */
    public static StatsSet parseParams(@Nullable String params, @NotNull String splitter1, @NotNull String splitter2, @NotNull Supplier<Map<String, Object>> mapFactory) {
        final Map<String, Object> map = mapFactory.get();
        if (params != null) {
            try {
                final String[] args = params.split(splitter1);

                for (String split : args) {
                    map.put(split.split(splitter2)[0], split.split(splitter2)[1]);
                }
            } catch (Exception e) {
                log.error("Can't parse params: {}", params, e);
            }
        }
        return StatsSet.wrap(map);
    }

    /**
     * Возвращает логическое значение по ключу.
     *
     * @param key ключ
     * @return логическое значение
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к логическому типу
     */
    public boolean getBooleanOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Boolean b -> b;
            case String s -> Boolean.parseBoolean(s);
            case null -> throw new IllegalArgumentException("Boolean value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Boolean value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает логическое значение по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return логическое значение или значение по умолчанию
     */
    public boolean getBooleanOrDefault(@NotNull String key, boolean defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Boolean b -> b;
            case String s -> Boolean.parseBoolean(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает байтовое значение по ключу.
     *
     * @param key ключ
     * @return байтовое значение
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к байтовому типу
     */
    public byte getByteOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.byteValue();
            case String s when NumberUtils.isParsable(s) -> Byte.parseByte(s);
            case null -> throw new IllegalArgumentException("Byte value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Byte value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает байтовое значение по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return байтовое значение или значение по умолчанию
     */
    public byte getByteOrDefault(@NotNull String key, byte defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.byteValue();
            case String s when NumberUtils.isParsable(s) -> Byte.parseByte(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает значение типа short по ключу.
     *
     * @param key ключ
     * @return значение типа short
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к типу short
     */
    public short getShortOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.shortValue();
            case String s when NumberUtils.isParsable(s) -> Short.parseShort(s);
            case null -> throw new IllegalArgumentException("Short value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Short value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает значение типа short по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return значение типа short или значение по умолчанию
     */
    public short getShortOrDefault(@NotNull String key, short defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.shortValue();
            case String s when NumberUtils.isParsable(s) -> Short.parseShort(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает целочисленное значение по ключу.
     *
     * @param key ключ
     * @return целочисленное значение
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к целочисленному типу
     */
    public int getIntOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.intValue();
            case String s when NumberUtils.isParsable(s) -> Integer.parseInt(s);
            case null -> throw new IllegalArgumentException("Integer value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Integer value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает целочисленное значение по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return целочисленное значение или значение по умолчанию
     */
    public int getIntOrDefault(@NotNull String key, int defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.intValue();
            case String s when NumberUtils.isParsable(s) -> Integer.parseInt(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает значение типа long по ключу.
     *
     * @param key ключ
     * @return значение типа long
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к типу long
     */
    public long getLongOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.longValue();
            case String s when NumberUtils.isParsable(s) -> Long.parseLong(s);
            case null -> throw new IllegalArgumentException("Long value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Long value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает значение типа long по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return значение типа long или значение по умолчанию
     */
    public long getLongOrDefault(@NotNull String key, long defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.longValue();
            case String s when NumberUtils.isParsable(s) -> Long.parseLong(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает значение типа float по ключу.
     *
     * @param key ключ
     * @return значение типа float
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к типу float
     */
    public float getFloatOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.floatValue();
            case String s when NumberUtils.isParsable(s) -> Float.parseFloat(s);
            case null -> throw new IllegalArgumentException("Float value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Float value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает значение типа float по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return значение типа float или значение по умолчанию
     */
    public float getFloatOrDefault(@NotNull String key, float defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.floatValue();
            case String s when NumberUtils.isParsable(s) -> Float.parseFloat(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает значение типа double по ключу.
     *
     * @param key ключ
     * @return значение типа double
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано к типу double
     */
    public double getDoubleOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.doubleValue();
            case String s when NumberUtils.isParsable(s) -> Double.parseDouble(s);
            case null -> throw new IllegalArgumentException("Double value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Double value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает значение типа double по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return значение типа double или значение по умолчанию
     */
    public double getDoubleOrDefault(@NotNull String key, double defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Number n -> n.doubleValue();
            case String s when NumberUtils.isParsable(s) -> Double.parseDouble(s);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает строковое значение по ключу.
     *
     * @param key ключ
     * @return строковое значение
     * @throws IllegalArgumentException если значение не найдено
     */
    public @NotNull String getStringOrThrow(@NotNull String key) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case null -> throw new IllegalArgumentException("String value required, but not found: " + key);
            case String s -> s;
            default -> String.valueOf(val);
        };
    }

    /**
     * Возвращает строковое значение по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return строковое значение или значение по умолчанию
     */
    public @Nullable String getStringOrDefault(@NotNull String key, @Nullable String defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case null -> defaultValue;
            case String s -> s;
            default -> String.valueOf(val);
        };
    }

    /**
     * Возвращает значение перечисления по ключу.
     *
     * @param key       ключ
     * @param enumClass класс перечисления
     * @param <T>       тип перечисления
     * @return значение перечисления
     * @throws IllegalArgumentException если значение не найдено, не может быть преобразовано к типу перечисления или класс не является перечислением
     */
    public <T extends Enum<T>> @NotNull T getEnumOrThrow(@NotNull String key, @NotNull Class<T> enumClass) throws IllegalArgumentException {
        if (!enumClass.isEnum()) {
            throw new IllegalArgumentException("Invalid enum class: " + enumClass);
        }
        final Object val = internalMap.get(key);
        return switch (val) {
            case Object o when enumClass.isInstance(o) -> enumClass.cast(o);
            case String s -> Enum.valueOf(enumClass, s);
            case null, default -> throw new IllegalArgumentException("Enum value of type " + enumClass.getName() + " required, but not found: " + key);
        };
    }

    /**
     * Возвращает значение перечисления по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key        ключ
     * @param enumClass  класс перечисления
     * @param defaultValue значение по умолчанию
     * @param <T>        тип перечисления
     * @return значение перечисления или значение по умолчанию
     * @throws IllegalArgumentException если класс не является перечислением
     */
    public @Nullable <T extends Enum<T>> T getEnumOrDefault(@NotNull String key, @NotNull Class<T> enumClass, @Nullable T defaultValue, @Nullable String start, @Nullable String end) {
        if (!enumClass.isEnum()) {
            throw new IllegalArgumentException("Invalid enum class: " + enumClass);
        }
        final Object val = internalMap.get(key);
        return switch (val) {
            case Object o when enumClass.isInstance(o) -> enumClass.cast(o);
            case String s -> Enum.valueOf(enumClass, cleanse(s, start, end));
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает необработанный объект по ключу.
     *
     * @param key ключ
     * @return необработанный объект или null, если ключ не найден
     */
    public @Nullable Object getRawObject(@NotNull String key) {
        return internalMap.get(key);
    }

    /**
     * Возвращает объект по ключу, приведенный к заданному типу.
     *
     * @param key  ключ
     * @param type класс, к которому нужно привести объект
     * @param <A>  тип объекта
     * @return объект приведенный к заданному типу
     * @throws IllegalArgumentException если значение не найдено или не может быть приведено к заданному типу
     */
    public <A> @NotNull A getObjectOrThrow(@NotNull String key, @NotNull Class<A> type) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Object o when type.isInstance(o) -> type.cast(o);
            case null -> throw new IllegalArgumentException("Object value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Object value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает объект по ключу, приведенный к заданному типу, или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param type         класс, к которому нужно привести объект
     * @param defaultValue значение по умолчанию
     * @param <A>          тип объекта
     * @return объект, приведенный к заданному типу, или значение по умолчанию
     */
    public <A> @Nullable A getObjectOrDefault(@NotNull String key, @NotNull Class<A> type, @Nullable A defaultValue) {
        final Object val = internalMap.get(key);
        return switch (val) {
            case Object o when type.isInstance(o) -> type.cast(o);
            case null, default -> defaultValue;
        };
    }

    /**
     * Возвращает массив логических значений по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив логических значений
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив логических значений
     */
    public boolean @NotNull [] getParsedBooleanArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case boolean[] v -> v;
            case Boolean[] v -> ArrayUtils.toPrimitive(v);
            case Boolean v -> new boolean[]{v};
            case String s -> ArrayUtils.toPrimitive(Arrays.stream(cleanse(s, start, end).split(splitOn)).map(Boolean::parseBoolean).toArray(Boolean[]::new));
            case null -> throw new IllegalArgumentException("Boolean value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Boolean value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив символов по ключу.
     *
     * @param key   ключ
     * @param start начальный символ для очистки строки
     * @param end   конечный символ для очистки строки
     * @return массив символов
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив символов
     */
    public char @NotNull [] getParsedCharArrayOrThrow(@NotNull String key, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case char[] v -> v;
            case Character[] v -> ArrayUtils.toPrimitive(v);
            case Character v -> new char[]{v};
            case String s -> cleanse(s, start, end).toCharArray();
            case null -> throw new IllegalArgumentException("Character value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Character value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив байтов по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив байтов
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив байтов
     */
    public byte @NotNull [] getParsedByteArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case byte[] v -> v;
            case Byte[] v -> ArrayUtils.toPrimitive(v);
            case Byte v -> new byte[]{v};
            case String s -> ArrayUtils.toPrimitive(Arrays.stream(cleanse(s, start, end).split(splitOn)).map(Byte::parseByte).toArray(Byte[]::new));
            case null -> throw new IllegalArgumentException("Byte value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Byte value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив значений типа short по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив значений типа short
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив значений типа short
     */
    public short @NotNull [] getParsedShortArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case short[] a -> a;
            case Short[] a -> ArrayUtils.toPrimitive(a);
            case Short v -> new short[]{v};
            case String s -> ArrayUtils.toPrimitive(Arrays.stream(cleanse(s, start, end).split(splitOn)).map(Short::parseShort).toArray(Short[]::new));
            case null -> throw new IllegalArgumentException("Short value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Short value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив целых чисел по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив целых чисел
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив целых чисел
     */
    public int @NotNull [] getParsedIntArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case int[] a -> a;
            case Integer[] a -> ArrayUtils.toPrimitive(a);
            case Integer v -> new int[]{v};
            case String s -> Arrays.stream(cleanse(s, start, end).split(splitOn)).mapToInt(Integer::parseInt).toArray();
            case null -> throw new IllegalArgumentException("Integer value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Integer value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив значений типа long по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив значений типа long
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив значений типа long
     */
    public long @NotNull [] getParsedLongArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case long[] a -> a;
            case Long[] a -> ArrayUtils.toPrimitive(a);
            case Long v -> new long[]{v};
            case String s -> Arrays.stream(cleanse(s, start, end).split(splitOn)).mapToLong(Long::parseLong).toArray();
            case null -> throw new IllegalArgumentException("Long value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Long value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив значений типа float по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив значений типа float
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив значений типа float
     */
    public float @NotNull [] getParsedFloatArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case float[] a -> a;
            case Float[] a -> ArrayUtils.toPrimitive(a);
            case Float v -> new float[]{v};
            case String s -> ArrayUtils.toPrimitive(Arrays.stream(cleanse(s, start, end).split(splitOn)).map(Float::parseFloat).toArray(Float[]::new));
            case null -> throw new IllegalArgumentException("Float value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Float value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Возвращает массив значений типа double по ключу.
     *
     * @param key     ключ
     * @param splitOn разделитель для парсинга строки в массив
     * @param start начальный символ для очистки строки
     * @param end конечный символ для очистки строки
     * @return массив значений типа double
     * @throws IllegalArgumentException если значение не найдено или не может быть преобразовано в массив значений типа double
     */
    public double @NotNull [] getParsedDoubleArrayOrThrow(@NotNull String key, @NotNull String splitOn, @Nullable String start, @Nullable String end) throws IllegalArgumentException {
        final Object val = internalMap.get(key);
        return switch (val) {
            case double[] a -> a;
            case Double[] a -> ArrayUtils.toPrimitive(a);
            case Double v -> new double[]{v};
            case String s -> Arrays.stream(cleanse(s, start, end).split(splitOn)).mapToDouble(Double::parseDouble).toArray();
            case null -> throw new IllegalArgumentException("Double value required, but not found: " + key);
            default -> throw new IllegalArgumentException("Double value required, but not specified: " + key + " = " + val);
        };
    }

    /**
     * Помещает объект в карту по ключу, если значение не null.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putObject(@NotNull String key, @Nullable Object value) {
        if (value == null) {
            return;
        }
        internalMap.put(key, value);
    }

    /**
     * Помещает логическое значение в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putBoolean(@NotNull String key, boolean value) {
        internalMap.put(key, value ? Boolean.TRUE : Boolean.FALSE);
    }

    /**
     * Помещает байтовое значение в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putByte(@NotNull String key, byte value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает значение типа short в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putShort(@NotNull String key, short value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает целочисленное значение в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putInt(@NotNull String key, int value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает значение типа long в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putLong(@NotNull String key, long value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает значение типа float в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putFloat(@NotNull String key, float value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает значение типа double в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putDouble(@NotNull String key, double value) {
        internalMap.put(key, value);
    }

    /**
     * Помещает строковое значение в карту по ключу, если значение не null.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putString(@NotNull String key, @Nullable String value) {
        if (value == null) {
            return;
        }
        internalMap.put(key, value);
    }

    /**
     * Помещает значение перечисления в карту по ключу, если значение не null.
     *
     * @param key   ключ
     * @param value значение
     */
    public void putEnum(@NotNull String key, @Nullable Enum<?> value) {
        if (value == null) {
            return;
        }
        internalMap.put(key, value);
    }

    /**
     * Возвращает строковое представление карты.
     *
     * @return строковое представление карты
     */
    @Override
    public String toString() {
        return "MultiTypesMap{wrapped=" + internalMap + "}";
    }

    /**
     * Возвращает набор ключей карты.
     *
     * @return набор ключей карты
     */
    @Override
    public @NotNull Set<String> keySet() {
        return internalMap.keySet();
    }

    /**
     * Возвращает коллекцию значений карты.
     *
     * @return коллекция значений карты
     */
    @Override
    public @NotNull Collection<Object> values() {
        return internalMap.values();
    }

    /**
     * Возвращает набор записей карты.
     *
     * @return набор записей карты
     */
    @Override
    public @NotNull Set<Entry<String, Object>> entrySet() {
        return internalMap.entrySet();
    }

    /**
     * Очищает карту.
     */
    @Override
    public void clear() {
        internalMap.clear();
    }

    /**
     * Возвращает размер карты.
     *
     * @return размер карты
     */
    @Override
    public int size() {
        return internalMap.size();
    }

    /**
     * Проверяет, является ли карта пустой.
     *
     * @return true, если карта пуста, иначе false
     */
    @Override
    public boolean isEmpty() {
        return internalMap.isEmpty();
    }

    /**
     * Проверяет, содержит ли карта ключ.
     *
     * @param key ключ
     * @return true, если карта содержит ключ, иначе false
     */
    @Override
    public boolean containsKey(Object key) {
        return internalMap.containsKey(key);
    }

    /**
     * Проверяет, содержит ли карта значение.
     *
     * @param value значение
     * @return true, если карта содержит значение, иначе false
     */
    @Override
    public boolean containsValue(Object value) {
        return internalMap.containsValue(value);
    }

    /**
     * Возвращает значение по ключу.
     *
     * @param key ключ
     * @return значение по ключу или null, если ключ не найден
     */
    @Override
    public @Nullable Object get(Object key) {
        return internalMap.get(key);
    }

    /**
     * Помещает значение в карту по ключу.
     *
     * @param key   ключ
     * @param value значение
     * @return предыдущее значение по ключу или null, если ключ не был найден
     */
    @Override
    public @Nullable Object put(String key, Object value) {
        return internalMap.put(key, value);
    }

    /**
     * Копирует все записи из другой карты в текущую.
     *
     * @param map карта, из которой нужно скопировать записи
     */
    @Override
    public void putAll(@NotNull Map<? extends String, ?> map) {
        if (Objects.equals(internalMap, map) || map.isEmpty()) {
            return;
        }
        internalMap.putAll(map);
    }

    /**
     * Удаляет запись по ключу.
     *
     * @param key ключ
     * @return удаленное значение или null, если ключ не был найден
     */
    @Override
    public Object remove(Object key) {
        return internalMap.remove(key);
    }

    /**
     * Возвращает значение по ключу или значение по умолчанию, если ключ не найден.
     *
     * @param key          ключ
     * @param defaultValue значение по умолчанию
     * @return значение по ключу или значение по умолчанию
     */
    @Override
    public Object getOrDefault(Object key, Object defaultValue) {
        return internalMap.getOrDefault(key, defaultValue);
    }

    /**
     * Выполняет заданное действие для каждой записи в карте.
     *
     * @param action действие, которое нужно выполнить
     */
    @Override
    public void forEach(BiConsumer<? super String, ? super Object> action) {
        internalMap.forEach(action);
    }

    /**
     * Заменяет все значения в карте результатами вызова заданной функции.
     *
     * @param function функция, которую нужно применить к значениям
     */
    @Override
    public void replaceAll(BiFunction<? super String, ? super Object, ?> function) {
        internalMap.replaceAll(function);
    }

    /**
     * Помещает значение в карту по ключу, если ключ отсутствует.
     *
     * @param key   ключ
     * @param value значение
     * @return предыдущее значение по ключу или null, если ключ не был найден
     */
    @Override
    public @Nullable Object putIfAbsent(String key, Object value) {
        return internalMap.putIfAbsent(key, value);
    }

    /**
     * Удаляет запись по ключу и значению.
     *
     * @param key   ключ
     * @param value значение
     * @return true, если запись была удалена, иначе false
     */
    @Override
    public boolean remove(Object key, Object value) {
        return internalMap.remove(key, value);
    }

    /**
     * Заменяет значение по ключу
     *
     * @param key      ключ
     * @param oldValue старое значение
     * @param newValue новое значение
     * @return true, если значение было заменено, иначе false
     */
    @Override
    public boolean replace(String key, Object oldValue, Object newValue) {
        return internalMap.replace(key, oldValue, newValue);
    }

    /**
     * Заменяет значение по ключу.
     *
     * @param key   ключ
     * @param value значение
     * @return предыдущее значение по ключу или null, если ключ не был найден
     */
    @Override
    public @Nullable Object replace(String key, Object value) {
        return internalMap.replace(key, value);
    }

    /**
     * Вычисляет значение для ключа, если ключ отсутствует, используя заданную функцию.
     *
     * @param key             ключ
     * @param mappingFunction функция для вычисления значения
     * @return вычисленное значение или null, если функция вернула null
     */
    @Override
    public Object computeIfAbsent(String key, @NotNull Function<? super String, ?> mappingFunction) {
        return internalMap.computeIfAbsent(key, mappingFunction);
    }

    /**
     * Вычисляет новое значение для ключа, если ключ присутствует, используя заданную функцию.
     *
     * @param key               ключ
     * @param remappingFunction функция для вычисления нового значения
     * @return новое значение или null, если функция вернула null
     */
    @Override
    public Object computeIfPresent(String key, @NotNull BiFunction<? super String, ? super Object, ?> remappingFunction) {
        return internalMap.computeIfPresent(key, remappingFunction);
    }

    /**
     * Вычисляет новое значение для ключа, используя заданную функцию.
     *
     * @param key               ключ
     * @param remappingFunction функция для вычисления нового значения
     * @return новое значение или null, если функция вернула null
     */
    @Override
    public Object compute(String key, @NotNull BiFunction<? super String, ? super @Nullable Object, ?> remappingFunction) {
        return internalMap.compute(key, remappingFunction);
    }

    /**
     * Объединяет значение по ключу с заданным значением, используя заданную функцию.
     *
     * @param key               ключ
     * @param value             значение
     * @param remappingFunction функция для объединения значений
     * @return новое значение или null, если функция вернула null
     */
    @Override
    public Object merge(String key, @NotNull Object value, @NotNull BiFunction<? super Object, ? super Object, ?> remappingFunction) {
        return internalMap.merge(key, value, remappingFunction);
    }

    /**
     * Удаляет начальные и конечные подстроки из строки, если они присутствуют.
     *
     * @param string строка
     * @param start  начальная подстрока
     * @param end    конечная подстрока
     * @return очищенная строка
     */
    private static String cleanse(String string, String start, String end) {
        if (string != null && start != null && end != null) {
            if (string.startsWith(start) && string.endsWith(end)) {
                string = string.substring(start.length(), string.length() - end.length());
            }
        }
        return string;
    }
}
 

Похожие темы

Назад
Сверху Снизу