SonarQubeのプラグイン作成(その2)

# 目的

SonarQubeを使って正誤表による単語チェックを行うプラグインを作成することで、SonarQubeのプラグイン作成方法を説明します。

この記事は[「SonarQubeのプラグイン作成(その1)」](/jca02266/items/12b7d2b515a393579d0f)からの続きです。先にそちらを読む必要があります。

# 単語チェックを行うクラスを作成

Javaで任意のファイルからある特定の単語の有無を確認するクラス`PatternSearcher`を作成します。

```java:PatternSearcher.java
package org.jca02266.sonarplugins.errata.rules;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PatternSearcher {
  Pattern pattern;

  public PatternSearcher(Pattern pattern) {
    this.pattern = pattern;
  }

  public void search(InputStream is, Charset cs, Function<Integer, Function<Integer, Consumer<Integer>>> consumer) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(is, cs))) {
      String line;
      int linenum = 0;
      while ((line = br.readLine()) != null) {
        linenum++;
        Matcher mat = pattern.matcher(line);
        int end = 0;
        while (mat.find(end)) {
          int start = mat.start();
          end = mat.end();

          consumer.apply(linenum).apply(start).accept(end);
        }
      }
    } catch (IOException e) {
      throw new RuntimeException("error", e);
    }
  }
}
```


SonarQubeでエラー箇所を指定するために、行、開始文字位置、終了文字位置が必要となるので、InputStream から行単位にpatternを探して見つかったら、見つかった位置を consumer に渡してあげるように作ってます。

consumer はラムダ式としていますが、複数引数を受け付けるConsumerインタフェース `Consumer3<Integer,Integer,Integer>`
のようなものはJavaでは用意されていないので、カリー化したラムダ式を渡すようにしています。

使い方の例として、テストクラスの抜粋を示します。
`linenum->start->end-> {...}`の部分がラムダ式になります。あまり見ない例だと思いますが、Interfaceを自分で定義せずにやるにはJavaではこうするしかないのだと思います。

型定義が、`Function<Integer, Function<Integer, Consumer<Integer>>>`というのはわかりにくいですが仕方ないですね。

```java:PatternSearcherTest.java
package org.jca02266.sonarplugins.errata.rules;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.regex.Pattern;

import org.junit.Test;

public class PatternSearcherTest {
  private static class Counter {
    private int counter = 0;

    public void increment() {
      counter++;
    }

    public int get() {
      return counter;
    }
  }

  @Test
  public void test_simple() throws Exception {
    String buffer = "aaaタイポbbb";

    Charset cs = Charset.forName("utf-8");
    InputStream is = new ByteArrayInputStream(buffer.getBytes(cs));

    Counter count = new Counter();

    new PatternSearcher(Pattern.compile("タイポ")).search(is, cs, linenum->start->end-> {
      assertThat(linenum, is(1));
      assertThat(start, is(3));
      assertThat(end, is(6));

      count.increment();
    });

    assertThat(count.get(), is(1));
  }
}
```

# センサーの修正

上記は単なるJavaプログラミングの話なので細かい点は無視して、これを使ってSonarQubeで単語チェックしてみましょう。
前の記事で作った `CreateIssuesOnTextFilesSensor.java` センサークラスを以下のように修正します。

```java:CreateIssuesOnTextFilesSensor.java
package org.jca02266.sonarplugins.errata.rules;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.regex.Pattern;

import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;

public class CreateIssuesOnTextFilesSensor implements Sensor {

  private static final double ARBITRARY_GAP = 1.0;

  @Override
  public void describe(SensorDescriptor descriptor) {
    descriptor.name("Check errata for text files");

    descriptor.onlyOnLanguage("java");
    descriptor.createIssuesForRuleRepositories(TextRulesDefinition.REPOSITORY);
  }

  @Override
  public void execute(SensorContext context) {
    FileSystem fs = context.fileSystem();
    Iterable<InputFile> textFiles = fs.inputFiles(fs.predicates().all());
    for (InputFile textFile : textFiles) {
      InputStream is;
      try {
        is = textFile.inputStream();
      } catch (IOException e) {
        throw new RuntimeException("failed to inputStream()", e);
      }

      Charset cs = textFile.charset();
      Pattern pat = Pattern.compile("タイポ");

      new PatternSearcher(pat).search(is, cs, linenum -> start -> end ->
        registerIssue(context, textFile, linenum, start, end)
      );
    }
  }

  private void registerIssue(SensorContext context, InputFile textFile, int line, int start, int end) {
    NewIssue newIssue = context.newIssue()
      .forRule(TextRulesDefinition.RULE)
      .gap(ARBITRARY_GAP);

    NewIssueLocation primaryLocation = newIssue.newLocation()
      .on(textFile)
      .at(textFile.newRange(textFile.newPointer(line, start), textFile.newPointer(line, end)))
      .message("Fix typo");
    newIssue.at(primaryLocation);

    newIssue.save();
  }
}
```

そして、pluginをコンパイル、差し替えて SonarQubeを再起動します。

```console
mvn clean package

cp target/sonar-errata-plugin-0.0.1.jar ../sonarqube_extensions/plugins/

docker-compose restart
```

SonarQubeが起動したら、sampleディレクトリに以下のファイルを追加して、scannerを動かしてみましょう。

```sample/test.txt
aタイポb
aaaタイポbbb
aaaタイポbbbタイポccc
```

```console
(cd sample && sonar-scanner)
```

http://localhost:9000 にアクセスして結果を見てみると、ちゃんと「タイポ」に赤の下線がついてエラーを指摘してくれています。うまくいってますね。

<img width="1274" alt="スクリーンショット 2019-11-04 12.18.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/490c14f0-05cf-d433-fb53-bf55ef015639.png">

admin でログインしていれば、この結果に対して、どうするか印をつけることができます。

<img width="659" alt="スクリーンショット 2019-11-04 12.24.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/2215f200-1274-7602-a129-aad0a01c79a4.png">

上記のように、Openのところをクリックすると

* Resolve as fixed: 解決済み
* Resolve as false positive: 誤判定
* Resolve as won't fix: 修正しない

といった、選択肢が現れますのでコメントを入れて保存することができます。以下は、"Resolve as false positive"を選んだ例です。

<img width="1273" alt="スクリーンショット 2019-11-04 12.28.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/6cc63674-de41-66f0-5b34-a06f38684700.png">

Resolution の False Positive が1件増えました。誤判定だったことを示す記録を残すことができるので便利です。

<img width="1271" alt="スクリーンショット 2019-11-04 12.32.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/a24a467e-8ece-f42d-e898-ab5e34ac7447.png">

# propertiesファイルから設定値を取得する

ここまでの実装では、正誤表は「タイポ」固定でセンサークラスに埋め込まれています。
これでは使いものにならないので、propertiesファイルから設定値を取得する方法を試しましょう。

まずは、propertiesファイルから値を取得する方法を`CreateIssuesOnTextFilesSensor.java`の差分で示します。

```diff:CreateIssuesOnTextFilesSensor.java
@@ -3,6 +3,7 @@ package org.jca02266.sonarplugins.errata.rules;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import java.util.regex.Pattern;
 
 import org.sonar.api.batch.fs.FileSystem;
@@ -12,13 +13,24 @@ import org.sonar.api.batch.sensor.SensorContext;
 import org.sonar.api.batch.sensor.SensorDescriptor;
 import org.sonar.api.batch.sensor.issue.NewIssue;
 import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
 
 public class CreateIssuesOnTextFilesSensor implements Sensor {
+  private static final Logger LOGGER = Loggers.get(CreateIssuesOnTextFilesSensor.class);
 
   private static final double ARBITRARY_GAP = 1.0;
 
+  protected final Configuration config;
+
+  public CreateIssuesOnTextFilesSensor(final Configuration config) {
+    LOGGER.info("errata sensor constructor called");
+    this.config = config;
+  }
   @Override
   public void describe(SensorDescriptor descriptor) {
+    LOGGER.info("errata sensor describe() called");
     descriptor.name("Check errata for text files");
 
     descriptor.onlyOnLanguage("java");
@@ -27,6 +39,11 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
 
   @Override
   public void execute(SensorContext context) {
+    LOGGER.info("errata sensor execute() called");
+
+    LOGGER.info("errata text: {}", config.get("errata.text").orElse("default"));
+    Arrays.stream(config.getStringArray("errata.text2")).forEach(val -> LOGGER.info("errata text2: {}", val));
+
     FileSystem fs = context.fileSystem();
     Iterable<InputFile> textFiles = fs.inputFiles(fs.predicates().all());
     for (InputFile textFile : textFiles) {
```

設定値を取得するには`org.sonar.api.config.Configuration`クラスを使います。また、取得した値を確認するために`org.sonar.api.utils.log.Logger`クラスを使っています。Loggerの使い方はjavaの一般的なそれと同じなので特に説明はしません。

最初に、コンストラクタでConfigurationオブジェクトを取得しています。
なお、このコンストラクタの引数のバリエーションはよくわかっていません。

ちゃんとコンストラクタが呼ばれていることを確認するために、ログを出しています。

```java
  public CreateIssuesOnTextFilesSensor(final Configuration config) {
    LOGGER.info("errata sensor constructor called");
    this.config = config;
  }
```

そして、以下でconfigから値を取得します。`config.get(key)`は、文字列の値を取得するメソッドですが、その戻り値は`Optional<String>`なので、orElse()などでStringを取得します。

この他に、`getBoolean()`, `getInt()`, `getLong()` などがあります。いずれも`Optional<>`を返します。

```java
   LOGGER.info("errata text: {}", config.get("errata.text").orElse("default"));
```

また配列を取得するために、`config.getStringArray(key)`があります。
これは、`String[]`を返します。

```java
   Arrays.stream(config.getStringArray("errata.text2")).forEach(val -> LOGGER.info("errata text2: {}", val));
```

試してみましょう。いつものようにコンパイル、プラグイン置き換え、再起動を行います。

```console
mvn clean package

cp target/sonar-errata-plugin-0.0.1.jar ../sonarqube_extensions/plugins/

docker-compose restart
```

そして、scannerを実行する前に、sample/sonar-project.properties に以下の行を追加します。

```sample/sonar-project.properties
# 〜略〜
errata.text = test
errata.text2 = test1,test2
```

そして、scannerを実行します。Loggerの出力はscannerの標準出力に現れますので、propertiesの設定内容が反映されているか確認してください。

```console
cd sample && sonar-scanner

〜略〜
INFO: Sensor Check errata for text files [errata]
INFO: errata sensor execute() called
INFO: errata text: test
INFO: errata text2: test1
INFO: errata text2: test2
INFO: Sensor Check errata for text files [errata] (done) | time=16ms
〜略〜
```

うまく、propertiesファイルから値を取得できました。

# Configurationを追加する

scannerのプロパティファイルだとプロジェクト毎に設定が必要になります。
SonarQubeに複数プロジェクトが登録されている場合を考慮し、全体に同じ設定を適用できるようにするためにConfigurationに設定を追加しましょう。

プラグインに以下のソースを追加します。

```java:org.jca02266.sonarplugins.errata.settings.ErrataProperties
package org.jca02266.sonarplugins.errata.settings;

import static java.util.Arrays.asList;

import java.util.List;

import org.sonar.api.config.PropertyDefinition;

public class ErrataProperties {

  public static final String CATEGORY = "Errata Properties Example";

  private ErrataProperties() {
  }

  public static List<PropertyDefinition> getProperties() {
    return asList(
      PropertyDefinition.builder("errata.text")
        .name("Errata1")
        .description("Errata properties 1")
        .defaultValue("タイポ")
        .category(CATEGORY)
        .build(),
      PropertyDefinition.builder("errata.text2")
        .name("Errata2")
        .description("Errata properties 2")
        .multiValues(true)
        .category(CATEGORY)
        .build());
  }
}
```

この例では先ほどと同じ、errata.textという設定とerrata.text2という設定を追加しています。
また、errata.textはデフォルト値(defaultValue())を設定し、errata.text2は複数の値(multiValues(true))を設定できるようにしています。

この ErrataProperties は以下のように、`ErrataProperties.getProperties()`の戻り値をプラグインとして登録します。

```java:src/main/java/org/jca02266/sonarplugins/errata/ErrataPlugin.java
  public void define(Context context) {
    context.addExtensions(TextRulesDefinition.class, CreateIssuesOnTextFilesSensor.class);
    context.addExtensions(ErrataProperties.getProperties());  // <- 追加
  }
```

コンパイル、プラグイン置き換え、再起動を行います。

```console
mvn clean package

cp target/sonar-errata-plugin-0.0.1.jar ../sonarqube_extensions/plugins/

docker-compose restart
```

SonarQubeにadminでログインし、Administration→Configurationを参照すると「Errata Properties Example」というタブが追加されています。Errata 1には「タイポ」デフォルト値
として設定されています。Errata 2 は値を入力すると次の値を入力するテキストボックスが動的に追加されます。(以下)

<img width="1270" alt="スクリーンショット 2019-11-04 15.14.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/f9384886-bd86-01cf-999e-ce2ac66bd852.png">

上記を設定しても scannar のプロパティが優先されるので、前に追加した以下のpropertiesファイルの設定はコメントアウトしておきます。

```sample/sonar-project.properties
# 〜略〜
#errata.text = test
#errata.text2 = test1,test2
```

そして、scannerを実行すると設定ページの内容が出力されます。

```console
(cd sample && sonar-scanner)

〜略〜
INFO: Sensor Check errata for text files [errata]
INFO: errata sensor execute() called
INFO: errata text: タイポ
INFO: errata text2: aaa
INFO: errata text2: bbb
INFO: Sensor Check errata for text files [errata] (done) | time=17ms
〜略〜
```

これで、設定ページの設定からもpropertiesファイルからも値を取得できるようになりました。

# 対象ファイルの限定

現在は、全てのファイルがチェック対象になってしまってます。
設定で対象を絞り込めるようになった方がいいのでその修正を行います。

全ファイルが対象になっているのは以下の部分によります。

```java:CreateIssuesOnTextFilesSensor.java
〜略〜
  @Override
  public void execute(SensorContext context) {
〜略〜
    Iterable<InputFile> textFiles = fs.inputFiles(fs.predicates().all());

```

`fs.predicates().all()`の部分です。これは全てのファイルが対象であることを示します。
predicates()は、FilePredicatesを返すメソッドで、FilePredicatesには以下のようなメソッドがあります。詳しくは[FilePredicates.javaのソース](https://github.com/SonarSource/sonarqube/blob/master/sonar-plugin-api/src/main/java/org/sonar/api/batch/fs/FilePredicates.java)をみてください。

* `all()`
* `none()`
* `and(FilePredicate... and)`
* `or(FilePredicate... or)`
* `hasAbsolutePath(String s)`
* `hasRelativePath(String s)`
* `hasExtension(String s)`
などなど

このor()とhasExtension()というのを使ってみましょう。ソースを例えば以下のように修正します。
(まだ固定値です)
なお、hasExtension()で指定する拡張子には"."を含めてはいけないようです。

```diff
--- a/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
+++ b/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
@@ -6,6 +6,8 @@ import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.regex.Pattern;
 
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FilePredicates;
 import org.sonar.api.batch.fs.FileSystem;
 import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.sensor.Sensor;
@@ -37,6 +39,11 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
     descriptor.createIssuesForRuleRepositories(TextRulesDefinition.REPOSITORY);
   }
 
+  private FilePredicate addExtensions(FilePredicates fps) {
+    return fps.or(fps.hasExtension("txt"),
+                  fps.hasExtension("java"));
+  }
+
   @Override
   public void execute(SensorContext context) {
     LOGGER.info("errata sensor execute() called");
@@ -45,7 +52,9 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
     Arrays.stream(config.getStringArray("errata.text2")).forEach(val -> LOGGER.info("errata text2: {}", val));
 
     FileSystem fs = context.fileSystem();
-    Iterable<InputFile> textFiles = fs.inputFiles(fs.predicates().all());
+    FilePredicate fp = addExtensions(fs.predicates());
+    Iterable<InputFile> textFiles = fs.inputFiles(fp);
+
     for (InputFile textFile : textFiles) {
       InputStream is;
       try {
```

度々ですが、コンパイル、プラグイン置き換え、再起動を行います。

```console
mvn clean package

cp target/sonar-errata-plugin-0.0.1.jar ../sonarqube_extensions/plugins/

docker-compose restart
```

試してみましょう。javaやpropertiesファイルに「タイポ」の文字を追加してscannerを実行してみてください。java,txtファイルだけがエラーとして引っかかっていれば成功です。)

うまくいったならこれを設定から取得するようにしましょう。`FilePredicates.or()`には、引数に`Collection<FilePredicate>`を受け付けるものもあるので、それを使えばあとは`String[]`から`List<FilePredicate>`に変換するだけなので簡単です。
ここでは、設定値に、".java"などと"."があっても良いようにしたので少しごちゃごちゃしてます。

```diff
--- a/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
+++ b/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
@@ -4,7 +4,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.util.Arrays;
+import java.util.List;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.sonar.api.batch.fs.FilePredicate;
 import org.sonar.api.batch.fs.FilePredicates;
@@ -40,8 +42,19 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
   }
 
   private FilePredicate addExtensions(FilePredicates fps) {
-    return fps.or(fps.hasExtension("txt"),
-                  fps.hasExtension("java"));
+    String[] suffixes = config.getStringArray("errata.file.suffixes");
+    List<FilePredicate> suffixPredicateList = Arrays.stream(suffixes).map(s -> {
+        // remove "." at first
+        if (s.startsWith(".")) {
+          return s.substring(1);
+        } else {
+          return s;
+        }
+      })
+      .map(s -> fps.hasExtension(s))
+      .collect(Collectors.toList());
+
+    return fps.or(suffixPredicateList);
   }
 
   @Override
```

例えば、以下のように対象のファイルの拡張子を設定できるようになりました。
最終的にはConfigurationページにも設定を追加します。

```sample/sonar-project.properties
〜略〜

#","の前後にスペースがあっても除去してくれます
errata.file.suffixes=.txt, .java, .properties
#誤検出用
errata.text=タイポ
```

# 正誤表の登録

最後に正誤表を設定から取得できるようにしましょう。

正誤表は、誤字を示す正規表現と正しい文字列を示したいので、以下のような形式にしてみます。

```
errata.table=タイポ -> タイプ,\
  (?<!一)昨日 -> 機能
```

"->" で誤字と正字を区切ります。これを選んだのは正規表現に現れにくいと思ったからです。
正規表現の否定後読み(negative lookbehind "?<!"の部分)を使うことで、「一昨日」は誤字ではないが、「昨日」は誤字かもしれないように指定しています。誤字の判定で、このようなことはよくあるので正規表現のlookbehind, lookaheadは使いこなせるようになった方が良いでしょう。

それでは上記のような設定から判定を行う処理を実装してみましょう。

```diff
--- a/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
+++ b/src/main/java/org/jca02266/sonarplugins/errata/rules/CreateIssuesOnTextFilesSensor.java
@@ -1,5 +1,7 @@
 package org.jca02266.sonarplugins.errata.rules;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
@@ -29,12 +31,10 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
   protected final Configuration config;
 
   public CreateIssuesOnTextFilesSensor(final Configuration config) {
-    LOGGER.info("errata sensor constructor called");
     this.config = config;
   }
   @Override
   public void describe(SensorDescriptor descriptor) {
-    LOGGER.info("errata sensor describe() called");
     descriptor.name("Check errata for text files");
 
     descriptor.onlyOnLanguage("java");
@@ -59,33 +59,39 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
 
   @Override
   public void execute(SensorContext context) {
-    LOGGER.info("errata sensor execute() called");
-
-    LOGGER.info("errata text: {}", config.get("errata.text").orElse("default"));
-    Arrays.stream(config.getStringArray("errata.text2")).forEach(val -> LOGGER.info("errata text2: {}", val));
+    List<Pair<PatternSearcher, String>> pairs = Arrays.stream(config.getStringArray("errata.table")).map(s -> {
+        String[] strs = s.split("->", 2);
+        LOGGER.info("errata wrong word: {}, right word: {}", strs[0], strs[1]);
+        if (strs.length != 2) {
+          throw new RuntimeException(String.format("invalid format on errata.table. it must be \" key -> val , ...\": \"%s\"", s));
+        }
+        return new Pair<PatternSearcher, String>(new PatternSearcher(Pattern.compile(strs[0].trim())), strs[1].trim());
+      })
+      .collect(Collectors.toList());
 
     FileSystem fs = context.fileSystem();
     FilePredicate fp = addExtensions(fs.predicates());
     Iterable<InputFile> textFiles = fs.inputFiles(fp);
 
     for (InputFile textFile : textFiles) {
-      InputStream is;
       try {
-        is = textFile.inputStream();
+        InputStream is = textFile.inputStream();
+        byte[] bytes = getBytes(is);
+
+        Charset cs = textFile.charset();
+
+        for (Pair<PatternSearcher, String> pair: pairs) {
+          pair.getFirst().search(new ByteArrayInputStream(bytes), cs, linenum -> start -> end ->
+            registerIssue(context, textFile, linenum, start, end, pair.getSecond())
+          );
+        }
       } catch (IOException e) {
         throw new RuntimeException("failed to inputStream()", e);
       }
-
-      Charset cs = textFile.charset();
-      Pattern pat = Pattern.compile("タイポ");
-
-      new PatternSearcher(pat).search(is, cs, linenum -> start -> end ->
-        registerIssue(context, textFile, linenum, start, end)
-      );
     }
   }
 
-  private void registerIssue(SensorContext context, InputFile textFile, int line, int start, int end) {
+  private void registerIssue(SensorContext context, InputFile textFile, int line, int start, int end, String rightWord) {
     NewIssue newIssue = context.newIssue()
       .forRule(TextRulesDefinition.RULE)
       .gap(ARBITRARY_GAP);
@@ -93,9 +99,31 @@ public class CreateIssuesOnTextFilesSensor implements Sensor {
     NewIssueLocation primaryLocation = newIssue.newLocation()
       .on(textFile)
       .at(textFile.newRange(textFile.newPointer(line, start), textFile.newPointer(line, end)))
-      .message("Fix typo");
+      .message("May be typo: " + rightWord);
     newIssue.at(primaryLocation);
 
     newIssue.save();
   }
+
+  static class Pair<T,U> {
+    T first;
+    U second;
+    Pair(T first, U second) {
+      this.first = first;
+      this.second = second;
+    }
+    public T getFirst() { return first; }
+    public U getSecond() { return second; }
+  }
+
+  public static byte[] getBytes(InputStream is) throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    byte[] bytes = new byte[512];
+
+    for (int len = is.read(bytes); len != -1; len = is.read(bytes)) {
+      baos.write(bytes, 0, len);
+    }
+
+    return baos.toByteArray();
+  }
```

実装として間違えてしまったのですが、InputStream に対して、繰り返しPatternSearcherを適用するため、InputStreamを巻き戻さないといけません。
そこで、getBytes()というメソッドを作って、InputStreamを一旦byte[]にして、byte[]から毎回InputStreamを生成(`new ByteArrayInputStream()`)することで逃げました。

ここは、気が向いたらちゃんと実装することとして後回しにします。

さらに、やってみて問題点がありました。それは、sonar-project.properties にUTF-8で書き込んでも、SonarQubeは標準のJava同様、ISO 8859-1として読んでしまうため、このままscannerを実行しても文字化けしてしまいました。

仕方ないので、native2asciiでpropertiesファイルをasciiにすることで回避しました。

```console
cd sample
mv sonar-project.properties{,.utf-8}
native2ascii sonar-project.properties{.utf-8,}
```

これで、一応期待通りに動作します。

Configuration で定義できるようにもしましょう。もう簡単ですね。

```diff
--- a/src/main/java/org/jca02266/sonarplugins/errata/settings/ErrataProperties.java
+++ b/src/main/java/org/jca02266/sonarplugins/errata/settings/ErrataProperties.java
@@ -8,22 +8,23 @@ import org.sonar.api.config.PropertyDefinition;
 
 public class ErrataProperties {
 
-  public static final String CATEGORY = "Errata Properties Example";
+  public static final String CATEGORY = "Errata";
 
   private ErrataProperties() {
   }
 
   public static List<PropertyDefinition> getProperties() {
     return asList(
-      PropertyDefinition.builder("errata.text")
-        .name("Errata1")
-        .description("Errata properties 1")
-        .defaultValue("タイポ")
+      PropertyDefinition.builder("errata.file.suffixes")
+        .name("File suffixes")
+        .description("Comma-separated list of suffixes for files to analyze")
+        .multiValues(true)
+        .defaultValue(".java")
         .category(CATEGORY)
         .build(),
-      PropertyDefinition.builder("errata.text2")
-        .name("Errata2")
-        .description("Errata properties 2")
+      PropertyDefinition.builder("errata.table")
+        .name("Erratum list")
+        .description("Errata which format is \"wrong word regexp -> right word\"")
         .multiValues(true)
         .category(CATEGORY)
```

これで以下のようにAdministration → Configuration → Errata から設定できるようになりました。こちらであれば native2ascii など考えなくて良いので楽です。

<img width="1269" alt="スクリーンショット 2019-11-04 19.45.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/3fd54269-7000-0e4c-4535-04c1785b5581.png">

より良いのは、プロジェクト → Administration の General Settings でプロジェクト毎の設定もできると良いのですが、これについては気が向いたら調べてみようと思います。

<img width="1269" alt="スクリーンショット 2019-11-04 19.49.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44797/48bf8465-347e-159b-cba6-167270968216.png">

効率や速度は無視してますが、一旦、要件を満足するプラグインが作成できました。使い物になるかはこれから自分でも確認してみようと思います。

SonarQubeプラグインの作成方法は情報が少ないのですが、サンプルプログラム([sonar-custom-plugin-example](https://github.com/SonarSource/sonar-custom-plugin-example))を見ると結構色々なことが書いてあります。参考にすると良いでしょう。

ここで作ったプラグインのソースは以下に公開しています。

[jca02266/sonar-errata-plugin](https://github.com/jca02266/sonar-errata-plugin)