ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2020年6月14日日曜日

MyBatisでヘッダ明細型のデータをINSERTする(自動採番もする)

MyBatisでヘッダ明細型のデータを扱うシリーズの3回目。今回はネストしたJavaオブジェクトをデータベースに新規作成する方法について書いてみたいと思う。またIDもデータベースで自動採番させようとちょっと欲張ったものにしている。 今回はSQLの都合上MySQLに特化した内容となっている。

今回のサンプルもいつもと同じ以下のクラスとテーブルを使用する。

public class Student {
  int id;
  String name;
  Result[] result;
}

public class Result {
  String course;
  int score;
}

studentテーブル(ヘッダ)

id name
100 やまだ

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90

データベースはMySQLを使用する。 idは連番を振りたいのでデータベースで自動採番させてしまおう、ということで以下のようにテーブル定義をした。

CREATE TABLE student (
  id INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
  name VARCHAR(100)
);

CREATE TABLE result (
  refid INTEGER NOT NULL,
  course VARCHAR(100),
  score INTEGER
);

MyBatisのmapper定義

さて、ヘッダ明細型のテーブルに対してJavaオブジェクトをINSERTするMyBatisのmapperの定義をする。 INSERTの処理としてまず思いつくのは、MyBatisの設定でstudentテーブルとresultテーブルにINSERTするmapperをそれぞれ作成してJavaコードから順に呼び出す方法だろう。この場合はStudentクラスのオブジェクトをMyBatis経由で書き込んだ後、resultの配列ごとにループを回してResultクラスのオブジェクトをMyBatis経由で書き込む処理となる。

しかし、ネストしたStudentクラスのオブジェクトをMyBatisに渡して、その中でネストした部分も書き込んでもらうことはできないだろうかということで、以下のようなmapperを作成した。

<mapper namespace="mapper.student">
  <resultmap id="student" type="student">
    <id column="id" property="id" />
    <result column="name" property="name" />

    <collection property="result" oftype="Result">
      <result column="course" property="course" />
      <result column="score" property="score" />
    </collection>
  </resultmap>

  <insert id="create" parameterType="student" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO student (name) VALUES (#{name});
    SET @uid = LAST_INSERT_ID();
    <if test="result != null">
      <foreach item="i" collection="result">
        INSERT INTO result (refid, course, source)
          VALUES (@uid, #{i.course}, #{i.source});
      </foreach>
    </if>
    SELECT @uid FROM dual;
  </insert>

</mapper>

まずは最初の "INSERT INTO student ~" でStudentクラスのオブジェクトをstudentテーブルに書き込む。

次の行で "SET @uid = LAST_INSERT_ID()" を行っているが、ここでstudentテーブルに書き込んで自動採番されたIDを取得している。 これはMySQL専用の構文となる。

次にMyBatisの動的SQLを使ってネストしたオブジェクトを1つづつINSERTする。
まず、<if test="result != null"> でStudentクラスのオブジェクトがネストしたResultオブジェクトを持っているか(nullでないか)を調べ、持っているなら <foreach item="i" collection="result"> でひとつづつ順にINSERT処理をしている。
resultテーブルではrefid列に紐づくstudentテーブルのidを保持するが、先にMySQLの変数 "@uid" に自動採番された値を保存しているので、その値を書き込んでいる。

自動採番された値はJavaのStudentクラスのオブジェクトにも反映したい。MyBatisの処理結果として返した値が反映されるので、最後に "SELECT @uid FROM dual" を実行して、自動採番された値を返している。

Javaコードから書き込み

以下のようなJavaコードでネストされたオブジェクトを1回のMyBatis呼び出しでヘッダ明細型のテーブルにINSERTできる。

// 英語の結果
Result result_english = new Result();
result_english.course = "英語";
result_english.score = 50;

// 英語の結果
Result result_math = new Result();
result_math.course = "数学";
result_math.score = 68;

// 生徒の情報
// IDは自動採番されるため
Student student = new Student();
student.name = "たなか";
student.result = new Result[] { result_english, result_math };

// オブジェクトをデータベースに書き込む
sqlSession.insert("mapper.student.create", student);

// 自動採番された生徒のIDを表示
System.out.println("ID=" + student.id);

おわりに:BadSqlGrammarExceptionが出た時

Springから使っていたらこんな例外が出てしまいました。

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SET @uid = LAST_INSERT_ID();
		 
		SELECT @uid FROM dual' at line 3

今回はMyBatisのmapperで複数のステートメントを一度に実行していますが、MySQLのJDBCドライバの仕様で複数ステートメントを実行するときは "allowMultiQueries=true" オプションを指定する必要があるようです。JDBC接続のURLに以下のようにオプションを追加したらうまく動くようになりました。


jdbc.url=jdbc:mysql://localhost/student?allowMultiQueries=true

2020年5月26日火曜日

JavaのSimpleDateFormatで日付文字列チェックではまる

java.text.SimpleDateFormatを使って日付文字列をチェックしようとしてはまったときのメモ。 parseメソッドに文字列を与えてParseExceptionが出るか出ないかでチェックしようとした。

SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");

// 不正な日付
Date d = fmt.parse("197405112"); // ParseExceptionが発生しない!!

しかし、エラーとはならず、yyyy=1970、MM=05、dd=112と解釈され、5月112日→8月20日として、d=1974/8/20として通ってしまい予想外の動きになってしまった。 SimpleDateFormatはデフォルトだとできるだけ日付として解釈するように動作をするらしい。 日付を3桁で拾ってしまうのは予想外だったが・・・

存在しない日付を厳密にチェックするときはsetLenientをfalseに設定する必要があるようだ。 以下にすることでチェックをすることができた。

SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");

// 厳密な日付チェックを行う設定
fmt.setLenient(false);

// 不正な日付
Date d = fmt.parse("197405112"); // ParseException発生!!

SimpleDateFormatはスレッドセーフではなく使いまわしが難しいので、そろそろ新しいスレッドセーフなDateTimeFormatterに移行するべきとは思うが、Dateを使っている手持ちライブラリも多く使い勝手の悪さからなかなか移行できていない・・・

2020年5月12日火曜日

MyBatisでヘッダ明細型のデータを扱う(行範囲を指定)

前回の記事でヘッダ明細型のデータをMyBatisで読み込む方法としてcollectionタグを使いネストしたJavaオブジェクトに読み込む方法を紹介した。ヘッダテーブルと明細テーブルを結合した1つのSQLで読み込みができるため楽な方法ではあるが使い方に癖があるようなので注意しないと思わぬところではまってしまう。その一つが行数を指定して読み込むときである。

例として以下のようなデータをcollectionタグを使いネストしたJavaオブジェクトで読み込んでみよう。

studentテーブル(ヘッダ)

id name
100 やまだ
101 たなか
102 おばた
103 ささおか

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90
101 英語 50
101 数学 68
102 英語 77
102 数学 70
103 英語 98
103 数学 92

読み込むオブジェクトのJavaクラスの定義とMyBasisのmapperは前回の記事と同じものを使用する。

Javaクラス定義

public class Student {
  int id;
  String name;
  Result[] result;
}

public class Result {
  String course;
  int score;
}

MyBasisのmapper定義

<mapper namespace="mapper.student">
  <resultmap id="student" type="student">
    <id column="id" property="id" />
    <result column="name" property="name" />

    <collection property="result" oftype="Result">
      <result column="course" property="course" />
      <result column="score" property="score" />
    </collection>
  </resultmap>

  <select id="find" resultmap="student">
    SELECT id, name, course, score
    FROM student, result
    WHERE id = refid
    ORDER BY id
  </select>
</mapper>

IDの小さい順に2人、やまだ君(100)、たなか君(101)、のデータだけ読み込むことを考えてみる。MyBatisではRowBoundsを使うことで指定した行範囲のデータを読み込むことができるので、以下のコードで0行目から2行の読み込みをしようとした。

// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;
RowBounds rowBounds = new RowBounds(offset, limit);

// SQLに渡すパラメータ(特に無し)
Map<String,Object> params = new HashMap();

// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find", params, rowBounds);

しかしcollectionを使っている場合はそんなに単純ではない。RowBoundsへの期待はstudentテーブルの先頭2行だけであるが、SELECTの結果は以下のようにstudentの内容とresultの内容が1行に結合されたものとなり、RowBoundsはその先頭2行を対象としてしまうようである。結果的にstudentsには、やまだ君のデータだけが読み込まれる。

SELECTの結果

id name course score RowBoundで選ばれる行
100 やまだ 英語 80
100 やまだ 数学 90
101 たなか 英語 50 ×
101 たなか 数学 68 ×
102 おばた 英語 77 ×
102 おばた 数学 70 ×
103 ささおか 英語 98 ×
103 ささおか 数学 92 ×

これを回避するにはRowBoundsを使わず、全行読み込んだ後にoffsetとlimitでリストを切り取る処理を行っている。 一旦全行読み込んで処理をするのでメモリ的にもCPU的にも負荷がかかるのが欠点ではあるが、それほどの規模でなければコードの読みやすさでこの方法を使っている。 ただ、行数が多いテーブルではSQLの書き方も工夫するなど何らかの対策が必要がと思われる。

// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;

// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find");

// 行範囲で切り取り
int s = offset;
int e = offset+limit;
if (s < 0) { s = 0; }
if (e >= students.size()) { e = students.size(); }
if (s == 0 && e >= students.size()) {
	return students;
} else {
	return students.subList(s, e);
}

2020年4月29日水曜日

MyBatisでヘッダ明細型のデータを扱う

「ヘッダ明細型」のデータはアプリを作るうえでよく登場するデータ構造である。 「ヘッダ」が親データ、「明細」が親に紐づく子データを表しており、例えば成績のデータなどで、各生徒の英語と数学の成績を表す場合に生徒の情報を親としてヘッダテーブルに格納、成績情報は親にidで紐づけて明細テーブルに格納すると以下のようにあらわすことができる。

studentテーブル(ヘッダ)

id name
100 やまだ
101 たなか

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90
101 英語 50
101 数学 68

やまだ君の成績は英語80点で数学90点。たなか君の成績は英語50点で数学68点を表している・・・たなか君はもう少し頑張ったほうがいいですね・・・

このようなデータの持ち方は「Summary」と「Detail」などとも呼ばれたり、呼び方はさまざまであるが、商品カートや伝票など業務の世界でも頻繁に登場する。

このようなデータを扱う場合はStudentクラスがResultクラスのオブジェクトを持つようなネストした構造で表現することができる。 例えばJavaの場合のクラス定義は以下のようになる。setter/getterメソッドは省略して書いているので適宜付与してもらうとよいだろう。

public class Student {
  int id;
  String name;
  Result[] result;
}

public class Result {
  String course;
  int score;
}

ここでMyBatisを使ってstudentテーブルとresultテーブルの内容を上記クラスに読み込むことを考えてみる。studentテーブルをSELECTしてからresultテーブルをSELECTするように2つのSELECTに分けてもよいが、MyBatisではテーブルを結合した1つのSQLでネストしたクラスを読み込むことができる。

mapperでcollectionタグを使いネストした部分を定義する。property属性に子オブジェクトを格納するプロパティ名、oftype属性にそのクラス名を指定する。 SELECTのSQLでは2つのテーブルを結合する。

<mapper namespace="mapper.student">
  <resultmap id="student" type="student">
    <id column="id" property="id" />
    <result column="name" property="name" />

    <collection property="result" oftype="Result">
      <result column="course" property="course" />
      <result column="score" property="score" />
    </collection>
  </resultmap>

  <select id="find" resultmap="student">
    SELECT id, name, course, score
    FROM student, result
    WHERE id = refid
    ORDER BY id
  </select>
</mapper>

SELECTの結果は以下のようにstudentの内容とresultの内容が1行に結合されたものとなるが、親クラスの内容はmapperのidタグで指定された列でまとめられオブジェクトが構築されるようである。

SELECTの結果

id name course score
100 やまだ 英語 80
100 やまだ 数学 90
101 たなか 英語 50
101 たなか 数学 68

以下のコードで読み込みを行うと・・・

List<Student> entities = sqlSession.selectList("mapper.student.find");

下図のようなオブジェクトとして読み込まれる。



使いこなせば複雑なデータ構造をシンプルに扱えそうな方法であるが、実際使ってみると気を付けなければならない点もあり、そのような点は次回以降また紹介していこうと思う。

2016年8月23日火曜日

JavaからJavaScriptを実行したときのパラメータ受け渡し

JavaでJavaScriptを実行するにはjavax.script.ScriptEngineを使用する。 実行の際にはパラメータを渡したり計算結果を受け取ったりするがそのための方法がリファレンスを読んでもわかりにくかったのでどう受け渡しができるのかを試してみた。

パラメータをひとつづつ渡す
パラメータを渡すにはScriptEngineのputメソッドにJavaScriptでの変数名を指定する。 JavaScript側では通常の変数として利用できる。

try {
  ScriptEngineManager factory = new ScriptEngineManager();
  ScriptEngine e = factory.getEngineByName("js");
  e.put("a", "hello");
  e.put("b", "world");
  
  String script = "ab=a + ' ' +b;";  
  Object ret = e.eval(script);
  Bindings b = e.getBindings(ScriptContext.ENGINE_SCOPE);
  for(String key : b.keySet()) {
    Object v= b.get(key);
    System.out.println("key=" + key + "; value=" + v + " (" + v.getClass() + ")");
  }
} catch (ScriptException ex) {
  ex.printStackTrace();
}
実行結果
key=a; value=hello (class java.lang.String)
key=b; value=world (class java.lang.String)
key=ab; value=hello world (class java.lang.String)

retには最後に実行した式の結果が入る。ここではabの値と同じ。 パラメータをMapで渡す
Mapで渡したパラメータはJavaScript側ではオブジェクトのプロパティとして使用できる。
try {
  ScriptEngineManager factory = new ScriptEngineManager();
  ScriptEngine e = factory.getEngineByName("js");
  Map<String, Object> map = new HashMap();
  map.put("x", "hello");
  map.put("y", "world");
  e.put("map", map);
  
  String script = "x = map.x; xy=map.x+' '+map.y;";  
  Object ret = e.eval(script);
  Bindings b = e.getBindings(ScriptContext.ENGINE_SCOPE);
  for(String key : b.keySet()) {
    Object v= b.get(key);
    System.out.println("key=" + key + "; value=" + v + " (" + v.getClass() + ")");
  }
} catch (ScriptException ex) {
  ex.printStackTrace();
}
実行結果
key=map; value={x=hello, y=world} (class java.util.HashMap)
key=x; value=hello (class java.lang.String)
key=xy; value=hello world (class java.lang.String)

パラメータをListで渡す
Listで渡したパラメータはJavaScript側では配列として使用できる。
try {
  ScriptEngineManager factory = new ScriptEngineManager();
  ScriptEngine e = factory.getEngineByName("js");
  List<String> list = new ArrayList();
  list.add("Tokyo");
  list.add("Nagoya");
  list.add("Osaka");
  e.put("list", list);
  
  String script = "list0=list[0]; list1=list[1]; list2=list[2]; length=list.size();";  
  Object ret = e.eval(script);
  Bindings b = e.getBindings(ScriptContext.ENGINE_SCOPE);
  for(String key : b.keySet()) {
    Object v= b.get(key);
    System.out.println("key=" + key + "; value=" + v + " (" + v.getClass() + ")");
  }
} catch (ScriptException ex) {
  ex.printStackTrace();
}
実行結果
key=list; value=[Tokyo, Nagoya, Osaka] (class java.util.ArrayList)
key=list0; value=Tokyo (class java.lang.String)
key=list1; value=Nagoya (class java.lang.String)
key=list2; value=Osaka (class java.lang.String)
key=length; value=3 (class java.lang.Integer)

Listの要素にMapを渡すこともできる。
try {
  ScriptEngineManager factory = new ScriptEngineManager();
  ScriptEngine e = factory.getEngineByName("js");
  List<Map<String, Object>> list = new ArrayList();
  Map<String, Object> map1 = new HashMap();
  map1.put("v1", 100);
  map1.put("v2", 200);
  map1.put("v3", 300);
  lista.add(map1);
  Map map2 = new HashMap();
  map2.put("v1", 110);
  map2.put("v2", 210);
  map2.put("v3", 310);
  lista.add(map2);
  e.put("list", list);
  
  String script = "list0_v1=lista[0].v1; list1_v1=lista[1].v1;";  
  Object ret = e.eval(script);
  Bindings b = e.getBindings(ScriptContext.ENGINE_SCOPE);
  for(String key : b.keySet()) {
    Object v= b.get(key);
    System.out.println("key=" + key + "; value=" + v + " (" + v.getClass() + ")");
  }
} catch (ScriptException ex) {
  ex.printStackTrace();
}
実行結果
key=list; value=[{v1=100, v2=200, v3=300}, {v1=110, v2=210, v3=310}] (class java.util.ArrayList)
key=list0_v1; value=100 (class java.lang.Integer)
key=list1_v1; value=110 (class java.lang.Integer)

2016年7月19日火曜日

JavaMail と AmazonSESでメールを送る

Amazon の提供するクラウドサービスに Amazon Simple Email Service(Amazon SES)というバルクメール送信サービスがある。SMTPのインターフェースが使えるとのことなので今回 JavaMail を使いJavaプログラムからメールを送信してみた。
Amazon SES を使うためには Amazon Web Service(AWS) のアカウントを持っている必要がある。 (アカウントの取り方は省略する)
EC2をすでに利用していれたので同じアカウントで利用することができた。Amazon SES のためには最低限、以下の設定が必要である。

  1. メールアドレスの登録
    送信元(From)およびテスト用の送信先(To)のメールアドレスの設定および承認をする。
  2. SMTP設定
    SMTPサーバーにアクセスするための認証情報などを設定する。
設定はどちらも Management Console から行う。 現時点でのそれぞれの設定方法は次のようになる。

1. メールアドレスの登録

送信元(From)とするメールアドレスの設定および承認をする。 また、サービス利用直後はsandboxモードというあらかじめ登録されたメールアドレスにしか送れないモードとなっているが、この送信先(To)のメールアドレスも同じように登録する。

メールアドレスは以下の手順で登録する。送信元(From)、送信先(To)のアドレスをそれぞれ登録する。

  1. Management Consoleのメニュー"Email Addresses"を選択
  2. [Verify a New Email Address]を押す。ダイアログボックスに送信元とするメールアドレスを入れる。
  3. 入力したメールアドレスに "Amazon SES Address Verification Request"というメールが来る。その中のリンクをクリック。
  4. 「Amazon Simple Email Service(Amazon SES)でのメールアドレスの検証が完了しました。このアドレスからのメール送信を開始できます。」というページが開く。
  5. 登録完了

2. SMTP設定

SMTPサーバーにアクセスするための認証情報などを設定する。

  1. Management Consoleのメニュー"SMTP Settings"を選択
  2. [Create My SMTP Credentials]を押す
  3. "IAM User Name" を要求される。既定の値をそのまま使い [Create] を押す。
  4. 認証情報が生成される。[Show User SMTP Security Credentials]を押すと以下のような内容が表示される。
ses-smtp-user.xxxx-xxxx
SMTP Username: xxxxxxxxxxxxxxx
SMTP Password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

SMTP Username と SMTP Password は後で使うのでメモしておく。

ここまでできたらAmazon SESでメール送信することができるすることができる。 早速、JavaMailを使いJavaプログラムからメールを送信してみよう。 SMTPサーバーはポート25か465か587が使えるので、自分の環境にあったものを選べばよいかと思う。 自分のローカルPCから送る場合は、プロバイダの方でポート25を制限していることが多いと思うのでそれ以外を選ぶのがいいだろう。 また、Transport Layer Security (TLS)が必要なのでJavaMailのほうでその設定をする。

以下のサンプルプログラムはAmazon SESのリファレンスガイドに載っていたものとほぼ同じである。 ビルドしてローカルPCで動かすと1で登録したメールアドレスにメールが配信される。

package test;

import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class AmazonSESTest {

  // 1で登録したメールアドレス
  static final String FROM = "xxx@xxx.jp";
  static final String TO = "yyy@yyy.jp";

  // 2で作成したSMTP Username/Passwordを設定
  static final String SMTP_USERNAME = "xxxxxxxx";
  static final String SMTP_PASSWORD = "xxxxxxxxxxxxxxxxxx";

  // Amazon SES SMTP ホスト名
  static final String HOST = "email-smtp.us-east-1.amazonaws.com";

  // ポートは25,465または587を指定する
  static final int PORT = 587;

  private static Logger logger = Logger.getLogger(AmazonSESTest.class.getName());

  public static void main(String[] args) {
    try {
      AmazonSESTest app = new AmazonSESTest();
      app.start(args);
    } catch (Throwable ex) {
      logger.log(Level.SEVERE, "エラーが発生: " + ex.toString(), ex);
    }
  }

  private void start(String[] args) throws MessagingException {
    // JavaMailの設定
    Properties props = System.getProperties();
    props.put("mail.transport.protocol", "smtp");
    props.put("mail.smtp.port", PORT);
    
    // JavaMailでTransport Layer Security (TLS)を使う設定
    props.put("mail.smtp.auth", "true");
    props.put("mail.smtp.starttls.enable", "true");
    props.put("mail.smtp.starttls.required", "true");

    // JavaMail Session 作成
    Session session = Session.getDefaultInstance(props);

    // 送信メッセージを作成
    MimeMessage msg = new MimeMessage(session);
    msg.setFrom(new InternetAddress(FROM));
    msg.setRecipient(Message.RecipientType.TO, new InternetAddress(TO));
    msg.setSubject("テストメール", "utf-8");
    msg.setContent("AmazonSESから出したメール", "text/plain;charset=utf-8");

    // メール送信
    Transport transport = session.getTransport();
    try {
      logger.info("Amazon SES SMTP で送信中...");
      transport.connect(HOST, SMTP_USERNAME, SMTP_PASSWORD);
      transport.sendMessage(msg, msg.getAllRecipients());
      logger.info("送信完了");
    } finally {
      // 後始末
      transport.close();
    }
  }

}

最後に、Amazon SESでは個別の申請をしない場合には以下のような利用制限があるようである。 後者はproduction版を申請すれば解除されるようだが、それはまた後日試してみることにする。

  • 送信できるメール数、200メール/日、1メール/秒
  • 登録済みのメールアドレスにしか送信できない(sandboxモード)

2013年6月20日木曜日

Apache Solr

Apache Solr(ソーラーと読む?)という全文検索エンジンを最近よく耳にするようになった。 どうやらApache Luceneをベースとした検索エンジンらしい。 Luceneはだいぶ前に触ったことがありインデックスの作成とその検索をするフレームワークエンジンのようなものだったが、それに周りを作って検索サーバーとして使えるように仕立てたようなもの?

日本語のドキュメントでどれくらい動いてくれるものか気になる。

2013年3月16日土曜日

Twitter4JでStatusNetにアクセスする

前回の記事でTwitter風のマイクロブログStatusNetをWindows環境に立ち上たが、今回はこれにJavaプログラムからアクセスしてみる。 StatusNetはTwitter準拠のAPIを持っているとのことなのでJavaでTwitterにアクセスするためのライブラリTwitter4Jが使えないかと思い試してみることにした。

結果としては以下のようなコードでユーザーのタイムラインが取得できた。 StatusNetはBasic認証でも認証ができるので今回はその方法を使った。

import twitter4j.ResponseList;
import twitter4j.Status;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;
import twitter4j.auth.BasicAuthorization;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;

public class TwitterConsole {
  
  public static void main(String[] args) {
    try {
      TwitterConsole app = new TwitterConsole();
      app.start();
    } catch(Throwable ex) {
       ex.printStackTrace(System.err);
    }
  }

    private void start() throws TwitterException {
        
    // StatusNetのAPIのBase URL
    String baseURL = "http://localhost/statusnet/api/";

    // Twitter4J設定。APIのBase URLをStatusNetのサーバーに変更
    ConfigurationBuilder cb = new ConfigurationBuilder();
    cb.setRestBaseURL(baseURL);
    cb.setIncludeEntitiesEnabled(true);
    cb.setJSONStoreEnabled(true);
    Configuration conf = cb.build() ;

    // Basic認証で認証する
    BasicAuthorization auth = new BasicAuthorization("username", "password");
    Twitter twitter = new TwitterFactory(conf).getInstance(auth);
    
    // ユーザーのタイムラインを取得
    ResponseList tweets = twitter.getHomeTimeline();

    // 取得したタイムラインを表示
    int i = 0;
    for(Status tweet : tweets) {
      i++;
      User tweetuser = tweet.getUser();
      System.out.printf("[%d] %s %s%n%s%n-----------%n",
          tweet.getId(),  // 投稿ID
          tweet.getCreatedAt().toString(),  // 投稿時刻
          tweetuser.getName(),  // 投稿者
          tweet.getText());     // 投稿内容
    }
  }
}

2013年3月14日木曜日

HttpClient4とMicrosoft Translator APIで翻訳する

前回の記事でAPIの呼び方を解説したが、その手順をJavaのプログラムにしてみた。 API呼び出しにはHttpClient4を使った。access_tokenの取得結果はJSONで返ってくるためその切り出しにはJSONICを使っている。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import net.arnx.jsonic.JSON;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;


public class MSTranslatorTest {

  public static void main(String[] args) {
    try {
      MSTranslatorTest app = new MSTranslatorTest();
      app.translate();
    } catch(Throwable ex) {
      ex.printStackTrace(System.err);
    }
  }

  private void translate() throws IOException {
    DefaultHttpClient client = new DefaultHttpClient();
    
    // Step1 access_tokenを取得
    // 取得のためのリクエストを準備
    HttpPost httpPost = new HttpPost("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13");
    List<NameValuePair> params = new ArrayList<NameValuePair>();
    params.add(new BasicNameValuePair("grant_type", "client_credentials"));
    // クライアント IDを設定
    params.add(new BasicNameValuePair("client_id", "...."));
    // 顧客の秘密を設定
    params.add(new BasicNameValuePair("client_secret", "...."));
    params.add(new BasicNameValuePair("scope", "http://api.microsofttranslator.com"));
    httpPost.setEntity(new UrlEncodedFormEntity(params));
    // 取得実行
    String accessToken;
    HttpResponse response1 = client.execute(httpPost);
    try {
      // 結果はJSONとして返ってくるのでJSONICでaccess_tokenを切り出す
      HttpEntity entity = response1.getEntity();
      String reponseText = EntityUtils.toString(entity);
      Map json = JSON.decode(reponseText, Map.class);
      accessToken = (String)json.get("access_token");
    } finally {
      httpPost.releaseConnection();
    }

    // Step2 翻訳する
    // 翻訳リクエストURLを作成
    // ・翻訳元=日本語 (from=ja)
    // ・翻訳先=スペイン語 (to=es)
    // ・翻訳する文字列 (text)
    String text = "こんにちは";
    String uri = String.format("http://api.microsofttranslator.com/V2/Http.svc/Translate?from=ja&to=es&text=%s", text);
    // access_tokenをヘッダに付与。"Bearer "を前につける。
    String authorization = String.format("Bearer %s", accessToken);
    HttpGet httpGet = new HttpGet(uri);
    httpGet.setHeader("Authorization", authorization);
    // 翻訳実行
    HttpResponse response2 = client.execute(httpGet);
    try {
      // 結果はXMLで返ってくる。そのままコンソールに表示。
      HttpEntity entity = response2.getEntity();
      String reponseText = EntityUtils.toString(entity);
      System.out.println(reponseText);
    } finally {
      httpGet.releaseConnection();
    }
  }
}

実行するとコンソールに次のような結果が表示される。

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">¡Buenas noches!</string>

関連記事

2013年3月7日木曜日

サーブレットで非同期処理

サーブレットで非同期処理をするためのAsyncContextというクラスがServlet3.0から追加されている。

そもそもサーブレットでは時間のかかる処理を書いたり、スレッドを作りバックグラウンドで処理をしてはいけないという決まりがあるらしい。しかしTomcatなど特にチェックなどしておらずやろうと思えばできてしまうため結構使っている人も多いのではないでしょうか?(自分もそうです)

そういう場合にServlet3.0からはAsyncContextを別スレッドを使って実行することができるようです。

2013年2月7日木曜日

JSONICでis-a関係を扱う

JSONとPOJO(Javaオブジェクト)を相互変換するのに JSONIC という便利なライブラリがある。 実際の使い方はリンク先を見ていただくとして、単純な変換であればJSON→POJO、POJO→JSONとも1行のメソッド呼び出しで書けるのでとても楽。 ただ、is-a関係のあるオブジェクトを含むJSONをPOJOに変換する場合にどうしたらよいのか今までやり方がわからなくて困っていた。 たとえば、以下のような3つのクラスがあって、AとBがis-a関係、SとAが has-a 関係にあるとする。
public class A {
    private String fieldA;
    public String  getFieldA() { return fieldA; }
    public void setFieldA(String fieldA) { this.fieldA = fieldA; }
}

public class B extend A {
    private String fieldB;
    public String  getFieldB() { return fieldB; }
    public void setFieldB(String fieldB) { this.fieldB = fieldB; }
}

public class S {
    private A obj;
    public A getObj() { return obj; }
    public void setObj(A obj) { this.obj = obj; }
}
このときSのsetObj()にクラスBを持たせたPOJOをJSONに変換すると以下のようになる。
{
    "Obj" : { "fieldA":"~", "fieldB":"~" }
}
しかしJSONになると型情報がなくなるため、このJSONを以下のようにPOJOに逆変換すると S.getObj()で得られるインスタンスはクラスAになってしまう。
S myobj = JSON.decode(jsonText, S.class);

// o はクラスAのインスタンス
A o = myObj.getObj();
このときクラスBを再生するにはJSONICの変換ロジックをオーバーライドしてカスタマイズすることでできるらしい。 このように変換方法をカスタマイズするにはJSON.decode()メソッドではなく、postparse()をオーバーライドしたJSONクラスのインスタンスを作りparse()メソッドで変換をするようにするらしい。 少しややこしいが以下の方法でクラスBが再生できるようになった。
JSON json = new JSON() {
  // parse時の変換方法をカスタマイズする
  @Override
  protected <T> T postparse(Context context, Object value, Class<? extends T> c, Type type) throws Exception {
    // クラスAの変換であればクラスBとみなして変換をする
    if (c.equals(Class.A)) {
      return c.cast(super.postparse(context, value, Class.B, Class.B));
    }
    // クラスA以外ならば既定の方法で変換
    else {
      return super.postparse(context, value, c, type);
    }
  }
};
S myobj = json.parse(jsonText, S.class);

// o はクラスBのインスタンス
A o = myObj.getObj();
この例だとSのsetObj()にあるのはクラスBと決め打ちしてしまっているが、ここを動的に変えるような場合はpostparse()メソッドの中で引数valueの内容を判別して処理を分岐すればできるのではないかと思う。 もしかするとannotationとか使うともっと楽なやり方があるのかもしれないが、まだよくわかっていないので今後の課題。

2013年1月10日木曜日

Java VisualVM

何時のJDKのバージョンから付属するようになったのかわからないがJava開発にJava VisualVMというツールがすごく便利である。 Javaプロセスのプロファイルを取得するツールで以下情報がGUIでモニタできる。

  • 設定されているJVMオプション
  • 設定されているシステムプロパティ 
  • CPU使用率
  • Heapメモリの最大サイズと使用済みサイズ
  • PermGen領域の最大サイズと使用済みサイズ
  • 動作中のスレッド一覧、タイムライン、スレッドダンプ
スレッドタイムラインやPermGen領域をリアルタイムモニタできる機能が便利でサーバー系システムでだんだんとゴミがたまっていく問題を分析するのにとても役立った。

ツールはJDK のjavaにパスを通した状態でコマンドラインよりjvisualvmで起動できる。GUIのツールなので特に説明を見なくてもJavaのことをある程度知っている人ならば使いこなせると思う。

英語ですがドキュメントへのリンクはっておきます。



2013年1月5日土曜日

サーブレットでリクエストパラメータを書き換える

サーブレットにリクエストパラメータを書き換えて渡したいときの方法。たぶんFilterを使っても同じことができると思うが、今回はRequestDispatcherでforwardする方法を使ってみた。

以下のサンプルでは /myWrapperServlet.do で受けたリクエストのパラメータ名p2をp1に書き換えて /myServlet.doに渡す。パラメータの書き換えは直接できないので、少々わかりにくいが HttpServletRequestWrapperのサブクラスを作り元のリクエストをラップしてgetParameter()などで書き換えロジックを書いてやる必要がある。パラメータ名p2をp1に書き換えるにはgetParameter()でパラメータp1を要求されたときに元リクエストのパラメータp2の値を返すようにする。
package test;
// import文は省略

public class MyWrapperServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {

    // パラメータの書き換えを行うHttpServletRequestを設定して
    // myServlet.doにforward
    RequestDispatcher dispatcher = req.getRequestDispatcher("/myServlet.do");
    MyServletRequestWrapper _req = new MyServletRequestWrapper(req);
    dispatcher.forward(_req, res);
  }

  public static class MyServletRequestWrapper extends HttpServletRequestWrapper {

    public MyServletRequestWrapper(HttpServletRequest request) {
      super(request);
    }

    @Override
    public String getParameter(String name) {
      if ("p1".equals(name)) { name = "p2"; }
      return getRequest().getParameter(name);
    }

    @Override
    public String[] getParameterValues(String name) {
      if ("p1".equals(name)) { name = "p2"; }
      return getRequest().getParameterValues(name);
    }
  }

}

以下はweb.xmlに記述するエントリの例。
<servlet>
  <servlet-name>myServlet</servlet-name>
  <servlet-class>test.MyServlet</servlet-class>
</servlet>
<servlet>
  <servlet-name>myWrapperServlet</servlet-name>
  <servlet-class>test.MyWrapperServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>myServlet</servlet-name>
  <url-pattern>/myServlet.do</url-pattern>
</servlet-mapping>
<servlet-mapping>
  <servlet-name>myWrapperServlet</servlet-name>
  <url-pattern>/myWrapperServlet.do</url-pattern>
</servlet-mapping>

結果として上URLのリクエストが下URLのリクエストと同じになりサーブレットtest.MyServletクラスが呼び出される。
http://{server}:{port}/myapp/myWrapperServlet.do?p2=abcdefg
http://{server}:{port}/myapp/myServlet.do?p1=abcdefg

パラメータ名を書き換えるのでMyServletRequestWrapperクラスの他メソッドも(特にパラメータ名に関連するもの)もちゃんと実装したほうが良いと思うのだが、とりあえず動くので今回はここまで…

2012年2月8日水曜日

Java Class.isAssignableFromメソッド

Javaクラスの継承関係を調べるClass.isAssignableFromメソッドの使い方をいつも忘れるのでメモ

ClassA,ClassBがあって、ClassBがClassAを継承している(ClassB extends ClassA)のときは以下のようになる。

class ClassB extends ClassA { ... }

ClassA.class.isAssignableFrom(ClassB.class) = true
ClassB.class.isAssignableFrom(ClassA.class) = false


instanceofとよく似ているが、instanceofはあるインスタンスがあるクラスを継承したものかを調べるのに対して、isAssignableFromメソッドはクラス同士で継承関係を調べるときに使う。

以下コードの(2)のように書くとinstanceofと同等の使い方となる。
まあinstanceofの場合はbがnullだった場合もnullチェックせずに使えるので便利だろう。

ClassB b = new ClassB();

// (1) これはtrueになる
if (b instanceof ClassA) { ... }

// (2) これもtrueになる
if (ClassA.class.isAssignableFrom(b.getClass())) { ... }


普通のプログラムだとisAssignableFromメソッドはあまり使わないが、クラスのメタ情報を使って動くようなプログラムを書くときは個人的にはよく使います。

2012年1月11日水曜日

Java JDBCのエラー ローにありません

SQL Anywhere 12 にJavaからiAnywhere JDBC ドライバ(ianywhere.ml.jdbcodbc.jdbc3.IDriver)で接続しSELECT文を実行、結果を読むところで「ローにありません」というエラーが出た。


Caused by: java.sql.SQLException: ローにありません。
at ianywhere.ml.jdbcodbc.jdbc3.IIResultSet.getInt(Native Method)
at ianywhere.ml.jdbcodbc.jdbc3.IResultSet.getInt(IResultSet.java:464)
... 2 more


まったく意味がわからないメッセージなので、どうしたものかと思い該当部分のコードを見る。


ResultSet rs = statement.executeQuery();
int n = rs.getInt(1); // ← ここでSQLException
rs.close();


結果を一行しか返さないSQL文だったのですっかり油断していた。

原因はResultSet.next()が抜けていただけでした。

正しくは…


ResultSet rs = statement.executeQuery();
int n;
if (rs.next()) {
n = rs.getInt(1);
}
rs.close();


これに気づく間、SQLを変えてみたり試行錯誤してました。
適切なエラーメッセージはとても大事です。

2011年1月31日月曜日

Java JDBC-ODBC 文字列またはバッファの長さが無効です

JavaアプリからJDBC-ODBCブリッジでデータベースSQL Anywhereに接続していると、不定期に「文字列またはバッファの長さが無効です」というエラーが出る現象に遭遇した。

DriverManager.getConnection()で出てみたり・・・

java.sql.SQLException: [Microsoft][ODBC Driver Manager] 文字列またはバッファの長さが無効です。
at sun.jdbc.odbc.JdbcOdbc.createSQLException(JdbcOdbc.java:6957)
at sun.jdbc.odbc.JdbcOdbc.standardError(JdbcOdbc.java:7114)
at sun.jdbc.odbc.JdbcOdbc.SQLGetDataString(JdbcOdbc.java:3907)
at sun.jdbc.odbc.JdbcOdbcResultSet.getDataString(JdbcOdbcResultSet.java:5698)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:354)
at sun.jdbc.odbc.JdbcOdbcConnection.buildTypeInfo(JdbcOdbcConnection.java:1503)
at sun.jdbc.odbc.JdbcOdbcConnection.initialize(JdbcOdbcConnection.java:381)
at sun.jdbc.odbc.JdbcOdbcDriver.connect(JdbcOdbcDriver.java:174)
at java.sql.DriverManager.getConnection(DriverManager.java:582)
at java.sql.DriverManager.getConnection(DriverManager.java:185)


ResultSet.getString()で出てみたりと発生箇所もさまざま。

java.sql.SQLException: [Microsoft][ODBC Driver Manager] 文字列またはバッファの長さが無効です。
at sun.jdbc.odbc.JdbcOdbc.createSQLException(JdbcOdbc.java:6957)
at sun.jdbc.odbc.JdbcOdbc.standardError(JdbcOdbc.java:7114)
at sun.jdbc.odbc.JdbcOdbc.SQLGetDataString(JdbcOdbc.java:3907)
at sun.jdbc.odbc.JdbcOdbcResultSet.getDataString(JdbcOdbcResultSet.java:5698)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:354)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:411)


調べてみるとある特定の環境だけで頻発するようだ。

英語のエラーメッセージ "Invalid string or buffer length" で検索すると、SQL ServerOracleでも発生している様子。これらは不定期でなくコードの決まった場所で必ず発生するとのことだが、共通するのはx64版のJavaVMを使っていると言うことである。

今エラーが起きているのもx64 JavaVMなので何か関係がありそうである。

JDBC-ODBCをやめてType4ドライバを使うようにしたほうがよさそうだ。

2010年11月18日木曜日

java.util.Dateとandroid.text.format.Time どちらを使うべきか?

Androidにはjava.util.Dateやjava.util.Calendarに似たクラスandroid.text.format.Timeがあり、日付データを扱うときどちらを使うべきか迷うところであるが、ドキュメントによるとjava.util.Calendarより高速な代替品と書いてあるので、文字列にして表示したり日付計算する場合は後者を使うのがよさそうに思える。

http://developer.android.com/reference/android/text/format/Time.html

まだドキュメントを読んだ限りなので、ちょっと実際に使って試してみることにしよう。

2010-12-20 追記
android.text.format.TimeはSerializableでないことが判明。つまりIntentに入れてActivity間で受け渡すデータとしては使えないので注意が必要です。

Timeをサブクラス化してSerializableをimplementsしたものを作ればいいのかもしれないが、将来的にTimeのインスタンス変数にSerializableでないものが入ってくるとアウトなので今回は利用を見送る。