初めまして。ルート42株式会社の高橋と申します。
弊社では自社プロダクトの一つとしてフルスタックフレームワークの構築をしています。
その過程で得られたノウハウを、備忘録も兼ねてブログで公開してみることにしました。
不定期の更新になると思いますが、なるべく更新するように頑張るかもしれませんし、挫折するかもしれません(-_-)
ちなみにさりげなく奥ゆかしく宣伝ですが、弊社のHPは http://root42.jp/ です。
今回のお題
弊社のフレームワーク(R42FWといいます)はStruts2をベースにしており、
各種の値を自動的にレンダリングするJSP部品をたくさん用意しています。
例えば、java.util.Dateの値を和暦にフォーマットして表示する、というようなものです。まあ、ありがちですね。
さて、R42FWの開発を始めてから今日まで、struts2の
最近いい加減に我慢できなくなってきた問題があります。それは、
というものです。
問題のあるコードの例
foo.jsp
<%@ page pageEncoding="UTF-8"%> <%@ taglib uri="/struts-tags" prefix="s"%> <s:set var="hoge" value="1" /> hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される <s:include value="./bar.jsp" /> hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される
bar.jsp
<%@ page pageEncoding="UTF-8"%> <%@ taglib uri="/struts-tags" prefix="s"%> <s:set var="hoge" value="2" /> hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される
この通り、呼び出し先のJSPで値が勝手に書き変わってしまいます。
これは、
フレームワークのように、高度に部品化したJSPを多数提供しているケースにおいては、3段階、4段階にネストした
なので、このまま強引に作業を進めていくと、その先に待っているのはグローバル変数だらけのおぞましい地獄…(笑)てな具合になります。
そこで、ローカル変数のスコープを宣言できるカスタムタグ(Javaでいうところの、ブロック構文のようなコーディングができる)を作ることにしました。
今回作成したタグは次の2つです。
作ったタグの使い方
foo.jsp
<%@ page pageEncoding="UTF-8"%> <%@ taglib uri="/struts-tags" prefix="s"%> <%@ taglib uri="(略)" prefix="r"%> <r:block> <r:set-local var="hoge" value="1" /> hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される <s:include value="./bar.jsp" /> hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される </r:block> hoge is <s:property value="(#hoge == null ? 'null' : 'not null')" /> ← "hoge is null"が表示される
bar.jsp
<%@ page pageEncoding="UTF-8"%> <%@ taglib uri="/struts-tags" prefix="s"%> <%@ taglib uri="(略)" prefix="r"%> <r:block> <r:set-local var="hoge" value="2" /> hoge is <s:property value="#hoge" /> ← "hoge is 2"が表示される </r:block> hoge is <s:property value="#hoge" /> ← "hoge is 1"が表示される
ソースコード
コメントをちゃんと書いてなくて恐縮ですが、ソースは以下の通りです。
ALV2にしますので、商用利用もどうぞ。お約束ですが無保証ですのであしからず。
tldファイル
<tag> <description><![CDATA[ ローカル変数を宣言する。<r:block>と対で使用する。 ]]></description> <name>set-local</name> <tag-class>jp.root42.r42fw.export.web.ui.jsptags.SetLocalTag</tag-class> <body-content>JSP</body-content> <attribute> <description></description> <name>value</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <attribute> <description></description> <name>var</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <dynamic-attributes>false</dynamic-attributes> </tag> <tag> <description><![CDATA[ ローカル変数のスコープを宣言する。<r:set-local>と対で使用する。 ]]></description> <name>block</name> <tag-class>jp.root42.r42fw.export.web.ui.jsptags.BlockTag</tag-class> <body-content>JSP</body-content> <dynamic-attributes>false</dynamic-attributes> </tag>
Block.java
package jp.root42.r42fw.export.web.ui.components; import java.io.Writer; import java.util.HashMap; import java.util.Map; import com.opensymphony.xwork2.util.ValueStack; import org.apache.commons.lang.StringUtils; import org.apache.struts2.components.Component; public class Block extends Component { private final Map<String, Object> overridedVars = new HashMap<String, Object>(); private final Map<String, Object> overridedAttrVars = new HashMap<String, Object>(); public Block(final ValueStack stack) { super(stack); } /** {@inheritDoc} */ @Override public boolean end(final Writer writer, final String body) { final boolean result = super.end(writer, body); // ---- // 値を復元する その1 for (final String key : this.overridedAttrVars.keySet()) { final Object value = this.overridedAttrVars.get(key); this.setAttrValue(key, value); } // ---- // 値を復元する その2 for (final String key : this.overridedVars.keySet()) { final Object value = this.overridedVars.get(key); this.setValue(key, value); } return result; } public void setVar(final String key, final Object value) { if (StringUtils.isBlank(key)) { return; } // ---- // 現在のブロックにおける値の状態を記録しておく その1 if (!this.overridedAttrVars.containsKey(key)) { final Object attrVar = this.getAttrValue(key); this.overridedAttrVars.put(key, attrVar); } this.setAttrValue(key, value); // ---- // 現在のブロックにおける値の状態を記録しておく その2 if (!this.overridedVars.containsKey(key)) { final Object var = this.getValue(key); this.overridedVars.put(key, var); } this.setValue(key, value); } protected Object getAttrValue(final String key) { return this.getStack().findValue("#attr['" + key + "']"); } protected void setAttrValue(final String key, final Object value) { this.getStack().setValue("#attr['" + key + "']", value, true); } protected Object getValue(final String key) { return this.getStack().findValue("#" + key); } protected void setValue(final String key, final Object value) { this.getStack().setValue("#" + key, value, true); } }
BlockTag.java
package jp.root42.r42fw.export.web.ui.jsptags; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.opensymphony.xwork2.util.ValueStack; import org.apache.struts2.views.jsp.ComponentTagSupport; import jp.root42.r42fw.export.web.ui.components.Block; public class BlockTag extends ComponentTagSupport { /** {@inheritDoc} */ @Override public Block getBean(final ValueStack stack, final HttpServletRequest req, final HttpServletResponse res) { return new Block(stack); } }
SetLocal.java
package jp.root42.r42fw.export.web.ui.components; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.opensymphony.xwork2.util.ValueStack; import org.apache.commons.lang.StringUtils; import org.apache.struts2.components.Component; import org.apache.struts2.components.ContextBean; public class SetLocal extends ContextBean { private String value; public SetLocal(final ValueStack stack) { super(stack); } /** {@inheritDoc} */ @Override public boolean end(final Writer writer, final String body) { final Object o; if (this.value == null) { if (StringUtils.isEmpty(body)) { o = this.findValue("top"); } else { o = body; } } else { o = this.findValue(this.value); } // ---- final Block block = this.getEnclosingBlock(); block.setVar(this.getVar(), o); return super.end(writer, ""); } /** * 現在のset-localタグを囲っているblockタグを取得する。 * * @return Block */ private Block getEnclosingBlock() { @SuppressWarnings("unchecked") final List<Component> componentStack = new ArrayList<Component>(this.getComponentStack()); Collections.reverse(componentStack); // スタックを上から順に辿っていき、最初に見つかったBlockを使う Block block = null; for (final Component comp : componentStack) { if (comp instanceof Block) { block = (Block) comp; break; } } if (block == null) { throw new IllegalStateException("block not found: " + componentStack); } else { return block; } } public void setValue(final String value) { this.value = value; } /** {@inheritDoc} */ @Override public boolean usesBody() { return true; } }
SetLocalTag.java
package jp.root42.r42fw.export.web.ui.jsptags; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.opensymphony.xwork2.util.ValueStack; import org.apache.struts2.views.jsp.ContextBeanTag; import jp.root42.r42fw.export.web.ui.components.SetLocal; public class SetLocalTag extends ContextBeanTag { /***/ private String value; /** {@inheritDoc} */ @Override public SetLocal getBean(final ValueStack stack, final HttpServletRequest req, final HttpServletResponse res) { return new SetLocal(stack); } /** {@inheritDoc} */ @Override protected void populateParams() { super.populateParams(); final SetLocal setLocal = (SetLocal) this.getComponent(); setLocal.setValue(this.value); } public void setValue(final String value) { this.value = value; } }