なんじゃくにっき

プログラミングの話題中心。

Dump Dump Revolution

 先生、Javaの汎用Dumpが欲しいです・・ って話。
誰かが作ってるかもしれないけど。

 toStringを出力するだけだと情報が足りないし、
シリアライズするにはSerializableを実装しないといけない。
 
 まあDebugモードで見られるときはそれでいいんだけど、
折角だからリフレクションの勉強を兼ねて適当に作ってみようか、と。
 
…思ったのが運の尽き。
ArrayとかListとかMapとかめんどくさかった。
 
とりあえずこんな感じ。
入れ子になっているFieldも探索。再帰の深さを設定可能(Defaultで5段)。
・PrivateなFieldも探索。
・FinalなFieldは無視。
・Array, Collection, Mapに対して展開するかどうか設定可能(Defaultでtrue)
・親クラスのFieldも見るか設定可能
・Text形式かHTMLのテーブル形式で出力。
PHPって殆ど触ったこと無いけど、PHPにVAR_DUMPってのがあるらしいのでそれっぽく(?)したつもり。
タグをごりごりJavaのソースの中で書いてるのはご愛嬌。 
 
汎用なのが欲しかったけど、リフレクション多用してるので重い・・
ログ取り用とかに使うと深刻なパフォーマンス低下を招くはず。きっと。多分。
再帰の深さを深く設定して、親クラスのFieldも見るようにしてあげると泣ける、はず。
あと、一度樹構造を作成してから文字列出力してるのも重い原因かも。

なんだか例外握りつぶしてたり怪しいこと色々してるけど
以下、ソース


本体
package hoge;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

class Node{
String fieldname;
String classname;
String value;
List<Node> children = new ArrayList<Node>();

Node(){
fieldname = "";
}

Node(String fieldname, String classname, String value){
this.fieldname = fieldname;
this.classname = classname;
this.value = value;
}

boolean addChild(Node node) {
return this.children.add(node);
}

@Override
public String toString() {
return fieldname + ": " + classname + ": " + value;
}
}

public class DumpUtil {
public static String getIndent(char ch, int depth, int width){
char chararray[] = new char[depth * width];
for (int i = 0; i < chararray.length; i++)
chararray[i] = ch;

return new String(chararray);
}

public static String dumpToText(DumpConfiguration config, Object ... arg){
DumpConfiguration dumpConfiguration =
(config == null ? new DumpConfiguration() : config);

Node node = dumpToTree(dumpConfiguration, arg);
StringBuilder builder = new StringBuilder();
projectToText(node, 0, builder, dumpConfiguration);

return builder.toString();
}

protected static void projectToText(Node node, int depth,
StringBuilder builder, DumpConfiguration config){
String indent = getIndent(config.indentChar, depth, config.indentWidth);
builder.append((node.fieldname == "" ? "" : indent + node.fieldname + " => \r\n")
+ indent + node.classname + " = " + node.value + "\r\n");

for (Node children: node.children)
projectToText(children, depth + 1, builder, config);
}

public static String dumpToHTML(DumpConfiguration config, Object ... arg){
DumpConfiguration dumpConfiguration =
(config == null ? new DumpConfiguration() : config);

Node node = dumpToTree(dumpConfiguration, arg);
StringBuilder builder = new StringBuilder();
projectToHTML(node, 0, builder);

return builder.toString();
}

public static void projectToHTML(Node node, int depth,
StringBuilder builder){
if (depth == 0){
builder.append("<!DOCTYPE HTML PUBLIC \"" +
"-//W3C//DTD HTML 4.0 Transitional//EN\">\r\n");
builder.append("<HTML>\r\n");
builder.append("<HEAD>\r\n");
builder.append("<META http-equiv=\"Content-Type\" " +
"content=\"text/html;charset=UTF-8\" />\r\n");
builder.append("<META http-equiv=\"Content-Style-Type\" " +
"content=\"text/css\" />\r\n");
builder.append("<LINK rel=\"stylesheet\" href=\"style.css\"" +
" type=\"text/css\" />\r\n");
builder.append("</HEAD>\r\n");
builder.append("<BODY>\r\n");
builder.append("<TABLE>\r\n");
}

if (node.children.size() == 0){
builder.append("<TR>\r\n");
builder.append("<TD>");
builder.append(node.fieldname);
builder.append("</TD>\r\n");
builder.append("<TD>");
builder.append(node.classname);
builder.append("</TD>\r\n");
builder.append("<TD>");
builder.append(node.value == null ? "null"
: node.value.replaceAll("\r\n", "<BR />")
.replaceAll("\n", "<BR />"));
builder.append("</TD>\r\n");
builder.append("</TR>\r\n");
} else {
builder.append("<TR>\r\n");
builder.append("<TD colspan=3>\r\n");
builder.append("<TABLE id=\"table" +
Integer.toString(depth + 1) + "\">\r\n");
builder.append("<TR>\r\n");
builder.append("<TH>");
builder.append(node.fieldname);
builder.append("</TH>\r\n");
builder.append("<TH>");
builder.append(node.classname);
builder.append("</TH>\r\n");
builder.append("<TH>");
builder.append(node.value == null ? "null"
: node.value.replaceAll("\r\n", "<BR />")
.replaceAll("\n", "<BR />"));
builder.append("</TH>\r\n");
builder.append("</TR>\r\n");

for (Node child: node.children)
projectToHTML(child, depth + 1, builder);

builder.append("</TABLE>\r\n");
builder.append("</TD>\r\n");
builder.append("</TR>\r\n");
}

if (depth == 0){
builder.append("</TR>\r\n");
builder.append("</BODY>\r\n");
builder.append("</HTML>\r\n");
}
}

protected static Node dumpToTree(DumpConfiguration conf, Object[] arg){
if (conf == null)
conf = new DumpConfiguration();

if (arg == null)
return null;
else if (arg.length == 1)
return search(conf, 0, arg[0], new Node());
else
return search(conf, 0, arg, new Node());
}

@SuppressWarnings("unchecked")
protected static Node search(DumpConfiguration config, int depth,
Object arg, Node node){

if (arg == null){
return node;
}

if (arg instanceof Object[]){
node.classname = arg.getClass().getName() + " ("
+ Integer.toString(((Object[])arg).length) + ")";
} else if (arg instanceof Collection){
node.classname = arg.getClass().getName() + " ("
+ Integer.toString(((Collection)arg).size()) + ")";
} else if (arg instanceof Map){
node.classname = arg.getClass().getName() + " ("
+ Integer.toString(((Map)arg).size()) + ")";
} else {
node.classname = arg.getClass().getName();
}

node.value = arg.toString();

if (depth >= config.maxDepth)
return null;

if (arg instanceof Object[] && config.isRecursiveForArray){
for (int i = 0; i < ((Object[])arg).length; i++) {
Node child = new Node();
child.fieldname = "[" + Integer.toString(i) + "]";
node.addChild(child);
search(config, depth, ((Object[])arg)[i], child);
}
} else if (arg instanceof Collection && config.isRecursiveForCollection){
Object[] objects = ((Collection)arg).toArray();
for (int i = 0; i < objects.length; i++) {
Node child = new Node();
child.fieldname = "[" + Integer.toString(i) + "]";
node.addChild(child);
search(config, depth, objects[i], child);
}
} else if (arg instanceof Map && config.isRecursiveForMap){
for(Entry<Object, Object> e:
((Map<Object, Object>)arg).entrySet()){
Node child = new Node();
child.fieldname = e.getKey().toString();
node.addChild(child);
search(config, depth + 1, e.getValue(), child);
}
} else {
Class clazz = arg.getClass();
while (clazz != null) {
try {
Field[] fields;

if (config.isRecursiveForPrivateField)
fields = clazz.getDeclaredFields();
else
fields = clazz.getFields();

for(Field field: fields){
if (Modifier.isFinal(field.getModifiers()))
continue;

field.setAccessible(true);
Object childObject = field.get(arg);

String fieldname = field.getName();

if (config.ignoreHash && "hash".equals(fieldname))
continue;

Node child = new Node(fieldname,
field.getClass().getName(),
null);

node.addChild(child);
search(config, depth + 1, childObject, child);
}
} catch (AccessControlException e) {
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}

clazz = (config.upToSuperClass ? clazz.getSuperclass() : null);
}
}
return node;
}
}


package hoge;

public class DumpConfiguration {
public int maxDepth;
public char indentChar;
public int indentWidth;
public boolean isRecursiveForArray;
public boolean isRecursiveForCollection;
public boolean isRecursiveForMap;
public boolean isRecursiveForPrivateField;
public boolean ignoreHash;
public boolean upToSuperClass;

public DumpConfiguration(){
maxDepth = 5;
indentChar = ' ';
indentWidth = 4;
isRecursiveForArray = true;
isRecursiveForCollection = true;
isRecursiveForMap = true;
isRecursiveForPrivateField = true;
ignoreHash = true;
upToSuperClass = false;
}

public DumpConfiguration(int maxDepth, char indentChar, int indentWidth,
boolean isRecursiveForArray, boolean isRecursiveForCollection,
boolean isRecursiveForMap, boolean isRecursiveForPrivateField,
boolean ignoreHash, boolean upToSuperClass){
this.maxDepth = maxDepth;
this.indentChar = indentChar;
this.indentWidth = indentWidth;
this.isRecursiveForArray = isRecursiveForArray;
this.isRecursiveForCollection = isRecursiveForCollection;
this.isRecursiveForMap = isRecursiveForMap;
this.isRecursiveForPrivateField = isRecursiveForPrivateField;
this.ignoreHash = ignoreHash;
this.upToSuperClass = upToSuperClass;
}

public DumpConfiguration setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
return this;
}

public DumpConfiguration setIndentChar(char indentChar) {
this.indentChar = indentChar;
return this;
}

public DumpConfiguration setIndentWidth(int indentWidth) {
this.indentWidth = indentWidth;
return this;
}

public DumpConfiguration setRecursiveForArray(boolean arg) {
this.isRecursiveForArray = arg;
return this;
}

public DumpConfiguration setRecursiveForCollection(boolean arg) {
this.isRecursiveForCollection = arg;
return this;
}

public DumpConfiguration setRecursiveForMap(boolean arg) {
this.isRecursiveForMap = arg;
return this;
}

public DumpConfiguration setRecursiveForPrivateField(boolean arg) {
this.isRecursiveForPrivateField = arg;
return this;
}

public DumpConfiguration setIgnoreHash(boolean arg) {
this.ignoreHash = arg;
return this;
}

public DumpConfiguration setUpToSuperClass(boolean arg) {
this.upToSuperClass = arg;
return this;
}
}

使い方は↓みたいな感じ。
dumpToHTML(or dumpToText)の引数の1つ目にDumpConfigurationを、
2つ目以降にDumpしたいObjectを指定(2つ目以降は可変長)。
Default設定で良ければ引数の1つ目はnullで。
 

package hoge;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

class A{
Object child;

void addChild(Object child){
this.child = child;
}
}

class B{
String hoge = "B";
final String PIYO = "FINAL_FIELD";
}

public class Main {
public static void main(String[] args) {
String[][][] string2Array =
{{{"piyo", "huga"}, {"foo"}},{{"bar"}}} ;

A hoge = new A();
hoge.addChild(new B());

List<String> strList = new ArrayList<String>();
strList.add("あいうえお");
strList.add("かきくけこ");

HashMap<Integer,String> map = new HashMap<Integer,String>();
map.put(1, "VAL1");
map.put(2, "VAL2");
map.put(3, "VAL3");

DumpConfiguration config = new DumpConfiguration()
.setIndentWidth(4);

String result = DumpUtil.dumpToHTML(config,
hoge, string2Array, strList, map);

System.out.println(result);
}
}

CSSの例


style.css
table{
border: 1px solid;
border-collapse: collapse;
cellpadding: 5px;
}

th{
background-color:#FF0000;
border: 1px solid;
padding:5;
text-align: left;
}

td{
border: 1px solid;
padding:5;
text-align: left;
}

#table1 th{
background-color: #00FFFF;
}

#table2 th{
background-color: #00FF00;
}

#table3 th{
background-color: #A0FF00;
}

#table4 th{
background-color: #FFFF00;
}

#table5 th{
background-color: #FFA000;
}

#table6 th{
background-color: #FF0000;
}

#table7 th{
background-color: #FF00A0;
}

#table8 th{
background-color: #FF00FF;
}

#table9 th{
background-color: #B080FF;
}

#table10 th{
background-color: #C0C0C0;
}

↓出力