Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その27 )( 貸出状況取得タスクの作成 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その26 )( 貸出希望書籍 CSV ファイルアップロード画面の作成5 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。3~4回に分けて書きます。
- 別の記事を書いていて気づいた以下の2点を反映します。
- Spring Boot で Doma 2 を使用するには で修正した DomaConfig クラスの反映
- TestDataResource クラスの before メソッド内のコメントアウトしている処理の削除
- 貸出状況取得タスクの作成
- RabbitMQ リスナーの作成
- カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成
- 別の記事を書いていて気づいた以下の2点を反映します。
参照したサイト・書籍
convert to different javatype when use @RabbitListener annonation
https://jira.spring.io/browse/AMQP-461Java 8 "Optional" ~ これからのnullとの付き合い方 ~
http://qiita.com/shindooo/items/815d651a72f568112910- Java 8 で導入された Optional の使い方を参考にしました。
目次
- はじめに
- 別の記事を書いていて気づいた点の反映
- RabbitMQ リスナーの作成
- カーリルの蔵書検索 WebAPI のレスポンスの出力形式を検討する
- カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成
- 動作確認
- 次回は。。。
- メモ書き
手順
はじめに
貸出希望書籍 CSV ファイルアップロード画面からデータが登録されたらカーリルの蔵書検索 WebAPIで貸出状況を取得する貸出状況取得タスクを作成します。
- RabbitMQ のキューを監視し、貸出希望書籍 CSV ファイルアップロード画面から貸出状況取得依頼のメッセージが送信されているかチェックします。
- データが登録されている場合にはカーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得します。
- 処理が完了したらデータを登録したユーザへメールを送信します。
以下の順序で進める予定です。
- RabbitMQ リスナーの作成
- カーリルの蔵書検索 WebAPI を呼び出して貸出状況を取得する処理の作成
- メール送信処理の作成
- テストの作成
別の記事を書いていて気づいた点の反映
貸出状況取得タスクの作成に入る前に Spring Boot で Doma 2 を使用するには 及び 自作したテスト用クラス ( src/test/java/ksbysample/common/test ) の使い方 を書いていて修正した方がよい点を見つけたので反映します。
DomaConfig クラスの修正
今回からブランチ名を "feature/" + [GitHubのIssueの番号] + "-issue" にします。feature/32-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending/config の下の DomaConfig.java を リンク先の内容 に変更します。
Commit&Push、feature/32-issue -> 1.0.x へ Pull Request、Merge、feature/32-issue ブランチを削除、#32 Issue をクローズします。
TestDataResource クラスの before メソッド内のコメントアウトしている処理の削除
feature/34-issue ブランチを作成します。
src/main/java/ksbysample/common/test の下の TestDataResource.java を リンク先の内容 に変更します。
Commit&Push、feature/34-issue -> 1.0.x へ Pull Request、Merge、feature/34-issue ブランチを削除、#34 Issue をクローズします。
RabbitMQ リスナーの作成
feature/37-issue ブランチを作成します。
src/main/java/ksbysample/webapp/lending の下に listener.rabbitmq パッケージを作成します。
MessageConverter をリスナークラスでも共通で使用できるようにするために Bean として定義します。src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/queue の下の InquiringStatusOfBookQueueService.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下に InquiringStatusOfBookQueueListener.java を作成します。作成後、リンク先のその1の内容 に変更します。
カーリルの蔵書検索 WebAPI のレスポンスの出力形式を検討する
カーリルの蔵書検索API は format パラメータを指定することで JSON、XML いずれかの出力形式を指定することができますが、図書館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 を呼び出して貸出状況を取得する処理の作成
最初にレスポンスのデータを展開するためのクラスを作成します。
src/main/java/ksbysample/webapp/lending/service/calilapi の下に CheckApiResponse.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/calilapi の下に Book.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/calilapi の下に SystemData.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/calilapi の下に Libkey.java を作成します。作成後、リンク先の内容 に変更します。
蔵書検索APIを呼び出す処理を実装します。src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.java を リンク先の内容 に変更します。
lending_app テーブルのデータをロックするためのメソッドを追加します。src/main/java/ksbysample/dao の下の LendingAppDao.java を リンク先の内容 に変更します。
データをロックするためのメソッドと lending_book.lending_state だけ更新するメソッドを作成します。src/main/java/ksbysample/dao の下の LendingBookDao.java を リンク先の内容 に変更します。
貸出状況を未確認の時と確認済の時で lending_app テーブルの status の値が異なるようにしたいので、値を追加します。src/main/java/ksbysample/webapp/lending/values の下の LendingAppStatusValues.java を リンク先の内容 に変更します。
※Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その5 )( DB、テーブルの作成 ) の記事も修正しました。
トランザクションを開始させるためにリスナーから呼び出す Service クラスを作成します。src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下に InquiringStatusOfBookQueueListenerService.java を作成します。作成後、リンク先の内容 に変更します。
リスナーの処理を実装します。src/main/java/ksbysample/webapp/lending/listener/rabbitmq の下の InquiringStatusOfBookQueueListener.java を リンク先のその2の内容 に変更します。
動作確認
ここまでの実装を動作確認します。
lending_app、lending_book テーブルのデータをクリアします。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動し http://localhost:8080/booklist へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。
テスト.csv をアップロードします。
リスナーが呼び出されて処理が実行されます。ログにカーリルの蔵書検索API のレスポンスを展開した結果が出力されていることが確認できます。
カーリルの蔵書検索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 の値は省略しています。
lending_app, lending_book テーブルも更新されていることが確認できます。
Ctrl+F2 を押して Tomcat を停止します。
一旦 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; } }
- setDataSource メソッドに @SuppressWarnings("SpringJavaAutowiringInspection") アノテーションを追加しました。
- Dialect Bean を生成するためのインナークラス DomaBeanConfig クラスを削除し、フィールド dialect から @Autowired アノテーションを削除しました。代わりに setDialect メソッドを追加して、このメソッドでフィールド dialect のインスタンスを生成するようにしています。
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); } }
- convertMessageToObject メソッドを追加します。
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
初版発行。