メッセージ管理

プログラムの中ではいろいろメッセージを使用することがある。
ほとんどの場合、メッセージの文言はソース埋め込みではなく、何かしら外部化されているものだが、そのやり方はプロジェクトによって様々だ。


また、自分でちょっとしたライブラリを作っていてもメッセージが必要になることがある。
そこで問題になるのが、そのライブラリをどうやってプロジェクトに組み込むかだ。


そのプロジェクトの流儀に従ってメッセージ文字列を取得してもいいのだが、それではそのライブラリがプロジェクト専用になってしまう。
ライブラリの本質はまったくプロジェクトに依存していないのに、メッセージのためだけに依存してしまうのは気に入らない。


そこで思い立ったのが、メッセージリソースとプログラムを仲介するクラス。
作り始めたばかりで実用になるかどうかは疑問だが・・・

/**
 * メッセージを提供するインターフェイス
 */
public interface MessageProvider {

  /**
   * 指定されたidのメッセージを返す。<br/>
   * idに対応するメッセージがない場合の挙動は実装次第。
   * @param id
   * @return
   */
  String getMessage(String id);

}
import java.util.HashMap;

/**
 * HashMapでメッセージを管理するMessageProvider実装
 */
public class SimpleMessageProvider extends HashMap<String, String> implements MessageProvider {

  /**
   * 指定されたidのメッセージを返す。<br/>
   * idに対応するメッセージが無ければnullを返す。
   * 
   * @see MessageProvider#getMessage(String)
   */
  @Override
  public String getMessage(String id) {
    return get(id);
  }

}
/**
 * プロパティ形式でメッセージを管理するMessageProvider実装。
 */
public class PropertyMessageProvider extends Properties implements MessageProvider {

  /**
   * 指定されたidのメッセージを返す。<br/>
   * idに対応するメッセージが無ければnullを返す。
   * 
   * @see MessageProvider#getMessage(String)
   */
  @Override
  public String getMessage(String id) {
    return getProperty(id);
  }

}
/**
 * データベースでメッセージ一覧を管理するMessageProvider実装
 */
public class DBMessageProvider implements MessageProvider {

  /**
   * メッセージの取得に使用するDB接続のファクトリ。<br/>
   * {@link javax.sql.DataSource}だとあまりお手軽に実装できないからね・・・
   */
  public static interface ConnectionFactory {
    /**
     * メッセージの取得に使用するDB接続を返す
     * @return
     * @throws SQLException
     */
    Connection getConnection() throws SQLException;
  }

  private ConnectionFactory factory;
  private String sql;
  private boolean initialized;
  private Map<String, String> messages = new HashMap<String, String>();

  /**
   * DB接続のファクトリとメッセージ一覧を取得するためのsql文を指定する。<br/>
   * 1つ目のカラムをメッセージID。2つ目のカラムをメッセージ本文として使用する。
   * @param factory
   * @param sql
   */
  public DBMessageProvider(ConnectionFactory factory, String sql) {
    this.factory = factory;
    this.sql = sql;
  }

  /**
   * 指定されたidのメッセージを返す。<br/>
   * idに対応するメッセージが無ければnullを返す。
   * 
   * @see MessageProvider#getMessage(String)
   */
  @Override
  public String getMessage(String id) {
    if (!initialized) {
      initialize();
    }
    return messages.get(id);
  }

  public void initialize() {
    // エラーが起きた時に初期化フラグを設定してないと繰り返しエラーが起きることになるので、エラーに関係なく初期化済みとする
    initialized = true;
    try {
      Connection con = factory.getConnection();
      try {
        Statement stmt = con.createStatement();
        try {
          ResultSet rs = stmt.executeQuery(sql);
          try {
            if (rs.getMetaData().getColumnCount() < 2) {
              throw new RuntimeException("invalid sql");
            }
            while (rs.next()) {
              messages.put(rs.getString(1), rs.getString(2));
            }
          } finally {
            rs.close();
          }
        } finally {
          stmt.close();
        }
      } finally {
        con.close();
      }
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }

}
import java.io.InputStream;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * xmlファイルでメッセージを管理するMessageProvider実装。<br/>
 * &lt;messages&gt;<br/>
 *   &lt;message id="msg1"&gt;メッセージ1&lt;/message&gt;<br/>
 *   &lt;message id="msg2"&gt;メッセージ2&lt;/message&gt;<br/>
 * &lt;/messages&gt;<br/>
 * のような形式を想定。<br/>
 * タグ名やidの属性名は変更可能。
 */
public class XmlMessageProvider implements MessageProvider {

  private InputSource source;
  private String messageTagPath;
  private String idName;
  private boolean initialized;
  private Map<String, String> messages = new HashMap<String, String>();

  /**
   * ストリームからxmlを読み込む。<br/>
   * idの属性名は'id'固定。
   * @param stream
   * @param messageTagPath メッセージタグのxpath表記
   */
  public XmlMessageProvider(InputStream stream, String messageTagPath) {
    this(new InputSource(stream), messageTagPath);
  }

  /**
   * リーダーからxmlを読み込む。<br/>
   * idの属性名は'id'固定。
   * @param reader
   * @param messageTagPath メッセージタグのxpath表記
   */
  public XmlMessageProvider(Reader reader, String messageTagPath) {
    this(new InputSource(reader), messageTagPath);
  }

  /**
   * ソースからxmlを読み込む。<br/>
   * idの属性名は'id'固定。
   * @param source
   * @param messageTagPath メッセージタグのxpath表記
   */
  public XmlMessageProvider(InputSource source, String messageTagPath) {
    this(source, messageTagPath, "id");
  }

  /**
   * ソースからxmlを読み込む
   * @param source
   * @param messageTagPath メッセージタグのxpath表記
   * @param idName id属性名
   */
  public XmlMessageProvider(InputSource source, String messageTagPath, String idName) {
    this.source = source;
    this.messageTagPath = messageTagPath;
    this.idName = idName;
  }

  /**
   * 指定されたidのメッセージを返す。<br/>
   * idに対応するメッセージが無ければnullを返す。
   * 
   * @see MessageProvider#getMessage(String)
   */
  @Override
  public String getMessage(String id) {
    if (!initialized) {
      initialize();
    }
    return messages.get(id);
  }

  private void initialize() {
    // エラーが起きた時に初期化フラグを設定してないと繰り返しエラーが起きることになるので、エラーに関係なく初期化済みとする
    initialized = true;
    try {
      DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
      Document document = builder.parse(source);
      XPathFactory xpathFactory = XPathFactory.newInstance();
      XPath xpath = xpathFactory.newXPath();
      NodeList nodeList = (NodeList) xpath.evaluate(messageTagPath, document.getDocumentElement(), XPathConstants.NODESET);
      for (int i = 0; i < nodeList.getLength(); i++) {
        Element e = (Element) nodeList.item(i);
        messages.put(e.getAttribute(idName), e.getFirstChild().getTextContent());
      }
    } catch (ParserConfigurationException e) {
      throw new InternalError(e.getMessage());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

}