package org.pirkaengine.core.template;
import static org.pirkaengine.core.util.Logger.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import org.pirkaengine.core.PirkaLoader;
import org.pirkaengine.core.PrkComponent;
import org.pirkaengine.core.PrkElement;
import org.pirkaengine.core.PrkNameSpace;
import org.pirkaengine.core.expression.ScriptFunction;
import org.pirkaengine.core.parser.Fragment;
import org.pirkaengine.core.parser.ParseException;
import org.pirkaengine.core.parser.StAXXmlParser;
import org.pirkaengine.core.parser.XhtmlStruct;
/**
* テンプレートビルダ.
* <p>
* テンプレートをビルドする。<br />
* テンプレートは、XHTMLを読み込み、断片(Fragment)化された後、
* 適切なNodeクラスへと変換され、XhtmlTemplateクラスが生成される。
* </p>
* <code><pre>
* TemplateBuilder builder = new TemplateBuilder();
* XhtmlTemplate template = builder.parse(file);
* </pre></code>
* @author shuji.w6e
* @since 0.1.0
* @see Fragment
* @see XhtmlTemplate
*/
public class TemplateBuilder {
final PirkaLoader loader;
final Map<String, BlockNode> blocks = new HashMap<String, BlockNode>();
/**
* コンストラクタ.
* @param loader PirkaLoader
*/
public TemplateBuilder(PirkaLoader loader) {
if (loader == null) throw new IllegalArgumentException("loader == null");
this.loader = loader;
}
/**
* テンプレートをビルドする.
* @param templateName テンプレート名
* @param file File
* @return テンプレートオブジェクト
* @throws FileNotFoundException
* @throws ParseException
*/
public XhtmlTemplate build(String templateName, File file) throws ParseException, FileNotFoundException {
return build(templateName, file, "UTF-8");
}
/**
* テンプレートをビルドする.
* @param templateName テンプレート名
* @param file File
* @return テンプレートオブジェクト
* @throws FileNotFoundException
* @throws ParseException
*/
public XhtmlTemplate build(String templateName, File file, String charset) throws ParseException,
FileNotFoundException {
return build(templateName, file, Charset.forName(charset));
}
/**
* テンプレートをビルドする.
* @param templateName テンプレート名
* @param file File
* @return テンプレートオブジェクト
* @throws FileNotFoundException
* @throws ParseException
*/
public XhtmlTemplate build(String templateName, File file, Charset charset) throws ParseException,
FileNotFoundException {
if (file == null) throw new IllegalArgumentException("file is null.");
if (isDebugEnabled()) debug("build: " + file.getAbsolutePath());
XhtmlTemplate template = new XhtmlTemplate(templateName);
template.setTimeStamp(file.lastModified());
XhtmlStruct struct = parse(file, charset);
// extendsの処理
if (struct.getBaseTemplate() != null) {
try {
// 継承テンプレート
// block部分を抽出する(this.blocksに格納される)
XhtmlTemplate subTemplate = new XhtmlTemplate(templateName);
buildTemplate(subTemplate, struct, null, charset);
// TODO 相対パスの解決
File baseFile = loader.toFile(struct.getBaseTemplate(), templateName);
// ベースのテンプレート
XhtmlStruct baseStruct = parse(baseFile, charset);
buildTemplate(template, baseStruct, blocks, charset);
template.components.addAll(subTemplate.components);
for (String name : subTemplate.functions.keySet()) {
template.functions.put(name, subTemplate.functions.get(name));
}
return template;
} catch (Exception e) {
throw new ParseException(e);
}
} else {
buildTemplate(template, struct, null, charset);
return template;
}
}
private XhtmlStruct parse(File file, Charset charset) throws ParseException, FileNotFoundException {
return new StAXXmlParser().parse(new FileInputStream(file), charset);
}
void buildTemplate(XhtmlTemplate template, XhtmlStruct struct, Map<String, BlockNode> blockNodes, Charset charset)
throws ParseException {
StringBuilder text = struct.getText();
ListIterator<Fragment> iter = struct.getFragments().listIterator();
assert iter.hasNext();
while (iter.hasNext()) {
Fragment fragment = iter.next();
if (PrkElement.COMPONENT.name.equals(fragment.prkKey)) {
PrkComponent comp = new PrkComponent();
comp.setType(fragment.prkValue);
for (String key : fragment.attributes().keySet()) {
comp.putAttr(key, fragment.attributes().get(key));
}
template.add(comp);
text.delete(fragment.offset, text.length());
continue;
}
Node node = toNode(template, text, fragment, iter, blockNodes, charset);
if (isDebugEnabled()) debug(node);
if (node instanceof ScriptNode) {
template.addScript((ScriptNode) node);
} else if (node instanceof FunctionsNode) {
template.addFunction((FunctionsNode) node);
} else {
template.stack(node);
}
}
}
// FragmentをNodeに変換する
Node toNode(XhtmlTemplate template, StringBuilder text, Fragment fragment, ListIterator<Fragment> iter,
Map<String, BlockNode> replaces, Charset charset) throws ParseException {
String str = text.substring(fragment.offset);
text.delete(fragment.offset, text.length());
switch (fragment.type) {
case TEXT:
// Fragmentsは逆順、先頭要素のみ処理
if (iter.hasPrevious()) {
str = str.replaceAll("xmlns:prk=\\\".+\\\"", "");
}
return new TextNode(str);
case EXPRESSION:
if (str.startsWith("$_{")) {
String expression = str.substring(3, str.length() - 1);
return new ExpressionNode(expression, false);
} else if (str.startsWith("${")) {
String expression = str.substring(2, str.length() - 1);
return new ExpressionNode(expression);
}
throw new AssertionError("EXPRESSION: " + str);
case TAG_START:
String tagText = getTagText(str);
return new StartTagNode(tagText, fragment.prkKey, fragment.prkValue, fragment.prkAttributes(),
fragment.pathAttributes(), fragment.attributes());
case TAG_BODY:
return new TextNode(str);
case TAG_END:
// TAG_ENDは対応するTAG_STARTが発生するまで、ノードをスタックする
EndTagNode end = new EndTagNode(str);
LinkedList<Node> nodes = new LinkedList<Node>();
Node nextNode = null;
while (true) {
if (!iter.hasNext()) throw new TemplateBuildFailedException("format error"); // TODO
// message
Fragment nextFrag = iter.next();
nextNode = this.toNode(template, text, nextFrag, iter, replaces, charset); // 再帰
if (nextNode instanceof StartTagNode) break; // 再帰終了
nodes.addFirst(nextNode);
}
StartTagNode start = (StartTagNode) nextNode;
Node compositeNode = CompositeNode.newNode(start, end, nodes.toArray(new Node[nodes.size()]));
if (compositeNode instanceof BlockNode) {
BlockNode block = (BlockNode) compositeNode;
String id = block.startTagNode.attrValue;
if (replaces == null) {
// 置換対象がない場合は積み上げる
this.blocks.put(id, block);
} else if (replaces.containsKey(id)) {
// 置換対象があれば置換する
return new BlockNode(start, end, replaces.get(id).nodes);
} else {
// そのまま使うのでなにもしない
}
}
return compositeNode;
case TAG_EMPTY_ELEMENTS:
if (PrkElement.REPLACE.name.equals(fragment.prkKey)) {
tagText = getTagText(str);
start = new StartTagNode(tagText, fragment.prkKey, fragment.prkValue, fragment.prkAttributes(),
fragment.pathAttributes(), fragment.attributes());
end = new EndTagNode(str);
return new ReplaceNode(start, end, new Node[] {});
} else if (PrkElement.INCLUDE.name.equals(fragment.prkKey)) {
// prk: include
// TODO 相対パスの解決
try {
XhtmlTemplate includeTemplate = new XhtmlTemplate(fragment.prkValue);
File includeFile = loader.toFile(fragment.prkValue, template.templateName);
XhtmlStruct includeStruct = parse(includeFile, charset);
buildTemplate(includeTemplate, includeStruct, null, charset);
// IncludeNode node = new IncludeNode(includeTemplate);
// template.stack(node);
template.components.addAll(includeTemplate.components);
for (String name : includeTemplate.functions.keySet()) {
template.functions.put(name, includeTemplate.functions.get(name));
}
// text.delete(fragment.offset, text.length());
return new IncludeNode(includeTemplate);
} catch (Exception e) {
throw new ParseException(e);
}
} else if (PrkElement.VAL.name.equals(fragment.prkKey)) {
return new ValNode(fragment.prkValue, fragment.attributes().get("value"));
} else {
String tagName = getTagText(str);
return new EmptyElementTagNode(tagName, fragment.prkKey, fragment.prkValue, fragment.prkAttributes(),
fragment.pathAttributes(), fragment.attributes());
}
case DEF_START:
throw new AssertionError();
case DEF_CDATA:
throw new AssertionError();
case DEF_END:
// ENDは無視
if (!iter.hasNext()) throw new TemplateBuildFailedException("format error"); // TODO
// message
Fragment cdataFrag = iter.next();
if (cdataFrag.type != Fragment.Type.DEF_CDATA) throw new TemplateBuildFailedException("format error"); // TODO
// message
StringBuilder script = new StringBuilder(text.substring(cdataFrag.offset));
script.delete(0, script.indexOf("<![CDATA[") + "<![CDATA[".length());
script.setLength(script.lastIndexOf("]]>"));
text.delete(cdataFrag.offset, text.length());
if (!iter.hasNext()) throw new TemplateBuildFailedException("format error"); // TODO
// message
Fragment startFrag = iter.next();
if (startFrag.type != Fragment.Type.DEF_START) throw new TemplateBuildFailedException("format error"); // TODO
// message
text.delete(startFrag.offset, text.length());
Fragment.Attribute[] flgAttrs = startFrag.attributeArray();
assert flgAttrs.length == 3 : startFrag;
assert flgAttrs[0].key.equals("language") : startFrag;
assert flgAttrs[1].key.equals("type") : startFrag;
assert flgAttrs[1].value.equals("function") : startFrag;
assert flgAttrs[2].key.equals("name") : startFrag;
return new ScriptNode(ScriptFunction.create(flgAttrs[0].value, flgAttrs[2].value, script.toString()));
case FUNCTIONS:
Fragment.Attribute[] attrs = fragment.attributeArray();
assert attrs.length == 2 : fragment;
assert attrs[0].key.equals("class") : fragment;
assert attrs[1].key.equals("name") : fragment;
try {
return new FunctionsNode(attrs[0].value, attrs[1].value);
} catch (ClassNotFoundException e) {
throw new TemplateBuildFailedException(e);
}
case ESCAPE:
return new EscapeNode(str);
default:
throw new AssertionError(str);
}
}
/** タグからprk属性を除去したタグを返す. */
private String getTagText(String str) {
if (str.startsWith("<prk")) return str;
int from = str.indexOf(PrkNameSpace.PREFIX) - 1;
if (from < 0) return str;
int to = str.indexOf('"', from);
to = str.indexOf('"', to + 1) + 1;
String tagText = new StringBuilder(str).delete(from, to).toString();
return getTagText(tagText);
}
}