クリップボードから画像を貼り付ける(Redmine View Customize Plugin)

ということで View customizeでやってみました。

設定内容

  • Path pattern: .*
  • Insertion position: Head of all pages
$(function() {

  $('form div.box').has('input:file.filedrop').on('paste', copyImageFromClipboard);

  function copyImageFromClipboard(e) {
    if (!$(e.target).hasClass('wiki-edit')) { return; }
    var clipboardData = e.clipboardData || e.originalEvent.clipboardData
    if (!clipboardData) { return; }
    var items = clipboardData.items
    for (var i = 0 ; i < items.length ; i++) {
      var item = items[i];
      if (item.type.indexOf("image") != -1) {
        var blob = item.getAsFile();
        var date = new Date();
        var filename = 'clipboard-'
          + date.getFullYear()
          + ('0'+(date.getMonth()+1)).slice(-2)
          + ('0'+date.getDate()).slice(-2)
          + ('0'+date.getHours()).slice(-2)
          + ('0'+date.getMinutes()).slice(-2)
          + '-' + randomKey(5).toLocaleLowerCase()
          + '.' + blob.name.split('.').pop();
        var file = new File([blob], filename, {type: blob.type});
        var inputEl = $('input:file.filedrop').first()
        handleFileDropEvent.target = e.target;
        addFile(inputEl, file, true);
      }
    }
  }
})

動作

ファイルアップロードができるようなテキストエリア(チケットの説明やWikiとか)でペーストすると画像のアップロード+Wiki記法で画像表示が埋め込まれます。Redmine 4.0 で確認しています。

f:id:onozaty:20190314000117g:plain

Spring BootでgRPCとRESTを比べてみる

前回gRPCを試したので、RESTとgRPCで同じAPIのパフォーマンスを比較してみます。

受け取ったメッセージをそのまま返す単純なAPIです。

REST

package com.example.server;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.Value;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("api/echo")
@Slf4j
public class EchoRestController {

    @PostMapping
    public EchoResponse echo(@RequestBody EchoRequest request) {

        log.info("REST: " + request.getMessage());

        return new EchoResponse(request.getMessage());
    }

    @Value
    public static class EchoRequest {
        private final String message;
    }

    @Value
    public static class EchoResponse {
        private final String message;
    }
}

gRPC

syntax = "proto3";

option java_package = "com.example.server";

service Echo {
  rpc echo (EchoRequest) returns (EchoReply) {}
}

message EchoRequest {
  string message = 1;
}

message EchoReply {
  string message = 1;
}
package com.example.server;

import org.lognet.springboot.grpc.GRpcService;

import com.example.server.EchoGrpc.EchoImplBase;
import com.example.server.EchoOuterClass.EchoReply;
import com.example.server.EchoOuterClass.EchoRequest;

import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;

@GRpcService
@Slf4j
public class EchoGrpcService extends EchoImplBase {

    @Override
    public void echo(EchoRequest request, StreamObserver<EchoReply> responseObserver) {

        log.info("gRPC: " + request.getMessage());

        EchoReply reply = EchoReply.newBuilder()
                .setMessage(request.getMessage())
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}

パフォーマンス比較

10文字のメッセージを10万回投げて比較してみましたが、手元のPCでgRPCが71秒、RESTが86秒といった形で、思ったほど差がでませんでした。

それぞれのコードは下記の通りです。

REST

package com.example.client;

import java.util.stream.IntStream;

import org.springframework.web.client.RestTemplate;

import com.example.server.EchoRestController.EchoRequest;
import com.example.server.EchoRestController.EchoResponse;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class EchoRestClient {

    private final RestTemplate restTemplate = new RestTemplate();

    private final String url;

    public static void main(String[] args) {

        long startTime = System.currentTimeMillis();

        String message = "1234567890";
        EchoRestClient client = new EchoRestClient("http://localhost:8080/api/echo");

        IntStream.range(0, 100000)
                .forEach(x -> client.echo(message));

        long totalTime = System.currentTimeMillis() - startTime;

        System.out.println(
                String.format(
                        "Finish. Total time: %,.3f seconds",
                        totalTime / 1000d));
    }

    public String echo(String message) {

        EchoResponse response = restTemplate.postForObject(
                url,
                new EchoRequest(message),
                EchoResponse.class);

        return response.getMessage();
    }
}

gRPC

package com.example.client;

import java.io.Closeable;
import java.util.stream.IntStream;

import com.example.server.EchoGrpc;
import com.example.server.EchoGrpc.EchoBlockingStub;
import com.example.server.EchoOuterClass.EchoReply;
import com.example.server.EchoOuterClass.EchoRequest;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class EchoGrpcClient implements Closeable {

    private final ManagedChannel channel;
    private final EchoBlockingStub stub;

    public static void main(String[] args) {

        long startTime = System.currentTimeMillis();

        String message = "1234567890";
        try (EchoGrpcClient client = new EchoGrpcClient("localhost:6565")) {

            IntStream.range(0, 100000)
                    .forEach(x -> client.echo(message));
        }

        long totalTime = System.currentTimeMillis() - startTime;

        System.out.println(
                String.format(
                        "Finish. Total time: %,.3f seconds",
                        totalTime / 1000d));
    }

    public EchoGrpcClient(String target) {

        channel = ManagedChannelBuilder.forTarget(target)
                .usePlaintext()
                .build();

        stub = EchoGrpc.newBlockingStub(channel);
    }

    public String echo(String message) {

        EchoRequest request = EchoRequest.newBuilder()
                .setMessage(message)
                .build();

        EchoReply reply = stub.echo(request);

        return reply.getMessage();
    }

    @Override
    public void close() {

        channel.shutdown();
    }
}

終わりに

全体のコードは下記になります。

Spring Boot使うとRESTもgRPCも簡単ですね!

ちなみにREST側はSwagger使うことによってOpenAPIの定義作れて、クライアントの生成も楽なのですが、gRPCはprotoファイル作るのが結構面倒な気がします。OpenAPIの情報から作れるとREST→gRPCへの移行が楽なので、そのあたりも探ってみたいと思います。

あとはパフォーマンスの差が思ったほど出なかったのが、シンプルなAPIのせいかもしれないので、もう少し複雑なメッセージのやり取りを行うようなものでも試してみたいと思います。

Spring BootでgRPCを試してみる

gRPCとRESTの比較をしてみたかったので、まずはgRPCを触ってみました。

Srping Bootだと、grpc-spring-boot-starterを使うと簡単にgRPCのサーバが実装できます。すばらしい。

build.gradle

build.gradle は下記のような感じです。Spring BootのStarterで作成したものに、gRPCに必要なものを付け加えています。 参考にしたのは、grpc-spring-boot-starter のサンプルです。

buildscript {
    ext { springBootVersion = '2.1.2.RELEASE' }
    repositories { mavenCentral() }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath('com.google.protobuf:protobuf-gradle-plugin:0.8.8')
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'com.google.protobuf'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories { mavenCentral() }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.github.lognet:grpc-spring-boot-starter:3.1.0'
    compileOnly 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.assertj:assertj-core:3.11.1'
}

protobuf {
    protoc { artifact = 'com.google.protobuf:protoc:3.5.1' }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.18.0" }
    }

    generateProtoTasks {
        ofSourceSet('main').each { task ->
            task.builtins {
                java{ outputSubDir = 'protogen' }
            }
            task.plugins {
                grpc { outputSubDir = 'protogen' }
            }
        }
    }
    generatedFilesBaseDir = "$projectDir/src/"
}

sourceSets {
    main {
        java { srcDir 'src/main/protogen' }
    }
}

task cleanProtoGen{
    doFirst{
           delete("$projectDir/src/main/protogen")
    }
}

clean.dependsOn cleanProtoGen

proto

proto の定義は、とりあえずgRPCの公式サイトにあるサンプルと同じで。

syntax = "proto3";

option java_package = "com.example.grpc";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

Gradle の generateProto タスクを実行すると、上記定義を元にJavaのコードが生成されます。

@GRpcService の実装

あとは@GRpcServiceを付与したサービスクラスを実装するだけです。

package com.example.grpc;

import org.lognet.springboot.grpc.GRpcService;

import com.example.grpc.GreeterOuterClass.HelloReply;
import com.example.grpc.GreeterOuterClass.HelloRequest;

import io.grpc.stub.StreamObserver;

@GRpcService
public class GreeterService extends GreeterGrpc.GreeterImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {

        HelloReply reply = HelloReply.newBuilder()
                .setMessage("Hello " + request.getName())
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}

クライアントのコード

クライアントのコードは下記のような感じで。(Spring Bootのテストコードとして書いてます)

package com.example.grpc;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.lognet.springboot.grpc.context.LocalRunningGrpcPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.example.grpc.GreeterOuterClass.HelloReply;
import com.example.grpc.GreeterOuterClass.HelloRequest;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GreeterServiceTest {

    @LocalRunningGrpcPort
    private int runningPort;

    @Test
    public void sayHello() {

        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", runningPort)
                .usePlaintext()
                .build();

        GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);

        String name = "Taro";

        HelloRequest request = HelloRequest.newBuilder()
                .setName(name)
                .build();

        HelloReply reply = stub.sayHello(request);

        assertThat(reply.getMessage()).isEqualTo("Hello " + name);
    }
}

おわりに

ちょっと試してみる分には、すごく簡単に実行できました。

コード全体は、下記のプロジェクトになります。

Channelは使いまわしできるのかとか、並列で呼び出す場合にはどのように使うのか、、など、まだまだわからないところがあるので、今後もう少し調べてみようと思います。

これ読むといいよ!!とかありましたら、ぜひ教えてください。

Redmine: View customize plugin の v2.0.1 をリリースしました

View customizeのバージョン2.0.1を先ほどリリースしました。

軽微なFixのみになります。

HTTP(S)通信のテストに、OkHttpのMockWebServerを利用する

JavaのHTTPクライアントとして有名なOkHttpですが、関連プロジェクトにMockWebServerという機能があります。

HTTPサーバとしてモックを提供するものになります。手順としては、、

  • モックサーバとして返却するレスポンスのシナリオを書く
  • モックサーバを起動する
  • モックサーバのURLに対して、アプリケーションでアクセスする
  • レスポンスが予期したものであることを検証する

といった形になります。とてもシンプルで使いやすいです。

なお、OkHttpの関連プロジェクトですが、クライアントはOkHttpである必要性はありません。なので、様々なHTTPクライアントからの通信をテストすることができます。

使い方

MockWebServerのサイトのサンプルコードを元に説明します。

まずは、MockWebServerを生成し、モックサーバとして返却するレスポンスを定義します。

  MockWebServer server = new MockWebServer();

  server.enqueue(new MockResponse().setBody("hello, world!"));
  server.enqueue(new MockResponse().setBody("sup, bra?"));
  server.enqueue(new MockResponse().setBody("yo dog"));

モックサーバを起動し、モックサーバのURLを取得します。

  server.start();

  HttpUrl baseUrl = server.url("/v1/chat/");

アプリケーションを実行します。この際に、モックサーバのURLに対してアクセスさせるように指定します。

  Chat chat = new Chat(baseUrl);

  chat.loadMore();
  chat.loadMore();
  chat.loadMore();

モックサーバで受け付けたリクエストを検証します。server.takeRequest()で順番にリクエスト内容を取得できます。

  RecordedRequest request1 = server.takeRequest();
  assertEquals("/v1/chat/messages/", request1.getPath());
  assertNotNull(request1.getHeader("Authorization"));

  RecordedRequest request2 = server.takeRequest();
  assertEquals("/v1/chat/messages/2", request2.getPath());

  RecordedRequest request3 = server.takeRequest();
  assertEquals("/v1/chat/messages/3", request3.getPath());

最後にモックサーバを停止するのをお忘れなく。

server.shutdown();

2018年 洋楽マイベスト10

2018年に聴いていた洋楽で、自分が好きなものを10曲あげてみます。なるべく2018年にリリースされた曲に絞ってます。

  • Girls Like You ft. Cardi B - Maroon 5
  • Eastside - benny blanco, Halsey & Khalid
  • Youngblood - 5 Seconds Of Summer
  • The Middle - Zedd, Maren Morris, Grey
  • Colour ft. Hailee Steinfeld - MNEK
  • 2002 - Anne-Marie
  • 1999 - Charli XCX & Troye Sivan
  • Electricity ft. Diplo & Mark Ronson - Silk City & Dua Lipa
  • Dance To This ft. Ariana Grande - Troye Sivan
  • Personal - HRVY
  • (おまけ) Jenny - RUANN

SpotifyとYouTubeでプレイリストとして公開していますので、とりあえず聴いてみたいというかたはぜひ。

以降は簡単なコメントで各曲を紹介です。

Girls Like You ft. Cardi B - Maroon 5

2018年に洋楽で一番聴いた曲です。マルーン5 大好きですが、その中でも一番好きな曲になりました。アダムの優しい声が、曲にとてもあってます。

PVには、いろいろな女性有名人(女性アーティストも)が出ているので、それを見るのも楽しめます。

別バージョンのPVもあって、こっちはオフショット感があってまた面白いです。

Eastside - benny blanco, Halsey & Khalid

売れっ子音楽プロデューサーのベニー・ブランコが、ホールジー、カリードをボーカルとして向かえてリリースした曲です。心地よい曲で、最初に耳にした時点で、これ好きな曲だ、、というのが一発でわかりました。

Youngblood - 5 Seconds Of Summer

5SOSはデビュー曲のShe Looks So Perfectから好きで、日本での初ライブで見て、さらに好きになったグループです。デビューから時間を重ねるにつれて、どんどん大人になっていっていて、曲も洗練されてきています。Youngbloodは、そんな大人になった5SOSの渾身の1曲だと思います。

The Middle - Zedd, Maren Morris, Grey

ゼッドの曲はどれも良いのですが、そんな中でも1番ではと思う曲です。毎回曲に合わせてボーカルを選んでるんだろうなぁと思いますが、今回の曲もマレン・モリスの声がぴったりあっています。

ベスト10には入れなかったのですが、ゼッドがRemixしたショーン・メンデスの Lost In Japan もとても良い曲です。

Colour ft. Hailee Steinfeld - MNEK

MNEKのことはまったく知らず、ヘイリー・スタインフェルドそろそろまた日本こないかなーなんて調べていて、たまたま耳にした曲です。心地よいリズムから始まり、後半に向けてアップビートな感じになっていく、、とても癖になる曲で、何度も繰り返し聞くことになりました。

2002 - Anne-Marie

エド・シーランと一緒に書き上げたというアン・マリーのこの曲ですが、エド・シーランのギターで一緒に歌っている下記がさらにお勧めです。ほんと仲がよさそう。

二人とも4月に来日してライブやるので、競演したりしないかな、、と思ってます。(一週間違いだから厳しいかな)

1999 - Charli XCX & Troye Sivan

チャーリーXCXとトロイ・シヴァンがコラボしたこの曲は、ポップで、ちょっとノスタルジックな感じもする曲に仕上がってます。

PVではタイトルにもなった1990年代のオマージュがたくさんあって、1990年代が青春!?だった自分には、とても面白かったです。(なんのオマージュだかわかるものばかり!!)

Electricity ft. Diplo & Mark Ronson - Silk City & Dua Lipa

ディプロとマーク・ロンソンによるユニットのシルクシティが、デュア・リパをボーカルに迎えたこの曲は、とてもかっこいい曲になっています。デュア・リパの声含めかっこいい。

2018年はデュア・リパの曲を良く聞いていて、IDGAF や One Kiss も良かったのですが、その中でも一番良かったこの曲をあげています。

Dance To This ft. Ariana Grande - Troye Sivan

トロイ・シヴァンがアリアナ・グランデとコラボした、キャッチーで、ちょっとアンニュイな感じなこの曲で、初めてトロイ・シヴァンを知りました。お気に入りのアーティストになりました。

Personal - HRVY

Spotifyでお勧めされて、ポップなこの曲にすっかりはまってしまいました。今まであげた曲が好きな人ならば、はまること間違いないと思います。

おまけ: Jenny - RUANN

洋楽じゃないのですが、、RUANNのJennyは、洋楽好きならば、ぜったいにはまる1曲だと思います。ストリーミング配信されていない曲で、SCRAMBLE 14 というCDでのみ収録されている曲です。

洋楽カバーでも、いろんなアレンジを魅せてくれていて、個人的には下記のエド・シーランのShape of Youのカバーが大好きです。洋楽好きにお勧めできるアーティストです!

CSVを読み込んでRedmineのチケットを新規作成、更新するツール(redmine-issue-loader)を作りました

もともとカスタムフィールドの更新しかできないツールとしていましたが、年末年始で改修して、対象とするフィールドを増やして、新規作成+更新ができるツールに変更しました。名前もredmine-issue-updaterからredmine-issue-loaderに変更しています。

Redmineのチケットのインポートには、既にいくつか方法があります。

それらと比べて、本ツールのメリットとなる部分は下記だと考えています。

  • コマンドラインで実行できるので、周期実行として組み込みやすい。たとえば他のシステムとの連携で、決められたタイミングでCSVでエクスポートし、本ツールで取り込むといった利用が可能。
  • 設定ファイルにマッピング情報を記載するので、一度設定ファイルを書いてしまえば、その設定ファイルを使って、何度も同じ条件で実行可能。(RedmineのCSVインポートだと、画面上で項目のマッピングを選ぶ必要あり)
  • カスタムフィールドをキーとしてチケットを更新できるので、他のシステムのIDをカスタムフィールドとして入れておき、それをキーとして更新することができる。(取り込み側のチケットIDを知らなくても更新できる)

詳しい利用方法などは、下記をご参照ください。