JodaSupport.java

package com.alibaba.fastjson2.util;

import com.alibaba.fastjson2.JSONB;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.codec.DateTimeCodec;
import com.alibaba.fastjson2.reader.ObjectReader;
import com.alibaba.fastjson2.support.LambdaMiscCodec;
import com.alibaba.fastjson2.writer.ObjectWriter;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.function.*;

import static com.alibaba.fastjson2.JSONB.Constants.BC_LOCAL_DATE;
import static com.alibaba.fastjson2.JSONB.Constants.BC_LOCAL_DATETIME;

public class JodaSupport {
    static final long HASH_YEAR = Fnv.hashCode64("year");
    static final long HASH_MONTH = Fnv.hashCode64("month");
    static final long HASH_DAY = Fnv.hashCode64("day");
    static final long HASH_HOUR = Fnv.hashCode64("hour");
    static final long HASH_MINUTE = Fnv.hashCode64("minute");
    static final long HASH_SECOND = Fnv.hashCode64("second");
    static final long HASH_MILLIS = Fnv.hashCode64("millis");
    static final long HASH_CHRONOLOGY = Fnv.hashCode64("chronology");

    public static ObjectWriter createLocalDateTimeWriter(Class objectClass, String format) {
        return new LocalDateTimeWriter(objectClass, format);
    }

    public static ObjectWriter createLocalDateWriter(Class objectClass, String format) {
        return new LocalDateWriter(objectClass, format);
    }

    public static ObjectReader createChronologyReader(Class objectClass) {
        return new ChronologyReader(objectClass);
    }

    public static ObjectReader createLocalDateReader(Class objectClass) {
        return new LocalDateReader(objectClass);
    }

    public static ObjectReader createLocalDateTimeReader(Class objectClass) {
        return new LocalDateTimeReader(objectClass);
    }

    public static ObjectReader createInstantReader(Class objectClass) {
        return new InstantReader(objectClass);
    }

    public static ObjectWriter createGregorianChronologyWriter(Class objectClass) {
        return new GregorianChronologyWriter(objectClass);
    }

    public static ObjectWriter createISOChronologyWriter(Class objectClass) {
        return new ISOChronologyWriter(objectClass);
    }

    static class InstantReader
            implements ObjectReader {
        final Class objectClass;
        final LongFunction constructor;

        InstantReader(Class objectClass) {
            this.objectClass = objectClass;
            try {
                this.constructor = LambdaMiscCodec.createLongFunction(
                        objectClass.getConstructor(long.class)
                );
            } catch (NoSuchMethodException e) {
                throw new JSONException("create joda instant reader error", e);
            }
        }

        @Override
        public Class getObjectClass() {
            return objectClass;
        }

        @Override
        public Object createInstance(Map map, long features) {
            Number millis = (Long) map.get("millis");
            if (millis != null) {
                return createInstanceFromMillis(millis.longValue());
            }

            Number epochSecond = (Number) map.get("epochSecond");
            if (epochSecond != null) {
                long epochMillis = epochSecond.longValue() * 1000L;
                return createInstanceFromMillis(epochMillis);
            }

            throw new JSONException("create joda instant error");
        }

        public Object createInstanceFromMillis(long millis) {
            return constructor.apply(millis);
        }

        @Override
        public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            if (jsonReader.nextIfNull()) {
                return null;
            }

            if (jsonReader.isInt()) {
                long millis = jsonReader.readInt64Value();
                return createInstanceFromMillis(millis);
            }

            if (jsonReader.isString()) {
                Instant jdkInstant = jsonReader.readInstant();
                if (jdkInstant == null) {
                    return null;
                }

                long millis = jdkInstant.toEpochMilli();
                return createInstanceFromMillis(millis);
            }

            if (jsonReader.isObject()) {
                Map object = jsonReader.readObject();
                return createInstance(object, features);
            }

            throw new JSONException(jsonReader.info("not support"));
        }

        @Override
        public Object readJSONBObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            return readObject(jsonReader, fieldType, fieldName, features);
        }
    }

    static class ChronologyReader
            implements ObjectReader {
        static final long HASH_ZONE_ID = Fnv.hashCode64("zoneId");

        final Class objectClass;
        final Class gregorianChronology;
        final Class dateTimeZone;
        final Function forID;
        final Function getInstance;
        final Object utc;

        ChronologyReader(Class objectClass) {
            this.objectClass = objectClass;
            ClassLoader classLoader = objectClass.getClassLoader();
            try {
                gregorianChronology = classLoader.loadClass("org.joda.time.chrono.GregorianChronology");
                dateTimeZone = classLoader.loadClass("org.joda.time.DateTimeZone");

                utc = gregorianChronology.getMethod("getInstanceUTC").invoke(null);
                forID = LambdaMiscCodec.createFunction(
                        dateTimeZone.getMethod("forID", String.class)
                );
                getInstance = LambdaMiscCodec.createFunction(
                        gregorianChronology.getMethod("getInstance", dateTimeZone)
                );
            } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new JSONException("create ChronologyReader error", e);
            }
        }

        @Override
        public Class getObjectClass() {
            return objectClass;
        }

        @Override
        public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            throw new JSONException(jsonReader.info("not support"));
        }

        @Override
        public Object readJSONBObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            Integer minimumDaysInFirstWeek = null;
            String zoneId = null;
            jsonReader.nextIfObjectStart();
            while (!jsonReader.nextIfObjectEnd()) {
                final long HASH_MINIMUM_DAYS_IN_FIRST_WEEK = 8244232525129275563L; // Fnv.hashCode64("minimumDaysInFirstWeek");
                long fieldNameHashCode = jsonReader.readFieldNameHashCode();
                if (fieldNameHashCode == HASH_MINIMUM_DAYS_IN_FIRST_WEEK) {
                    minimumDaysInFirstWeek = jsonReader.readInt32Value();
                } else if (fieldNameHashCode == HASH_ZONE_ID) {
                    zoneId = jsonReader.readString();
                } else {
                    throw new JSONException(jsonReader.info("not support fieldName " + jsonReader.getFieldName()));
                }
            }

            if (minimumDaysInFirstWeek == null) {
                if ("UTC".equals(zoneId)) {
                    return utc;
                }

                Object datetimeZone = forID.apply(zoneId);
                return getInstance.apply(datetimeZone);
            }

            throw new JSONException(jsonReader.info("not support"));
        }
    }

    static class GregorianChronologyWriter
            implements ObjectWriter {
        final Class objectClass;
        final ToIntFunction getMinimumDaysInFirstWeek;
        final Function getZone;
        final Function getID;

        GregorianChronologyWriter(Class objectClass) {
            this.objectClass = objectClass;
            try {
                this.getMinimumDaysInFirstWeek = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getMinimumDaysInFirstWeek")
                );
                Method method = objectClass.getMethod("getZone");
                this.getZone = LambdaMiscCodec.createFunction(method);
                this.getID = LambdaMiscCodec.createFunction(
                        method.getReturnType().getMethod("getID")
                );
            } catch (NoSuchMethodException e) {
                throw new JSONException("getMethod error", e);
            }
        }

        @Override
        public void writeJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            Object zone = getZone.apply(object);
            String zoneId = (String) getID.apply(zone);

            int minDaysInFirstWeek = getMinimumDaysInFirstWeek.applyAsInt(object);

            jsonWriter.startObject();
            if (minDaysInFirstWeek != 4) {
                jsonWriter.writeName("minimumDaysInFirstWeek");
                jsonWriter.writeInt32(minDaysInFirstWeek);
            }
            jsonWriter.writeName("zoneId");
            jsonWriter.writeString(zoneId);
            jsonWriter.endObject();
        }

        @Override
        public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            Object zone = getZone.apply(object);
            String zoneId = (String) getID.apply(zone);

            int minDaysInFirstWeek = getMinimumDaysInFirstWeek.applyAsInt(object);
            jsonWriter.startObject();
            jsonWriter.writeName("minimumDaysInFirstWeek");
            jsonWriter.writeInt32(minDaysInFirstWeek);
            jsonWriter.writeName("zoneId");
            jsonWriter.writeString(zoneId);
            jsonWriter.endObject();
        }
    }

    static class ISOChronologyWriter
            implements ObjectWriter {
        final Class objectClass;
        final Function getZone;
        final Function getID;

        ISOChronologyWriter(Class objectClass) {
            this.objectClass = objectClass;
            try {
                Method method = objectClass.getMethod("getZone");
                this.getZone = LambdaMiscCodec.createFunction(method);
                this.getID = LambdaMiscCodec.createFunction(
                        method.getReturnType().getMethod("getID")
                );
            } catch (NoSuchMethodException e) {
                throw new JSONException("getMethod error", e);
            }
        }

        @Override
        public void writeJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            Object zone = getZone.apply(object);
            String zoneId = (String) getID.apply(zone);

            jsonWriter.startObject();
            jsonWriter.writeName("zoneId");
            jsonWriter.writeString(zoneId);
            jsonWriter.endObject();
        }

        @Override
        public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            Object zone = getZone.apply(object);
            String zoneId = (String) getID.apply(zone);

            jsonWriter.startObject();
            jsonWriter.writeName("zoneId");
            jsonWriter.writeString(zoneId);
            jsonWriter.endObject();
        }
    }

    static class LocalDateReader
            implements ObjectReader {
        final Class objectClass;
        final Constructor constructor3;
        final Constructor constructor4;

        final Class classISOChronology;
        final Class classChronology;
        final Object utc;

        LocalDateReader(Class objectClass) {
            this.objectClass = objectClass;
            try {
                ClassLoader classLoader = objectClass.getClassLoader();
                classChronology = classLoader.loadClass("org.joda.time.Chronology");

                constructor3 = objectClass.getConstructor(int.class, int.class, int.class);
                constructor4 = objectClass.getConstructor(int.class, int.class, int.class, classChronology);

                classISOChronology = classLoader.loadClass("org.joda.time.chrono.ISOChronology");
                utc = classISOChronology.getMethod("getInstance").invoke(null);
            } catch (ClassNotFoundException
                    | NoSuchMethodException
                    | IllegalAccessException
                    | IllegalArgumentException
                    | InvocationTargetException e) {
                throw new JSONException("create LocalDateWriter error", e);
            }
        }

        @Override
        public Class getObjectClass() {
            return objectClass;
        }

        @Override
        public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            if (jsonReader.nextIfNull()) {
                return null;
            }

            LocalDate localDate = jsonReader.readLocalDate();
            if (localDate == null) {
                return null;
            }

            try {
                return constructor4.newInstance(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth(), null);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
            }
        }

        @Override
        public Object readJSONBObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            byte type = jsonReader.getType();

            if (type == BC_LOCAL_DATE) {
                LocalDate localDate = jsonReader.readLocalDate();
                try {
                    return constructor3.newInstance(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth());
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            if (jsonReader.isObject()) {
                Integer year = null, month = null, day = null;
                Object chronology = null;
                jsonReader.nextIfObjectStart();
                while (!jsonReader.nextIfObjectEnd()) {
                    long fieldNameHashCode = jsonReader.readFieldNameHashCode();
                    if (fieldNameHashCode == HASH_YEAR) {
                        year = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_MONTH) {
                        month = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_DAY) {
                        day = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_CHRONOLOGY) {
                        chronology = jsonReader.read(classChronology);
                    } else {
                        throw new JSONException(jsonReader.info("not support fieldName " + jsonReader.getFieldName()));
                    }
                }

                try {
                    return constructor4.newInstance(year, month, day, chronology);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            throw new JSONException(jsonReader.info("not support " + JSONB.typeName(type)));
        }
    }

    static class LocalDateWriter
            extends DateTimeCodec
            implements ObjectWriter {
        final Class objectClass;
        final ToIntFunction getYear;
        final ToIntFunction getMonthOfYear;
        final ToIntFunction getDayOfMonth;
        final Function getChronology;

        final Class isoChronology;
        final Object utc;

        LocalDateWriter(Class objectClass, String format) {
            super(format);

            this.objectClass = objectClass;
            try {
                ClassLoader classLoader = objectClass.getClassLoader();
                isoChronology = classLoader.loadClass("org.joda.time.chrono.ISOChronology");
                Object instance = isoChronology.getMethod("getInstance").invoke(null);
                utc = isoChronology.getMethod("withUTC").invoke(instance);

                getYear = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getYear")
                );
                getMonthOfYear = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getMonthOfYear")
                );
                getDayOfMonth = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getDayOfMonth")
                );
                getChronology = LambdaMiscCodec.createFunction(
                        objectClass.getMethod("getChronology")
                );
            } catch (ClassNotFoundException
                    | NoSuchMethodException
                    | IllegalAccessException
                    | IllegalArgumentException
                    | InvocationTargetException e) {
                throw new JSONException("create LocalDateWriter error", e);
            }
        }

        @Override
        public void writeJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            int year = getYear.applyAsInt(object);
            int monthOfYear = getMonthOfYear.applyAsInt(object);
            int dayOfMonth = getDayOfMonth.applyAsInt(object);
            Object chronology = getChronology.apply(object);

            if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
                jsonWriter.writeTypeName(TypeUtils.getTypeName(object.getClass()));
            }

            if (chronology == utc || chronology == null) {
                jsonWriter.writeLocalDate(LocalDate.of(year, monthOfYear, dayOfMonth));
                return;
            }

            jsonWriter.startObject();

            jsonWriter.writeName("year");
            jsonWriter.writeInt32(year);

            jsonWriter.writeName("month");
            jsonWriter.writeInt32(monthOfYear);

            jsonWriter.writeName("day");
            jsonWriter.writeInt32(dayOfMonth);

            jsonWriter.writeName("chronology");
            jsonWriter.writeAny(chronology);

            jsonWriter.endObject();
        }

        @Override
        public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            int year = getYear.applyAsInt(object);
            int monthOfYear = getMonthOfYear.applyAsInt(object);
            int dayOfMonth = getDayOfMonth.applyAsInt(object);
            Object chronology = getChronology.apply(object);

            if (chronology == utc || chronology == null) {
                LocalDate localDate = LocalDate.of(year, monthOfYear, dayOfMonth);

                DateTimeFormatter formatter = this.getDateFormatter();
                if (formatter == null) {
                    formatter = jsonWriter.context.getDateFormatter();
                }

                if (formatter == null) {
                    jsonWriter.writeLocalDate(localDate);
                    return;
                }

                String str = formatter.format(localDate);
                jsonWriter.writeString(str);
                return;
            }

            jsonWriter.startObject();

            jsonWriter.writeName("year");
            jsonWriter.writeInt32(year);

            jsonWriter.writeName("month");
            jsonWriter.writeInt32(monthOfYear);

            jsonWriter.writeName("day");
            jsonWriter.writeInt32(dayOfMonth);

            jsonWriter.writeName("chronology");
            jsonWriter.writeAny(chronology);

            jsonWriter.endObject();
        }
    }

    static class LocalDateTimeReader
            implements ObjectReader {
        final Class objectClass;
        final Constructor constructor7;
        final Constructor constructor8;

        final Class classISOChronology;
        final Class classChronology;
        final Object utc;

        LocalDateTimeReader(Class objectClass) {
            this.objectClass = objectClass;
            try {
                ClassLoader classLoader = objectClass.getClassLoader();
                classChronology = classLoader.loadClass("org.joda.time.Chronology");

                constructor7 = objectClass.getConstructor(int.class, int.class, int.class, int.class, int.class, int.class, int.class);
                constructor8 = objectClass.getConstructor(int.class, int.class, int.class, int.class, int.class, int.class, int.class, classChronology);

                classISOChronology = classLoader.loadClass("org.joda.time.chrono.ISOChronology");
                utc = classISOChronology.getMethod("getInstance").invoke(null);
            } catch (ClassNotFoundException
                    | NoSuchMethodException
                    | IllegalAccessException
                    | IllegalArgumentException
                    | InvocationTargetException e) {
                throw new JSONException("create LocalDateWriter error", e);
            }
        }

        @Override
        public Class getObjectClass() {
            return objectClass;
        }

        @Override
        public Object readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            if (jsonReader.isString() || jsonReader.isInt()) {
                LocalDateTime ldt = jsonReader.readLocalDateTime();
                if (ldt == null) {
                    return null;
                }

                try {
                    return constructor7.newInstance(ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano() / 1000_000);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            throw new JSONException(jsonReader.info("not support"));
        }

        @Override
        public Object readJSONBObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
            byte type = jsonReader.getType();

            if (type == BC_LOCAL_DATE) {
                LocalDate localDate = jsonReader.readLocalDate();
                try {
                    return constructor7.newInstance(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth(), 0, 0, 0, 0);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            if (type == BC_LOCAL_DATETIME) {
                LocalDateTime ldt = jsonReader.readLocalDateTime();
                try {
                    return constructor7.newInstance(ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(), ldt.getHour(), ldt.getMinute(), ldt.getSecond(), ldt.getNano() / 1000_000);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            if (jsonReader.isObject()) {
                Integer year = null, month = null, day = null, hour = null, minute = null, second = null, millis = null;
                Object chronology = null;
                jsonReader.nextIfObjectStart();
                while (!jsonReader.nextIfObjectEnd()) {
                    long fieldNameHashCode = jsonReader.readFieldNameHashCode();
                    if (fieldNameHashCode == HASH_YEAR) {
                        year = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_MONTH) {
                        month = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_DAY) {
                        day = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_HOUR) {
                        hour = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_MINUTE) {
                        minute = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_SECOND) {
                        second = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_MILLIS) {
                        millis = jsonReader.readInt32Value();
                    } else if (fieldNameHashCode == HASH_CHRONOLOGY) {
                        chronology = jsonReader.read(classChronology);
                    } else {
                        throw new JSONException(jsonReader.info("not support fieldName " + jsonReader.getFieldName()));
                    }
                }

                try {
                    return constructor8.newInstance(year, month, day, hour, minute, second, millis, chronology);
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                    throw new JSONException(jsonReader.info("read org.joda.time.LocalDate error"), e);
                }
            }

            throw new JSONException(jsonReader.info("not support " + JSONB.typeName(type)));
        }
    }

    static class LocalDateTimeWriter
            extends DateTimeCodec
            implements ObjectWriter {
        final Class objectClass;

        final Method getYear;
        final Method getMonthOfYear;
        final Method getDayOfMonth;

        final ToIntFunction getHourOfDay;
        final ToIntFunction getMinuteOfHour;
        final ToIntFunction getSecondOfMinute;
        final ToIntFunction getMillisOfSecond;

        final Function getChronology;

        final Class isoChronology;
        final Object utc;

        LocalDateTimeWriter(Class objectClass, String format) {
            super(format);

            this.objectClass = objectClass;
            try {
                ClassLoader classLoader = objectClass.getClassLoader();
                isoChronology = classLoader.loadClass("org.joda.time.chrono.ISOChronology");
                Object instance = isoChronology.getMethod("getInstance").invoke(null);
                utc = isoChronology.getMethod("withUTC").invoke(instance);

                getYear = objectClass.getMethod("getYear");
                getMonthOfYear = objectClass.getMethod("getMonthOfYear");
                getDayOfMonth = objectClass.getMethod("getDayOfMonth");

                getHourOfDay = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getHourOfDay")
                );
                getMinuteOfHour = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getMinuteOfHour")
                );
                getSecondOfMinute = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getSecondOfMinute")
                );
                getMillisOfSecond = LambdaMiscCodec.createToIntFunction(
                        objectClass.getMethod("getMillisOfSecond")
                );

                getChronology = LambdaMiscCodec.createFunction(
                        objectClass.getMethod("getChronology")
                );
            } catch (ClassNotFoundException
                    | NoSuchMethodException
                    | IllegalAccessException
                    | IllegalArgumentException
                    | InvocationTargetException e) {
                throw new JSONException("create LocalDateWriter error", e);
            }
        }

        @Override
        public void writeJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            try {
                int year = (Integer) getYear.invoke(object);
                int monthOfYear = (Integer) getMonthOfYear.invoke(object);
                int dayOfMonth = (Integer) getDayOfMonth.invoke(object);

                int hour = getHourOfDay.applyAsInt(object);
                int minute = getMinuteOfHour.applyAsInt(object);
                int second = getSecondOfMinute.applyAsInt(object);
                int millis = getMillisOfSecond.applyAsInt(object);

                Object chronology = getChronology.apply(object);

                if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
                    jsonWriter.writeTypeName(TypeUtils.getTypeName(object.getClass()));
                }

                if (chronology == utc || chronology == null) {
                    jsonWriter.writeLocalDateTime(
                            LocalDateTime.of(year, monthOfYear, dayOfMonth, hour, minute, second, millis * 1000000));
                    return;
                }

                jsonWriter.startObject();

                jsonWriter.writeName("year");
                jsonWriter.writeInt32(year);

                jsonWriter.writeName("month");
                jsonWriter.writeInt32(monthOfYear);

                jsonWriter.writeName("day");
                jsonWriter.writeInt32(dayOfMonth);

                jsonWriter.writeName("hour");
                jsonWriter.writeInt32(hour);

                jsonWriter.writeName("minute");
                jsonWriter.writeInt32(minute);

                jsonWriter.writeName("second");
                jsonWriter.writeInt32(second);

                jsonWriter.writeName("millis");
                jsonWriter.writeInt32(millis);

                jsonWriter.writeName("chronology");
                jsonWriter.writeAny(chronology);

                jsonWriter.endObject();
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new JSONException("write LocalDateWriter error", e);
            }
        }

        @Override
        public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
            try {
                int year = (Integer) getYear.invoke(object);
                int monthOfYear = (Integer) getMonthOfYear.invoke(object);
                int dayOfMonth = (Integer) getDayOfMonth.invoke(object);

                int hour = getHourOfDay.applyAsInt(object);
                int minute = getMinuteOfHour.applyAsInt(object);
                int second = getSecondOfMinute.applyAsInt(object);
                int millis = getMillisOfSecond.applyAsInt(object);

                Object chronology = getChronology.apply(object);

                if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
                    jsonWriter.writeTypeName(TypeUtils.getTypeName(object.getClass()));
                }

                if (chronology == utc || chronology == null) {
                    int nanoOfSecond = millis * 1000_000;
                    LocalDateTime ldt = LocalDateTime.of(year, monthOfYear, dayOfMonth, hour, minute, second, nanoOfSecond);

                    DateTimeFormatter formatter = this.getDateFormatter();
                    if (formatter == null) {
                        formatter = jsonWriter.context.getDateFormatter();
                    }

                    if (formatter == null) {
                        jsonWriter.writeLocalDateTime(ldt);
                        return;
                    }

                    String str = formatter.format(ldt);
                    jsonWriter.writeString(str);
                    return;
                }

                jsonWriter.startObject();

                jsonWriter.writeName("year");
                jsonWriter.writeInt32(year);

                jsonWriter.writeName("month");
                jsonWriter.writeInt32(monthOfYear);

                jsonWriter.writeName("day");
                jsonWriter.writeInt32(dayOfMonth);

                jsonWriter.writeName("hour");
                jsonWriter.writeInt32(hour);

                jsonWriter.writeName("minute");
                jsonWriter.writeInt32(minute);

                jsonWriter.writeName("second");
                jsonWriter.writeInt32(second);

                jsonWriter.writeName("millis");
                jsonWriter.writeInt32(millis);

                jsonWriter.writeName("chronology");
                jsonWriter.writeAny(chronology);

                jsonWriter.endObject();
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new JSONException("write LocalDateWriter error", e);
            }
        }
    }

    public static final class DateTimeFromZDT
            implements Function {
        static Constructor CONS;
        static Method FOR_ID;

        @Override
        public Object apply(Object o) {
            ZonedDateTime zdt = (ZonedDateTime) o;
            try {
                if (FOR_ID == null) {
                    Class<?> zoneClass = Class.forName("org.joda.time.DateTimeZone");
                    FOR_ID = zoneClass.getMethod("forID", String.class);
                }

                if (CONS == null) {
                    CONS = Class.forName("org.joda.time.DateTime")
                            .getConstructor(
                                    int.class, int.class, int.class, int.class, int.class, int.class, int.class,
                                    FOR_ID.getDeclaringClass()
                            );
                }

                String zondId = zdt.getZone().getId();
                if ("Z".equals(zondId)) {
                    zondId = "UTC";
                }

                return CONS.newInstance(
                        zdt.getYear(),
                        zdt.getMonthValue(),
                        zdt.getDayOfMonth(),
                        zdt.getHour(),
                        zdt.getMinute(),
                        zdt.getSecond(),
                        zdt.getNano() / 1_000_000,
                        FOR_ID.invoke(null, zondId)
                        );
            } catch (Exception e) {
                throw new JSONException("build DateTime error", e);
            }
        }
    }

    public static final class DateTime2ZDT
            implements Function {
        static Class CLASS;
        static ToIntFunction YEAR;
        static ToIntFunction MONTH;
        static ToIntFunction DAY_OF_MONTH;
        static ToIntFunction HOUR;
        static ToIntFunction MINUTE;
        static ToIntFunction SECOND;
        static ToIntFunction MILLIS;
        static Function GET_ZONE;
        static Function GET_ID;

        @Override
        public Object apply(Object o) {
            try {
                if (CLASS == null) {
                    CLASS = Class.forName("org.joda.time.DateTime");
                }
                if (YEAR == null) {
                    YEAR = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getYear"));
                }
                if (MONTH == null) {
                    MONTH = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getMonthOfYear"));
                }
                if (DAY_OF_MONTH == null) {
                    DAY_OF_MONTH = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getDayOfMonth"));
                }
                if (HOUR == null) {
                    HOUR = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getHourOfDay"));
                }
                if (MINUTE == null) {
                    MINUTE = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getMinuteOfHour"));
                }
                if (SECOND == null) {
                    SECOND = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getSecondOfMinute"));
                }
                if (MILLIS == null) {
                    MILLIS = LambdaMiscCodec.createToIntFunction(CLASS.getMethod("getMillisOfSecond"));
                }
                if (GET_ZONE == null) {
                    GET_ZONE = LambdaMiscCodec.createFunction(CLASS.getMethod("getZone"));
                }
                if (GET_ID == null) {
                    GET_ID = LambdaMiscCodec.createFunction(Class.forName("org.joda.time.DateTimeZone").getMethod("getID"));
                }

                Object zone = GET_ZONE.apply(o);
                String zonIdStr = (String) GET_ID.apply(zone);
                ZoneId zoneId = ZoneId.of(zonIdStr);
                return ZonedDateTime.of(
                        YEAR.applyAsInt(o),
                        MONTH.applyAsInt(o),
                        DAY_OF_MONTH.applyAsInt(o),
                        HOUR.applyAsInt(o),
                        MINUTE.applyAsInt(o),
                        SECOND.applyAsInt(o),
                        MILLIS.applyAsInt(o) * 1_000_000,
                        zoneId
                );
            } catch (Exception e) {
                throw new JSONException("convert joda org.joda.time.DateTime to java.time.ZonedDateTime error", e);
            }
        }
    }
}