かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その27 )( 貸出状況取得タスクの作成 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その26 )( 貸出希望書籍 CSV ファイルアップロード画面の作成5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。3~4回に分けて書きます。
    • 別の記事を書いていて気づいた以下の2点を反映します。
    • 貸出状況取得タスクの作成
      • RabbitMQ リスナーの作成
      • カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成

参照したサイト・書籍

  1. convert to different javatype when use @RabbitListener annonation
    https://jira.spring.io/browse/AMQP-461

    • @RabbitListener アノテーションを付加したメソッドで受信したメッセージをオブジェクトに変換する方法を参考にしました。
  2. Java 8 "Optional" ~ これからのnullとの付き合い方 ~
    http://qiita.com/shindooo/items/815d651a72f568112910

    • Java 8 で導入された Optional の使い方を参考にしました。

目次

  1. はじめに
  2. 別の記事を書いていて気づいた点の反映
    1. DomaConfig クラスの修正
    2. TestDataResource クラスの before メソッド内のコメントアウトしている処理の削除
  3. RabbitMQ リスナーの作成
  4. カーリルの蔵書検索 WebAPI のレスポンスの出力形式を検討する
  5. カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成
  6. 動作確認
  7. 次回は。。。
  8. メモ書き

手順

はじめに

貸出希望書籍 CSV ファイルアップロード画面からデータが登録されたらカーリルの蔵書検索 WebAPIで貸出状況を取得する貸出状況取得タスクを作成します。

  • RabbitMQ のキューを監視し、貸出希望書籍 CSV ファイルアップロード画面から貸出状況取得依頼のメッセージが送信されているかチェックします。
  • データが登録されている場合にはカーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得します。
  • 処理が完了したらデータを登録したユーザへメールを送信します。

以下の順序で進める予定です。

  1. RabbitMQ リスナーの作成
  2. カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成
  3. メール送信処理の作成
  4. テストの作成

別の記事を書いていて気づいた点の反映

貸出状況取得タスクの作成に入る前に Spring Boot で Doma 2 を使用するには 及び 自作したテスト用クラス ( src/test/java/ksbysample/common/test ) の使い方 を書いていて修正した方がよい点を見つけたので反映します。

DomaConfig クラスの修正

  1. 今回からブランチ名を "feature/" + [GitHubのIssueの番号] + "-issue" にします。feature/32-issue ブランチを作成します。

  2. src/main/java/ksbysample/webapp/lending/config の下の DomaConfig.javaリンク先の内容 に変更します。

  3. Commit&Push、feature/32-issue -> 1.0.x へ Pull Request、Merge、feature/32-issue ブランチを削除、#32 Issue をクローズします。

TestDataResource クラスの before メソッド内のコメントアウトしている処理の削除

  1. feature/34-issue ブランチを作成します。

  2. src/main/java/ksbysample/common/test の下の TestDataResource.javaリンク先の内容 に変更します。

  3. Commit&Push、feature/34-issue -> 1.0.x へ Pull Request、Merge、feature/34-issue ブランチを削除、#34 Issue をクローズします。

RabbitMQ リスナーの作成

  1. feature/37-issue ブランチを作成します。

  2. src/main/java/ksbysample/webapp/lending の下に listener.rabbitmq パッケージを作成します。

  3. MessageConverter をリスナークラスでも共通で使用できるようにするために Bean として定義します。src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.javaリンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/service/queue の下の InquiringStatusOfBookQueueService.javaリンク先の内容 に変更します。

  5. src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下に InquiringStatusOfBookQueueListener.java を作成します。作成後、リンク先のその1の内容 に変更します。

カーリルの蔵書検索 WebAPI のレスポンスの出力形式を検討する

カーリルの蔵書検索API は format パラメータを指定することで JSONXML いずれかの出力形式を指定することができますが、図書館API仕様書 の結果の例を見ると JSON の場合に "項目名: 値" の形式ではなく項目名の部分に値がセットされている場合があり、Jackson で処理させるのが難しい ( というより項目名にも値がセットされている場合、どうやって処理させればよいのだろう? ) ように思えました。XML の方が処理しやすそうであれば XML にしようと思いますので、一旦両方のフォーマットで出力してみることにします。

まずは JSON の場合です。http://api.calil.jp/check?appkey={appkey}&systemid=Tokyo_NDL&isbn=9784774163666&format=json で呼び出します ( {appkey} の部分は置き換えてください )。

callback({
  "session": "f6e5.....6f70",
  "books": {
    "978-4-7741-6366-6": {
      "Tokyo_NDL": {
        "status": "Cache",
        "reserveurl": "https://ndlopac.ndl.go.jp/F/.....(長いので省略).....",
        "libkey": {
          "東京本館": "蔵書あり"
        }
      }
    }
  },
  "continue": 0
});

次は XML の場合です。http://api.calil.jp/check?appkey={appkey}&systemid=Tokyo_NDL&isbn=9784774163666&format=xml で呼び出します。

<?xml version="1.0" encoding="UTF-8"?>
<result>
    <session>169d.....654a</session>
    <continue>0</continue>
    <books>
        <book isbn="978-4-7741-6366-6" calilurl="http://calil.jp/book/477416366X">
            <system systemid="Tokyo_NDL">
                <status>Cache</status>
                <reserveurl>
                    https://ndlopac.ndl.go.jp/.....(長いので省略).....
                </reserveurl>
                <libkeys>
                    <libkey name="東京本館">蔵書あり</libkey>
                </libkeys>
            </system>
        </book>
    </books>
</result>

XML の方がきちんと構造化されていて処理しやすそうなので、XML を使用することにします。

カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成

  1. 最初にレスポンスのデータを展開するためのクラスを作成します。

  2. src/main/java/ksbysample/webapp/lending/service/calilapi の下に CheckApiResponse.java を作成します。作成後、リンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/service/calilapi の下に Book.java を作成します。作成後、リンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/service/calilapi の下に SystemData.java を作成します。作成後、リンク先の内容 に変更します。

  5. src/main/java/ksbysample/webapp/lending/service/calilapi の下に Libkey.java を作成します。作成後、リンク先の内容 に変更します。

  6. 蔵書検索APIを呼び出す処理を実装します。src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.javaリンク先の内容 に変更します。

  7. lending_app テーブルのデータをロックするためのメソッドを追加します。src/main/java/ksbysample/dao の下の LendingAppDao.javaリンク先の内容 に変更します。

  8. データをロックするためのメソッドと lending_book.lending_state だけ更新するメソッドを作成します。src/main/java/ksbysample/dao の下の LendingBookDao.javaリンク先の内容 に変更します。

  9. 貸出状況を未確認の時と確認済の時で lending_app テーブルの status の値が異なるようにしたいので、値を追加します。src/main/java/ksbysample/webapp/lending/values の下の LendingAppStatusValues.javaリンク先の内容 に変更します。

    Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その5 )( DB、テーブルの作成 ) の記事も修正しました。

  10. トランザクションを開始させるためにリスナーから呼び出す Service クラスを作成します。src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下に InquiringStatusOfBookQueueListenerService.java を作成します。作成後、リンク先の内容 に変更します。

  11. リスナーの処理を実装します。src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.javaリンク先のその2の内容 に変更します。

動作確認

  1. ここまでの実装を動作確認します。

  2. lending_app、lending_book テーブルのデータをクリアします。

  3. Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  4. ブラウザを起動し http://localhost:8080/booklist へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

  5. テスト.csv をアップロードします。

    f:id:ksby:20151025210147p:plain

  6. リスナーが呼び出されて処理が実行されます。ログにカーリルの蔵書検索API のレスポンスを展開した結果が出力されていることが確認できます。

    f:id:ksby:20151025211054p:plain

    カーリルの蔵書検索API を呼び出し、レスポンスを取得しました。 CheckApiResponse(session=cde7...4459, continueValue=0, bookList=[ Book(isbn=978-4-7973-8014-9, calilurl=http://calil.jp/book/4797380144, system=SystemData(systemid=Tokyo_NDL, status=Cache, reserveurl=https://ndlopac.ndl.go.jp/..., libkeyList=[Libkey(name=東京本館, value=蔵書あり)])) , Book(isbn=978-4-7973-4778-4, calilurl=http://calil.jp/book/4797347783, system=SystemData(systemid=Tokyo_NDL, status=Cache, reserveurl=https://ndlopac.ndl.go.jp/..., libkeyList=[Libkey(name=東京本館, value=蔵書あり)])) , Book(isbn=978-4-7741-6366-6, calilurl=http://calil.jp/book/477416366X, system=SystemData(systemid=Tokyo_NDL, status=Cache, reserveurl=https://ndlopac.ndl.go.jp/..., libkeyList=[Libkey(name=東京本館, value=蔵書あり)])) , Book(isbn=978-4-87311-704-1, calilurl=http://calil.jp/book/4873117046, system=SystemData(systemid=Tokyo_NDL, status=Cache, reserveurl=https://ndlopac.ndl.go.jp/..., libkeyList=[Libkey(name=東京本館, value=蔵書あり)])) , Book(isbn=978-4-7741-5377-3, calilurl=http://calil.jp/book/477415377X, system=SystemData(systemid=Tokyo_NDL, status=Cache, reserveurl=https://ndlopac.ndl.go.jp/..., libkeyList=[Libkey(name=東京本館, value=蔵書あり)]))])

    ※session, reserveurl の値は省略しています。

  7. lending_app, lending_book テーブルも更新されていることが確認できます。

    f:id:ksby:20151025211804p:plain

  8. Ctrl+F2 を押して Tomcat を停止します。

  9. 一旦 commit します。

次回は。。。

  • 今回実装した処理ではアップロードされた貸出希望書籍データには ISBN が重複していないことが前提になるので、貸出希望書籍 CSV ファイルアップロード画面でアップロードした CSV ファイル内に ISBN が重複しているデータがあった場合にはエラーにする処理を追加します。

  • その後で貸出状況取得タスクのメール送信処理を作成します。

メモ書き

  • カーリルの蔵書検索 WebAPIを呼び出す処理を書いていて思いました。JRebel 超便利です! レスポンスを展開するクラスの作成はかなり試行錯誤したので、Tomcat を再起動することなく開発ができる JRebel があるとなしでは開発効率が全然違います。Spring Boot の 1.3 から利用できる spring-boot-devtools の自動リロード機能が JRebel 並みではなくてもある程度使えるといいな。。。 ( JRebel は購入するとちょっと高いんですよね )

  • @RabbitListener アノテーションが付加されたメソッド内で例外 ( 非チェック例外だけ? ) が発生した場合、RabbitMQ の Queue のメッセージは削除されず、今の実装だと延々とリスナーのメソッドが呼び出され続けました。RabbitMQ のトランザクション処理のようなものが行われているような気がします。貸出状況取得タスクを一通り実装した後でその辺の動作がどうなっているのかを確認してみたいと思います。

ソースコード

DomaConfig.java

package ksbysample.webapp.lending.config;

import org.apache.commons.lang3.StringUtils;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.GreedyCacheSqlFileRepository;
import org.seasar.doma.jdbc.NoCacheSqlFileRepository;
import org.seasar.doma.jdbc.SqlFileRepository;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
public class DomaConfig implements Config {

    private DataSource dataSource;

    private Dialect dialect;

    private SqlFileRepository sqlFileRepository;

    public DomaConfig() {
    }

    @SuppressWarnings("SpringJavaAutowiringInspection")
    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = new TransactionAwareDataSourceProxy(dataSource);
    }

    @Autowired
    public void setDialect(@Value("${doma.dialect}") String domaDialect)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        this.dialect = (Dialect) Class.forName(domaDialect).newInstance();
    }

    @Autowired
    public void setSqlFileRepository(@Value("${spring.profiles.active}") String springProfilesActive) {
        // develop モードの時は SQL ファイルがキャッシュされないようにする
        if (StringUtils.equals(springProfilesActive, "develop")) {
            this.sqlFileRepository = new NoCacheSqlFileRepository();
        } else {
            this.sqlFileRepository = new GreedyCacheSqlFileRepository();
        }
    }

    @Override
    public DataSource getDataSource() {
        return this.dataSource;
    }

    @Override
    public Dialect getDialect() {
        return this.dialect;
    }

    @Override
    public SqlFileRepository getSqlFileRepository() {
        return this.sqlFileRepository;
    }

}

TestDataResource.java

    @Override
    protected void before() throws Exception {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            // バックアップを取得する
            QueryDataSet partialDataSet = new QueryDataSet(conn);
            for (String backupTable : BACKUP_TABLES) {
                partialDataSet.addTable(backupTable);
            }
            ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
            replacementDatasetBackup.addReplacementObject("", "[null]");
            backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
            try (FileOutputStream fos = new FileOutputStream(backupFile)) {
                FlatXmlDataSet.write(replacementDatasetBackup, fos);
            }

            // テストデータに入れ替える
            IDataSet dataSet = new CsvDataSet(new File(TESTDATA_DIR));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject("[null]", null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        } finally {
            if (conn != null) conn.close();
        }
    }
  • 処理の修正方法を検討していてコメントアウトして残していた DatabaseOperation.TRUNCATE_TABLE.execute(...); の2行を削除します。

ApplicationConfig.java

package ksbysample.webapp.lending.config;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

    @Autowired
    private ConnectionFactory connectionFactory;
    
    @Bean
    public Queue inquiringStatusOfBookQueue() {
        return new Queue(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, false);
    }

    @Bean
    public MessageConverter messageConverter() {
        Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        return converter;
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(this.connectionFactory);
        rabbitTemplate.setMessageConverter(this.messageConverter());
        return rabbitTemplate;
    }

}
  • MessageConverter Bean を追加します。
  • RabbitTemplate Bean 内の rabbitTemplate.setMessageConverter(...) に渡す引数を new Jackson2JsonMessageConverter()this.messageConverter() へ変更します。

InquiringStatusOfBookQueueService.java

package ksbysample.webapp.lending.service.queue;

import ksbysample.webapp.lending.config.Constant;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class InquiringStatusOfBookQueueService {

    @Autowired
    private MessageConverter converter;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(Long lendingAppId) {
        InquiringStatusOfBookQueueMessage message = new InquiringStatusOfBookQueueMessage();
        message.setLendingAppId(lendingAppId);
        rabbitTemplate.convertAndSend(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, message);
    }

    public InquiringStatusOfBookQueueMessage convertMessageToObject(Message message) {
        return (InquiringStatusOfBookQueueMessage) converter.fromMessage(message);
    }
    
}

InquiringStatusOfBookQueueListener.java

■その1

package ksbysample.webapp.lending.listener.rabbitmq;

import ksbysample.webapp.lending.config.Constant;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class InquiringStatusOfBookQueueListener {
    
    @Autowired
    private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService;
    
    @RabbitListener(queues = {Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK})
    public void receiveMessage(Message message) {
        // 受信したメッセージを InquiringStatusOfBookQueueMessage クラスのインスタンスに変換する
        InquiringStatusOfBookQueueMessage convertedMessage
                = inquiringStatusOfBookQueueService.convertMessageToObject(message);
    }

}

■その2

package ksbysample.webapp.lending.listener.rabbitmq;

import ksbysample.webapp.lending.config.Constant;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class InquiringStatusOfBookQueueListener {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Autowired
    private InquiringStatusOfBookQueueService inquiringStatusOfBookQueueService;
    
    @Autowired
    private InquiringStatusOfBookQueueListenerService inquiringStatusOfBookQueueListenerService;
    
    @RabbitListener(queues = {Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK})
    public void receiveMessage(Message message) {
        // 受信したメッセージを InquiringStatusOfBookQueueMessage クラスのインスタンスに変換する
        InquiringStatusOfBookQueueMessage convertedMessage
                = inquiringStatusOfBookQueueService.convertMessageToObject(message);

        // カーリルの蔵書検索API を呼び出して貸出状況を取得し、lending_app, lending_book テーブルを更新する
        inquiringStatusOfBookQueueListenerService.callCheckApiAndupdateLendingData(convertedMessage);
        
        // TODO データを登録したユーザへメールを送信する
    }
    
}
  • private InquiringStatusOfBookQueueListenerService inquiringStatusOfBookQueueListenerService; を追加します。
  • receiveMessage メソッド内に inquiringStatusOfBookQueueListenerService.callCheckApiAndupdateLendingData(convertedMessage); を追加します。

CheckApiResponse.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.ToString;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;

@XmlRootElement(name = "result")
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public class CheckApiResponse {

    @XmlElement(name = "session")
    private String session;
    
    @XmlElement(name = "continue")
    private String continueValue;

    @XmlElement(name = "books")
    private List<Book> bookList  = new ArrayList<>();
    
}

Book.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.ToString;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public class Book {

    @XmlAttribute(name = "isbn")
    private String isbn;

    @XmlAttribute(name = "calilurl")
    private String calilurl;

    @XmlElement(name = "system")
    private SystemData system;
    
}

SystemData.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.ToString;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import java.util.ArrayList;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public class SystemData {

    @XmlAttribute(name = "systemid")
    private String systemid;

    @XmlElement(name = "status")
    private String status;

    @XmlElement(name = "reserveurl")
    private String reserveurl;

    @XmlElement(name = "libkeys")
    private List<Libkey> libkeyList = new ArrayList<>();
    
}

Libkey.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.ToString;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public class Libkey {

    @XmlAttribute(name = "name")
    private String name;

    @XmlValue
    private String value;

}
  • libkey は <libkey name="東京本館">蔵書あり</libkey> のように Element 無しで値が出力されているので、@XmlValue を使用します。

CalilApiService.java

package ksbysample.webapp.lending.service.calilapi;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.common.base.Joiner;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@PropertySource("classpath:calilapi.properties")
public class CalilApiService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    private int CONNECT_TIMEOUT = 5000;
    private int READ_TIMEOUT = 5000;

    private final int RETRY_MAX_CNT = 5;
    private final long RETRY_SLEEP_MILLS = 3000;
    
    private final String URL_CALILAPI_LIBRALY = "http://api.calil.jp/library?appkey={appkey}&pref={pref}";
    private final String URL_CALILAPI_CHECK = "http://api.calil.jp/check?appkey={appkey}&systemid={systemid}&isbn={isbn}&format=xml";
    private final String URL_CALILAPI_CHECK_FOR_RETRY = "http://api.calil.jp/check?session={session}&format=xml";

    @Value("${calil.apikey}")
    private String calilApiKey;

    public Libraries getLibraryList(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        ResponseEntity<String> response
                = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, String.class, this.calilApiKey, pref);
        
        // 受信した XMLレスポンスを Javaオブジェクトに変換する
        Serializer serializer = new Persister();
        Libraries libraries = serializer.read(Libraries.class, response.getBody());
        
        return libraries;
    }

    public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception {
        // 図書館データベースAPIを呼び出して XMLレスポンスを受信する
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        restTemplate.setMessageConverters(getMessageConvertersforJackson2XML());
        ResponseEntity<LibrariesForJackson2Xml> response
                = restTemplate.getForEntity(URL_CALILAPI_LIBRALY, LibrariesForJackson2Xml.class, this.calilApiKey, pref);
        return response.getBody();
    }

    public  List<Book> check(String systemid, List<String> isbnList) {
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        restTemplate.setMessageConverters(getMessageConvertersforJackson2XML());

        Map<String, String> vars = new HashMap<>();
        vars.put("appkey", this.calilApiKey);
        vars.put("systemid", systemid);
        vars.put("isbn", Joiner.on(",").join(isbnList));

        ResponseEntity<CheckApiResponse> response = null;
        String url = URL_CALILAPI_CHECK;
        for (int retry = 0; retry < RETRY_MAX_CNT; retry++) {
            // 蔵書検索APIを呼び出して蔵書の有無と貸出状況を取得する
            response = restTemplate.getForEntity(url, CheckApiResponse.class, vars);
            logger.info("カーリルの蔵書検索API を呼び出し、レスポンスを取得しました。{}", response.getBody().toString());
            if (response.getBody().getContinueValue() == 0) {
                break;
            }

            // continue の値が 0 でない場合には2秒以上待機した後、URLパラメータを session に変更して再度リクエストを送信する
            try {
                Thread.sleep(RETRY_SLEEP_MILLS);
            } catch (InterruptedException e) {
                logger.warn("カーリルの蔵書検索APIのsleep中にInterruptedExceptionが発生しましたが、処理は継続します。", e);
            }
            url = URL_CALILAPI_CHECK_FOR_RETRY;
            vars.clear();
            vars.put("session", response.getBody().getSession());
        }

        return response.getBody().getBookList();
    }
    
    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        // 接続タイムアウト、受信タイムアウトを 5秒に設定する
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(CONNECT_TIMEOUT);
        factory.setReadTimeout(READ_TIMEOUT);
        return factory;
    }

    private List<HttpMessageConverter<?>> getMessageConvertersforJackson2XML() {
        // build.gralde に compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:...") を記述して jackson-dataformat-xml
        // が使用できるようになっていない場合にはエラーにする
        assert(ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()));

        MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter
                = new MappingJackson2XmlHttpMessageConverter();
        // findAndRegisterModules メソッドを呼び出して jackson-dataformat-xml が機能するようにする
        mappingJackson2XmlHttpMessageConverter.setObjectMapper(new XmlMapper().findAndRegisterModules());

        List<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.APPLICATION_XML);
        mediaTypes.add(MediaType.TEXT_XML);
        mappingJackson2XmlHttpMessageConverter.setSupportedMediaTypes(mediaTypes);

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(mappingJackson2XmlHttpMessageConverter);
        return messageConverters;
    }
    
}
  • private final Logger logger = LoggerFactory.getLogger(this.getClass()); を追加します。
  • 以下の定数を追加します。
    • RETRY_MAX_CNT
    • RETRY_SLEEP_MILLS
    • URL_CALILAPI_CHECK
    • URL_CALILAPI_CHECK_FOR_RETRY
  • check メソッドを追加します。

LendingAppDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;
import org.seasar.doma.jdbc.SelectOptions;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LendingAppDao {

    /**
     * @param lendingAppId
     * @return the LendingApp entity
     */
    @Select
    LendingApp selectById(Long lendingAppId);
    @Select
    LendingApp selectById(Long lendingAppId, SelectOptions options);

    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LendingApp entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LendingApp entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LendingApp entity);
}
  • LendingApp selectById(Long lendingAppId, SelectOptions options); を追加します。

LendingBookDao.java

package ksbysample.webapp.lending.dao;

import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig;
import org.seasar.doma.Dao;
import org.seasar.doma.Delete;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;
import org.seasar.doma.jdbc.SelectOptions;

import java.util.List;

/**
 */
@Dao
@ComponentAndAutowiredDomaConfig
public interface LendingBookDao {

    /**
     * @param lendingBookId
     * @return the LendingBook entity
     */
    @Select
    LendingBook selectById(Long lendingBookId);

    @Select
    List<LendingBook> selectByLendingAppId(Long lendingAppId);
    @Select
    List<LendingBook> selectByLendingAppId(Long lendingAppId, SelectOptions options);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Insert
    int insert(LendingBook entity);

    /**
     * @param entity
     * @return affected rows
     */
    @Update
    int update(LendingBook entity);

    @Update(include = {"lendingState"})
    int updateLendingState(LendingBook entity);
    
    /**
     * @param entity
     * @return affected rows
     */
    @Delete
    int delete(LendingBook entity);
}
  • List<LendingBook> selectByLendingAppId(Long lendingAppId, SelectOptions options); を追加します。
  • updateLendingState メソッドを追加します。

LendingAppStatusValues.java

package ksbysample.webapp.lending.values;

import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

@Getter
public enum LendingAppStatusValues {

    TENPORARY_SAVE("1", "一時保存")
    , UNAPPLIED("2", "未申請")
    , PENDING("3", "申請中")
    , APPLOVED("4", "承認済");
  • , UNAPPLIED("2", "未申請") を追加します。

InquiringStatusOfBookQueueListenerService.java

package ksbysample.webapp.lending.listener.rabbitmq;

import ksbysample.webapp.lending.dao.LendingAppDao;
import ksbysample.webapp.lending.dao.LendingBookDao;
import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import ksbysample.webapp.lending.service.calilapi.Book;
import ksbysample.webapp.lending.service.calilapi.CalilApiService;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage;
import ksbysample.webapp.lending.values.LendingAppStatusValues;
import org.apache.commons.lang3.StringUtils;
import org.seasar.doma.jdbc.SelectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class InquiringStatusOfBookQueueListenerService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private CalilApiService calilApiService;

    @Autowired
    private LibraryForsearchDao libraryForsearchDao;

    @Autowired
    private LendingAppDao lendingAppDao;

    @Autowired
    private LendingBookDao lendingBookDao;

    public void callCheckApiAndupdateLendingData(InquiringStatusOfBookQueueMessage convertedMessage) {
        // 選択中の図書館を取得する
        LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary();

        // 更新対象の lending_app テーブルのデータを取得する
        LendingApp lendingApp = lendingAppDao.selectById(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate());
        if (lendingApp == null) {
            logger.error("lending_app テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId());
            return;
        }

        // lending_book テーブルから調査対象の ISBN 一覧を取得する
        List<LendingBook> lendingBookList
                = lendingBookDao.selectByLendingAppId(convertedMessage.getLendingAppId(), SelectOptions.get().forUpdate());
        if (lendingBookList == null) {
            logger.error("lending_book テーブルに対象のデータがありませんでした ( lending_app_id = {} )。", convertedMessage.getLendingAppId());
            return;
        }
        List<String> isbnList = lendingBookList.stream()
                .map(LendingBook::getIsbn)
                .collect(Collectors.toList());

        // カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する
        List<Book> bookList = calilApiService.check(libraryForsearch.getSystemid(), isbnList);

        // lending_book テーブルに取得した貸出状況を反映し、lending_app テーブルの status を 2(未申請) に更新する
        copyLendingStateFromBookListToEntityList(bookList, lendingBookList);
        updateLendingData(lendingBookList, lendingApp);
    }

    private void copyLendingStateFromBookListToEntityList(List<Book> bookList, List<LendingBook> lendingBookList) {
        for (LendingBook lendingBook : lendingBookList) {
            for (Book book : bookList) {
                if (StringUtils.equals(lendingBook.getIsbn(), book.getIsbn())) {
                    lendingBook.setLendingState(book.getFirstLibkeyValue());
                    break;
                }
            }
        }
    }

    private void updateLendingData(List<LendingBook> lendingBookList, LendingApp lendingApp) {
        // lending_book テーブルに取得した貸出状況を反映する
        for (LendingBook lendingBook : lendingBookList) {
            lendingBookDao.updateLendingState(lendingBook);
        }
        
        // lending_app テーブルの status を 2(未申請) に更新する
        lendingApp.setStatus(LendingAppStatusValues.UNAPPLIED.getValue());
        lendingAppDao.update(lendingApp);
    }

}

履歴

2015/10/26
初版発行。