SonaQubeをCentOSにインストールする

SonarQubeサーバの実行には、JavaMySQL等のデータベースが必要です。 データベースは初期設定でH2を使うようになっており、必ずしも用意しなければならないものではありません。しかし、これはあくまでも評価用であり、実際に利用するときは別途データベースを用意することが推奨されています。

この記事では、CentOS 6.5でMySQL 5.6を用意することにします。

Javaインストール

OracleのサイトからJavaをダウンロードし、yumでインストールします。Javaのダウンロードには、ライセンス条項に同意する必要があります。ライセンスに同意した上でLinux上でダウンロードするには、以下のコマンドを実行します。

wget --no-check-certificate --no-cookies - --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u5-b13/jdk-8u5-linux-x64.rpm
sudo yum -y install jdk-8u5-linux-x64.rpm

Javaのバージョンが表示されたら成功です。

$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

MySQLインストール

CentOSレポジトリに含まれるMySQLは結構古い(5.1)ため、Oracleレポジトリから新しいバージョンをyumでインストールします。

sudo yum -y install http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
sudo yum -y install mysql-community-server

Oracleリポジトリからのインストールでは、データベースの作成等は行われないため、手動で実行する必要があります。

sudo -u mysql mysql_install_db
sudo service mysqld start
sudo mysql_secure_installation

SonarQube インストール

ZIPをダウンロードして展開するだけですが、ここでは Sonar native packages を利用してyumでインストールします。

sudo wget -O /etc/yum.repos.d/sonar.repo http://downloads.sourceforge.net/project/sonar-pkg/rpm/sonar.repo
sudo yum -y install sonar

データベース作成

インストールしたMySQL上に、SonarQube用のデータベースを作成します。

CREATE DATABASE sonar CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE USER 'sonar' IDENTIFIED BY 'sonar';
GRANT ALL ON sonar.* TO 'sonar'@'%' IDENTIFIED BY 'sonar';
GRANT ALL ON sonar.* TO 'sonar'@'localhost' IDENTIFIED BY 'sonar';
FLUSH PRIVILEGES;

SonarQubeのDB設定

SonarQubeのJDBC設定を、MySQLを使うよう変更します。

# sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar // コメントアウトする

sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true // コメントを外す

SonarQubeサーバ起動

ここまでの作業が終了したら、後は起動するだけです。

sudo service sonar start

初回起動時に、自動的にテーブルなどが作成されます。 /opt/sonar/logs/sonar.logWeb server is started というログが出れば準備完了です。ブラウザで http://hostname:9000/ にアクセスすると、トップページが表示されるはずです。

SonarQubeトップページ お疲れ様でした。これで、SonarQubeを使い始める準備が整いました。

右上の Log in リンクで管理者(初期値は admin/admin)としてログインすることで、プラグインの追加やルールの管理を行うことができます。

Tomcat再起動時に [Abandoned connection cleanup thread] がメモリリークしてるという警告が出る場合の対応

Tomcat上のWebアプリケーションでMySQLJDBCドライバを使用すると、Tomcat再起動時に以下のような警告が出力されます。

The web application [] appears to have started a thread named [Abandoned connection cleanup thread] but has failed to stop it. This is very likely to create a memory leak.

これはMySQLにバグチケットが上がっていて、対処方法もそちらに記載があります。

MySQL Bugs: #65909: referenceThread causes memory leak in Tomcat

ServletContextListener#contextDestroyed() で、該当のスレッドを止めてやれば良い訳ですね。こんな感じです。

@WebListener
public class StopLeakThreadListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // do nothing
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        try {
            AbandonedConnectionCleanupThread.shutdown();
        } catch (final InterruptedException e) {
        }
    }
}

これをやるときは、mysql-connector-java が compile スコープで必要になります。通常JDBCドライバは runtime にしていると思いますのでご注意を。

(おまけ)JDBCドライバも登録解除する

Tomcat再起動時、下記のようなログもでることがあります。

SEVERE: A web application registered the JBDC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.

こちらはTomcatのメモリリーク防止機能による「JDBCドライバが残ってたから登録解除しといたよ!」というメッセージのため、無視しても問題ありません。 ただ、SEVEREでログが出ているので、何となく気持ち悪かったり、「本当に大丈夫ですか、検証しましたか、だれが責任取るんですか」みたいなことを言われる可能性が多少でもあったりする場合は、明示的にドライバを登録解除することで精神の安定を得ることができます。

上の #contextDestroyed() メソッドに以下のようなコードを追加します。

Collections.list(DriverManager.getDrivers()).forEach(driver -> {
    try {
        DriverManager.deregisterDriver(driver);
    } catch (final Exception e) {}
});

DriverManager.getDrivers()Enumeration<Driver> を返すのですが、for文は禁止 です。

Spring SecurityのCSRF対策とServlet 3.0のMultipartをJava Configで設定する

はまったのでメモ。

やりたかったこと

HTMLのFORMをPOSTするとき <input hidden name="_csrf" value="..." />CSRFトークンを送りつつMultipartでファイルアップロードしたい。 Springの設定はJava Configでやっていて、web.xmlは作っていない。今さらいろいろな設定をxmlにしたくない。

やってみたこと

Springのリファレンス 16.11 Spring’s multipart (file upload) support にある通り、MultipartResolverをコンテキストに登録してみた。

@Bean
public MultipartResolver multipartResolver() {
    return new StandardServletMultipartResolver();
}

HTML側からPOSTすると、サーバ側のログに Invalid csrf token というメッセージが出て、ステータスコード403が返ってきた。

原因は、Spring SecurityのCsrfFilterがリクエストパラメータからトークンを取得するときに、Multipartリクエストに対し getParameter をしているため。

解決

しばらくググってみたところ、 TERASOLUNA Global Framework のCSRF対策 に方法が書いてあった。

6.7.2.4.1. MultipartFilterを使用する方法

通常、マルチパートリクエストの場合、formから送信された値はFilter内で取得できない。 org.springframework.web.multipart.support.MultipartFilterを使用することで、マルチパートリクエストでも、Filter内で、 formから送信された値を取得することができる。

サイトにはweb.xmlでの設定例が載っているが、要はspringSecurityFilterChainより前にMultipartFilterを動かす必要があるということ。 Java Configでやるには、SecurityWebApplicationInitializer#beforeSpringSecurityFilterChain()でフィルタを登録する。

public class SecurityWebApplicationInitializer
        extends AbstractSecurityWebApplicationInitializer {
    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        final MultipartFilter multipartFilter = new MultipartFilter();
        servletContext.addFilter("multipartFilter", multipartFilter)
                .addMappingForUrlPatterns(
                        EnumSet.allOf(DispatcherType.class), false, "/*");
    }
}

MultipartFilterは、デフォルトでServlet 3.0のAPIを使うStandardServletMultipartResolverを使うようになっているため、自前でコンテキストに登録する必要はない。もしcommons-fileuploadを使いたい場合は、MultipartFilter#DEFAULT_MULTIPART_RESOLVER_BEAN_NAMEで定義された名前 ("filterMultipartResolver") でコンテキストに CommonsMultipartResolver を登録すればよい。

Servlet 3.0のMultipart有効化

web.xmlでの設定例や、自前Servletにつけるアノテーションはすぐに見つかるのだけど、DispatcherServletJava Configの設定例が見当たらない。けど、サーブレット<multipart-config> をつければ良いだろうということで、WebApplicationInitializerに設定を追加。

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setMultipartConfig(new MultipartConfigElement(
                System.getProperty("java.io.tmpdir"), -1, -1, 1024 * 1024));
    }
}

MultipartConfigElementで、アップロード可能な最大ファイルサイズや、添付ファイルに書きだすサイズ閾値など指定可能。

これで、Java Configのみで、CSRFとMultipartを設定して動かすことができた。

SonarQubeでMavenのMulti-moduleかつMulti-language

SonarQube 4.2からMulti-languageがサポートされました。これにより、例えばJavaJavascriptが1つのMavenプロジェクト内に存在する場合でも、両方の言語の解析結果を見ることができるようになりました。

Multi-languageとして解析するために特別な設定は不要です。sonar.sourcesプロパティに指定されたディレクトリ以下のファイルタイプに応じて、自動的に言語が特定され、対応する品質プロファイルでの解析が行われるようになっています。

ただ、このsonar.sourcesプロパティなのですが、Mavenプロジェクトではデフォルトでsrc/main/java*1になります。結果として「Javaしか解析されないじゃん\(^o^)/」みたいなことになっちゃいます。

対策としては、POMの<properties>要素で、sonar.sourcesプロパティに解析対象としたいディレクトリをカンマ区切りで指定すればOKです。 よくありがちなMavenのwebapp構成では、以下のようにすることでwebapp以下のjs/htmlも解析対象にできます。

<properties>
    <sonar.sources>${project.build.sourceDirectory},${basedir}/src/main/webapp</sonar.sources>
    <sonar.exclusions>
        **/jquery*.js
    </sonar.exclusions>
</properties>

jqueryなど、解析対象としたくないファイルは、sonar.exclusionsでうまいこと除外してください。

<properties>はPOMファイルごとに設定でき、parentを上書きするので、モジュールごとに別々のディレクトリを解析対象とすることができます。

*1:正確には ${project.build.sourceDirectory}

SonarQubeでMavenのレポートを再利用する設定

最近リリースされたSonarQube 4.3から、SonarQube実行時にユニットテストを実行する機能である sonar.dynamicAnalysis が非推奨になりました。*1 SonarQube自身が様々なの言語への対応を進める中で、各言語のテストフレームワーク全てを実行できるようにするのは無理があるので、この変更は仕方のないものだと思います。

自分のプロジェクトでも対応が必要になったので、どう設定したかなどまとめておきます。

Unit Test、Integration Testの実行設定

テスト実行に前提条件(DBのセットアップ、コンテナ起動など)がある場合、JUnitのカテゴリ機能を利用して実行可否を制御しておくと便利ですよね。 うちのプロジェクトでは、環境に依存せずサクサク動くテストはsurefireで実行し、DBFluteのReplaceSchemaでデータベースをセットアップしたり、SpringのWebApplicationContextを利用するような重いテストはfailsafeで実行するようにしています。

SonarではUnit TestとIntegration Testを分けてカバレッジを出せるので、surefireをUnit Test、failsafeをIntegration Testとすることにし、以下のように設定しました。

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.17</version>
                <configuration>
                    <excludedGroups>
                        com.example.IntegrationTest
                    </excludedGroups>
                    <argLine>${argLine}</argLine>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.17</version>
                <configuration>
                    <includes>
                        <include>**/*Test.java</include>
                    </includes>
                    <groups>com.example.IntegrationTest</groups>
                    <failIfNoTests>false</failIfNoTests>
                    <argLine>${argLine}</argLine>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>2.17</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

<profiles>
    <profile>
        <id>sonar</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <testFailureIgnore>true</testFailureIgnore>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <testFailureIgnore>true</testFailureIgnore>
                        <reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

pluginManagementにて、Integration Testの目印としたcom.example.IntegrationTestカテゴリを、surefireからは除外、failsafeには対象として、それぞれ設定します。 また、sonarプロファイルでfailsafeが実行されるようexecutionを設定しています。

今回はIntegration Testの件数もSonarでカウントしてもらいたかったので、failsafeのreportsDirectorysurefireと同じ場所に設定しました。

テストカバレッジを取得する

テストカバレッジの取得にはjacocoを使用します。これもSonar実行時のみ行えばよいので、sonarプロファイルに設定を追加します。

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.7.0.201403182114</version>
    <executions>
        <execution>
            <id>pre-test</id>
            <goals><goal>prepare-agent</goal></goals>
        </execution>
        <execution>
            <id>post-test</id>
            <goals><goal>report</goal></goals>
        </execution>
        <execution>
            <id>pre-integration-test</id>
            <phase>pre-integration-test</phase>
            <goals><goal>prepare-agent-integration</goal></goals>
        </execution>
        <execution>
            <id>post-integration-test</id>
            <phase>post-integration-test</phase>
            <goals><goal>report-integration</goal></goals>
        </execution>
    </executions>
</plugin>

report|report-integrationゴールで、target/jacoco.exec|target/jacoco-it.execが出力されます。Sonarの解析時、これらのファイルを再利用することになります。

実行

ここまで設定したら、後はSonarで解析を実行するだけです。最初にMavensonarプロファイルでverifyフェーズまでビルドしておき、SonarRunnerを実行します。

Mavenでやるならこんな感じで。

mvn -P sonar verify
mvn sonar:sonar

Jenkinsであれば、Mavenプロジェクトとしてジョブを作成し、ビルド後にSonarプラグインが動くようにすればOKです。

Redmineのカレンダーに祝日を表示するプラグインを作りました

先日Redmineのカレンダーで祝日を表示する方法を見つけたわけですが、本体のソースを直接修正するのがだいぶイケてない感じでした。 せっかくなので、Redmine(とRails)の勉強がてらプラグインにしました。

taktos/redmine_holidays_plugin · GitHub

インストール、使い方はREADMEをご覧ください。 プラグインの設定で祝日判定に使用する国・地域を選択できます。

日本での表示は↓のようになります。振替休日も表示してくれるのがいいですね! Calendar

公開ついでにRedmineプラグインページに登録しておきました。

Redmine Holidays Plugin - Plugins - Redmine

Redmineのカレンダーで祝日を表示する

2013-01-30追記

本体のコードを修正しないで済むようにプラグイン化しました。 記事はこちら↓

Redmineのカレンダーに祝日を表示するプラグインを作りました - taktosの日記

基本的には以下のサイトと同じですが、祝日名を表示したいので、祝日判定にholidaysを使います。

http://suwork.hatenablog.com/entry/2013/09/04/003647

カレンダー表示時のクラス追加

holidaysのgemを入れます(redmine_backlogsプラグインが使うものでOK)

$ gem install holidays

カレンダー表示を書き換えて、祝日のとき(ついでに土日も)「holiday」クラスを追加します。

diff --git a/app/views/common/_calendar.html.erb b/app/views/common/_calendar.html.erb
index 7951b68..6fafcae 100644
--- a/app/views/common/_calendar.html.erb
+++ b/app/views/common/_calendar.html.erb
@@ -7,8 +7,11 @@
 <% day = calendar.startdt
 while day <= calendar.enddt %>
 <%= ("<td class='week-number' title='#{ l(:label_week) }'>#{(day+(11-day.cwday)%7).cweek}</td>".html_safe) if day.cwday == calendar.first_wday %>
-<td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>">
-<p class="day-num"><%= day.day %></p>
+<td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %><%= ' holiday' if day.holiday?(:jp) || day.wday == 0 || day.wday == 6 %>">
+<p class="day-num">
+<% if day.holiday?(:jp) %><span style="float:left; padding: 1px;"><%= day.holidays(:jp).first[:name] %></span><% end %>
+<%= day.day %>
+</p>
 <% calendar.events_on(day).each do |i| %>
   <% if i.is_a? Issue %>
   <div class="<%= i.css_classes %> <%= 'starting' if day == i.start_date %> <%= 'ending' if day == i.due_date %> tooltip">

css修正

application.css など適当なCSSに、holidayクラスを追加します。

/* holiday */
table.cal td.odd.holiday {
    background-color: #e3e4e5;
}
table.cal td.even.holiday {
    background-color: #ff9999;
}