redmine-issue-loader のバージョン2.3.0をリリースしました

CSVを読み込んでRedmineのチケットを新規作成、更新するツール、redmine-issue-loaderのバージョン2.3.0をリリースしました。

変更点は下記の2つです。

  1. HTTPタイムアウトの設定を変更可能に
    • デフォルト10秒だったのですが、親子関係が激しい状態だとタイムアウトしかねないと思ったので、タイムアウトを変更可能にしました。
  2. 文字置換を設定可能に
    • Redmineに登録時に問題となる文字を除外したいために、正規表現による文字置換を行えるようにしました。古いMySQLでutf8だったりすると、サロゲートペアがエラーになるので、それを除外したい場合に使えると思います。(というか、そのために入れました)

自分で作ったライブラリをJCenterとMaven Centralで公開する

下記自作ライブラリをJCenterとMaven Centralに公開した際の手順を残しておきます。
(後から記憶をたどって書いているので、抜けがあったらすいません...)

公開の流れとしては、下記のようになります。

  1. Bintrayにアップロード
  2. JCenterに公開
  3. JCenterからMaven Centralへ公開

Bintrayのアカウント+Mavenリポジトリ作成

JCenterへ公開するためには、BintrayのMavenリポジトリにライブラリを公開する必要があります。

下記にアクセスし、Bintrayのアカウントを作成します。

ログインしたら、Add New Repositoryで、Mavenリポジトリを追加します。Nameは適当で、TypeにMavenを選びます。

Gradleの設定

Bintrayへのアップロードを行うためのGradleプラグイン(com.jfrog.bintray)があるので、そちらを利用します。

plugins {
    id 'java-library'
    id 'maven-publish'
    id 'com.jfrog.bintray' version "1.8.4"
}

JCenterとMaven Centralの両方で公開するためには、満たさなければならない条件がいくつかあります。それを満たしていないとアップロード時にエラーとなります。

  • POMの各種項目
  • ソースコードとJavadoc

以下はそれを満たした設定例です。

task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

artifacts {
    archives sourcesJar, javadocJar
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
            artifact sourcesJar
            artifact javadocJar

            pom {
                name = 'your_project'
                description = 'Your project dscription.'
                url = 'https://example.com/your_name/your_project/'
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0' // 適用するライセンスを適宜記載
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                scm {
                    connection = 'scm:git:https://example.com/your_name/your_project.git'
                    url = 'https://example.com/your_name/your_project/'
                }
                developers {
                    developer {
                        id = "your_id"
                        name = "your_name"
                        email = "your_name@example.com"
                    }
                }
            }
        }
    }
    repositories {
        maven {
            url "file://$buildDir/maven"
        }
    }
}

Bintray用の設定ですが、アップロードするためにBintrayのユーザ名のAPI Keyを指定する必要があります。
どちらもそのままGradle上で記載するわけにはいかないので、gradle.propertiesに記載する方法を取ります。

自分のホームディレクトリの .gradle/gradle.properties に下記のように記載します。

bintray_user=<BintrayでのID>
bintray_api_key=<API Key>

API KeyはBintray上で Edit Profile → API Key で確認できます。

build.gradle上は下記のようにgradle.propertiesから取得するように書きます。

bintray {
    user = project.hasProperty('bintray_user') ? bintray_user : ''
    key = project.hasProperty('bintray_api_key') ? bintray_api_key : ''

    publications = ['mavenJava'] // publishing/publications で指定したのと同じ名前

    pkg {
        repo = 'maven' // Bintrayで作成したMavenリポジトリ名
        name = group + ':' + project.name
        licenses = ['Apache-2.0']
        vcsUrl = 'https://example.com/your_name/your_project/'
   }
}

build.gralde 全体は下記を参考にしてみてください。

Bintrayへのアップロード、公開

GradleタスクのbintrayUploadで、Bintray上にアップロードされます。この時点ではまだ非公開です。
Bintray上でアップロードしたリポジトリを確認し、問題なければPublishを押下します。これでBintray上で公開されます。

JCenterへの公開

BintrayのGeneralタブのLitnked toのところに「Add to JCenter」というボタンがあるので押下します。
そうすると、Compose Messageという画面になり、JCenterへ公開するためのリクエストがタイトルに書かれた状態になるので、そのままSendでメッセージを送信します。
1、2時間で承認されました的なメッセージが来て、JCenterに公開されました。

Sonatype JIRA のアカウント+Project作成の申請

続いてMaven Centralへの公開です。

Sonatype JIRA上でProject作成のIssueを作成する必要があります。

まずはSonatype JIRAのアカウントを作成します。

ログインしたら、CreateボタンでIssueの作成フォームを開きます。
Projectは「Community Support - Open Source Project Repository Hosting (OSSRH)」、Issue Typeは「New Project」となっているはずなので、そのままで、必要な情報を記入して作成します。

とりあえず必須項目の下記を埋めます。

  • Summary : Create new repository for xxxxxx みたいな感じでOK
  • Group Id : プロジェクトのGroup IDです。Gradleで指定しているのと同じものにします。自分がドメインの所有者であることを後から証明する必要があるので、自由な形にすることはできません。GitHubで公開しているものならば、com.github.onozaty みたいな自分のGitHubのページ指定すればOKです。
  • Project URL : プロジェクトのURLです。
  • SCM url : ソースコード管理のURLです。Gitならばcloneする際のURLです。

自分が書いたIssueを参考までに。

この後すぐにGroup Idの所有者の確認のためのコメントが来ます。
GitHubの場合は、Issueの番号と同じプロジェクトを作ってくれとくるので、プロジェクトを作って、作ったよとコメントします。(コメント来る前に作ったよといってしまってもOKです)

確認が終わると、プロジェクトの準備できたよーとコメントが返ってきます。
これで準備はOKです。

JCenterからMaven Centralへの同期

直接Maven Centralへアップロードする方法もありますが、JCenterにせっかくあげているのでJCenter経由でMaven Centralにアップロードします。
JCenter経由とすると良い点もあって、JCenterの鍵を使ってGPG署名を行うことができます。Maven Centralに公開するものは、GPG署名が必須ですが、個人でやるのはちょいと面倒に感じたので、この方法を取ることにしました。

まず、JCenterにアップロードした際にJCenterの鍵で署名をするように設定します。 Edit RepositoryのGeneral Settingsに「GPG sign uploaded files using Bintray's public/private key pair.」という項目があるので、これをチェックします。
こうすると、アップロードされたタイミングで、Bintrayの鍵を使ってGPG署名が行われるようになります。

PackageのMaven Centralのタブから、Maven Centralへアップロードできます。
Sonatype OSSのUser token keyとUser token passwordの入力を行い「Sync」ボタンを押下してアップロードします。

右側にあるSync Statusでエラー無く完了するとアップロードされたことになります。

下記からアップロードされていることを確認することができます。

Maven Centralとの同期がとられるまでは時間がかかるようです。通常は10分程度とのことですが、最大で2時間かかるとのことです。

参考にしたサイト

下記のサイトが参考になりました。ありがとうございました。

ShortcutKey2URLの新バージョン(Chrome版1.4.0、Firefox版4.3.1)をリリースしました

ShortcutKey2URLの新バージョンをリリースしました。

2つの機能追加を行っています。どちらも利用者の方から要望があったものになります。

候補のインタラクティブな絞り込み

ショートカットキーの候補をインタラクティブに絞り込んで表示できるようにしました。

f:id:onozaty:20200329210943g:plain

たとえば、"G1" "G2" といったショートカットキーが登録されていた場合、"G"の時点で候補となるショートカットキーのみに絞って表示されるようなイメージです。
ショートカットキーがたくさん登録されている場合に有効かと思います。

この機能を有効にするためには、ShortcutKey2URLの管理画面で「Interactive filter of shortcut keys on the popup」にチェックを付ける必要があります。

ポップアップ上で非表示に

ショートカットキーの設定で「Hide in shortcut key list displayed in popup」にチェックを付けると、ポップアップ上でショートカットキーを非表示とできるようにしました。
一覧に出ないだけで、ショートカットキーとしては有効なままです。

人に見られたくないショートカットキーなどは、非表示にしておくと良いかもしれませんね。

期日近くになったら警告を表示する(Redmine View Customize Plugin)

上記の問い合わせに対応したサンプルコードを書いてみました。(これが求めているものなのかはちょっと怪しいかも...)

期日まで残り3日になったらチケット画面に警告を表示します。

設定内容

  • Path pattern: .*
  • Insertion position: Bottom of issue detail
$(function() {

  const daysLeft = 3;

  const dueDateArray = $('#issue_due_date').val().split('-');
  const alertDate = new Date(dueDateArray[0], dueDateArray[1] - 1, dueDateArray[2] - daysLeft);

  const now = new Date();
  const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  if (nowDate >= alertDate) {
    $('#content').prepend('<div class="warning">It is only ' + daysLeft + ' days until the due date.</div>');
  }
})

動作

f:id:onozaty:20200327233745p:plain

PostgreSQL COPY Helper を作りました

PostgreSQLのCOPYコマンドをJavaで簡単に利用するためのライブラリとして、PostgreSQL COPY Helper を作りました。

登録したいデータの構造を表すクラスを定義しておいて、、

@Table("items")
public class Item {

    @Column("id")
    private final int id;

    @Column("name")
    private final String name;

    public Item(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

CopyHelper.copyFrom でそのオブジェクトの一覧を渡すだけです。

List<Item> items = generateItems();
CopyHelper.copyFrom(connection, items, Item.class);

COPYコマンドは、Batch INSERTと比べてもかなり(手元で測定したところ10倍くらい)高速なので、大量データのINSERTを高速化したいというときにぜひお試しください。

java.sql.Timeとjava.time.LocalTime間の変換でミリ秒が破棄される

java.sql.Timeには、下記のようなLocalTimeとの間で変換を行うメソッドがあります。

  • public LocalTime toLocalTime()
  • Time java.sql.Time.valueOf(LocalTime time)

ただ、これらメソッドは秒までしか対象にしておらず、ミリ秒が破棄されています。

    /**
     * Obtains an instance of {@code Time} from a {@link LocalTime} object
     * with the same hour, minute and second time value as the given
     * {@code LocalTime}.
     *
     * @param time a {@code LocalTime} to convert
     * @return a {@code Time} object
     * @exception NullPointerException if {@code time} is null
     * @since 1.8
     */
    @SuppressWarnings("deprecation")
    public static Time valueOf(LocalTime time) {
        return new Time(time.getHour(), time.getMinute(), time.getSecond());
    }

    /**
     * Converts this {@code Time} object to a {@code LocalTime}.
     * <p>
     * The conversion creates a {@code LocalTime} that represents the same
     * hour, minute, and second time value as this {@code Time}.
     *
     * @return a {@code LocalTime} object representing the same time value
     * @since 1.8
     */
    @SuppressWarnings("deprecation")
    public LocalTime toLocalTime() {
        return LocalTime.of(getHours(), getMinutes(), getSeconds());
    }

面倒ではありますが、LocalDateTimeなどを経由させて変換することによって、ミリ秒を維持したまま変換できます。

SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
DateTimeFormatter localTimeFormat = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

LocalTime baseLocalTime = LocalTime.now();

// baseLocalTime: 01:41:44.369
System.out.printf(
        "baseLocalTime: %s\n",
        baseLocalTime.format(localTimeFormat));

{
    // NGなパターン
    Time time = Time.valueOf(baseLocalTime);
    LocalTime localTime = time.toLocalTime();

    // time: 01:41:44.000 localTime: 01:41:44.000 equals: false
    System.out.printf(
            "time: %s localTime: %s equals: %s\n",
            timeFormat.format(time),
            localTime.format(localTimeFormat),
            baseLocalTime.equals(localTime));
}

{
    // OKなパターン
    Time time = new Time(
            baseLocalTime.atDate(LocalDate.ofEpochDay(0)) // LocalDateTimeに変換
                    .atZone(ZoneId.systemDefault()) // ZonedDateTimeに変換
                    .toInstant().toEpochMilli()); // Epochミリ秒へ

    LocalTime localTime = LocalDateTime.ofInstant( // Epochミリ秒からLocalDateTimeへ
            Instant.ofEpochMilli(time.getTime()),
            ZoneId.systemDefault()).toLocalTime();

    // time: 01:41:44.369 localTime: 01:41:44.369 equals: true
    System.out.printf(
            "time: %s localTime: %s equals: %s\n",
            timeFormat.format(time),
            localTime.format(localTimeFormat),
            baseLocalTime.equals(localTime));
}