2010/02/19

mobyletでSMTP認証を利用する

前回の記事でmobyletではSMTP認証を利用したメール送信ができないと書きましたが、いくつかのクラスを拡張することによってSMTP認証の利用は可能となります。その方法についてまとめてみました。

1. MobyletMailConfig
mobylet標準のメール設定保持クラス(MobyletMailConfig)では、SMTP認証を行う場合のプロパティ(mail.smtp.auth)の設定を行えないため、MobyletMailConfigの拡張クラスを作成します。

/**
* メール送信時にSMTP認証を行うように設定するための設定情報クラスです。
*
* @author namiki
*
*/
public class MyMobyletMailConfig extends MobyletMailConfig {

/** SMTP認証設定のキーです。 */
private static final String KEY_SMTP_AUTH = "mobylet.smtp.auth";

/** SMTP認証の初期値です。 */
private static final String DEF_SMTP_AUTH = "false";

/**
* コンストラクタです。
*/
public MyMobyletMailConfig() {
super();
}

/**
* SMTP認証設定を取得します。
*
* @return SMTP認証設定
*/
public String getSMTPAuth() {
String smtpAuth = props.getProperty(KEY_SMTP_AUTH);
if (StringUtils.isEmpty(smtpAuth)) {
smtpAuth = DEF_SMTP_AUTH;
}
return smtpAuth;
}

@Override
public Session createSession() {
Properties sessionProperties = new Properties();
sessionProperties.setProperty("mail.host", getHost());
sessionProperties.setProperty("mail.smtp.host", getHost());
sessionProperties.setProperty("mail.smtp.localhost", getHost());
sessionProperties.setProperty("mail.smtp.port", getPort());
sessionProperties.setProperty("mail.smtp.user", getUser());
sessionProperties.setProperty("mail.smtp.pass", getPassword());
sessionProperties.setProperty("mail.smtp.auth", getSMTPAuth());
return Session.getInstance(sessionProperties);
}

}


2. MobyletMailInitializer
mobylet標準のコンポーネント初期化クラスであるMobyletInitializerでは、1.で作成したMailConfigクラスを生成できないので、MobyletMailInitializerの拡張クラスを作成します。

/**
* Mobyletでメール送信を行うためのコンポーネント初期化クラスです。

* Mobylet標準の{@link org.mobylet.mail.initializer.MobyletMailInitializer}
* が行う初期化処理に加えて、 SMTP認証の設定を行うクラスを初期化します。
*
* @author namiki
*
*/
public class MyMobyletMailInitializer extends MobyletMailInitializer {
@Override
public void initialize() {
super.initialize();
SingletonUtils.put(new MyMobyletMailConfig());
}
}


3. mobylet.mail.properties
mobyletのメール設定ファイルであるmobylet.mail.propertiesに新しいプロパティ設定を追加します。

mobylet.smtp.host=192.168.1.1
mobylet.smtp.port=587
mobylet.smtp.user=sample@example.com
mobylet.smtp.password=aaaaa
# 新しいプロパティです。
mobylet.smtp.auth=true


4. MobyletMessage
次にMobyletMessageを拡張します。MobyletMessageはjavax.mail.Sessionを保持していますが、アクセス修飾子がprotectedとなっているため、後述するMobyletMailerのサブクラスからアクセスできないので、アクセスできる状態にするように拡張します。

/**
* {@link org.mobylet.mail.message.MobyletMessage}では{@link javax.mail.Session}
* にアクセスできないため、{@link javax.mail.Session}へのアクセスを確保するためのメッセージクラスです。
*
* @author namiki
*
*/
public class MyMobyletMessage extends MobyletMessage {

private Session localSession;

/**
* コンストラクタです。
*
* @param toCarrier
* キャリア情報
* @param session
* セッション情報
*/
public MyMobyletMessage(Carrier toCarrier, Session session) {
super(toCarrier, session);
this.localSession = session;
}

/**
* セッション情報を返却します。
*
* @return
*/
public Session getSession() {
return this.localSession;
}

}


5. MobyletMailer
実際にメールを送信するMobyletMailerの拡張クラスを作成します。MobyletMessageのサブクラスを返却するcreateMessage()と実際にメール送信を行うsend()を実装します。

/**
* mobyletのメール送信でSMTP認証を行うメール送信クラスです。
*
* @author namiki
*
*/
public class MyMobyletMailer extends MobyletMailer {

/**
* 指定された送信先アドレスをもとに、メッセージを作成します。
*
* @param to
* 送信先アドレス
* @return メッセージ
*/
public static MyMobyletMessage createMessage(String to) {

MailConfig config = SingletonUtils.get(MailConfig.class);
Session session = config.createSession();
// Carrier
MailCarrierDetector carrierDetector = SingletonUtils
.get(MailCarrierDetector.class);
Carrier carrier = carrierDetector.getCarrierByAddress(to);
// Message
MyMobyletMessage message = new MyMobyletMessage(carrier, session);
return (MyMobyletMessage) message.to(to);

}

/**
* 指定されたメッセージをSMTP認証でメールを送信します。
*
* @param message
* メッセージ
*/
public static void sendBySMTP(MyMobyletMessage message) {

Session session = message.getSession();
Transport transport = null;
try {
transport = session.getTransport("smtp");
transport.connect(session.getProperty("mail.smtp.host"), session
.getProperty("mail.smtp.user"), session
.getProperty("mail.smtp.pass"));
transport.sendMessage(message.construct(), message
.getAllRecipients());
transport.close();

} catch (Exception e) {
e.printStackTrace();
}
}
}


6. 使い方
最後にこれらのクラスを使ったメール送信の方法です。

/**
* Mobyletの機能を利用して、メール送信を行うロジッククラスです。
*
* @author namiki
*
*/
public class MobyletMailSenderLogic {

// ------------------------------------------------------------ [Properties]

/** メールメッセージオブジェクトです。 */
private MailMessageDto messageDto;

// -------------------------------------------------------------- [Accessor]

/**
* @param messageDto
* the messageDto to set
*/
public void setMailMessageDto(MailMessageDto messageDto) {
this.messageDto = messageDto;
}

// -------------------------------------------------------- [Public methods]

public void send() {

if (messageDto.to.length <= 0) {
throw new RuntimeException("送信先アドレスが定義されていません。");
}

for (int i = 0; i < messageDto.to.length; i++) {
MyMobyletMessage message = MyMobyletMailer
.createMessage(messageDto.to[i]);

MessageBody body = new MessageBody();
body.setText(MyEmojiUtils
.convertEmojiCharByKey(messageDto.bodyText));

message.from(messageDto.from).subject(
MyEmojiUtils.convertEmojiCharByKey(messageDto.subject))
.setBody(body);

MyMobyletMailer.sendBySMTP(message);

}
}
}

上記のMailMessageDtoは送信元メールアドレス、送信先メールアドレス、件名、本文を保持するためのクラスです。MessageDto.toの数だけ処理を繰り返していますが、これは送信先のキャリアによって絵文字の文字コードを変更する必要があるためです。

以上がmobyletでSMTP認証を利用する場合の方法となります。かなり大急ぎで作ったものなので、エラーハンドリングをほとんど無視していたり、作り自体まだまだ詰めが甘いと思いますが、参考になれば幸いです。

SAStruts+Mobyletのメール送信問題点まとめ

SAStruts+mobyletという環境で、絵文字入りのメールを携帯に向けて送信する機能を作っています。
mobyletは携帯向けWebアプリ用のフレームワークですが、WebアプリはPCで閲覧されるのを前提にしているので画面はSAStrutsを利用して作成し、メール送信の部分だけmobyletを利用するようにしています。

絵文字入りメールを3キャリア(Docomo、au、SoftBank)に送った場合、携帯側では正常に絵文字が表示されますが、3キャリア以外に送った場合(Gmail)だと絵文字部分がimgタグに変換されてしまいます。この事象について調べてみた結果をまとめてみました。

まずmobyletでメール送信を行う場合は以下のような処理を行います。

// 送信先メールアドレス
MobyletMessage message = MobyletMailer.createMessage("sample@docomo.ne.jp");
MessageBody body = new MessageBody();
body.setText();
message.from("sample@example.com")
.subject()
.setBody(body);
MobyletMailer.send(message);

MobyletMailer#send()ではMobyletMessage#construct()を呼び出しており、そこからは以下のような流れで処理が呼ばれます。(テキストメールの場合)

  • MobyletMessage#construct()

  • MobyletTextMailBuilder#build()

  • MobyletTextMailBuilder#buildSimpleTextMail()

  • MobyletTextMailBuilder#buildTextPart()

  • DataHandlerUtils#getDataHandler()

  • MailEmojiUtils#convert()

  • MobyletPrintWriter#write()


このうち、MobyletPrintWriterでは、useImageEmojiという変数で絵文字を画像として扱うかという判定を行っており、このuseImageEmojiはmobylet.xmlでemojiタグが宣言されている場合にtrueとなります。useImageEmoji==trueの場合、絵文字をimgタグに置き換えるという処理が行われます。
このMobyletPrintWriterがメール送信時だけに使われているならば、判定基準を変更すれば問題なさそうですが、実際はレスポンスを書き出す処理にも使われているため、単純に変更するというのでは上手くいきません。どこかで差し替えができるのが理想ですが、そういった作りにはなっていないため、どうしようかと考え中です。

またこれは別件となりますが、mobyletではSMTP認証を行うメールを送信することができません。MobyletMailConfigでmobylet.mail.propertiesを読み込み、その値を基にjavax.mail.Sessionを生成しますが、そのSession生成時にSMTP認証を行う場合に必要なmail.smtp.authというプロパティが指定できないためです。こちらについても無理矢理ですが、MobyletMailInitializer、MobyletMailConfig、MobyletMessage、MobyletMailerの拡張クラスを作ることでSMTP認証を行うメール送信が可能となります。

追記:
メールにimgタグが埋め込まれてしまう件については、mobylet.xmlからemojiタグの宣言を削除するようにして回避しました。
そもそもemojiタグを宣言している理由としては、メールの確認画面で絵文字を表示させるのにEmojiDesigner#getImageEmoji()を呼び出しているためでしたが、このメソッドを真似たコードをユーティリティクラスに定義してそちらを使うように変更しました。

/** 画像が含まれるディレクトリのパスです。 */
private static final String IMG_PATH = "/sastruts/images/";

/** imgタグのプリフィックスです。 */
private static final String PREFIX_IMG = "
/** imgタグのサフィックスです。 */
private static final String SUFIX_IMG = "\" />";

/**
* 指定された絵文字情報をもとにimgタグを生成します。
*
* @param emoji
* 絵文字情報
* @return imgタグ
*/
public static String getImageEmoji(Emoji emoji) {
char[] codes = emoji.getCodes();
if (codes != null && codes.length == 1) {
return PREFIX_IMG + IMG_PATH
+ Integer.toHexString((int) codes[0]).toUpperCase()
+ ".gif" + SUFIX_IMG;
}
return null;
}

これで3キャリアにはそれぞれの絵文字が、それ以外のキャリアには「〓」が埋め込まれたメールが送信できるようになりました。
ただ、画像のパスが宣言されている場合に、メールにimgタグが埋め込まれるという作りは少々違うんじゃないかと思いますがどうでしょうか。

2010/02/16

GAE/j開発環境構築

社内システムをappengine上に構築しようかと考えています。
最近Wicketについて調べているのもその一環なんですが、一度開発環境構築手順をまとめておきます。
正式な手順については公式サイトにドキュメントがありますので、そちらを参照してください。本記事はダイジェスト版です。

1. Java、Eclipseのインストール
Javaをインストールしていない場合はこちらから最新版をダウンロードして、インストールしてください。現在はJDK6 update18が最新版です。JavaEEがバンドルされているものもありますが、SEでOKです。
次にEclipseですが、現在の最新版は3.5となっていますが、GooglePlugin for Eclipseは3.3/3.4しかサポートしていませんのでこちらからEclipse3.4をダウンロードしてください。ダウンロードするのは「Eclipse IDE for Java Developers」でOKです。

2. Eclipseの設定
ダウンロードしたEclipseを展開して起動してください。次にGooglePlugin for Eclipseをインストールします。
Windows版の場合は、[Help]->[Software Updates...]->[Available Software]->[Add site]で表示されたダイアログに以下のURLを入力してください。
http://dl.google.com/eclipse/plugin/3.4

入力が終わったら再起動するかどうか聞かれるので[Yes]をクリックして再起動します。

これでEclipseを使ってappengine用のアプリケーション開発が行えます。

参考:Subversiveプラグインのインストール
最後におまけでSubversiveプラグインのインストール方法です。
[Software Updates...]の[Add site]で以下のURLを入力してください。
http://download.eclipse.org/technology/subversive/0.7/update-site/

Subversive Siteの下にある「Subversive Integration Plug-in's」と[Subversive SVN Team Provider Plugin]->[Subversive SVN Team Provider]にチェックを入れてインストールします。
次に再度[Add site]から以下のURLを入力してください。
http://www.polarion.org/projects/subversive/download/eclipse/2.0/update-site/

[Subversive SVN Connectors]にチェックを入れてインストールします。
Eclipseを再起動すると、Perspectiveに「SVN Repository Exploring」が追加され、CVSと同様にSVNを利用することができます。

2010/02/15

Wicketでページテンプレートを利用

Apache WicketStrutsのTilesのように画面のレイアウトを共通化する方法を調べてみました。
(via Apache Wicket -> Examples -> Markup inheritance

1. BasePage.html
まずベースとなるHTMLを作成します。ここではBasePage.htmlとします。




ここに本文が入ります。




ポイントとなるのは<wicket:child/>です。
このタグで囲った部分が各コンテンツによって差し替えられますので、上記の場合だとヘッダーとフッターはこのままで、ボディの部分がコンテンツによって変化することになります。

2. BasePage.java
次にBasePage.htmlのページクラスを作成します。

/**
* 共通テンプレート用のページクラスです。
*
* @author namiki
*
*/
public abstract class BasePage extends WebPage {
public BasePage() {
}
public BasePage(final PageParameters params) {
}
}

見ての通り特に何もしていません。今後、ヘッダーやフッターに共通の機能を埋め込む場合は必要となりますが、今のところは何もしないままにしておきます。

3. SubPage.html
次にBasePage.htmlにコンテンツを埋め込む側のSubPage.htmlを作成します。




テンプレートを利用したページのサンプルです。






ポイントとなるのは<wicket:extend/>です。
BasePage.htmlの<wicket:child/>で囲まれた部分に、SubPage.htmlの<wicket:extend/>で囲まれた部分が埋め込まれます。ここでは独自のヘッダーやフッターを定義していますが、実際に動作させた場合はBasePage.htmlのヘッダー・フッターが利用されます。

4. SubPage.java
最後にSubPage.htmlのページクラスを作成します。このクラスではBasePage.javaを継承する必要があります。それによってBasePage.htmlとSubPage.htmlの継承関係を表します。

/**
* SubPage.html用のページクラスです。
*
* @author namiki
*
*/
public class SubPage extends BasePage {
public HelloWicketPage(final PageParameters params) {
add(new Label("messages", "Hello, wicket world!!"));
}
}

これで画面レイアウトを共通化することができました。

2010/02/13

車の点検

ライフくんがイグナイター交換後かなり調子を取り戻してきてくれましたが、念には念を入れてディーラーさんで12ヶ月点検を行ってもらいました。

エンジン内部についてはメインリレーとイグニッションスイッチがだいぶ古くなっているので交換、足回りもアウトボードブーツがだいぶ痛んでいるようなので交換した方が良いと勧められたため、再来週にもう一度パーツ交換で入院です。

車体自体はだいぶ安く譲ってもらったものの、なんだかんだでお金が掛かってしまいましたが、まぁこんなものでしょう。これでしばらくは安心して乗れる、はず。

2010/02/09

Wicket on GoogleAppEngine

GoogleAppEngine(以下、GAE)上で動作するWebアプリフレームワークを調べています。Apache Strutsに関しては特に苦労することなく動作させることができたので、次はApache Wicketに挑戦してみます。

1. appengine-web.xml
appengine-web.xmlを編集して、セッションを利用可能にします。appengine-web.xmlに以下の内容を追記します。

true


2. web.xml
web.xmlの内容は以下の通りです。


xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

wicket.yonko
org.apache.wicket.protocol.http.WicketFilter

applicationClassName
net.masa.wicket.YonkoWicketApplication



wicket.yonko
/*




3. WicketApplication
次にWicketApplicationクラスを作成します。GAEでWicketを動作させる場合、以下のポイントがあります。

  • WicketApplication#newSessionStore()でHttpSessionStoreを返すようにさせる

  • WicketをDEPLOYMENTモードで動作させる

  • リソースの更新チェックを停止させる


Wicketのデフォルト設定では、セッションオブジェクトをファイルに保存します。が、GAEではファイルの作成は制限されていますので、HttpSessionに保存するように設定します。
次にWicketのdevelopmentモードではThreadを生成してリソースの更新チェックを行いますが、GAEではThreadの生成を制限しているため、deploymentモードで動作させる必要があります。これにより、Threadの生成を停止できます。
上記のポイントを踏まえ、実際のソースは以下の通りとなります。

/**
* GAE/j用WicketApplicationクラスです。
*
* @author namiki
*
*/
public class YonkoWicketApplication extends WebApplication {

/** リクエスト・レスポンスおよびHTMLのエンコードです。 */
private static final String ENCODE = "UTF-8";

/** HTMLテンプレートファイルのベースフォルダです。 */
private static final String RESOURCE_FOLDER = "WEB-INF";

/** HTMLテンプレートファイルの本来の階層から除去するパスです。 */
private static final String PAGES_PATH = "net/masa/wicket";

/**
* コンストラクタです。
*/
public YonkoWicketApplication() {
}

/*
* (non-Javadoc)
*
* @see org.apache.wicket.Application#getHomePage()
*/
@Override
public Class getHomePage() {
return HelloWicketPage.class;
}

/**
* Wicketの動作モードを設定します。

* WicketをGAE/jで動作させる場合、{@link org.apache.wicket.Application.DEPLOYMENT}
* モードで動作する必要があります。
*
* @see org.apache.wicket.protocol.http.WebApplication#getConfigurationType()
*/
@Override
public String getConfigurationType() {
return Application.DEPLOYMENT;
}

/**
* 初期化処理です。
*
* @see org.apache.wicket.protocol.http.WebApplication#init()
*/
@Override
protected void init() {
super.init();
// リクエスト・レスポンスの文字コード設定
getRequestCycleSettings().setResponseRequestEncoding(ENCODE);
// HTMLテンプレートの文字コード設定
getMarkupSettings().setDefaultMarkupEncoding(ENCODE);
// リソースの更新チェックを停止
getResourceSettings().setResourcePollFrequency(null);

// テンプレートファイルの場所を指定
getResourceSettings().addResourceFolder(RESOURCE_FOLDER);
getResourceSettings().setResourceStreamLocator(
new ResourceStreamLocator() {
@SuppressWarnings("unchecked")
@Override
public IResourceStream locate(Class clazz, String path) {
if (path.indexOf(PAGES_PATH, 0) != -1) {
IResourceStream located = super.locate(clazz, path
.substring(PAGES_PATH.length() + 1));
if (located != null) {
return located;
}
}
return super.locate(clazz, path);
}
});

}

/**
* セッションストアを生成します。

* WicketをGAE/jで動作させる場合、セッション情報を{@link javax.servlet.http.HttpSession}
* に格納する必要があります。
*/
@Override
protected ISessionStore newSessionStore() {
return new HttpSessionStore(this);
}
}

上記init()内でHTMLテンプレートファイルの場所を指定しています。Wicketは通常の場合クラスファイルと同じ階層からHTMLテンプレートファイルを探しますが、RESOURCE_FOLDER(=WEB-INF)から探すように指定しています。また、PAGES_PATH(=net/masa/wicket/)は、パッケージ名から除去すべきパスを定義しています。よって、これらの設定からWicketはHTMLテンプレートファイルをWEB-INF/pagesから探すようになっています。

4. HelloWicketPage
次にWicketApplicationクラスが最初に呼び出すページを作成します。最初に呼び出すページはApplication#getHomePage()で定義しますが、上記の例ではHelloWicketPage.classを呼び出していますのでそのクラスを作成します。パッケージはnet.masa.wicket.pagesです。

/**
* 初回表示用画面クラスです。
*
* @author namiki
*
*/
public class HelloWicketPage extends WebPage {
public HelloWicketPage(final PageParameters params) {
add(new Label("messages", "Hello, wicket world!!"));
}
}

ここではHTMLに表示するメッセージを定義しています。
次にこのメッセージを表示するためのHTMLテンプレートファイルを作成します。
${project}\war\WEB-INF\pagesというディレクトリを作成し、その直下にHelloWicketPage.htmlを作成します。



これで準備が整いましたのでEclipseから起動して、http://localhost:8888にアクセスしてみると、画面にはHelloWicketPage.javaで定義したメッセージが表示されます。

これでGAE上でWicketを動作させることができました。今後はこれをベースにして色々な機能を追加していこうと思います。

2010/02/08

Blogger SyntaxHighlighterの導入

SyntaxHighlighterは、ブログ上のソースコードの文法部分をハイライトしてくれるフリーウェアです。はてなダイアリーの場合は特に問題ありませんでしたが、blogger移転を機に少し調べてみました。

ここではBlogger向けのWigetとして提供されているFaziBear's Blogger Widgetsを使ってみます。
導入方法は、以下のURLにアクセスして、「Add to Blogger」をクリックするだけ。
FaziBear's Blogger Widgets
このブログにも導入してみたので、正常に動作するか以下のJavaコードで確認してみます。

public class HelloMain {
public static void main(String[] args) {
System.out.println("hello, world.");
}
}

どうやら正常に動作しているようですね。対応しているプログラム言語はこちらで公開されていますので、Java以外の言語でも大丈夫なようです。

今回は簡単のためにFaziBear's Blogger Widgetsを導入してみましたが、こちらはSyntaxHighlighterのバージョンが1.5.1と少し古く、最新版がSyntaxHighlighterで公開されているので、近いうちに最新版を導入してみようと思います。

2010/02/07

クルマ

ここ最近トラブル続きで非常に苦しんでいました。
駐車場でエンジンが掛からないのに始まり、コンビニの駐車場でエンジンが掛からなかったり、彼女を送っていったはいいけど、その場でエンジン掛からなかったり、挙句には西湘BPの出口で止まってしまったり。
都合3回ほどレッカーのお世話になってしまいました。

が、それも昨日修理工場の方に見て頂き、やっと原因が判明しました。どうやらイグナイターというパーツがダメになってしまっているとのこと。新品だと結構高価なので、正常動作しているモノを中古から取ってきて交換するみたいです。

自分のライフはJA4というエンジン型なんですが、この型はイグナイターが結構壊れやすいらしいです。どうやらデスビがエンジンに直付けとなっているため、熱ですぐにヤラれてしまうみたい。イグナイターの他にも、イグニッションスイッチやメインリレーなども注意が必要です。

イグナイター交換が終わったら近所のディーラーに持ち込んで一通り点検してもらおうと思います。

追記:
今日、14時過ぎに修理工場の方が来てくれ、早速パーツ交換してくれました。交換したパーツは、デスビ、イグナイター、イグニッションコイルとのこと。工賃込みで20k。まだ試走してないけど、エンジンの始動が安定した気がします。

2010/02/04

WTPでTomcatのJNDIを利用する

環境はEclipse3.4.2、WTP3.0.4、Tomcat5.5.27。プロジェクトはMavenのmaven-archetype-webappで作成しました。DBはSQLServer2005です。
まずはsrc/main/webapp/META-INF配下にcontext.xmlを作成します。context.xmlの内容は以下の通り。



auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory"
driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
url="jdbc:sqlserver://127.0.0.1:1433;databaseName=pj;"
username="sa"
password="pj"
maxActive="20"
maxIdle="10"/>


上記のContext要素の内容は、Servers -> Tomcat v5.5 Server at localhost-config -> server.xmlからコピーしました。

次にsrc/main/webapp/WEB-INF/web.xmlに以下の内容を追加します。


SQLServer2005 DataSource
jdbc/sample
javax.sql.DataSource
Container
Shareable


最後に%CATALINA_HOME%\common\lib配下にJDBCドライバのjarファイルを放り込むと、WTPで起動したTomcatでもJNDIによるDataSourceの取得を行うことができます。

ちなみにWTPでTomcatを設定した場合、以下のディレクトリをTomcat環境と見立ててServerを起動します。

  • workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0

2010/02/03

SQLServerで自動生成キーの取得

SQLServer2005で、IDENTITY値を利用したテーブルに対してInsertを行った場合に、生成されたキーを取得する方法です。いまのプロジェクトではdbutilsを利用しているので、QueryRunnerをオーバーライドして、専用のQueryRunnerを作成しました。

public class GeneratedKeysReturnQueryRunner extends QueryRunner {

@Override
public int update(Connection conn, String sql, Object[] params)
throws SQLException {
PreparedStatement stmt = null;
ResultSet rs = null;
ResultSetMetaData meta = null;
int key = 0;
try {
stmt = this.prepareStatement(conn, sql);
this.fillStatement(stmt, params);
stmt.executeUpdate();
rs = stmt.getGeneratedKeys();
meta = rs.getMetaData();
if (rs.next()) {
key = rs.getInt(meta.getColumnCount());
}
} catch (SQLException e) {
this.rethrow(e, sql, params);
} finally {
close(stmt);
}
return key;
}

@Override
protected PreparedStatement prepareStatement(Connection conn, String sql)
throws SQLException {
return conn.prepareStatement(sql,
PreparedStatement.RETURN_GENERATED_KEYS);
}
}

J2SE1.4以上がサポートしているJDBC 3.0 APIにある、PreparedStatement.RETURN_GENERATED_KEYSを利用するのがポイントです。PostgreSQLやMySQL、Oracleで同じようなことが出来るのかは未検証です。
参考:MSDN 自動生成キーの使用

2010/02/02

FTPの実装

javaでFTPを実装する場合、commons-netを利用することで簡単に実装することができます。

public class FtpUtils {

/** FTPホストのIPアドレスです。 */
private static final String HOST_ADDRESS = "192.168.0.1";

/** FTPユーザー名です。 */
private static final String USER_NAME = "ftpuser";

/** FTPログインパスワードです。 */
private static final String PASSWORD = "password";

/** FTP転送先ディレクトリ名です。 */
private static final String STORE_DIR = "/home/ftpuser/";

/**
* 指定されたファイルを順次FTP転送します。
*
* @param files
* 転送対象ファイル
* @throws IOException
* FTP中に何らかの例外が発生した場合
*/
public static void storeFiles(File[] files) throws IOException {

FTPClient client = new FTPClient();
InputStream in = null;

try {
connection(client);
login(client);

for (File file : files) {
in = new FileInputStream(file);
client.storeFile(STORE_DIR + file.getName(), in);
IOUtils.closeQuietly(in);
}

} finally {
IOUtils.closeQuietly(in);
client.disconnect();
}
}

/**
* FTPコネクションを確立します。
*
* @param client
* クライアント
* @throws IOException
* コネクションに失敗した場合
*/
private static void connection(FTPClient client) throws IOException {
client.connect(HOST_ADDRESS);
if (!FTPReply.isPositiveCompletion(client.getReplyCode())) {
throw new IOException("Connection failed.");
}
}

/**
* FTPログインを行います。
*
* @param client
* クライアント
* @throws IOException
* FTPログインに失敗した場合
*/
private static void login(FTPClient client) throws IOException {
if (!client.login(USER_NAME, PASSWORD)) {
throw new IOException("Login failed.");
}
}
}

上記の場合、FtpUtils#storeFiles()にFTPしたいFileオブジェクトの配列を引き渡すと、そのFileを順次転送します。

2010/02/01

SQLServerで変換デッドロックの回避

デッドロックといえば、2つのテーブル(ここではA、B)に対して、トランザクション1はA→Bの順で更新、トランザクション2はB→Aの順で更新しようとした場合に発生する、というのは良く知られていると思います。MicrosoftのTechNetによると、このデッドロックはサイクルデッドロックと言います。
こちらの「3.3 デッドロックの種類 2.サイクルデッドロック」を参照してください。

一方で1つのテーブルに対して、複数のトランザクションから参照・更新を行う場合もデッドロックが発生します。TechNetによると、こちらは変換デッドロックと言います。
こちらの「3.3 デッドロックの種類 1.変換デッドロック」を参照してください。

今回遭遇したのは変換デッドロックの方で、1つのテーブルに対して、2つのトランザクションから同一のキーで参照・更新を行おうとしてデッドロックが発生しました。今までこのタイプのデッドロックに遭遇したことはありませんでしたが、今回の対応で一応の回避策が分かったのでそれをまとめておきます。

JDBC接続文字列


リファレンス等を読むと、SQLServerを利用する場合のJDBC接続文字列は以下のようにします。

jdbc:sqlserver://192.168.1.1:1433;databaseName=dbnamae;

ただし、このままだとデータの読み出しにカーソルが使えませんので、以下のようにパラメータを追加する必要があります。

jdbc:sqlserver://192.168.1.1:1433;databaseName=dbnamae;selectMethod=cursor

selectMethod~が追加した部分となります。javaでSQLServerを利用する場合には、必ず追加しなければならない重要なパラメータです。

SELECT ~ FOR UPDATE


SQLServerではPostgreSQLなどで使うSELECT ~ FOR UPDATEが利用出来ません。その代わりにロックヒントなるものを追加して、更新ロックを掛けるとSELECT ~ FOR UPDATEと同等の処理を行うことができます。

SELECT
hoge.key
, hoge.value
FROM
hoge WITH(ROWLOCK, UPDLOCK)
WHERE
hoge.key = 1;

FROM句のテーブル名後ろにあるWITH~がロックヒントです。上記では行ロックと更新ロックを掛ける場合のSQL文となっています。ロックヒントで指定できるロックの種類は、こちらを参照してください。

たったこれだけのことなのに、えらく時間が掛かってしまいました。使い慣れていないDBはやっぱり怖いですね。気になるのはOracleやPostgreSQLではこの変換デッドロックと同じ現象が起こるのでしょうか。今までの経験と少し調べてみたところ見つかりませんでしたが。