ryhmrt’s blog

意識低い系プログラマの雑記

Spring Boot + DBUnit でテスト用のDBを定義する

Spring Boot のプロジェクトで DBUnit を使うときに、テスト用の DB を定義する自分なりの方法をメモ。

通常以下のようなブートストラップ用の Application クラスがあるはず。

package myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {

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

}

で、以下の様に @SpringApplicationConfiguration にこのクラスを設定して Spring の設定をすると思う。

package myapp;

...(省略)...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DbUnitTestExecutionListener.class })
public class SomeTest {

    @Test
    @DatabaseSetup("sample-data.xml")
    public void test() {
...(省略)...

これとは別にテスト用の DB 設定のクラスを追加する。定義はこんな感じ。

package myapp;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@PropertySource("classpath:/test.properties")
public class TestConfiguration {

    @Autowired
    private Environment environment;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(
                environment.getProperty("test.datasource.url"),
                environment.getProperty("test.datasource.username"),
                environment.getProperty("test.datasource.password"));
    }

}

接続定義のプロパティファイルも忘れずに。

test.datasource.url=jdbc:postgresql://localhost:15432/myapp_test
test.datasource.username=myapp
test.datasource.password=myapp
test.datasource.driver-class-name=org.postgresql.Driver

テストクラスは @SpringApplicationConfiguration アノテーションだけ変更。

@SpringApplicationConfiguration(classes = {Application.class, TestConfiguration.class})

これで、テスト実行時は TestConfiguration で test.properties の設定に基づいて生成された DB 接続が DataSource として登録されるはず。

もっとスマートな方法があればいいが、そう面倒ではないので当座はこれで良い気がしている。

MacBook Pro Retina 13" の最大解像度

MacBook ProRetina 13" モデルは OSX 標準の設定画面からだと最大 1680x1050 までしか解像度を上げられませんが、実はもっと大きな解像度をサポートしています。

AppStore にいくつか有料のアプリがありますが、フリーのコマンドラインソフトがあるのでオススメ。
https://github.com/jhford/screenresolution
Xcodeコマンドラインツールが入っていれば、 make を叩くだけで実行ファイルが出来るはず。

フォークしたこのブランチには、最大解像度に一発で変更する max オプションが追加されています。
https://github.com/ryhmrt/screenresolution/tree/max_option

尚、この魔改造を施す間に Retina 13" の最大解像が 2560x1600 であることを知りました。 対応解像度一覧にバグがあって、 2560x1600 が表示されていなかったのです。
このバグ修正は本家に Pull Request を投げておいたので、マージされると嬉しいな。

Atom の Add Selection Above/Below のキーボードショートカット

Atom の Add Selection Above/Below のキーボードショートカットはデフォルトで Shift  +Control + Up/Down なんだけど、OSXYosemite になってから Control + Up/Down をデフォルトでミッションコントロールに割り当てているため、どちらかを設定変更しなければうまくいかない。

自分は OSX の Preference を開いて、ミッションコントロールのキーバインドを削除しました。 

Googleカレンダーで書込権限のあるカレンダーをその他のカレンダーに追加する

自分は会社でも個人でもメールとカレンダーはGoogleにべったりです。

会社でGoogleカレンダーを使い始めてから一つずっと気になっていたのが、他人のカレンダーがマイカレンダーに追加されて、Macのカレンダーアプリで予定承認の通知が表示されてうざいということでした。

Googleカレンダーの仕様として、書込権限のあるカレンダーを追加するとマイカレンダーのところに追加されます。

そして、マイカレンダーにあるカレンダーはMacのカレンダーアプリでは問答無用で表示されます。

今回、Googleカレンダーにて以下の儀式を行うことで、書込権限のあるカレンダーをその他のカレンダーに追加できることを発見しました。
  1. Googleカレンダーで対象のカレンダーを追加する(マイカレンダーに追加される)
  2. MacのカレンダーアプリのDelegateで対象のカレンダーを選択する
  3. Googleカレンダーで対象のカレンダーを削除する
  4. Macのカレンダーアプリでカレンダーを更新する
すると、Googleカレンダーで対象のカレンダーがその他のカレンダーに追加されます。あら不思議。

以上の手順は2015/1/29時点のGoogleカレンダーMacにて動作が確認されております。
 

Spring Roo の database コマンドでスキーマが見つからないと言われる

database.propertiesに

database.url = jdbc:postgresql://localhost:5432/hoge

と設定されているとき

roo> database reverse engineer --schema hoge --package ~.entity

とか

roo> database introspect --schema hoge

とかコマンドを打っても

Schema(s) 'hoge' do not exist or does not have any tables. Note that the schema names of some databases are case-sensitive

などと言われてしまう。

--schema オプションには public と指定するらしい。これは分かりづらい...

Spring MVC で独自バリデーション

組込のバリデーション

Spring MVC では JSR-303 のバリデーションが使える。

@NotNull
@Size(min=1, max=50)
@Pattern(regexp = "[a-zA-Z-_.]+@[a-zA-Z-\\.]+")

とかのバリデーションをBeanのフィールドに宣言して、アクションのメソッド

public String save(Model model, @Valid @ModelAttribute("form") HogeForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        model.addAttribute("form", form);
        return "hoge/edit";
    }

こんな感じにエラー分岐すれば、form:errorsタグでエラーが表示できる。

<form:errors path="email" cssClass="text-error" />

独自のバリデーション

標準のバリデーションで足りない分をどうすすか。

最初は org.springframework.validation.Validator を実装して、アクションの頭で

ValidationUtils.invokeValidator(hogeValidator, form, bindingResult);

とかやってみたけど、これはきれいじゃない。

調べたら、独自のアノテーションを定義できるらしい。

以下のように ConstraintValidator と、ペアになるアノテーションをセットで定義してやればOK

/**
 * JSR-303 Validation Annotation for List empty check
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=ListNotEmpty.ListNotEmptyValidator.class)
public @interface ListNotEmpty {
    public String message() default "{ListNotEmpty}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    /**
     * Validator
     */
    public static class ListNotEmptyValidator implements ConstraintValidator<ListNotEmpty, List<?>>  {

        @Override
        public void initialize(ListNotEmpty annotation) {
        }

        @Override
        public boolean isValid(List<?> target, ConstraintValidatorContext context) {
            if (target == null || target.size() == 0) {
                return false;
            }
            return true;
        }

    }
}

複数フィールドをチェックする必要がある場合はクラスをバリデーションの対象にすればOK

/**
 * JSR-303 Validation Annotation for Password confirmation.
 * Target bean should implement PasswordConfirmation.PasswordConfirmable.
 * (No need to put PasswordConfirmation annotation)
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=PasswordConfirmation.PasswordConfirmationValidator.class)
public @interface PasswordConfirmation {
    public String message() default "{PasswordConfirmation}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    /**
     * Interface for password confirmation
     */
    @PasswordConfirmation
    public interface PasswordConfirmable {
        public String getPassword();
        public String getPasswordConfirmation();
    }

    /**
     * Validator
     */
    public class PasswordConfirmationValidator implements ConstraintValidator<PasswordConfirmation, PasswordConfirmable> {

        @Override
        public void initialize(PasswordConfirmation annotation) {
        }

        @Override
        public boolean isValid(PasswordConfirmable bean, ConstraintValidatorContext context) {
            if (bean.getPassword() != null && !bean.getPassword().equals(bean.getPasswordConfirmation())) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                    .addNode("passwordConfirmation")
                    .addConstraintViolation();
                return false;
            }
            return true;
        }

    }
}