/*
* Copyright (c) 2007 NTT DATA Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jp.terasoluna.fw.beans;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import jp.terasoluna.fw.beans.jxpath.BeanPointerFactoryEx;
import jp.terasoluna.fw.beans.jxpath.DynamicPointerFactoryEx;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* JXPathを用いたIndexBeanWrapperの実装。
* <p>
* JavaBean、Map、DynaBeanからプロパティ名を指定することにより、 属性値を取得することができる。 属性が配列・List型の場合、該当する属性値を全て取得する。
* <h5>取得できる属性の型</h5>
* <ul>
* <li>プリミティブ型</li>
* <li>プリミティブ型の配列</li>
* <li>JavaBean</li>
* <li>JavaBeanの配列・List型</li>
* <li>Map型</li>
* </ul>
* </p>
* <p>
* Mapオブジェクト、またはMap型属性を使用する場合、 以下の文字はMapキーに使用できない。
* <ul>
* <li>/ …スラッシュ</li>
* <li>[ …角かっこ(開く)</li>
* <li>] …角かっこ(閉じ)</li>
* <li>. …ドット</li>
* <li>' …シングルクォート</li>
* <li>" …ダブルクォート</li>
* <li>( …かっこ(開く)</li>
* <li>) …かっこ(閉じ)</li>
* </ul>
* </p>
* <hr>
* <h4>簡単な使用例</h4>
* <p>
* 以下のようなEmployeeオブジェクトのfirstName属性にアクセスする。
*
* <pre>
* public class Employee {
* private String firstName;
*
* public void setFirstName(String firstName) {
* this.firstName = firstName;
* }
*
* public String getFirstName() {
* return firstName;
* }
* }
* </pre>
* </p>
* <p>
* <u>1.コンストラクタでアクセス対象のJavaBeanをラップする。</u>
*
* <pre>
* // アクセス対象となるEmployeeオブジェクト
* Employee emp = new Employee();
* emp.setFirstName("めい");
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(emp);
* </pre>
* </p>
* <p>
* <u>2.firstName属性にアクセスする。</u> 引数のStringには属性名を指定する。
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>firstName</strong>");
* </pre>
*
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* firstName:めい
* </pre>
* </p>
* <hr>
* <h4>配列属性へのアクセス</h4>
* <p>
* 以下のようなAddressオブジェクトの配列型属性numbersにアクセスする。
*
* <pre>
* public class Address {
* private int[] numbers;
*
* public void setNumbers(int[] numbers) {
* this.numbers = numbers;
* }
*
* public int[] getNumbers() {
* return numbers;
* }
* }
* </pre>
* </p>
* <p>
* <u>1.コンストラクタでアクセス対象のJavaBeanをラップする。</u>
*
* <pre>
* // Employeeの属性となるAddressオブジェクト
* Address address = new Address();
* address.setNumbers(new int[] { 1, 2, 3 });
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(address);
* </pre>
* </p>
* <p>
* <u>2.numbers属性にアクセスする。</u> <em>'numbers[]'のように配列記号を付ける必要はなく、
* 属性名を指定すればよいことに注意すること。</em>
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>numbers</strong>");
* </pre>
* </p>
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* numbers[0]:1
* numbers[1]:2
* numbers[2]:3
* </pre>
*
* List型のオブジェクトに対しても、同様の方法で値が取得できる。
* </p>
* <hr>
* <h4>ネストした属性へのアクセス</h4>
* <p>
* 下記のようなEmployeeオブジェクトから、 ネストされたAddressクラスのstreetNumber属性にアクセスする。
*
* <pre>
* public class Employee {
* private Address homeAddress;
*
* public void setHomeAddress(Address homeAddress) {
* this.homeAddress = homeAddress;
* }
*
* public Address getHomeAddress() {
* return homeAddress;
* }
* }
*
* public class Address {
* private String streetNumber;
*
* public void setStreetNumber(String streetNumber) {
* this.streetNumber = streetNumber;
* }
*
* public String getStreetNumber() {
* return streetNumber;
* }
* }
* </pre>
* </p>
* <p>
* <u>1.コンストラクタでアクセス対象のJavaBeanをラップする。</u>
*
* <pre>
* // Employeeの属性となるAddressオブジェクト
* Address address = new Address();
* address.setStreetNumber("住所");
*
* // Employee
* Employee emp = new Employee();
* emp.setHomeAddress(address);
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(emp);
* </pre>
* </p>
* <p>
* <u>2.EmployeeオブジェクトのhomeAddress属性にネストされた、 streetNumber属性にアクセスする。</u> ネストした属性を指定する場合、以下のコードのように'.'(ドット)で 属性名を連結する。
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>homeAddress.streetNumber</strong>");
* </pre>
* </p>
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* homeAddress.streetNumber:住所
* </pre>
*
* ネストした属性が配列・List型であっても、値を取得することができる。
* </p>
* <hr>
* <h4>Map型属性へのアクセス</h4>
* <p>
* 下記のようなEmployeeオブジェクトのMap属性addressMapにアクセスする。
*
* <pre>
* public class Employee {
* private Map addressMap;
*
* public void setAddressMap(Map addressMap) {
* this.addressMap = addressMap;
* }
*
* public Map getAddressMap() {
* return addressMap;
* }
* }
* </pre>
* </p>
* <p>
* <u>1.コンストラクタでアクセス対象のJavaBeanをラップする。</u>
*
* <pre>
* // Employeeの属性となるMap
* Map addressMap = new HashMap();
* addressMap.put("home", "address1");
*
* // Employee
* Employee emp = new Employee();
* emp.setAddressMap(addressMap);
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(emp);
* </pre>
* </p>
* <p>
* <u>2.EmployeeのaddressMap属性中にセットしたhomeキーにアクセスする。</u> Map型属性のキーを指定する場合、以下のコードのようにかっこでキー名を連結する。
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>addressMap(home)</strong>");
* </pre>
* </p>
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* addressMap(home):address1
* </pre>
*
* Map型属性のキー名は()(括弧)で囲われることに注意すること。
* </p>
* <hr>
* <h4>Mapオブジェクトへのアクセス</h4>
* <p>
* 本クラスはJavaBeanだけではなく、Mapオブジェクトへのアクセスが可能である。
* <p>
* <u>1.コンストラクタでアクセス対象のMapをラップする。</u>
*
* <pre>
* // Employeeの属性となるMap
* Map addressMap = new HashMap();
* addressMap.put("home", "address1");
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(addressMap);
* </pre>
* </p>
* <p>
* <u>2.addressMapにセットしたhomeキーにアクセスする。</u>
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>home</strong>");
* </pre>
* </p>
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* home:address1
* </pre>
*
* Mapオブジェクトに対しても、配列・List型属性、 ネストした属性の取得が可能である。
* </p>
* <hr>
* <h4>DynaBeanへのアクセス</h4>
* <p>
* 本クラスはJavaBeanだけではなく、DynaBeanへのアクセスが可能である。
* <p>
* <u>1.コンストラクタでアクセス対象のDynaBeanをラップする。</u>
*
* <pre>
* // DynaBeanにラップされるJavaBean
* Address address = new Address();
* address.setStreetNumber("住所");
*
* // DynaBean
* DynaBean dynaBean = new WrapDynaBean(address);
*
* IndexedBeanWrapper bw = new JXPathIndexedBeanWrapperImpl(dynaBean);
*
* --------------------------------------------------------
* 上記のコードで使用しているAddressオブジェクトは以下のようなクラスである。
*
* public class Address {
* private String streetNumber;
*
* public void setStreetNumber(String streetNumber) {
* this.streetNumber = streetNumber;
* }
* public String getStreetNumber() {
* return streetNumber;
* }
* }
* </pre>
* </p>
* <p>
* <u>2.DynaBeanのstreetNumber属性にアクセスする。</u>
*
* <pre>
* Map<String, Object> map = bw.getIndexedPropertyValues(
* "<strong>streetNumber</strong>");
* </pre>
* </p>
* キーがプロパティ名、値が属性値のMapインスタンスが返される。 以下のコードでは全ての要素をコンソールに出力している。
*
* <pre>
* System.out.println("Mapのキー:Mapの値");
* System.out.println("========================");
* Set<String> keyset = map.keySet();
* for (String key : keyset) {
* System.out.print(key + ":");
* System.out.println(map.get(key).toString());
* }
* </pre>
*
* 結果は以下のように出力される。
*
* <pre>
* Mapのキー:Mapの値
* ========================
* streetNumber:住所
* </pre>
* </p>
*/
public class JXPathIndexedBeanWrapperImpl implements IndexedBeanWrapper {
/**
* ログクラス。
*/
private static Log log = LogFactory.getLog(
JXPathIndexedBeanWrapperImpl.class);
/**
* JXPathコンテキスト。
*/
protected JXPathContext context = null;
/**
* 初期化処理。
* <p>
* 拡張したNodePointerファクトリを追加する。 NodePointerファクトリはstaticメソッドで、一度だけ呼び出す。 実行中にNodePointerファクトリ追加を行なうと、
* マルチスレッド環境にてNullPointerExceptionが発生する可能性がある。
* </p>
*/
static {
JXPathContextReferenceImpl.addNodePointerFactory(
new BeanPointerFactoryEx());
JXPathContextReferenceImpl.addNodePointerFactory(
new DynamicPointerFactoryEx());
}
/**
* コンストラクタ。
* @param target 対象のオブジェクト
*/
public JXPathIndexedBeanWrapperImpl(Object target) {
// ターゲットとなるJavaBeanがNullの場合は例外
if (target == null) {
log.error("TargetBean is null!");
throw new IllegalArgumentException("TargetBean is null!");
}
context = JXPathContext.newContext(target);
}
/**
* 指定したプロパティ名に一致する属性値を返す。
* @param propertyName プロパティ名
* @return プロパティ名に一致する属性値を格納するMap(位置情報、属性値)
*/
@Override
public Map<String, Object> getIndexedPropertyValues(String propertyName) {
// プロパティ名がNull・空文字
if (StringUtils.isEmpty(propertyName)) {
String message = "PropertyName is empty!";
log.error(message);
throw new IllegalArgumentException(message);
}
// プロパティ名に不正な文字
if (StringUtils.indexOfAny(propertyName, new char[] { '/', '"',
'\'' }) != -1) {
String message = "Invalid character has found within property name."
+ " '" + propertyName + "' " + "Cannot use [ / \" ' ]";
log.error(message);
throw new IllegalArgumentException(message);
}
// 配列の[]以外に[]が使われている
String stringIndex = extractIndex(propertyName);
if (stringIndex.length() > 0) {
try {
Integer.parseInt(stringIndex);
} catch (NumberFormatException e) {
String message = "Invalid character has found within property name."
+ " '" + propertyName + "' " + "Cannot use [ [] ]";
log.error(message);
throw new IllegalArgumentException(message);
}
}
Map<String, Object> result = new LinkedHashMap<String, Object>();
String requestXpath = toXPath(propertyName);
// JXPathからプロパティ取得
Iterator<?> ite = null;
try {
ite = context.iteratePointers(requestXpath);
} catch (JXPathException e) {
// プロパティ名が不正
String message = "Invalid property name. " + "PropertyName: '"
+ propertyName + "'" + "XPath: '" + requestXpath + "'";
log.error(message, e);
throw new IllegalArgumentException(message, e);
}
// XPath ⇒ Java property
while (ite.hasNext()) {
Pointer p = (Pointer) ite.next();
result.put(this.toPropertyName(p.asPath()), p.getValue());
}
return result;
}
/**
* プロパティ形式の文字列をXPath形式の文字列に変換する。
* @param propertyName プロパティ形式文字列
* @return XPath形式文字列
*/
protected String toXPath(String propertyName) {
StringBuilder builder = new StringBuilder("/");
String[] properties = StringUtils.split(propertyName, '.');
if (properties == null || properties.length == 0) {
String message = "Property name is null or blank.";
log.error(message);
throw new IllegalArgumentException(message);
}
for (String property : properties) {
// ネスト
if (builder.length() > 1) {
builder.append('/');
}
// Map属性
if (isMapProperty(property)) {
builder.append(escapeMapProperty(property));
// JavaBean または Primitive
} else {
builder.append(extractAttributeName(property));
}
// 配列インデックス
builder.append(extractIncrementIndex(property));
}
return builder.toString();
}
/**
* インクリメントされた添え字を取り出す。
* @param property Javaプロパティ名。
* @return String XPath形式の添え字。
*/
protected String extractIncrementIndex(String property) {
return extractIncrementIndex(property, 1);
}
/**
* インクリメントされた添え字を取り出す。
* @param property プロパティ名。
* @param increment インクリメントする値。
* @return String インクリメントされた添え字。
*/
protected String extractIncrementIndex(String property, int increment) {
String stringIndex = extractIndex(property);
if ("".equals(stringIndex)) {
return "";
}
// 添え字が取得できた場合、インクリメントする
try {
int index = Integer.parseInt(stringIndex);
return new StringBuilder().append('[').append(index + increment)
.append(']').toString();
} catch (NumberFormatException e) {
// 配列の[]ではない
return "";
}
}
/**
* 配列インデックスを取得する。
* @param property プロパティ名。
* @return 配列インデックス。
*/
protected String extractIndex(String property) {
int start = property.lastIndexOf('[');
int end = property.lastIndexOf(']');
// []がないので配列ではない
if (start == -1 && end == -1) {
return "";
}
// ']aaa[' のように[]の位置が不正、または[]のどちらかしかない
if (start == -1 || end == -1 || start > end) {
String message = "Cannot get Index. " + "Invalid property name. '"
+ property + "'";
log.error(message);
throw new IllegalArgumentException(message);
}
return property.substring(start + 1, end);
}
/**
* MapプロパティをXPath形式にエスケープする。
* @param property Javaプロパティ名。
* @return String XPath。
*/
protected String escapeMapProperty(String property) {
// aaa(bbb) → aaa/bbb
String mapPropertyName = extractMapPropertyName(property);
String mapKey = extractMapPropertyKey(property);
return mapPropertyName + "/" + mapKey;
}
/**
* Map型属性のプロパティ名を取り出す。
* @param property Javaプロパティ名。
* @return String XPath。
*/
protected String extractMapPropertyName(String property) {
int pos = property.indexOf('(');
// '('がない場合は例外
if (pos == -1) {
String message = "Cannot get Map attribute. "
+ "Invalid property name. '" + property + "'";
log.error(message);
throw new IllegalArgumentException(message);
}
return property.substring(0, pos);
}
/**
* Map型属性のキー名を取り出す。
* @param property Javaプロパティ名。
* @return String XPath。
*/
protected String extractMapPropertyKey(String property) {
// aaa(bbb) → bbb
int start = property.indexOf('(');
int end = property.indexOf(')');
// '()'がない、または()の位置が不正な場合は例外
if (start == -1 || end == -1 || start > end) {
String message = "Cannot get Map key. " + "Invalid property name. '"
+ property + "'";
log.error(message);
throw new IllegalArgumentException(message);
}
return property.substring(start + 1, end);
}
/**
* Map型属性かどうか判断する。
* @param property Javaプロパティ名。
* @return boolean Map型属性ならばtrue、それ以外はfalseを返す。
*/
protected boolean isMapProperty(String property) {
// '()'があればMap型属性
if (property.indexOf('(') != -1 && property.indexOf(')') != -1) {
return true;
}
return false;
}
/**
* XPath形式の文字列をプロパティ形式の文字列に変換する。
* @param xpath XPath形式文字列
* @return プロパティ形式文字列
*/
protected String toPropertyName(String xpath) {
StringBuilder builder = new StringBuilder("");
String[] nodes = StringUtils.split(xpath, '/');
if (nodes == null || nodes.length == 0) {
String message = "XPath is null or blank.";
log.error(message);
throw new IllegalArgumentException(message);
}
for (int i = 0; i < nodes.length; i++) {
String node = nodes[i];
// Mapオブジェクト
if (i == 0 && isMapObject(node)) {
builder.append(extractMapKey(node));
builder.append(extractDecrementIndex(node));
continue;
}
// ネスト
if (builder.length() > 0) {
builder.append('.');
}
// Map属性
if (isMapAttribute(node)) {
builder.append(extractMapAttributeName(node));
builder.append('(');
builder.append(extractMapKey(node));
builder.append(')');
// JavaBean または primitive
} else {
builder.append(extractAttributeName(node));
}
// 配列インデックス
builder.append(extractDecrementIndex(node));
}
return builder.toString();
}
/**
* 属性名を取り出す。 配列の場合、添え字はカットされる。
* @param node XPathのノード。
* @return 属性名。
*/
protected String extractAttributeName(String node) {
int pos = node.lastIndexOf('[');
if (pos == -1) {
return node;
}
// 配列の添え字はカット
return node.substring(0, pos);
}
/**
* Mapの属性名を取り出す。
* @param node XPathのノード。
* @return 属性名。
*/
protected String extractMapAttributeName(String node) {
// 最初の'['までの文字列をMapの属性名とする
int pos = node.indexOf('[');
// '['がない場合は例外
if (pos == -1) {
String message = "Cannot get Map attribute. "
+ "Invalid property name. '" + node + "'";
log.error(message);
throw new IllegalArgumentException(message);
}
return node.substring(0, pos);
}
/**
* Mapキーを取り出す。
* @param node XPathのノード。
* @return 属性名。
*/
protected String extractMapKey(String node) {
// aaa[@name='bbb'] → bbb
int start = node.indexOf('[');
int end = node.indexOf(']');
// '[]'がない、または[]の位置が不正な場合は例外
if (start == -1 || end == -1 || start > end) {
String message = "Cannot get Map key. " + "Invalid property name. '"
+ node + "'";
log.error(message);
throw new IllegalArgumentException(message);
}
return node.substring(start + "[@name='".length(), end - "'".length());
}
/**
* デクリメントした添え字を取り出す。
* @param node XPathのノード。
* @return 属性名。
*/
protected String extractDecrementIndex(String node) {
return extractIncrementIndex(node, -1);
}
/**
* Map属性を持つオブジェクトかどうか判断する。
* @param node XPathのノード。
* @return Map属性ならばtrue、それ以外はfalseを返す。
*/
protected boolean isMapAttribute(String node) {
// '[@name'があればMap属性
if (node.indexOf("[@name") != -1) {
return true;
}
return false;
}
/**
* Mapオブジェクトかどうか判断する。
* @param node XPathのノード。
* @return Mapオブジェクトならばtrue、それ以外はfalseを返す。
*/
protected boolean isMapObject(String node) {
// '.[@name'…で始まるならばMapオブジェクト
if (node.startsWith(".[@name")) {
return true;
}
return false;
}
}