Greasemonkeyでテキストエリアに入力補完を追加する

Redmineのtextile記法で、コードハイライトは<pre><code class="java"></code></pre>のような書き方をするのですが、これを入力するのが面倒になってきたので、Greasemonkeyを使ってテキストエリアで入力補完を行ってみました。

なお、Redmine 3.3 からは、ツールバーにコードハイライト用のボタンが追加されていますので、それを使うことによっても手間は軽減できるかと思います。

実装方法

テキストエリアでの入力補完は、カーソル位置を取るのが面倒なため、他のライブラリを利用します。

いくつかよさそうなものがありましたが、手軽そうなものということで、At.jsを今回は利用しました。

Greasemonkeyの@requireで外部JavaScriptを読み込めるので、cdnjsにあるAt.jsと、At.jsが依存しているCaret.jsというライブラリを読み込むようにしました。At.jsはjQueryに依存していますが、Redmineの各画面でも読み込んでいるので、指定しなくて大丈夫です。

At.js用のスタイルは、直接styleタグとしてヘッダに追加しました。

後は対象のテキストエリアを指定して、入力補完の設定をしていくだけです。 At.jsでは、atwhoという関数に対して、必要な情報を指定することよって、補完が行われるようになります。 とてもシンプルなので、迷うことはありませんでした。

$('.atwho-inputor').atwho({
  at: "@",
  data: ["one", "two", "three"],
}).atwho({
  at: ":",
  data: ["+1", "-1", "smile"]
});

動作イメージ

f:id:onozaty:20170228025858g:plain

スクリプト

ということで出来上がったスクリプトは、下記のようになります。

// ==UserScript==
// @name        Redmine wiki textcomplete
// @namespace   com.enjoyxstudy.redmine.wiki.textcomplete
// @version     1
// @grant       none
// @require     https://cdnjs.cloudflare.com/ajax/libs/at.js/1.5.2/js/jquery.atwho.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/Caret.js/0.3.1/jquery.caret.min.js
// ==/UserScript==
(function($) {
  $('textarea.wiki-edit').atwho({
    at: '<',
    data: [
      {name: 'java', content: '<pre><code class="java">\n</code></pre>'},
      {name: 'sql', content: '<pre><code class="sql">\n</code></pre>'}],
    insertTpl: '${content}',
    suffix: ''
  }).atwho({
    at: '{',
    data: [
      {name: 'collapse', content: '{{collapse(詳細を表示...)\n}}'}],
    insertTpl: '${content}',
    suffix: ''
  }).atwho({
    at: 'a',
    data: [
      {name: 'attachment', content: 'attachment:'}],
    insertTpl: '${content}',
    suffix: ''
  }).atwho({
    at: 'c',
    data: [
      {name: 'commit', content: 'commit:'}],
    insertTpl: '${content}',
    suffix: ''
  });

  $('head').append(
    `<style>
.atwho-view {
    position:absolute;
    top: 0;
    left: 0;
    display: none;
    margin-top: 18px;
    background: white;
    color: black;
    border: 1px solid #DDD;
    border-radius: 3px;
    box-shadow: 0 0 5px rgba(0,0,0,0.1);
    min-width: 120px;
    z-index: 11110 !important;
}

.atwho-view .atwho-header {
    padding: 5px;
    margin: 5px;
    cursor: pointer;
    border-bottom: solid 1px #eaeff1;
    color: #6f8092;
    font-size: 11px;
    font-weight: bold;
}

.atwho-view .atwho-header .small {
    color: #6f8092;
    float: right;
    padding-top: 2px;
    margin-right: -5px;
    font-size: 12px;
    font-weight: normal;
}

.atwho-view .atwho-header:hover {
    cursor: default;
}

.atwho-view .cur {
    background: #3366FF;
    color: white;
}
.atwho-view .cur small {
    color: white;
}
.atwho-view strong {
    color: #3366FF;
}
.atwho-view .cur strong {
    color: white;
    font:bold;
}
.atwho-view ul {
    /* width: 100px; */
    list-style:none;
    padding:0;
    margin:auto;
    max-height: 200px;
    overflow-y: auto;
}
.atwho-view ul li {
    display: block;
    padding: 5px 10px;
    border-bottom: 1px solid #DDD;
    cursor: pointer;
    /* border-top: 1px solid #C8C8C8; */
}
.atwho-view small {
    font-size: smaller;
    color: #777;
    font-weight: normal;
}
    </style>`
  );
})(jQuery);

おわりに

いろいろ候補を追加して、自分好みのものに変えていこうと思っています。

Spring Boot: Scheduledアノテーションを使用して、スケジュールされたタイミングでメソッドを実行する

Scheduledアノテーションを使うと、スケジュールされたタイミングでメソッドを実行することができます。 これで周期実行的なものは、簡単に実装できます。

実装方法

実行したいメソッドにScheduledアノテーションを付けます。

@Component
public class Scheduler {

    @Scheduled(fixedRate = 5000)
    public void doSomething() {
        // 5秒周期で行いたい処理
    }
}

Scheduledアノテーションによる実行を有効とするためには、EableSchedulingアノテーションを付けます。

@SpringBootApplication
@EnableScheduling
public class SchedulerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }
}

これだけで周期実行が実装できます。簡単ですね!

Scheduledで指定できるパターン

指定方法は大きく分けて3パターン用意されています。

  1. fixedDelay : 前回タスクの実行完了時点から指定時間後にタスクを実行する。単位はms。
  2. fixedRate : 前回タスクの実行開始時点から指定時間後にタスクを実行する。単位はms。
  3. cron : 指定した周期でタスクを実行する。(Linuxのcronと似た書式)

また、fixedDelayfixedRateでは、初回のタスクの実行開始時間を指定するものとして、initialDelayがあります。

下記のようにすると、初回はアプリケーション起動から30秒後に実行され、その次からは、前回タスクの実行完了から60秒後に実行されることになります。

@Scheduled(fixedDelay = 60000, initialDelay = 30000)
public void doSomething() {
}

cronでは、zoneというフィールドで、cronの起動時間のタイムゾーンを指定できます。(未指定の場合は、デフォルトのタイムゾーン)

下記のようにすると、東京のタイムゾーンで、8時と9時と10時に実行されます。(8時から10時の間で、0秒、0分のタイミングで実行といった指定になっている)

@Scheduled(cron = "0 0 8-10 * * *", zone = "Asia/Tokyo")
public void doSomething() {
}

設定ファイルで指定する

ソース上に固定値で設定するのではなく、設定ファイルに記載することができます。

${設定名}で指定しておいて、

@Scheduled(cron = "${scheduler.cron}")
public void doSomething() {
}

application.properties で設定値を書きます。

scheduler.cron=*/5 * * * * *

fixedDelayfixedRateinitialDelayも、設定値とできるように、fixedDelayStringfixedRateStringinitialDelayStringというものがフィールドとしてあるので、そちらを使えばOKです。

@Scheduled(fixedRateString = "${scheduler.fixed-rate}")
public void doFixedRateString() throws InterruptedException {
}

ユニットテストの際に、周期実行を抑止する

テストの時には、勝手に周期実行が動いて欲しく無い場合もあるかと思います。

fixedDelayStringfixedRateString ならば、テスト時の設定値を変えてとても大きな数字にしておけば、抑止と同等のことができそうですが、cronだと、ぜったいに実行されないタイミングを指定するのは難しそうです。

こういった場合は、EnableSchedulingが指定されないように設定してあげれば回避できます。

下記のようなクラスでSpring Bootを起動しているならば、

@SpringBootApplication
@EnableScheduling
public class SchedulerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }
}

テスト用に別の起動クラスを作成し、そちらでは@EnableSchedulingを指定しないようにします。 また、コンポーネントスキャンで、@EnableSchedulingを指定しているものを拾ってしまうと有効になってしまうので、該当のクラスを除外するように指定しておきます。

@SpringBootApplication
@ComponentScan(excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, value = SchedulerApplication.class))
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

このクラスを@SpringBootTestに指定してあげれば、@EnableSchedulingが指定されずに、周期実行を抑止することが出来ます。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SchedulerTest.TestApplication.class)
public class SchedulerTest {

Redmine: リポジトリタブでデフォルト表示されるbranchを変更する(View customize plugin)

View customizeで『「リポジトリ」タブをクリック時にデフォルトで表示されるブランチをmasterではなく、ある特定のブランチに設定したい』といった問い合わせをいただいたので対応してみました。

ブランチの指定は、revというパラメータで行われているので、「リポジトリ」タブのリンクを、パラメータ付きのものに変えて対応します。

View customize の設定内容

Path pattern

全画面を対象にします。

.*

Code

Type:JavaScriptとして下記を設定します。

$(function() {
  var branchName = '3.3-stable'; // デフォルト
  var baseUrl = $('a.repository').attr('href');
  
  $('a.repository').attr('href', baseUrl + '?rev=' + encodeURIComponent(branchName));
});

もし特定のプロジェクトに対してのみ指定したい場合には、body.project-{プロジェクト名}と指定すると対象を絞ることができます。

$('body.project-xxx a.repository')

設定後のイメージ

リポジトリタブのリンク先が、指定したブランチになりました。

f:id:onozaty:20170212172229p:plain

MyBatisのArrayTypeHandler使用時の注意点

MyBatisには、org.apache.ibatis.type.ArrayTypeHandlerがあって、java.sql.Arrayとのマッピングを行ってくれますが、パラメータ設定時と、結果取得時でマッピングが異なるので注意が必要です。

実際のコードを見たほうがわかりやすいので、現時点のコードを引用します。

public class ArrayTypeHandler extends BaseTypeHandler<Object> {

  public ArrayTypeHandler() {
    super();
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
    ps.setArray(i, (Array) parameter);
  }

  @Override
  public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
    Array array = rs.getArray(columnName);
    return array == null ? null : array.getArray();
  }

  @Override
  public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    Array array = rs.getArray(columnIndex);
    return array == null ? null : array.getArray();
  }

  @Override
  public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    Array array = cs.getArray(columnIndex);
    return array == null ? null : array.getArray();
  }
}

パラメータ設定時(setNonNullParameterメソッド)では、java.sql.Arrayを パラメータとして求めますが、結果取得時(getNullableResultメソッド)だと、java.sql.ArrayからgetArrayしたもの(たとえばPostgreSQLのintの配列(int[])だと、Javaのint[])が返却されます。 そのため、同じEntityを使ってInsertとSelectを行うということができません。

java.sql.Arrayを生成する際に、配列のタイプを文字列で指定する必要があり、汎用的なTypeHandlerを作るのが難しいのでこのようになってしまっているのではと思いますが、そのあたりの理由を知っていないと、嵌りそうですね。

同じEntityを使いたい場合には、ArrayTypeHandlerを使わずに、そのARRAYの型に応じたTypeHandlerを独自に作ることになりそうです。TypeHandler自体はとてもシンプルなので、実装も簡単です。 参考までに、List<Integer>と、PostgreSQLのint[]をマッピングするTypeHandlerの実装を以前作ったので、リンクしておきます。

Spring Bootのspring-boot-starter-webでTomcatではなくJettyを使う

Spring Bootのspring-boot-starter-webのデフォルトだとTomcatがサーブレットコンテナとして組み込まれますが、設定を変えればJettyに切り替えられます。

configurations {
    compile.exclude module: "spring-boot-starter-tomcat"
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    // ...
}

Tomcatを除外して、Jettyを依存関係に追加するといった手順なのですが、除外の設定はconfigurationsではなく、dependenciesでも書けます。

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE") {
        exclude module: 'spring-boot-starter-tomcat'
    }
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    // ...
}

ただ、この方法では注意が必要で、他の依存関係の設定でTomcatが指定されていると、除外できていないことになります。

たとえば、下記の例だと、spring-boot-starter-thymeleafspring-boot-starter-webに依存しているので、そこからのルートでTomcatを引き込んでしまいます。

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE") {
        exclude module: 'spring-boot-starter-tomcat'
    }
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    // ...
}

なので、除外するときには、Springの公式サイトにあるとおり、configurationsで書いたほうが確実です。

どのような依存関係になっているかは、dependenciesタスクで確認できますので、そちらで意図した形になっているか確認してみるのが良いかと思います。

Amazon Product Advertising APIを使ってISBNから書籍情報を取得する

ISBNから書籍の情報を取りたかったので、Amazon Product Advertising APIを使ってみました。

必要な情報

Amazon Product Advertising API を使うためには、下記の情報が必要になります。

  • アソシエイトタグ
  • アクセスキー
  • シークレットキー

アソシエイトのアカウントが必要で、取得方法は下記などが参考になります。

実装方法

より簡単にということで、makingさんの下記ライブラリを利用し、Javaで書いてみました。

mavenリポジトリで公開されているので、利用も簡単です。

build.gradleで下記のように依存関係を追加します。

apply plugin: 'java'

repositories { jcenter() }

dependencies {

    compile 'org.slf4j:slf4j-api:1.7.21'
    compile 'ch.qos.logback:logback-classic:1.1.7'

    compile group: 'am.ik.aws', name: 'aws-apa', version: '0.9.5'
}

Amazon Product Advertising API へ接続するための情報は、aws-config.propertiesに記載し、クラスパスに配置します。

aws.endpoint=https://ecs.amazonaws.jp
aws.accesskey.id=<Your Accesskey ID for AWS>
aws.secret.accesskey=<Your Secret Accesskey for AWS>
aws.associate.tag=<Associate Tag>

ISBNを指定して情報を取得する場合は下記のように書きます。 (ISBNの場合には、SearchIndexを指定する必要があります)

public class BookLookupClient {

    public Optional<Item> lookup(String isbn) {

        ItemLookupRequest request = new ItemLookupRequest();

        request.setSearchIndex("Books");
        request.getResponseGroup().add("Large");
        request.setIdType("ISBN");
        request.getItemId().add(isbn);

        AwsApaRequester requester = new AwsApaRequesterImpl();
        ItemLookupResponse response = requester.itemLookup(request);

        if (response.getItems().isEmpty()) {
            return Optional.empty();
        }

        return response.getItems().get(0).getItem().stream().findFirst();
    }
}
public class BookLookupClientTest {

    @Test
    public void testLookup() {

        BookLookupClient client = new BookLookupClient();

        Optional<Item> item = client.lookup("489471499X");

        assertTrue(item.isPresent());

        ItemAttributes itemAttributes = item.get().getItemAttributes();
        assertEquals("489471499X", itemAttributes.getISBN());
        assertEquals("Effective Java 第2版 (The Java Series)", itemAttributes.getTitle());
        assertEquals("ピアソンエデュケーション", itemAttributes.getPublisher());
    }
}

リクエストで設定すべき内容と、レスポンスの内容は、下記を参考にしました。

取りたい情報を辿るのがちょっと面倒でしたが、試すのはすごく簡単でした。

Spring BootでAssertJの3系を使う

Spring Boot(現在の最新の1.4系)はJava7もサポートしているため、spring-boot-starter-testで依存するAssertJは、Java8対応の3系ではなく、2系となっています。

3系に変えたい場合には、AssetJ3系を依存関係に追加するだけです。 (Gradleの依存関係の解決で、同じライブラリで複数のバージョンがあった場合は、デフォルトだと最新のものが利用される)

dependencies {
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.assertj:assertj-core:3.6.1')
}

これでAssertJの3系で追加されたラムダ対応のメソッドが利用できます。

List<String> names = Arrays.asList("Taro", "Hanako", "Jiro");

assertThat(names)
    .filteredOn(x -> x.contains("ro"))
    .containsExactly("Taro", "Jiro");

なお、extractingは、AssertJの2系でもラムダ使えます。 これは、下記のようなメソッドが定義されていて、Extractorが1つしかメソッドを持たないインタフェースとなっているためです。(ラムダで書けるのは、抽象メソッドが1つだけ定義されたインタフェース)

public <V> ListAssert<V> extracting(Extractor<? super ELEMENT, V> extractor) 
public interface Extractor<F, T> {
  T extract(F input);
}