package com.ozacc.mail.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

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

/**
 * <a href="http://www.jdom.org/">JDOM</a>を利用してXMLファイルからMailインスタンスを生成するクラス。
 * <p>
 * ソースXMLを読み込む際に、DTDバリデーションが実行されますので妥当なXMLデータ(Valid XML Document)でなければいけません。
 * 
 * @since 1.0
 * 
 * @author Tomohiro Otsuka
 * @version $Id: JDomXMLMailBuilder.java,v 1.10.2.5 2005/02/01 20:37:49 otsuka Exp $
 */
public class JDomXMLMailBuilder implements MultipleMailBuilder, VelocityMultipleMailBuilder {

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

	private static String CACHE_KEY_SEPARATOR = "#";

	private static String DEFAULT_MAIL_ID = "DEFAULT";

	protected LogSystem velocityLogSystem = new VelocityLogSystem();

	private boolean cacheEnabled = false;

	protected Map templateCache = new HashMap();

	/**
	 * コンストラクタ。
	 */
	public JDomXMLMailBuilder() {}

	/**
	 * 指定されたクラスパス上のXMLファイルからMailインスタンスを生成します。
	 * 
	 * @param classPath メール内容を記述したXMLファイルのパス
	 * @return 生成されたMailインスタンス
	 * @throws MailBuildException Mailインスタンスの生成に失敗した場合
	 */
	public Mail buildMail(String classPath) throws MailBuildException {
		Document doc = getDocumentFromClassPath(classPath);
		return build(doc.getRootElement());
	}

	/**
	 * 指定されたクラスパス上のXMLファイルからMailインスタンスを生成します。
	 * 指定されたVelocityContextを使って、XMLファイルの内容を動的に生成できます。
	 * 
	 * @param classPath メール内容を記述したXMLファイルのパス
	 * @param context VelocityContext
	 * @return 生成されたMailインスタンス
	 * @throws MailBuildException Mailインスタンスの生成に失敗した場合
	 */
	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 = getDocumentFromClassPath(classPath);
			templateXmlText = cacheTemplateText(doc, cacheKey);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}
		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	/**
	 * 指定されたXMLファイルからMailインスタンスを生成します。
	 * 
	 * @param file メール内容を記述したXMLファイル
	 * @return 生成されたMailインスタンス
	 * @throws MailBuildException Mailインスタンスの生成に失敗した場合
	 */
	public Mail buildMail(File file) throws MailBuildException {
		Document doc = getDocumentFromFile(file);
		return build(doc.getRootElement());
	}

	/**
	 * 指定されたXMLファイルからMailインスタンスを生成します。
	 * 指定されたVelocityContextを使って、XMLファイルの内容を動的に生成できます。
	 * 
	 * @param file メール内容を記述したXMLファイル
	 * @param context VelocityContext
	 * @return 生成されたMailインスタンス
	 * @throws MailBuildException Mailインスタンスの生成に失敗した場合
	 */
	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 = getDocumentFromFile(file);
			templateXmlText = cacheTemplateText(doc, cacheKey);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}
		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	private String cacheTemplateText(Document doc, String cacheKey) {
		XMLOutputter output = new XMLOutputter();
		String templateXmlText = "<!DOCTYPE mail PUBLIC \"" + Mail.DOCTYPE_PUBLIC + "\" \""
				+ Mail.DOCTYPE_SYSTEM + "\">\n" + output.outputString(doc.getRootElement());
		log.debug("以下のXMLデータをキャッシュします。\n" + templateXmlText);
		putTemplateCache(cacheKey, templateXmlText);
		return templateXmlText;
	}

	/**
	 * 指定されたクラスパス上のファイルを読み込んで、XMLドキュメントを生成します。
	 * 
	 * @param classPath
	 * @return JDOM Document
	 */
	protected Document getDocumentFromClassPath(String classPath) throws MailBuildException {
		InputStream is = getClass().getResourceAsStream(classPath);
		SAXBuilder builder = new SAXBuilder(true);
		builder.setEntityResolver(new DTDEntityResolver());
		Document doc;
		try {
			doc = builder.build(is);
		} catch (JDOMException e) {
			throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
		} catch (IOException e) {
			throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
		return doc;
	}

	/**
	 * 指定されたファイルを読み込んで、XMLドキュメントを生成します。
	 * 
	 * @param file
	 * @return JDOM Document
	 */
	protected Document getDocumentFromFile(File file) {
		SAXBuilder builder = new SAXBuilder(true);
		builder.setEntityResolver(new DTDEntityResolver());
		Document doc;
		try {
			doc = builder.build(file);
		} catch (JDOMException e) {
			throw new MailBuildException("XMLのパースに失敗しました。" + e.getMessage(), e);
		} catch (IOException e) {
			throw new MailBuildException("XMLファイルの読み込みに失敗しました。", e);
		}
		return doc;
	}

	/**
	 * XMLのmailルートエレメントからMailインスタンスを生成します。
	 * 
	 * @param mailElement mail要素を示すElementインスタンス
	 * @return Mail 生成されたMail
	 */
	protected Mail build(Element mailElement) {
		Mail mail = new Mail();
		setFrom(mailElement, mail);
		setRecipients(mailElement, mail);
		setSubject(mailElement, mail);
		setBody(mailElement, mail);
		setReplyTo(mailElement, mail);
		setReturnPath(mailElement, mail);

		setHtml(mailElement, mail);

		return mail;
	}

	/**
	 * VelocityContextとXMLテンプレートをマージさせ、Mailインスタンスを生成します。
	 * 
	 * @param templateText マージするXMLテンプレートの文字列
	 * @param context マージするVelocityContext
	 * @return Mail
	 * 
	 * @throws Exception
	 * @throws ParseErrorException
	 * @throws MethodInvocationException
	 * @throws ResourceNotFoundException
	 * @throws IOException
	 * @throws JDOMException 
	 */
	protected Mail build(String templateText, VelocityContext context) throws Exception,
																		ParseErrorException,
																		MethodInvocationException,
																		ResourceNotFoundException,
																		IOException, JDOMException {
		if (log.isDebugEnabled()) {
			log.debug("ソースXMLデータ\n" + templateText);
		}

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

		if (log.isDebugEnabled()) {
			log.debug("VelocityContextとマージ後のXMLデータ\n" + w.toString());
		}

		StringReader reader = new StringReader(w.toString());
		SAXBuilder builder = new SAXBuilder(true);
		builder.setEntityResolver(new DTDEntityResolver());
		Document mergedDoc = builder.build(reader);

		return build(mergedDoc.getRootElement());
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setReturnPath(Element root, Mail mail) {
		Element returnPathElem = root.getChild("returnPath");
		if (returnPathElem != null && returnPathElem.getAttributeValue("email") != null) {
			mail.setReturnPath(returnPathElem.getAttributeValue("email"));
		}
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setReplyTo(Element root, Mail mail) {
		Element replyToElem = root.getChild("replyTo");
		if (replyToElem != null && replyToElem.getAttributeValue("email") != null) {
			mail.setReplyTo(replyToElem.getAttributeValue("email"));
		}
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setBody(Element root, Mail mail) {
		Element bodyElem = root.getChild("body");
		if (bodyElem != null) {
			mail.setText(bodyElem.getTextTrim());
		}
	}

	/**
	 * @param root
	 * @param mail
	 */
	protected void setHtml(Element root, Mail mail) {
		Element htmlElem = root.getChild("html");
		if (htmlElem != null) {
			mail.setHtmlText(htmlElem.getTextTrim());
		}
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setSubject(Element root, Mail mail) {
		Element subjectElem = root.getChild("subject");
		if (subjectElem != null) {
			mail.setSubject(subjectElem.getTextTrim());
		}
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setRecipients(Element root, Mail mail) {
		Element recipientsElem = root.getChild("recipients");
		if (recipientsElem == null) {
			return;
		}

		List recipientElemList = recipientsElem.getChildren();
		for (int i = 0, max = recipientElemList.size(); i < max; i++) {
			Element e = (Element)recipientElemList.get(i);
			if ("to".equals(e.getName())) { // to
				if (e.getAttributeValue("email") != null) {
					if (e.getAttributeValue("name") != null) {
						mail.addTo(e.getAttributeValue("email"), e.getAttributeValue("name"));
					} else {
						mail.addTo(e.getAttributeValue("email"));
					}
				}
			} else if ("cc".equals(e.getName())) { // cc
				if (e.getAttributeValue("email") != null) {
					if (e.getAttributeValue("name") != null) {
						mail.addCc(e.getAttributeValue("email"), e.getAttributeValue("name"));
					} else {
						mail.addCc(e.getAttributeValue("email"));
					}
				}
			} else {
				if (e.getAttributeValue("email") != null) { // bcc
					mail.addBcc(e.getAttributeValue("email"));
				}
			}
		}
	}

	/**
	 * @param root
	 * @param mail 
	 */
	protected void setFrom(Element root, Mail mail) {
		Element fromElem = root.getChild("from");
		if (fromElem != null && fromElem.getAttributeValue("email") != null) {
			if (fromElem.getAttributeValue("name") != null) {
				mail.setFrom(fromElem.getAttributeValue("email"), fromElem
						.getAttributeValue("name"));
			} else {
				mail.setFrom(fromElem.getAttributeValue("email"));
			}
		}
	}

	/**
	 * @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;
	}

	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.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 = getDocumentFromClassPath(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 MailBuildException {
		Element mailElem = getElementById(doc, mailId);
		XMLOutputter output = new XMLOutputter();
		String templateXmlText = output.outputString(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 = getDocumentFromFile(file);
			templateXmlText = getAndCacheTemplateText(doc, mailId, cacheKey);
		} else {
			templateXmlText = getTemplateCache(cacheKey);
		}
		try {
			return build(templateXmlText, context);
		} catch (Exception e) {
			throw new MailBuildException("メールの生成に失敗しました。", e);
		}
	}

	/**
	 * @see com.ozacc.mail.MultipleMailBuilder#buildMail(java.lang.String, java.lang.String)
	 */
	public Mail buildMail(String classPath, String mailId) throws MailBuildException {
		Document doc = getDocumentFromClassPath(classPath);
		Element mailElem = getElementById(doc, mailId);
		return build(mailElem);
	}

	/**
	 * @see com.ozacc.mail.MultipleMailBuilder#buildMail(java.io.File, java.lang.String)
	 */
	public Mail buildMail(File file, String mailId) throws MailBuildException {
		Document doc = getDocumentFromFile(file);
		Element mailElem = getElementById(doc, mailId);
		return build(mailElem);
	}

	/**
	 * 指定されたXMLドキュメントの中から、指定されたid属性がセットされている要素を取得します。
	 * 
	 * @param doc XMLドキュメント
	 * @param id 抽出する要素のid属性値
	 * @return XMLドキュメントで見つかったid属性を持つ要素
	 */
	private Element getElementById(Document doc, String id) {
		Element mailsElem = doc.getRootElement(); // <mails>
		List mailElemList = mailsElem.getChildren("mail");
		for (Iterator itr = mailElemList.iterator(); itr.hasNext();) {
			Element mailElem = (Element)itr.next();
			String mailId = mailElem.getAttributeValue("id");
			if (mailId.equals(id)) {
				return mailElem;
			}
		}
		throw new MailBuildException("指定されたID[" + id + "]のメールデータは見つかりませんでした。");
	}

}