package com.ozacc.mail.impl;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.runtime.log.LogSystem;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.ozacc.mail.Mail;
import com.ozacc.mail.MailBuildException;
import com.ozacc.mail.VelocityMultipleMailBuilder;

/**
 * XMLファイルを読み込み、Velocityと連携して動的にメールデータを生成し、そのデータからMailインスタンスを生成するクラス。
 * 
 * @since 1.0.1
 * @author Tomohiro Otsuka
 * @version $Id: XMLVelocityMailBuilderImpl.java,v 1.4.2.4 2005/01/23 06:13:10 otsuka Exp $
 */
public class XMLVelocityMailBuilderImpl extends XMLMailBuilderImpl implements
		VelocityMultipleMailBuilder {

	private static Log log = LogFactory.getLog(XMLVelocityMailBuilderImpl.class);

	private static String CACHE_KEY_SEPARATOR = "#";

	private static String DEFAULT_MAIL_ID = "DEFAULT";

	protected String charset = "UTF-8";

	protected LogSystem velocityLogSystem = new VelocityLogSystem();

	protected Map templateCache = new HashMap();

	private boolean cacheEnabled = false;

	protected boolean hasTemplateCache(String key) {
		if (cacheEnabled) {
			return templateCache.containsKey(key);
		}
		return false;
	}

	protected void putTemplateCache(String key, String templateXmlText) {
		if (cacheEnabled) {
			log.debug("テンプレートをキャッシュします。[key='" + key + "']");
			templateCache.put(key, templateXmlText);
		}
	}

	protected String getTemplateCache(String key) {
		if (hasTemplateCache(key)) {
			log.debug("テンプレートキャッシュを返します。[key='" + key + "']");
			return (String)templateCache.get(key);
		}
		return null;
	}

	/**
	 * @see com.ozacc.mail.VelocityMailBuilder#clearCache()
	 */
	public synchronized void clearCache() {
		log.debug("テンプレートキャッシュをクリアします。");
		templateCache.clear();
	}

	/**
	 * @see com.ozacc.mail.VelocityMailBuilder#isCacheEnabled()
	 */
	public boolean isCacheEnabled() {
		return cacheEnabled;
	}

	/**
	 * @see com.ozacc.mail.VelocityMailBuilder#setCacheEnabled(boolean)
	 */
	public void setCacheEnabled(boolean cacheEnabled) {
		if (!cacheEnabled) {
			clearCache();
		}
		this.cacheEnabled = cacheEnabled;
	}

	/**
	 * @see com.ozacc.mail.VelocityMailBuilder#buildMail(java.lang.String, org.apache.velocity.VelocityContext)
	 */
	public Mail buildMail(String classPath, VelocityContext context) throws MailBuildException {
		String cacheKey = classPath + CACHE_KEY_SEPARATOR + DEFAULT_MAIL_ID;

		String templateXmlText;
		if (!hasTemplateCache(cacheKey)) {
			Document doc;
			try {
				// Velocityマージ前のXMLではコメントを許可する
				doc = getDocumentFromClassPath(classPath, false);
			} catch (SAXException e) {
				throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
			} catch (IOException e) {
				throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
			}
			templateXmlText = convertDocumentIntoString(doc.getDocumentElement());
			putTemplateCache(cacheKey, templateXmlText);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}

		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	/**
	 * @see com.ozacc.mail.VelocityMailBuilder#buildMail(java.io.File, org.apache.velocity.VelocityContext)
	 */
	public Mail buildMail(File file, VelocityContext context) throws MailBuildException {
		String cacheKey = file.getAbsolutePath() + CACHE_KEY_SEPARATOR + DEFAULT_MAIL_ID;

		String templateXmlText;
		if (!hasTemplateCache(cacheKey)) {
			Document doc;
			try {
				// Velocityマージ前のXMLではコメントを許可する
				doc = getDocumentFromFile(file, false);
			} catch (SAXException e) {
				throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
			} catch (IOException e) {
				throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
			}
			templateXmlText = convertDocumentIntoString(doc.getDocumentElement());
			putTemplateCache(cacheKey, templateXmlText);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}

		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	/**
	 * メールデータをVelocityContextとマージして生成されたXMLからMailインスタンスを生成します。
	 * 
	 * @param templateXmlText メールデータのテンプレート
	 * @param context テンプレートにマージする内容を格納したVelocityContext
	 * @return VelocityContextをテンプレートにマージして生成されたXMLから生成されたMailインスタンス
	 * @throws TransformerFactoryConfigurationError
	 * @throws Exception
	 * @throws ParseErrorException
	 * @throws MethodInvocationException
	 * @throws ResourceNotFoundException
	 * @throws IOException
	 */
	protected Mail build(String templateXmlText, VelocityContext context)
																			throws TransformerFactoryConfigurationError,
																			Exception,
																			ParseErrorException,
																			MethodInvocationException,
																			ResourceNotFoundException,
																			IOException {
		if (log.isDebugEnabled()) {
			log.debug("Source XML Mail Data\n" + templateXmlText);
		}

		Velocity.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM, velocityLogSystem);
		Velocity.init();
		StringWriter w = new StringWriter();
		Velocity.evaluate(context, w, "XML Mail Data", templateXmlText);
		StringReader reader = new StringReader(w.toString());

		DocumentBuilder db = createDocumentBuilder();
		InputSource source = new InputSource(reader);
		Document newDoc = db.parse(source);

		if (log.isDebugEnabled()) {
			String newXmlContent = convertDocumentIntoString(newDoc.getDocumentElement());
			log.debug("VelocityContext-merged XML Mail Data\n" + newXmlContent);
		}

		return buildMail(newDoc.getDocumentElement());
	}

	/**
	 * 指定されたDOM Documentを文字列に変換します。
	 * 
	 * @param mailElement
	 * @return XMLドキュメントの文字列
	 * @throws TransformerFactoryConfigurationError 
	 */
	protected String convertDocumentIntoString(Element mailElement)
																	throws TransformerFactoryConfigurationError {
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer t;
		try {
			t = tf.newTransformer();
		} catch (TransformerConfigurationException e) {
			throw new MailBuildException(e.getMessage(), e);
		}
		t.setOutputProperties(getOutputProperties());

		DOMSource source = new DOMSource(mailElement);
		StringWriter w = new StringWriter();
		StreamResult result = new StreamResult(w);
		try {
			t.transform(source, result);
		} catch (TransformerException e) {
			throw new MailBuildException(e.getMessage(), e);
		}

		return w.toString();
	}

	/**
	 * 出力プロパティを生成。
	 * @return 出力プロパティを設定したPropertiesインスタンス
	 */
	protected Properties getOutputProperties() {
		Properties p = new Properties();
		p.put(OutputKeys.ENCODING, charset);
		p.put(OutputKeys.DOCTYPE_PUBLIC, Mail.DOCTYPE_PUBLIC);
		p.put(OutputKeys.DOCTYPE_SYSTEM, Mail.DOCTYPE_SYSTEM);
		return p;
	}

	/**
	 * @see com.ozacc.mail.VelocityMultipleMailBuilder#buildMail(java.lang.String, org.apache.velocity.VelocityContext, java.lang.String)
	 */
	public Mail buildMail(String classPath, VelocityContext context, String mailId)
																					throws MailBuildException {
		if (mailId == null || "".equals(mailId)) {
			throw new IllegalArgumentException("メールIDが指定されていません。");
		}

		String cacheKey = classPath + CACHE_KEY_SEPARATOR + mailId;

		String templateXmlText;
		if (!hasTemplateCache(cacheKey)) {
			Document doc;
			try {
				// Velocityマージ前のXMLではコメントを許可する
				doc = getDocumentFromClassPath(classPath, false);
			} catch (SAXException e) {
				throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
			} catch (IOException e) {
				throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
			}
			if (Mail.DOCTYPE_PUBLIC.equals(doc.getDoctype().getPublicId())) {
				throw new MailBuildException("指定されたクラスパスのXMLはシングルメールテンプレートです。[classPath='"
						+ classPath + "']");
			}
			templateXmlText = getAndCacheTemplateText(doc, mailId, cacheKey);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}

		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	private String getAndCacheTemplateText(Document doc, String mailId, String cacheKey)
																						throws TransformerFactoryConfigurationError {
		Element mailElem = doc.getElementById(mailId);
		if (mailElem == null) {
			throw new MailBuildException("指定されたID[" + mailId + "]のメールデータは見つかりませんでした。");
		}
		String templateXmlText = templateXmlText = convertDocumentIntoString(mailElem);
		putTemplateCache(cacheKey, templateXmlText);
		return templateXmlText;
	}

	/**
	 * @see com.ozacc.mail.VelocityMultipleMailBuilder#buildMail(java.io.File, org.apache.velocity.VelocityContext, java.lang.String)
	 */
	public Mail buildMail(File file, VelocityContext context, String mailId)
																			throws MailBuildException {
		if (mailId == null || "".equals(mailId)) {
			throw new IllegalArgumentException("メールIDが指定されていません。");
		}

		String cacheKey = file.getAbsolutePath() + CACHE_KEY_SEPARATOR + mailId;

		String templateXmlText;
		if (!hasTemplateCache(cacheKey)) {
			Document doc;
			try {
				// Velocityマージ前のXMLではコメントを許可する
				doc = getDocumentFromFile(file, false);
			} catch (SAXException e) {
				throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
			} catch (IOException e) {
				throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
			}
			if (Mail.DOCTYPE_PUBLIC.equals(doc.getDoctype().getPublicId())) {
				throw new MailBuildException("指定されたファイルのXMLはシングルメールテンプレートです。[filePath='"
						+ file.getAbsolutePath() + "']");
			}
			templateXmlText = getAndCacheTemplateText(doc, mailId, cacheKey);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}

		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

}