Plugin
Embulk

Embulk 0.8.29 で Deprecated となった TimestampParser、TimestampFormatter API に下位バージョンをサポートしたまま対応する

More than 1 year has passed since last update.

背景

https://github.com/embulk/embulk/issues/745 にあるように embulk 0.8.29 で

  • TimestampFormatter(ScriptingContainer jruby, String format, DateTimeZone timeZone)
  • TimestampParser(ScriptingContainer jruby, String format, DateTimeZone defaultTimeZone, String defaultDate)

の API が Deprecated となり、代わりに

  • TimestampFormatter(String format, DateTimeZone timeZone)
  • TimestampParser(String format, DateTimeZone defaultTimeZone, String defaultDate)

が新設された。

embulk 0.8.27 で Java 実装の高速な TimestampParser が導入され、JRuby が不要になった流れを組むものであると思われる。jruby オブジェクトを渡さなければいけなかった以前はテストも書きづらかったので、必要なくなったこと自体はとても歓迎している。

0.9が出る6ヶ月後ぐらい(不確定)には削除されるのと、現状でも Deprecated Warning ログがそこそこ大量に出るようになったので、回避しつつ、embulk < 0.8.29もサポートしたままプラグインを改修したい。その対応方法を解説する。

対応方針

https://github.com/embulk/embulk/issues/745 に記述があるが、

  • TimestampFormatter(Task task, Optional<? extends TimestampColumnOption> columnOption)
  • TimestampParser(Task task, TimestampColumnOption columnOption)

こちらの API は以前 (0.6.14) から存在しつつ、今後も消える予定がない(Deprecated ではない)ため、これらの API を使うようにする。

TimestampFormatter の改修

TimestampFormatter.Task および TimestampFormatter.TimestampColumnOption を継承したプラグインの場合

TimestampFormatter.Task および TimestampFormatter.TimestampColumnOption を継承した標準的なインターフェースを持つプラグインの場合は、シンプルに TimestampFormatter(Task task, Optional<? extends TimestampColumnOption> columnOption) の API を使うように置き換えてしまうのが良い。

public class AwesomeFormatterPlugin implements FormatterPlugin
{
    interface PluginTask extends Task, TimestampFormatter.Task
    {
        @Config("columns")
        @ConfigDefault("[]")
        public List<ColumnOption> getColumns();
    }

    interface ColumnOption extends Task, TimestampFormatter.TimestampColumnOption
    {
    }
}

これはつまり、プラグインが以下のような設定項目を持つということである。

  • columns: columns to retain (array of hash)
    • format: specify the format of the timestamp (string, default is default_timestamp_format)
    • timezone: specify the timezone of the timestamp (string, default is default_timezone)
  • default_timestamp_format: default timestamp format (string, default is %Y-%m-%d %H:%M:%S.%6N %z)
  • default_timezone: default timezone (string, default is UTC)

transaction ないしは open 内の好きなタイミングで TimestampFormatter オブジェクトを以下のように生成すれば良い。

for (ColumnOption clumnOption : task.getColumns()) {
    TimestampFormatter formatter = new TimestampFormatter(task, Optional.of(columnOption));
}

TimestampFormatter(String format, DateTimeZone timeZone) のインターフェースを使いたい場合

プラグインが TimestampFormatter.Task および TimestampFormatter.TimestampColumnOption を継承していなかったり、TimestampFormatter と TimestampParser を両方使いたいなどの理由で継承できない場合。

TimestampFormatter.Task を実装したクラスと TimestampFormatter.TimestampColumnOption を実装したクラスを定義しておき、インスタンスを作り TimestampFormatter(Task task, Optional<? extends TimestampColumnOption> columnOption) を呼び出して対応する。

    private class TimestampFormatterTaskImpl implements TimestampFormatter.Task
    {
        private final DateTimeZone defaultTimeZone;
        private final String defaultTimestampFormat;
        public TimestampFormatterTaskImpl(
                DateTimeZone defaultTimeZone,
                String defaultTimestampFormat)
        {
            this.defaultTimeZone = defaultTimeZone;
            this.defaultTimestampFormat = defaultTimestampFormat;
        }
        @Override
        public DateTimeZone getDefaultTimeZone()
        {
            return this.defaultTimeZone;
        }
        @Override
        public String getDefaultTimestampFormat()
        {
            return this.defaultTimestampFormat;
        }
        @Override
        public ScriptingContainer getJRuby()
        {
            return null;
        }
    }

    private class TimestampFormatterColumnOptionImpl implements TimestampFormatter.TimestampColumnOption
    {
        private final Optional<DateTimeZone> timeZone;
        private final Optional<String> format;
        public TimestampFormatterColumnOptionImpl(
                Optional<DateTimeZone> timeZone,
                Optional<String> format)
        {
            this.timeZone = timeZone;
            this.format = format;
        }
        @Override
        public Optional<DateTimeZone> getTimeZone()
        {
            return this.timeZone;
        }
        @Override
        public Optional<String> getFormat()
        {
            return this.format;
        }
    }

    // ToDo: Replace with `new TimestampFormatter(format, timezone)`
    // after deciding to drop supporting embulk < 0.8.29.
    private TimestampFormatter createTimestampFormatter(String format, DateTimeZone timezone)
    {
        TimestampFormatterTaskImpl task = new TimestampFormatterTaskImpl(timezone, format);
        TimestampFormatterColumnOptionImpl columnOption = new TimestampFormatterColumnOptionImpl(
                Optional.of(timezone), Optional.of(format));
        return new TimestampFormatter(task, Optional.of(columnOption));
    }

TimestampFormatter(ScriptingContainer jruby, String format, DateTimeZone timeZone) をここで定義した createTimestampFormatter(String format, DateTimeZone timeZone) に置き換える。

TimestampParser の改修

TimestampParser.Task および TimestampParser.TimestampColumnOption を継承したプラグインの場合

TimestampParser.Task および TimestampParser.TimestampColumnOption を継承した標準的なインターフェースを持つプラグインの場合は、シンプルに TimestampParser(Task task, TimestampColumnOption columnOption) の API を使うように置き換えてしまうのが良い。

public class AwesomeParserPlugin implements ParserPlugin
{
    interface PluginTask extends Task, TimestampParser.Task
    {
        @Config("columns")
        @ConfigDefault("[]")
        public List<ColumnOption> getColumns();
    }

    interface ColumnOption extends Task, TimestampParser.TimestampColumnOption
    {
    }
}

transaction ないしは open 内の好きなタイミングで TimestampFormatter オブジェクトを以下のように生成すれば良い。

for (ColumnOption clumnOption : task.getColumns()) {
    TimestampParser parser = new TimestampParser(task, columnOption);
}

TimestampParser(String format, DateTimeZone timeZone, String date) のインターフェースを使いたい場合

プラグインが TimestampParser.Task および TimestampParser.TimestampColumnOption を継承していなかったり、TimestampFormatter と TimestampParser を両方使いたいなどの理由で継承できない場合。

TimestampParser.Task を実装したクラスと TimestampParser.TimestampColumnOption を実装したクラスを定義しておき、インスタンスを作り TimestampParser(Task task, TimestampColumnOption columnOption) を呼び出して対応する。

    private class TimestampParserTaskImpl implements TimestampParser.Task
    {
        private final DateTimeZone defaultTimeZone;
        private final String defaultTimestampFormat;
        private final String defaultDate;
        public TimestampParserTaskImpl(
                DateTimeZone defaultTimeZone,
                String defaultTimestampFormat,
                String defaultDate)
        {
            this.defaultTimeZone = defaultTimeZone;
            this.defaultTimestampFormat = defaultTimestampFormat;
            this.defaultDate = defaultDate;
        }
        @Override
        public DateTimeZone getDefaultTimeZone()
        {
            return this.defaultTimeZone;
        }
        @Override
        public String getDefaultTimestampFormat()
        {
            return this.defaultTimestampFormat;
        }
        @Override
        public String getDefaultDate()
        {
            return this.defaultDate;
        }
        @Override
        public ScriptingContainer getJRuby()
        {
            return null;
        }
    }

    private class TimestampParserColumnOptionImpl implements TimestampParser.TimestampColumnOption
    {
        private final Optional<DateTimeZone> timeZone;
        private final Optional<String> format;
        private final Optional<String> date;
        public TimestampParserColumnOptionImpl(
                Optional<DateTimeZone> timeZone,
                Optional<String> format,
                Optional<String> date)
        {
            this.timeZone = timeZone;
            this.format = format;
            this.date = date;
        }
        @Override
        public Optional<DateTimeZone> getTimeZone()
        {
            return this.timeZone;
        }
        @Override
        public Optional<String> getFormat()
        {
            return this.format;
        }
        @Override
        public Optional<String> getDate()
        {
            return this.date;
        }
    }

    // ToDo: Replace with `new TimestampParser(format, timezone)`
    // after deciding to drop supporting embulk < 0.8.29.
    private TimestampParser createTimestampParser(String format, DateTimeZone timezone)
    {
        return createTimestampParser(format, timezone, "1970-01-01");
    }

    // ToDo: Replace with `new TimestampParser(format, timezone, date)`
    // after deciding to drop supporting embulk < 0.8.29.
    private TimestampParser createTimestampParser(String format, DateTimeZone timezone, String date)
    {
        TimestampParserTaskImpl task = new TimestampParserTaskImpl(timezone, format, date);
        TimestampParserColumnOptionImpl columnOption = new TimestampParserColumnOptionImpl(
                Optional.of(timezone), Optional.of(format), Optional.of(date));
        return new TimestampParser(task, columnOption);
    }

TimestampParser(ScriptingContainer jruby, String format, DateTimeZone timeZone, String date) をここで定義した createTimestampParser(String format, DateTimeZone timeZone, String date) に置き換える。