2010/04/27

CSVファイルの読込

社内の受託案件で、以前作ったメール送信用のアプリを使いたいという要望があり、隙間の時間で少しずつカスタマイズしてます。その中で送信先のユーザー情報をCSVで一括取込したいというのがあったので、簡単にCSVを任意のBeanのListに変換するようなクラスを作ってみました。この機能はアノテーションを使うのでJava5以上が必要となります。

1. アノテーション
まずはBeanのフィールドに指定するアノテーションです。CSVを読み込んだときに、このアノテーションが宣言されているフィールドに対して、値を設定します。
/**
 * CSVデータの保持対象を表すアノテーションです。
 * このアノテーションはビーンクラスのフィールドに対して宣言します。
 * 
 * @author namiki
 * 
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface CsvColumns {
 /**
  * CSVのカラム順を指定します。
  * 
  * @return カラム順
  */
 int index();
}
indexでCSVの何カラム目を設定するか、指定します。

2. 任意のBean
続いてCSVの内容を設定するBeanです。
public class Entity {
 @CsvColumns(index = 0)
 public Integer id;

 @CsvColumns(index = 2)
 public String name;

 @CsvColumns(index = 1)
 public String email;
}
Seasar2には依存していませんが、基本的にSeasar2環境で使うことを想定しているため、publicフィールドを採用しています。

3. CSVParser
最後にCSVを読み込むためのクラスです。
/**
 * CSVファイルを読み込み、型パラメータで指定されたクラスのリストを作成するクラスです。
 * 
 * @author namiki
 * 
 */
public class CsvFileParser<T> {

    // ------------------------------------------------------------- [Constants]

    /** CSVの区切り文字です。 */
    private static final String DELIMITER = ",";

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

    /** 読込対象ファイルです。 */
    private File target;

    // ----------------------------------------------------------- [Constructor]

    /**
     * コンストラクタです。
     * 
     * @param target
     *            対象ファイル
     */
    public DcssCsvFileParser(File target) {
        this.target = target;
    }

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

    /**
     * CSVファイルを読込み、型パラメータで指定されたクラスのリストを返します。
     * 
     * @param c
     *            型
     * @return リスト
     */
    @SuppressWarnings("unchecked")
    public List<T> readLines(Class<T> c) {

        List<Object> list = new ArrayList<Object>();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(target));
            String line = null;

            while ((line = reader.readLine()) != null) {
                String[] array = line.split(DELIMITER);

                Object entity = c.newInstance();
                Field[] fields = c.getFields();
                for (Field field : fields) {
                    CsvColumns col = field.getAnnotation(CsvColumns.class);
                    if (null != col) {
                        if (field.getType() == Integer.class) {
                            field.set(entity, Integer
                                    .valueOf(array[col.index()]));
                        } else if (field.getType() == Long.class) {
                            field.set(entity, Long.valueOf(array[col.index()]));
                        } else {
                            field.set(entity, array[col.index()]);
                        }
                    }
                }
                list.add(entity);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);

        } finally {
            IOUtils.closeQuietly(reader);
        }
        return (List<T>) list;
    }
}

4. 使い方
最後にこのクラスの使い方です。
CsvFileParser parser = new CsvFileParser(file);
List list = parser.readLines(Entity.class);
GenericsやらClass指定やらが続いて、少し気持ち悪いですね…。ちょっとここら辺は直したいところですが、取り敢えずこんな感じでCSV読み込みを簡略化することができました。
ただあまりCSVの行数が多いとOutOfMemoryになってしまいそうなので注意が必要ですね。そこら辺も合わせてもう少し手を入れていこうと思います。

2010/04/23

またまた本を買う

休みの日にTVを見てたら村上春樹の「1Q84」のBook3が出版されたと大騒ぎになってました。
なんとなく騒がれてたのは知ってましたが、「1Q84」は読んでなかったのでこれを機に購入。残念ながらBook3は売り切れていましたが、Book1、Book2を読み終えた頃には買えるだろうと勝手に予測してます。
取り敢えずBook1は読了してBook2に突入していますが、読んでいるとなんとも不思議な感覚に陥ってしまいますね。思わず月を見上げたり、ソファの下を覗き込んだりしてしまいました。

あと、Amazonから「Seasar2徹底入門」が届きました。相変わらずこの方の文章は読み易く、分かり易いです。SAStrutsに関しては最近関わったプロジェクト群でよく使っていますが、まだまだ奥が深いです。自分自身この本で初めて知ったことなどもありますので、これをベースにまた社内用資料を作成していこうと思っています。

2010/03/19

書籍まとめ買い

やっぱり書籍から学ぶことは多いよなと思うところがあったので、久し振りにまとめ買いしてみました。

買ったのは以下の書籍たち。
  • Java 並行処理プログラミング ―その「基盤」と「最新API」を究める―
  • Effective Java 第2版 (The Java Series)
  • ソースコードリーディングから学ぶ Javaの設計と実装
  • アート・オブ・プロジェクトマネジメント ―マイクロソフトで培われた実践手法
  • 小さなチーム、大きな仕事―37シグナルズ成功の法則 (ハヤカワ新書juice)

まだ読んだことなかったの!?と言われてしまいそうなラインナップですね…orz
今までなんとなくの理解で来てしまっているので、一度きちんと名著と言われるものでしっかり勉強しなおそうと思った次第です。

一通り読み終えたら感想などを記していきたいと思います。

2010/03/18

EclipseでApache Felix

OSGi実装であるApache Felixを触る機会があったので、Eclipse上で動作させる方法をメモしておきます。

1. Apache Felixのダウンロード
公式サイトから downloads->Felix Framework Distributionをダウンロードします。
現時点での最新版は2.0.4。 Windows環境なので、zip版をダウンロードして、任意の場所に展開します。

2. Projectの作成
続いてEclipseでJavaプロジェクトを作成します。
File -> New -> Java Project
Project name: felix-tutorial
として[Next]をクリック。
次のパネルでは
Default output folder: felix-tutorial/target/classes
として[Finish]をクリック。
次にプロジェクトのプロパティでソースフォルダをsrc/main/javaとsrc/main/resourcesに変更します。
ここら辺は後々Mavenプロジェクト化するときのことを考えてのことなので、
Mavenを使わない場合ではデフォルトのままで問題ないです。

3. Felixのコピー

1.でダウンロードしたzipファイルを展開するとfelix-framework-2.0.4というディレクトリが作成されますが、
この配下のbin、bundle、confを2.で作成したプロジェクトの直下にコピーします。
コピーしたら、 bin/felix.jarをビルドパスに追加してください。

4. Felix Shellの起動
次に Eclipse上でFelix Shellを起動します。
Run -> Run ConfigurationsでJava Applicationに起動構成を追加します。
Name: felix-tutorial - run
Project: felix-tutorial
Main class: org.apache.felix.main.Main
※Include system libraries when searching for a main classにチェック
Classpathタブを開いて、User Entriesにfelix.jarが追加されていることを確認してください。
追加されていない場合は[Add JARs]をクリックして追加してください。

ここまでが完了したら、[Apply]をクリック、[Run]をクリックしてください。
Console に「Welcome Felix」とメッセージが表示されれば成功です。
このとき、プロジェクト直下にfelix-cacheというディレクトリが作成されますが、これはFelixが利用するディレクトリです。

5. バンドルの作成
Felixのチュートリアルそのまま。

package net.masa.felix.tutorial1;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;

/**
* @author namiki
*
*/
public class Activator implements BundleActivator, ServiceListener {

@Override
public void start(BundleContext context) throws Exception {
System.out.println("Starting to listen for service events.");
context.addServiceListener(this);
}

@Override
public void stop(BundleContext context) throws Exception {
context.removeServiceListener(this);
System.out.println("Stopped listennig for service events.");

}

@Override
public void serviceChanged(ServiceEvent event) {
String[] obj = (String[]) event.getServiceReference().getProperty(
"objectClass");
if (ServiceEvent.REGISTERED == event.getType()) {
System.out.println("Service of type " + obj[0] + " registered.");
} else if (ServiceEvent.UNREGISTERING == event.getType()) {
System.out.println("Service of type " + obj[0] + " unregistered.");
} else if (ServiceEvent.MODIFIED == event.getType()) {
System.out.println("Service of type " + obj[0] + " modified.");
}
}

}

次にMANIFEST.MFを作成します。
src/main /resources/META-INF/tutorial1の配下にMANIFEST.MFを作成します。内容は以下の通りです。

Manifest-Version: 1.0
Bundle-Name: Service listener example
Bundle-Description: A bundle that displays messages at startup and when service events occur
Bundle-Vendor: Apache Felix
Bundle-Version: 1.0.0
Bundle-Activator: net.masa.felix.tutorial1.Activator
Import-Package: org.osgi.framework

バンドルの実体はjarなので、jarファイルを作成するためにここではantを利用します。
プロジェクト直下にbuildディレクトリを作成し、build.xmlを作成します。内容は以下の通りです。























もうちょっと考えなければいけませんが今は簡単のためにこれで。

6. バンドルのインストール
Felix Shellで以下のコマンドを実行します。
start file:/c:/%workspacepath%/felix-tutorial/target/jar/tutorial1.jar
%workspacepath% には自分のworkspaceのパスを入れてください。
実行してコンソールに「Starting to listen for service events.」というメッセージが表示されれば成功です。
停止・アンインストールについては、Felix Shellでpsコマンドを実行してIDを確認し、
stop %ID%
uninstall %ID%
で行うことができます。 Felix Shellを停止する場合hはshutdownコマンドを実行します。

追記:
build.xmlの表示がおかしいですね。
「Blogger SyntaxHighlighter」が悪さをしてそうなんですが、
回避方法が不明なのでそのままにしておきます。
本来は、property要素は1行で記述していますし、target要素も適宜終了させています。

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を利用することができます。