ConvertKit.java

package space.sunqian.fs.object.convert;

import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.annotation.RetainedParam;
import space.sunqian.fs.base.date.DateFormatter;
import space.sunqian.fs.base.number.NumFormatter;
import space.sunqian.fs.base.option.Option;
import space.sunqian.fs.base.option.OptionKit;
import space.sunqian.fs.base.value.SimpleKey;
import space.sunqian.fs.cache.SimpleCache;
import space.sunqian.fs.object.annotation.DatePattern;
import space.sunqian.fs.object.annotation.NumPattern;
import space.sunqian.fs.object.schema.ObjectProperty;

import java.lang.annotation.Annotation;
import java.time.ZoneId;

/**
 * Utilities for object conversion.
 *
 * @author sunqian
 */
public class ConvertKit {

    /**
     * Returns a {@link Option} of {@link DateFormatter} for the given {@link DatePattern}. This method is based on a
     * soft-reference cache (from {@link SimpleCache#ofSoft()}), so the same {@link Option} instance could be returned
     * for the same pattern and zone id.
     *
     * @param datePattern the pattern of the date formatter
     * @return the {@link Option} of {@link DateFormatter} for the given {@link DatePattern}
     */
    public static @Nonnull Option<@Nonnull ConvertOption, @Nonnull DateFormatter> getDateFormatterOption(
        @Nonnull DatePattern datePattern) {
        ZoneId zoneId;
        if ("".equals(datePattern.zoneId())) {
            zoneId = ZoneId.systemDefault();
        } else {
            zoneId = ZoneId.of(datePattern.zoneId());
        }
        return getDateFormatterOption(datePattern.value(), zoneId);
    }

    /**
     * Returns a {@link Option} of {@link DateFormatter} for the given pattern and zone id. This method is based on a
     * soft-reference cache (from {@link SimpleCache#ofSoft()}), so the same {@link Option} instance could be returned
     * for the same pattern and zone id.
     *
     * @param pattern the pattern of the date formatter
     * @param zoneId  the zone id of the date formatter
     * @return the {@link Option} of {@link DateFormatter} for the given pattern and zone id
     */
    public static @Nonnull Option<@Nonnull ConvertOption, @Nonnull DateFormatter> getDateFormatterOption(
        @Nonnull String pattern, @Nonnull ZoneId zoneId) {
        return DateFormatterCache.INST.get(pattern, zoneId);
    }

    /**
     * Returns a {@link Option} of {@link NumFormatter} for the given {@link NumPattern}. This method is based on a
     * soft-reference cache (from {@link SimpleCache#ofSoft()}), so the same {@link Option} instance could be returned
     * for the same pattern.
     *
     * @param numPattern the pattern of the number formatter
     * @return the {@link Option} of {@link NumFormatter} for the given {@link NumPattern}
     */
    public static @Nonnull Option<@Nonnull ConvertOption, @Nonnull NumFormatter> getNumFormatterOption(
        @Nonnull NumPattern numPattern) {
        return getNumFormatterOption(numPattern.value());
    }

    /**
     * Returns a {@link Option} of {@link NumFormatter} for the given pattern. This method is based on a soft-reference
     * cache (from {@link SimpleCache#ofSoft()}), so the same {@link Option} instance could be returned for the same
     * pattern.
     *
     * @param pattern the pattern of the number formatter
     * @return the {@link Option} of {@link NumFormatter} for the given pattern
     */
    public static @Nonnull Option<@Nonnull ConvertOption, @Nonnull NumFormatter> getNumFormatterOption(
        @Nonnull String pattern) {
        return NumFormatterCache.INST.get(pattern);
    }

    /**
     * Merges the default options with the date formatter and number formatter if they are not null.
     * <p>
     * If both date pattern and number pattern are {@code null}, the default options are returned. If the date pattern
     * is {@code null}, the number formatter is merged with the default options in a new array. If the number pattern is
     * {@code null}, the date formatter is merged with the default options in a new array. If both date pattern and
     * number pattern are not {@code null}, the date formatter and number formatter are merged with the default options
     * in a new array.
     *
     * @param defaultOptions the default options
     * @param datePattern    the date pattern
     * @param numPattern     the number pattern
     * @return the merged options
     */
    public static @Nonnull Option<?, ?> @Nonnull [] mergeOptions(
        @Nonnull Option<?, ?> @Nonnull @RetainedParam [] defaultOptions,
        @Nullable DatePattern datePattern,
        @Nullable NumPattern numPattern
    ) {
        if (datePattern == null) {
            if (numPattern == null) {
                return defaultOptions;
            } else {
                Option<ConvertOption, NumFormatter> numFormatter = ConvertKit.getNumFormatterOption(numPattern);
                return OptionKit.mergeOption(defaultOptions, numFormatter);
            }
        } else {
            Option<ConvertOption, DateFormatter> dateFormatter = ConvertKit.getDateFormatterOption(datePattern);
            if (numPattern == null) {
                return OptionKit.mergeOption(defaultOptions, dateFormatter);
            } else {
                Option<ConvertOption, NumFormatter> numFormatter = ConvertKit.getNumFormatterOption(numPattern);
                return OptionKit.mergeOptions(defaultOptions, dateFormatter, numFormatter);
            }
        }
    }

    /**
     * Returns the annotation for the given type from the source property if it exists, otherwise from the destination
     * property.
     *
     * @param annotationType the type of the annotation
     * @param srcProperty    the source property
     * @param dstProperty    the destination property
     * @param <A>            the type of the annotation
     * @return the annotation for the given type from the source property if it exists, otherwise from the destination
     * property
     */
    public static <A extends Annotation> @Nullable A getAnnotation(
        @Nonnull Class<A> annotationType,
        @Nonnull ObjectProperty srcProperty,
        @Nonnull ObjectProperty dstProperty
    ) {
        A srcAnnotation = srcProperty.getAnnotation(annotationType);
        A dstAnnotation = dstProperty.getAnnotation(annotationType);
        if (dstAnnotation == null) {
            return srcAnnotation;
        } else {
            return dstAnnotation;
        }
    }

    private enum DateFormatterCache {
        INST;

        private final @Nonnull SimpleCache<
            @Nonnull SimpleKey,
            @Nonnull Option<@Nonnull ConvertOption, @Nonnull DateFormatter>
            > cache = SimpleCache.ofSoft();

        public @Nonnull Option<@Nonnull ConvertOption, @Nonnull DateFormatter> get(
            @Nonnull String pattern, @Nonnull ZoneId zoneId
        ) {
            return cache.get(
                SimpleKey.of(pattern, zoneId),
                k -> ConvertOption.dateFormatter(DateFormatter.ofPattern(k.getAs(0), k.getAs(1))
                )
            );
        }
    }

    private enum NumFormatterCache {
        INST;

        private final @Nonnull SimpleCache<
            @Nonnull String,
            @Nonnull Option<@Nonnull ConvertOption, @Nonnull NumFormatter>
            > cache = SimpleCache.ofSoft();

        public @Nonnull Option<@Nonnull ConvertOption, @Nonnull NumFormatter> get(@Nonnull String pattern) {
            return cache.get(
                pattern,
                p -> ConvertOption.numFormatter(NumFormatter.ofPattern(p))
            );
        }
    }

    private ConvertKit() {
    }
}