PHP-Java連携

以前から、PHP5とJavaの連携について色々と調べていたのだが、なかなか情報が見つからず苦労していた。PHP4であれば、以下の様にして連携できる事は確認済み。

LXVI. PHP / Java の連携
extentionディレクトリにある、php_java.dllとphp_java.jarがあれば連携は可能。

しかし、PHP5となると拡張ディレクトリが「extention」から「ext」に変更になっていたり、その配下にphp_java.dllやphp_java.jarがなかったりで、そもそも試す事すらできなかった。

色々とググってみて分かった事だが、PHP Java 連携」で検索するのではなくPHP Java bridge」で検索したほうが、欲しい情報にヒットするようだ。で、以下のページにたどり着く事ができた。

PHP/Java Bridgeバージョン3.1.7リリース

公式サイト:Integrate PHP & Java - PHP / Java Bridgeには繋がらなかったものの、モジュールをダウンロードできるページには繋ぐ事ができたので、そこからphp-java-bridge_3.1.8_j2ee.zipをダウンロード。以下の手順で連携を行った。
※ちなみにPHPはバージョン5.1.6でWindows版。

1. php-java-bridge_3.1.8_j2ee.zipを解凍
2. ディレクトリの中にある、test.batを実行。
3. 実行後作成される、testphp.batを実行。
4. extディレクトリの中にある、JavaBrige.jarとphp_java.dllをPHPをインストールしたディレクトリの中のextディレクトリにコピー。
5. php.iniファイルをエディタで開き、以下の部分をクリップボードにコピー。

extension=php_java.dll

;;;;;;;;;;;;;;;;;;;
; Module Settings ;
;;;;;;;;;;;;;;;;;;;
java.java_home=C:\Program Files\Java\jre1.5.0_06
java.java=C:\Program Files\Java\jre1.5.0_06\bin\javaw.exe
java.log_level=2

6. コピーした情報を実際のPHP環境のphp.iniへ貼り付けて編集。
7. Apacheの再起動。

上記手順を行った事で、phpinfo()の内容にJavaの項目が追加され、以下のようなJavaコードを実行する事ができるようになりました。

<?php
// Java クラス java.lang.System のインスタンスをPHPに作成する
$system = new Java('java.lang.System');
// プロパティへのアクセスのデモ
echo 'Java version=' . $system->getProperty('java.version') . '<br />';
echo 'Java vendor=' . $system->getProperty('java.vendor') . '<br />';
echo 'OS=' . $system->getProperty('os.name') . ' ' .
             $system->getProperty('os.version') . ' on ' .
             $system->getProperty('os.arch') . ' <br />';

// java.util.Dateの例
$formatter = new Java('java.text.SimpleDateFormat',
                      "EEEE, MMMM dd, yyyy 'at' h:mm:ss a zzzz");

echo $formatter->format(new Java('java.util.Date'));
?>

JSFメモ3

Light Weight Javaを見ながら、JSF拡張タグの勉強中。
但し、書籍が若干古い為、拡張タグライブラリが既に存在しないものを指していた。

  誤:<%@ taglib uri="http://myfaces.apache.org/extensions" prefix="x" %>
  正:<%@ taglib uri="http://myfaces.apache.org/tomahawk" prefix="t"%>

拡張ライブラリは「tomahawk」というプロジェクト(?)に統合されたらしいです。

今日試してみたのは以下の2つ。

  • commandSortHeader(クリックによるソート)
  • dataScroller(ページャ)

早速テスツ。

commandSortHeader
Light Weight Javaの97ページを参考に、以下のようなコードを組んでみました。

JSP

<t:dataTable id="tb1" cellspacing="0" cellpadding="0" border="0" 
  var="item" value="#{BookList.list}"
  sortColumn="#{BookList.sort}" sortAscending="#{BookList.ascending}" 
  preserveDataModel="false" preserveSort="true">
  <h:column>
	<f:facet name="header">
	  <t:commandSortHeader columnName="name" arrow="true">
		  <h:outputText value="タイトル"/>
		</t:commandSortHeader>
	</f:facet>
	<h:commandLink><h:outputText id="id1" value="#{item.itemName}"/></h:commandLink>
  </h:column>
  <h:column>
	<f:facet name="header">
	  <t:commandSortHeader columnName="price" arrow="true">
		  <h:outputText value="価格"/>
		</t:commandSortHeader>
	</f:facet>
	<h:outputText id="id2" value="#{item.price}" >
	  <f:convertNumber type="currency" currencySymbol="\\" maxFractionDigits="0"/>
	</h:outputText>
  </h:column>
</t:dataTable>

もともと、マネージドビーン「BookList」には「sort」や「ascending」なんていうフィールドはなかったのですが、何やら必要そうなので新規に追加しました。
じゃあ、肝心の型は何?と思ったものの、Light Weight Javaを読んだだけでは分からずじまい。結局、Myfacesからサンプルを落としてきてソースを追いました。sort=String, ascending=booleanでございます。
さらにさらに、ソースを読んでいるとサンプルのマネージドビーンにはsortなんてメソッドが用意してあり、そこでソート処理をゴリゴリ記述している模様。Light Weight Java読んだ感じだと、拡張タグ使えばサクっとソートまでできるなんて凄い!と思ったのに、結局自前で書くことを知り、酷くがっかりしました。

ちなみに、修正したマネージドビーン(BookList.java)は以下のような感じ。
ソート可能なリストを実装しているという意味で、スーパクラス(SotableList.java)も追加しました。
SotableList.java

public abstract class SortableList {
  private String _sort;

  private boolean _ascending;

  protected SortableList(String defaultSortColumn) {
    _sort = defaultSortColumn;
    _ascending = isDefaultAscending(defaultSortColumn);
  }

  /**
   * Sort the list.
   */
  protected abstract void sort(String column, boolean ascending);

  /**
   * Is the default sort direction for the given column "ascending" ?
   */
  protected abstract boolean isDefaultAscending(String sortColumn);

  public void sort(String sortColumn) {
    if (sortColumn == null) {
      throw new IllegalArgumentException(
          "Argument sortColumn must not be null.");
    }

    if (_sort.equals(sortColumn)) {
      // current sort equals new sortColumn -> reverse sort order
      _ascending = !_ascending;
    } else {
      // sort new column in default direction
      _sort = sortColumn;
      _ascending = isDefaultAscending(_sort);
    }

    sort(_sort, _ascending);
  }

  public String getSort() {
    return _sort;
  }

  public void setSort(String sort) {
    _sort = sort;
  }

  public boolean isAscending() {
    return _ascending;
  }

  public void setAscending(boolean ascending) {
    _ascending = ascending;
  }

}

BookList.java

public class BookList extends SortableList implements Serializable  {
  /**
   * デフォルトコンストラクタ
   */
  public BookList() {
    super(null);
  }

  @Override
  protected void sort(final String column, final boolean ascending) {
    Comparator comparator = new Comparator() {
      public int compare(Object o1, Object o2) {
        Item c1 = (Item) o1;
        Item c2 = (Item) o2;
        if (column == null) {
          return 0;
        }
        if (column.equals("name")) {
          return ascending ? 
              c1.getItemName().compareTo(c2.getItemName()) : 
              c2.getItemName().compareTo(c1.getItemName());
        } else if (column.equals("price")) {
          return ascending ?
              c1.getPrice().compareTo(c2.getPrice()) :
              c2.getPrice().compareTo(c1.getPrice());
        } else
          return 0;
      }
    };
    Collections.sort(this.list, comparator);
  }

  @Override
  protected boolean isDefaultAscending(String sortColumn) {
    return true;
  }

  <途中省略>

  public List getList() {
    this.list = this.bsService.getAllItems();
    sort(getSort(), isAscending());
    return this.list;
  }
}

端的にまとめると、ソート処理をマネージドビーンに追加し、リスト返却時にソートをかますって事ですね。

dataScroller
次はページ送り処理について。ソート処理が自前実装だったのにがっかりしていたので、こっちもそうなんじゃ?って思ってましたが、どうやらそんな事はなさそう。Light Weight Javaの98ページを参考に、以下のようなコードを組みました。

JSP

<t:dataTable id="tb1" cellspacing="0" cellpadding="0" border="0" var="item" value="#{BookList.list}"  preserveDataModel="false"  rows="5">
  <h:column>
	<f:facet name="header">
    <h:outputText value="タイトル"/>
	</f:facet>
	<h:commandLink><h:outputText id="id1" value="#{item.itemName}"/></h:commandLink>
  </h:column>
  <h:column>
	<f:facet name="header">
		<h:outputText value="価格"/>
	</f:facet>
	<h:outputText id="id2" value="#{item.price}" >
	  <f:convertNumber type="currency" currencySymbol="\\" maxFractionDigits="0"/>
	</h:outputText>
  </h:column>
</t:dataTable>
<t:dataScroller id="scroll_1" for="tb1" fastStep="5" pageCountVar="pageCount" pageIndexVar="pageIndex"
     paginator="true" paginatorMaxPages="9" immediate="true">
  <f:facet name="first"><h:graphicImage url="/htdocs/images/arrow-first.gif" /></f:facet>
  <f:facet name="last"><h:graphicImage url="/htdocs/images/arrow-last.gif" /></f:facet>
  <f:facet name="previous"><h:graphicImage url="/htdocs/images/arrow-previous.gif" /></f:facet>
  <f:facet name="next"><h:graphicImage url="/htdocs/images/arrow-next.gif" /></f:facet>
  <f:facet name="fastforward"><h:graphicImage url="/htdocs/images/arrow-ff.gif" /></f:facet>
  <f:facet name="fastrewind"><h:graphicImage url="/htdocs/images/arrow-fr.gif" /></f:facet>
</t:dataScroller>
<t:dataScroller id="scroll_2" for="tb1" pageCountVar="pageCount" pageIndexVar="pageIndex" immediate="true">
  <h:outputFormat value="ページ{0}/{1}">
    <f:param value="#{pageIndex}" />
    <f:param value="#{pageCount}" />
  </h:outputFormat>
</t:dataScroller>

5件ずつでページ送りを行うというもの。これは簡単に実装できました。
しかし、ひとつ問題が。どうやらこのページ送り、動的にテーブルから必要なデータを取ってくるようなものではなく、コレクション(List)の中から該当オブジェクトを引っ張ってくるだけなので、常に全件取得しないといけないという問題がありました。
まあ、サンプルだからいいけどこのままの実装じゃ実運用には使えないな。何か代替案があるんだろうけど・・・


追記:こんなページがあった。内容がまだ良く分からないけれど、どうも特定のデータだけを引っ張ってくるように実装する術について述べているらしい。もうちょっと調べる必要がありそうだ。

JSFメモ2

今回はJSFのValidatorについて。
JSFには3種類のデフォルトValidatorが存在します。

クラス::タグ
DoubleRangeValidator ::validateDoubleRange
LengthValidator ::validateLength
LongRangeValidator ::validateLongRange

文字列の長さをチェックしたい場合などは以下のように記述します。
この場合、価格を入力させて送信ボタンを押すと、入力された価格の長さが「8文字」以上の場合に<h:message>タグの位置にエラーメッセージが表示されます。

<h:form>
    <h:inputText id="price" value="#{Sample.price}"
        <f:validateLength maximum="8"/>
    </h:inputText>
    <h:message for="price" />
    <h:commandButton value="送信" action="action01"/>
</h:form>

しかし、デフォルトでチェック出来る事には限りがある為、自前のValidatorクラスを作る事も可能です。流れとしては、

  1. 自前のValidatorクラスを作成する
  2. faces-config.xmlに作成したvalidatorを追加する
  3. jspで登録したvalidatorを呼び出す

こんな感じ。

それでは、順に追っていきましょう。

1. 自前のValidatorクラスを作成する
Validatorクラスを作るにはValidatorインタフェースを実装すればよさげ。

package org.myapp.validator;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

public class NumberValidator implements Validator {

  /**
   * 数値チェックを行います。
   * @param context
   * @param component
   * @param value
   * @throws ValidatorException
   */
  public void validate(FacesContext context, UIComponent component, Object value) 
   throws ValidatorException {
    String text = value.toString();
    try {
      int num = Integer.parseInt(text);
      if (num == 0) {
        showErrorMessage("0を設定する事はできません。");
      }
    } catch (NumberFormatException ne) {
      showErrorMessage("数値以外を入力する事はできません。");
    }
  }
  
  /**
   * 引数で与えられた文字列をMessageコンポーネントに表示する。
   * @param value - エラーメッセージ
   */
  private void showErrorMessage(String errorMessage){
      FacesMessage message = new FacesMessage(errorMessage);
      message.setSeverity(FacesMessage.SEVERITY_FATAL);
      throw new ValidatorException(message);
  }
}

2. faces-config.xmlに作成したvalidatorを追加する
次に作成したクラスを登録します。

  <validator>
    <validator-id>OrgNumberValidator</validator-id>
    <validator-class>org.myapp.validator.NumberValidator</validator-class>
  </validator>

これは簡単。

3. jspで登録したvalidatorを呼び出す
後は呼び出すだけ。

<h:form>
    <h:inputText id="price" value="#{Sample.price}"
        <f:validator validatorId="OrgNumberValidator"  />
    </h:inputText>
    <h:message for="price" />
    <h:commandButton value="送信" action="action01"/>
</h:form>

さあ、できた。というわけで価格を「0」にして送信!
ちゃんと、「0を設定する事はできません。」というメッセージが表示されました。
次に価格に「一万円」と入力して送信!

"price": 正しい値を入力して下さい.

あれ?意図したメッセージと違う...。デバッグ用のログとかを組み込んでみても、「一万円」と入力した場合は、Validatorクラスも呼ばれていないっぽい。
いろいろ調べてみると、JSFのライフサイクルでは「Validator」の前に「Converter」なるものが走るようです。この「Converter」はフォームに入力されたデータをマーネージドビーン(上記サンプルではSampleという名前)にマッピングする際、型変換を行うというもの。確かにSampleクラスではpriceをintで持っているので、「一万円」(文字列)はintに変換できないな。
で、このエラーメッセージはどこに記述してあるかというと、myfaces.jar(私の場合はmyfaces-impl-1.1.3.jarでした)のjavax/faces/Messages.propertiesの中。で、こいつを上書きする為には自前のリソースバンドルに同じプロパティ名で上書きしてやれば良いようです。

javax/faces/Messages.properties

javax.faces.convert.IntegerConverter.CONVERSION        = 変換エラー
javax.faces.convert.IntegerConverter.CONVERSION_detail = "{0}": 正しい値を入力して下さい.

とあったので、リソースバンドルに以下のように記述。
(※自分の場合は/WEB-INF/classes/org/myapp/resources/ApplicationResources.properties)

javax.faces.convert.IntegerConverter.CONVERSION        = 変換エラー
javax.faces.convert.IntegerConverter.CONVERSION_detail = "{0}": 正しいInteger値を入力して下さい.

これで再度「一万円」と入力し実行!

"price": 正しいInteger値を入力して下さい.

OK。ちゃんと上書きされました。なるほど、型チェックはValidatorを使わずとも勝手にやってくれるのだなー。

JSFメモ

久々の技術ネタ。
Spring, JSF, Hibernateを使ったアプリケーション開発時の注意点をまとめてみる。とは云っても知識ゼロからスタートしているので、自分がつまづいたトコをメモっているだけですが。

では、JSFから。

・メッセージリソースの利用方法
これ小一時間程嵌りました。どのサイトを眺めてても、faces-config.xmlに「message-bundle」を追加すればOKと書いてあるものの、jspで実際にどうやって記述するのかが分からなかった。英語サイトとかまで手を伸ばして漸く解決。だけど合っているのだろうか?実際に書いたコードは以下のようなもの。

・faces-config.xml

  <application>
    <message-bundle>mypackage.resources.ApplicationResources</message-bundle>
    <locale-config>
      <default-locale>ja</default-locale>
    </locale-config>
  </application>

・/WEB-INF/classes/mypackage/resources/ApplicationResources.properties
・/WEB-INF/classes/mypackage/resources/ApplicationResources_ja.properties

def_keyword=Sample Keyword
def_description=JSF Sample Application
def_css=/css/default.css

・/jsp/index.jsp

<f:loadBundle basename="mypackage.resources.ApplicationResources" var="msg"/>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta name="keywords" content="<h:outputText value='#{msg.def_keyword}'/>">
<meta name="description" content="<h:outputText value='#{msg.def_description}'/>">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>JSF Amazon Sample Application</title>

<link rel="stylesheet" type="text/css" href="<h:outputText value='#{msg.def_css}'/>">

** 以下省略 **

faces-config.xmlに羅列したlocaleの分だけpropertiesファイルを用意する必要がある。デフォルトエンコードの場合「_ja.properties」などの地域名は必要ないのだが、今回は一応それも用意しておいた。
一方で、jsp側でメッセージリソースを利用する場合、「f:loadBundle」でリソースを読み込む必要があるらしい。faces-config.xmlに記述してるんだから二度手間のような気がするのは俺だけか?

・タグの書き方
これも自分がハマったのでメモ。
「h:outputText」「f:loadBundel」など、JSFのタグを利用する場合、必ず「f:view」タグで囲んである必要がある。これってJSFの常識なんでしょうが、気づかずに結構悩みました。
画面的にはエラーは出ないんだけど、全く表示が行われません。tomcatの方のログを見ると以下のようなエラーが出ていたので、こんなエラーの場合は「f:view」を疑いましょう。

ERROR [http-8080-Processor24] UIComponentTagUtils.setValueProperty(172) | Component javax.faces.component.UIViewRoot is no ValueHolder, cannot set value.


・includeを使用する場合の注意点
次。includeの注意点。んー、これ厄介。心が折れそうになったw。
jsp内でいつも通り、下記のように記述していたら、表示される順番(表示順)がバラバラだった。いや、バラバラというか、JSFのカスタムタグの記述が先行して表示されてしまっていた。

・header.jsp

<%@ page contentType="text/html;charset=Shift_JIS" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<f:view>
<f:loadBundle basename="mypackage.resources.ApplicationResources" var="msg"/>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
<meta name="keywords" content="<h:outputText value='#{msg.def_keyword}'/>">
<meta name="description" content="<h:outputText value='#{msg.def_description}'/>">
<meta http-equiv="Content-Style-Type" content="text/css">
<meta http-equiv="Content-Script-Type" content="text/javascript">
<title>JSF Sample Application</title>

<link rel="stylesheet" type="text/css" href="<h:outputText value='#{msg.def_css}'/>">
</head>
</f:view>

<body>
<div id="header">
JSF Sample Application
</div>

・index.jsp

<%@ page contentType="text/html;charset=Shift_JIS" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

<%@ include file="parts/header.jsp" %>
<f:view>
<%-- body start --%>
<h:outputText value="ただいまJSFサンプルサイトを構築中です。"/>
<div id="books">
<h:outputText value="#{Books.headTitle}" />
</div>
<%-- body end --%>
</f:view>
<%@ include file="parts/footer.jsp" %>

表示結果を見ると、index.jsp内のカスタムタグ内容が先に出力され、その後<div id="books">が出力されていた。
結論から云うと、「inculdeタグ」「f:view」の中に記述しておかないと表示順がおかしくなってしまうらしい。(※このあたりは、コンパイルされたjavaファイルをきちんと見ていないのでなんともいえないが)
んで、「includeタグ」「f:view」の中に含まれるようにコードを修正。

すると、こんどは以下のようなエラー!

Caused by: javax.servlet.ServletException: javax.servlet.jsp.JspException: Illegal to flush within a custom tag

カスタムタグの中で不正にフラッシュされちゃってるよってか。少し悩んでincludeされた方(この場合は、header.jsp)の「f:view」タグを取っ払ってやったら、正しく表示されました。いやー、長かった。たかがこんだけやるのに4時間近く費やしてしましました。まだまだ勉強する事が多いぞJSF!!

疲れたので今日はここまで。

気を付ける

XML::Atom::Syndicationというモジュールがありまして。
こいつを利用するとAtomフィードを簡単に扱えちゃったりします。

my $atom = XML::Atom::Syndication->instance;
my $element = $atomic->parse($feed);
$elementに対して色々処理できる。

何の気なしに、CPANからこのモジュールをアップデートしたら
プログラムがエラーを吐くようになってしまいました。

Can't locate object method "instance" via package "XML::Atom::Syndication"

instanceメソッドが見つからない?
んなバカなと思って、ファイルを開いて見ると・・・


ない。
てか、なんかコードが大幅に違うんですけど。


元々使ってたバージョンが「0.091」でアップデートしたら「0.901」になっていた。雰囲気似てるし気づかなかった。


ちょろっと見る限り、
XML::Atom::Syndicationがインタフェースのようになってて、実際使うのはXML::Atom::Syndication::Objectなのかな?
んー、でもXMLのparse処理をしてくれそうなものが見つからネス。


取りあえず、見なかった事にして元に戻しておこう・・・

Memo

Firefoxの消費メモリサイズがでかくて困る。
と思ってたらこんな記事。

ただし本家記事でのコメントによると、about:config から「browser.sessionhistory.max_total_viewers」の設定値を0 (タレコミ人Firefox1.5.0.1の初期設定値は50になっていた。この値が -1 の場合はPCのRAMサイズによって動的に設定されるらしい) に設定すると、例えば高解像度のイメージ (4000x6000等の大きすぎる画像は読み込み中にフリーズする可能性があるので注意)でも読み込み後すぐにメモリを解放し、Firefox のメモリ使用サイズが小さくなるそうだ。

Firefoxのメモリ大量消費は『仕様』、だが回避方法あり