FreeMarker(ftl)のデフォルト値演算子と条件分岐の補足

こんにちは!Free(略)


昨日のやつの補足を少々。
FreeMarker(ftl)のデフォルト値演算子と条件分岐 - するめとめがね


変数numが存在するとき、これはエラーを吐きますが

<#if num!0 == 0>
true
</#if>

これはエラーを吐きません。

<#if 0 == num!0>
true
</#if>


ええーじゃあ左辺と右辺逆転すれば済む話じゃーん、みたいに思ったりもしなくもないですが、この後に条件を追加すると結局エラー吐いて死にます。

<#if 0 == num!0 && str?has_content == false>
true
</#if>

この場合、変数numが存在しない場合でも死にます。


というわけで結論は変わらず、デフォルト演算子をifの中で使う場合、デフォルト値ごと括弧でくくってやる必要がある
もしくはくくってやったほうが後々幸せになれるみたいな。そんなかんじ。


以上補足でございました。

FreeMarker(ftl)のデフォルト値演算子と条件分岐

こんにちは!FreeMarker、書いてますか!
僕はあんまり書いてません!
書いてませんけど、書いてる人から質問されて挙動を調べたので覚書です!


もうめんどくさいのでFreeMarkerじゃなくてftlって書きますけど、ftlにはデフォルト値演算子があります。

${str!}
${str!"hoge"}
${num!0}

こういうの。
FreeMarker Manual - Expressions


上みたいな感じで純粋に文字列を表示する分には特に困りません。
むしろ以下のような書き方をしなくて済むので便利です。

<#if str??>${str}<#else>hoge</#if>


ちょっと困るのはこいつをifの中で使う場合です。
たとえばnumという変数が存在しない、または0の場合にtrueをそれ以外はfalseを返す場合を考えます。
上の書き方を踏まえて単純に書くとこんなかんじですね。

<#if num!0 == 0>
true
</#if>


でもこれだと以下のようなエラーを吐きます。

Error on line 2, column 6 in Expecting a boolean (true/false) expression here Expression num!0 == 0 does not evaluate to true/false it is an instance of freemarker.template.SimpleNumber 

お前が指定したやつtrue/falseで評価できねーから!みたいな。かんじの。


あたかもこんなの書いたような感じのエラーです。

<#if 0>

ちゃんと数値比較してほしいので以下のように変更してみます。

<#if (num!0) == 0>
true
</#if>

エラーが出なくなりました!


次に数値じゃなくて文字列でやってみます。

<#if str!"hoge" == "hoge">
true
</#if>


案の定以下のようなエラーを吐きます。

Error on line 2, column 6 in Expecting a boolean (true/false) expression here Expression str!"hoge" == "hoge" does not evaluate to true/false it is an instance of freemarker.template.SimpleScalar 

同じような感じなので同じように直してみます。

<#if (str!"hoge") == "hoge">
true
</#if>

エラーが出なくなりました!


つまり、デフォルト演算子をifの中で使う場合、デフォルト値ごと括弧でくくってやる必要があるわけですね。


beanの中のnumって値に対してやる場合は以下のようにしてやらないといけないわけです。

<#if ((bean.str)!0) == 0>
true
</#if>

めんどい。
ちなみにエラーが出てたやつらですが、値が存在しない場合はエラー吐かないのでたちが悪いです。こわい。


あとデフォルト値演算子使うけどデフォルト値を明示的に指定しないことも出来るんですが、stringの比較の場合はこれが使えて、かつ括弧で囲わなくてもエラーが出なかったりします。

<#if str! == "hoge">
true
</#if>

もちろんこの場合は上の例と違ってstrが存在しないときfalseになりますけど。


余談ですが、じわじわブックマーク数が伸びたり伸びてなかったりする前に書いたエントリ。
FreeMarkerおぼえがき - するめとめがね


この中でデフォルト演算子でbooleanを扱うときは括弧で囲わないと必ず宣言された値になるみたいなことを書いてます。
こんなかんじ。

<#assign hoge = true>
trueを表示
<#if hoge!false == true>
true
</#if>

これもtrueを表示
<#if hoge!false == false>
true
</#if>

これも多分この問題の関係なんじゃないかなーと思ったりしました。
内部的には以下のように解釈されてるんじゃないかなー的な。

こう書いても
<#if hoge!false == true>
true
</#if>
値が存在する場合はこう解釈される
<#if hoge>
true
</#if>


ソース読んでないので想像ですけど!


日本ってftlのドキュメント少ないのでもうちょっと書きたいんですけど、大してネタないんですよね。
また何かあったら書こう。あったら。

taglibにfreemarkerからListを渡す

ほんとは配列を渡したかったんですけどうまくいかなかったのでとりあえず。


とにもかくにもとりあえずtaglibを作ります。
適当にこんな。

public class TestTag extends TagSupport {

	private static final long serialVersionUID = 1L;
	
	private List<String> testList;

	public void setTestList(List<String> testList) {
		this.testList= testList;
	}

	public int doStartTag() throws JspException {
	
		System.out.println(testList.toString());

		return SKIP_BODY;
	}
}


次にftlから呼び出すためにtldファイルを作ります。

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_1.dtd">

<taglib>
	<tlibversion>1.0</tlibversion>
	<jspversion>1.1</jspversion>
	<shortname>Test Tag Libraryy</shortname>

	<uri>http://tm8r.com/test.tld</uri>

	<tag>
		<name>testTag</name>
		<tagclass>com.tm8r.TestTag</tagclass>
		<bodycontent>empty</bodycontent>
		<attribute>
			<name>testList</name>
			<required>true</required>
			<rtexprvalue>true</rtexprvalue>
		</attribute>
	</tag>
</taglib>

jspのバージョンは1.1でも1.2でもよいです。
1.2の場合は若干xmlの要素の名前が違うので気をつけましょう。

最後にftlを作ります。

<#assign test=JspTaglibs["http://tm8r.com/test.tld"]>

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset=utf-8>
<title>test</title>
</head>
<body>
test.
<#assign testArr=["piyo","fuga"]>
<@test.testTag id="hoge" testList=testArr/>
</body>
</html>


できた!


ちなみにこう書くとargument type missmatch的なことを言われます。

<#assign testArr={"piyo","fuga"}>


変数定義しないで直接ぶち込む場合はこんな。

<@test.testTag id="hoge" testList=["piyo","fuga"]/>


気になったのでやってみたけどあんまり使わなさそう…!

複数のJMeterプロセスを立ち上げる

はまったのでめもめも。


JMetercuiで立ち上げるときはbin配下のjmeter.shを使います。
こんなかんじ。

$ sh jmeter.sh -n -t test.jmx 
Created the tree successfully using test.jmx
Starting the test @ Mon Jul 30 13:19:01 JST 2012 (1343621941287)
Waiting for possible shutdown message on port 4445

「-n」はnonguiモードで起動、「-t」はテストプランのファイルを指定するものです。


特に設定を変えていない場合、JMeterは4445ポートを使用し、テストプランが無限ループになっていればshutdownメッセージがこのポートに来るのを待ちます。
終了する場合は同ディレクトリにあるshutdown.shを使用します。

$ sh shutdown.sh
Sending Shutdown request to port 4445


かんたん!
ただJMeterプロセスを複数立ち上げる場合はちょっと勝手が違います。


JMeterの実行自体は同じようにできます。
ただ、2つ目以降のプロセスは異なるポートを使用します。
具体的には2つ目のプロセスは+1したポート、つまり4446が使われるわけです。
shutdown.shは引数なしで実行すると4445ポートのJMeterを終了しようとします。
なので、4445以外のポートを使用しているJMeterプロセスを終了したい場合はポートを指定してやる必要があります。

$ sh shutdown.sh 4446
Sending Shutdown request to port 4446

かん…たん…!
すごい必死にリファレンスで引数調べてたわ。すごい単純にできたわ。


ちなみにJMeterが使用するポート番号はshファイルと同ディレクトリのjmeter.propertiesで変更できます。
デフォルトポートの変更は「jmeterengine.nongui.port」ってやつをいじればおっけーです。
ただ、shutdown.shさんはここの値を変えても4445さんを殺そうとするので、4445以外のポートを使用している場合はポートを教えて上げる必要がありそうです。


また、複数プロセス立ち上げると自動的に+1した値が使用されるという話でしたが、こいつには上限があるみたいです。
こいつも同じ設定ファイルに記述があり、「jmeterengine.nongui.maxport」というやつになりますが、デフォルトは4455になってます。
つまりデフォルトでは4445〜4455までの計10プロセスまではどんどん立ち上げればJMeterが勝手にportを割り振ってくれるわけですね。便利!

JMeterでCSVファイルのランダムな行を抽出する!

JMeterで「userid,entryid」みたいなデータをもとにランダムアクセスしたい!
と思ったんですがCSV Data Set Configは順番にアクセスするだけだし、
CSVRead関数はカラムしか指定できないし…!
かといってランダムに並び替えたCSVファイルを読み込むとかも嫌だし…!


だからBeanShellでやるお!


BeanShellはJavaベースのスクリプト言語です。
ようはJMeterJavaのコード書いてそこで作った変数とか使えちゃう感じです。
参考:BeanShell - Lightweight Scripting for Java


BeanShellアサーションとかListenerとかなんか色々あるんですが、
今回は前処理のところにあるBeanShell PreProcessorを使います。
作ってスレッド実行の度に読み込まれるようなところにぶちこんでおきましょう。


いくつか設定出来る値があるんですが、今回はParametersとScriptをいじります。
まずはParametersに読み込みたいファイルの絶対パスを入力!


次にScriptを書きます!

if (bsh.shared.myDataList == void || bsh.shared.myDataList.size() == 0){
    log.info("initialize myDataList");
    String[] params = Parameters.split(",");
    String filePath = params[0];
    bsh.shared.myDataList = new ArrayList();
    try {
        File file = new File(filePath);
 
        if (!file.exists()) {
            throw new Exception ("ERROR");
        }
 
        BufferedReader bufRdr = new BufferedReader(new FileReader(file));
        String line = null;
 
        while((line = bufRdr.readLine()) != null) {
            bsh.shared.myDataList.add(line);
        }
        bufRdr.close();
    }
    catch (Exception ex) {
        IsSuccess = false;
        log.error(ex.getMessage());
        System.err.println(ex.getMessage());
    }
    catch (Throwable thex) {
        System.err.println(thex.getMessage());
    }
} 
Random rnd = new java.util.Random();
int size = bsh.shared.myDataList.size();
String data = bsh.shared.myDataList.get(rnd.nextInt(size));
String[] columns = data.split(",");
vars.put("userId",columns[0]);
vars.put("entryId",columns[1]);

こんな!かんじで!
これで${userId}とか${entryId}とかやれば他の変数と同じように呼び出せます。すてきね!
この方法ならそもそもCSVにこだわることもありません。


ファイルの各行をリストで持って好きな区切り文字使って配列にしてるだけなので、
スペース区切りでもタブ区切りでも好きなフォーマットのファイルを読み込めます。
まあCVS Data Set Configもフォーマット指定できますが…。


最初データをstaticで持ってたんですが、
どうやら実行の度新しく作られてるっぽく意味ないじゃんしにたい!
と思って調べたところ、bsh.sharedを使えばいけることが分かりました。


JMeterのプロセスが生きてる限り共通で使われる領域みたい。
上のコードでもやってますが、たとえば「myDataList」って変数を作りたい場合、

bsh.shared.myDataList = new ArrayList();

とかやればおっけーです。
既に作成されているかどうかのチェックをしたい場合は

if (bsh.shared.myDataList == void) {
    log.error("まだないよ!");
} else {
    log.error("もうあるよ!");
}

という感じでできます。やったー!


あとおまけですが、パラメータの取得は2つ方法があります。
一つ目は上のコードでもやってる方法。

String[] params = Parameters.split(",");
String filePath = params[0];

Parametersはパラメータ全体を一つのStringとして持ってるので好きな区切り文字で区切って使えます。


もう一つはbsh.argsを使う方法です。

String hoge = bsh.args[0];

みたいな。みたいな。
これだと問答無用でスペース区切りになりますが、
パラメータをそもそもスペース区切りで指定してるならこれで事足ります。


それではみなさますてきなJMeterライフを!

SpringのEL式脆弱性対応

こいつの対応のめも。
CVE-2011-2730 | support.springsource.com


とりあえずSpringのバージョンを確認。3.1系なら問題なさげ。
使ってたのが3.0.5なのでアウト。


Springのtagsの属性のうち、EL式を評価する属性にパラメータなどの指定で値が入ってしまう場合に問題が起きるっぽい。
Cookie抜かれたりとかとか。おおこわいこわい。


各ライブラリの最新バージョンで新しく「springJspExpressionSupport」というContextParameterが利用できるようになっているので、ライブラリのバージョンアップを行ったのち、web.xmlに以下を記述すればよさげ。

<context-param>
	<param-name>springJspExpressionSupport</param-name>
	<param-value>false</param-value>
</context-param>

SpringMVCでjsonとxmlを返すときハマったところめも

大枠は以下を参考に。
Spring 3 MVC の HttpMessageConverter 機能を使って RESTful な Web サービスを作成する

@ResponseBodyでクラスを返すときにnullのフィールドを結果に含めたくない

XMLとjsonの変換にはそれぞれJaxb2Marshallerとjacksonを使いました。
Jaxb2Marshallerの方はフィールドがnullだとデフォルトで結果に含めないみたい。
jacksonの方はクラスに以下を付与してやればいいみたい。

@JsonSerialize(include = Inclusion.NON_NULL)
public class TestResultBean {

べんり!

XMLで返すときのルートのElement名を指定したい

クラスに@XmlRootElementを付与するだけだと…

@XmlRootElement
public class TestResultBean {

結果はこんなかんじ。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><testResultBean><count>0</count><success>true</success></testResultBean>


@XmlRootElementのname属性に明示的にルートのElement名としたいものを指定してやると…

@XmlRootElement(name="response")
public class TestResultBean {

結果は…

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><response><count>0</count><success>true</success></response>

できた!

条件に合わせてレスポンスコードを変更したい

ExceptionHandlerを使って発生したExceptionにあわせてResponseStatus指定して結果を返そうと思ったんですが、どうやら3.1M1からしかExceptionHandlerではResponseBodyが使えないっぽい。
[#SPR-6902] @ResponseBody does not work with @ExceptionHandler - Spring Projects Issue Tracker
使ってるのが3.0.xなのと、仮に使えてもなんかたとえば401返したいときにそのために独自でException作るのもなんかいやんな気がしたので結局実行するメソッドの引数にHttpServletResponseを含めてsetStatusする感じにしました。

スポンサーリンク