/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * 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 woolpack.samples.crud;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.regex.Pattern;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Node;

import woolpack.action.ActionDefMaker;
import woolpack.action.ActionInvoker;
import woolpack.action.ForwardDef;
import woolpack.adapter.JXPUtils;
import woolpack.adapter.OGE;
import woolpack.adapter.OGNLUtils;
import woolpack.bool.BoolUtils;
import woolpack.config.ConfigUtils;
import woolpack.container.ContainerContext;
import woolpack.container.ContainerUtils;
import woolpack.convert.ConvertUtils;
import woolpack.ee.ActionBuilder;
import woolpack.ee.EEContext;
import woolpack.ee.HttpSessionMap;
import woolpack.ee.ServletContextMap;
import woolpack.ee.ServletInputStreamFactory;
import woolpack.ee.ServletRequestAttributeMap;
import woolpack.ee.ValidatorBuilder;
import woolpack.el.ELUtils;
import woolpack.el.PathEL;
import woolpack.factory.FactoryUtils;
import woolpack.fn.Delegator;
import woolpack.fn.Fn;
import woolpack.fn.FnUtils;
import woolpack.html.HtmlUtils;
import woolpack.id.IdUtils;
import woolpack.utils.Utils;
import woolpack.validator.ValidatorContext;
import woolpack.validator.ValidatorUtils;
import woolpack.visitor.Visitor;
import woolpack.visitor.VisitorAppUtils;
import woolpack.web.WebContext;
import woolpack.web.WebUtils;
import woolpack.xml.XmlTransformerContext;
import woolpack.xml.XmlTransformerUtils;
import woolpack.xml.XmlUtils;

/**
 * CRUD のサンプルウェブアプリケーションです。
 * @author nakamura
 *
 */
public class SampleServlet extends HttpServlet {

	public final transient Delegator<String, Node, Exception> toNode;
	public final transient Fn<EEContext, Void, Exception> fn;
	public final transient Fn<ValidatorContext, Boolean, RuntimeException> validatorFn;
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	private Fn<ValidatorContext, Boolean, RuntimeException> createValidatorExpression() {
		final Fn<ValidatorContext, Boolean, RuntimeException> parseInt = FnUtils.trying(
				ValidatorUtils.convertValue(FnUtils.join(
						ConvertUtils.TO_STRING,
						ConvertUtils.parse(ConvertUtils.formatFactory(new DecimalFormat())),
						ConvertUtils.TO_INTEGER
				)),
				FnUtils.fix(false),
				FnUtils.fix(true));
		
		return BoolUtils.boolSeq(BoolUtils.ANDAND, Utils.<Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
				list(BoolUtils.ifNot(
						FnUtils.join(ValidatorUtils.MAP, ConvertUtils.<String>keySet(), BoolUtils.containsAll(
								new HashSet<String>(Arrays.asList("userId", "name", "sex", "address", "age", "hobby", "comment")))),
						ValidatorUtils.message("validator.acceptable")))
				.list(ValidatorUtils.branchByNameIfExists(Utils.<String, Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
						linkedMap("userId", BoolUtils.ifNot(
								parseInt,
								ValidatorUtils.message("validator.userId.parse")))
						.map("name", BoolUtils.ifNot(
								FnUtils.join(ValidatorUtils.VALUE, ConvertUtils.TO_STRING, BoolUtils.checkMaxLength(40)),
								ValidatorUtils.message("validator.name.maxLength")))
						.map("sex", BoolUtils.boolSeq(Utils.<Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
								list(BoolUtils.ifNot(
										FnUtils.join(ValidatorUtils.VALUE, BoolUtils.contains(Arrays.asList("0", "1"))),
										ValidatorUtils.message("validator.sex.acceptable")))
								.list(BoolUtils.ifNot(
										parseInt,
										ValidatorUtils.message("validator.sex.parse")))
						))
						.map("address", BoolUtils.ifNot(
								FnUtils.join(ValidatorUtils.VALUE, ConvertUtils.TO_STRING, BoolUtils.checkMaxLength(80)),
								ValidatorUtils.message("validator.address.maxLength")))
						.map("age", BoolUtils.boolSeq(Utils.<Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
								list(BoolUtils.ifNot(
										parseInt,
										ValidatorUtils.message("validator.age.parse")))
								.list(BoolUtils.ifNot(
										FnUtils.join(
												ValidatorUtils.VALUE,
												FnUtils.castTo(Integer.class),
												BoolUtils.checkMin(0)),
										ValidatorUtils.message("validator.age.min")))
								.list(BoolUtils.ifNot(
										FnUtils.join(
												ValidatorUtils.VALUE,
												FnUtils.castTo(Integer.class),
												BoolUtils.checkMax(200)),
										ValidatorUtils.message("validator.age.max")))
						))
						.map("hobby", BoolUtils.ifNot(
								FnUtils.join(ValidatorUtils.VALUES, BoolUtils.containsAll(Arrays.asList("0", "1", "2", "3"))),
								ValidatorUtils.message("validator.hobby.acceptable")))
						.map("comment", BoolUtils.ifNot(
								FnUtils.join(ValidatorUtils.VALUE, ConvertUtils.TO_STRING, BoolUtils.checkMaxLength(200)),
								ValidatorUtils.message("validator.comment.maxLength"))))
				)
				.list(FnUtils.ifTrue(
						FnUtils.join(IdUtils.GET_ID, BoolUtils.checkRegExp(Pattern.compile("user_(deleteConfirm|updateInput)"))),
						ValidatorUtils.branchByName(Utils.<String, Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
								linkedMap("userId", BoolUtils.ifNot(
												FnUtils.join(ValidatorUtils.VALUE, BoolUtils.NOT_EMPTY),
												ValidatorUtils.message("validator.userId.required")))
						), FnUtils.fix(true)))
				.list(FnUtils.ifTrue(
						FnUtils.join(IdUtils.GET_ID, BoolUtils.checkRegExp(Pattern.compile("user_(register|update)(Confirm|Result)"))),
						ValidatorUtils.branchByName(Utils.<String, Fn<ValidatorContext, Boolean, ? extends RuntimeException>>
								linkedMap("name", BoolUtils.ifNot(
												FnUtils.join(ValidatorUtils.VALUE, BoolUtils.NOT_EMPTY),
												ValidatorUtils.message("validator.name.required")))
								.map("sex", BoolUtils.ifNot(
												FnUtils.join(ValidatorUtils.VALUE, BoolUtils.NOT_EMPTY),
												ValidatorUtils.message("validator.sex.required")))
								.map("address", BoolUtils.ifNot(
												FnUtils.join(ValidatorUtils.VALUE, BoolUtils.NOT_EMPTY),
												ValidatorUtils.message("validator.address.required")))
								.map("age", BoolUtils.ifNot(
												FnUtils.join(ValidatorUtils.VALUE, BoolUtils.NOT_EMPTY),
												ValidatorUtils.message("validator.age.required")))
						), FnUtils.fix(true)))
				);
	}
	
	public SampleServlet() {
		super();
		OGNLUtils.setting();
		
		validatorFn = createValidatorExpression();
		{
			// Javascript 値検証コンストラクタツリーを出力します。
			final StringBuilder sb = new StringBuilder();
			final Visitor<StringBuilder, RuntimeException> visitor = new Visitor<StringBuilder, RuntimeException>();
			visitor.setMap(VisitorAppUtils.JS);
			visitor.setSubContext(sb);
			visitor.visit(validatorFn);
			System.out.println("// ---- validator start ----");
			System.out.println(sb.toString());
			System.out.println("// ---- validator end ----");
		}
		final ValidatorBuilder<RuntimeException> validatorBuilder = new ValidatorBuilder<RuntimeException>(validatorFn);
		
		final ActionBuilder actionBuilder = new ActionBuilder(
				new ActionInvoker(
						FnUtils.switching(new ActionDefMaker()
						.putForward("simple_error")
						.putForward("simple_errorValidate")
						.putForward("common_top")
						// 登録の指示画面にSampleBeanの初期値をそのまま流し込むための画面遷移定義です。
						.put(
								"user_registerInput",
								ELUtils.NULL,
								ELUtils.NULL,
								new OGE("container.userBean"))
						// 入力画面の入力値を確認画面にそのまま流し込むための画面遷移定義です。
						.putEcho("user_registerConfirm")
						// HTML hidden として送信されたリクエストの入力値を userBeanに設定し、それを引数に userController.register に渡す画面遷移定義です。
						// 代わりにS2DAOのDAOインスタンスを直接呼び出すことも可能です。
						.put(
								"user_registerResult",
								new OGE("container.userBean"),
								new OGE("container.userController.register(container.userBean)"),
								ELUtils.NULL)
						.putForward("user_searchInput")
						// 検索を実行する画面遷移定義です。
						// 入力値(request.getParameterMap())は値の一覧になっているので、ゼロ番めの値を引数に指定しています。
						// そして検索結果を local スコープに格納し画面に渡しています。
						.put(
								"user_searchResult",
								ELUtils.NULL,
								new OGE("local.searchResult = container.userController.searchList(input.name[0])"),
								new PathEL("local"))
						.put(
								"user_updateInput",
								ELUtils.NULL,
								new OGE("local.bean = container.userController.searchOne(input.userId[0])"),
								new PathEL("local.bean"))
						.putEcho("user_updateConfirm")
						.put(
								"user_updateResult",
								new OGE("container.userBean"),
								new OGE("container.userController.update(container.userBean)"),
								ELUtils.NULL)
						.put(
								"user_deleteConfirm",
								ELUtils.NULL,
								new OGE("local.bean = container.userController.searchOne(input.userId[0])"),
								new PathEL("local.bean"))
						.put(
								"user_deleteResult",
								ELUtils.NULL,
								new OGE("container.userController.delete(input.userId[0])"),
								ELUtils.NULL)
						.get()),
						// 各画面遷移定義でマッチしなかったアクション結果に対するデフォルトの遷移先定義です。
						new ForwardDef("simple_error", new PathEL("local"), FnUtils.fix(true))),
						// 自動的に画面に値を設定するための、更新対象の属性名の一覧の定義です。
						Arrays.asList("name", "id"));

		toNode = new Delegator<String, Node, Exception>(null);
		
		fn = FnUtils.seq(Utils.<Fn<? super EEContext, Void, ? extends Exception>>
				list(IdUtils.<Void, RuntimeException>convertId(ConvertUtils.convertRegExp(Pattern.compile("^.*/([^/]+)$"), "$1")))
				.list(WebUtils.setContainerContext(
						FnUtils.exec(FnUtils.join(
								ContainerUtils.getKey(),
								FnUtils.switching(Utils.<Object, Fn<? super ContainerContext<WebContext>, ?, ? extends Exception>>
									map(
										"userBean",
										WebUtils.request(FactoryUtils.newInstance(SampleBean.class)))
									.map(
										"userController",
										WebUtils.application(FactoryUtils.newInstance(SampleBeanTable.class)))
								)))))
				
				.list(ConfigUtils.setConfig(FnUtils.fix(new HashMap<String, Object>())))
				.list(ConfigUtils.putResourceBundle(FnUtils.fix(ResourceBundle.getBundle("woolpack.samples.crud.selection"))))
				.list(ConfigUtils.putResourceBundle(FnUtils.fix(ResourceBundle.getBundle("woolpack.samples.crud.messages"))))
				.list(ConfigUtils.toLinkedHashMap("sex", "part.sex.value", "part.sex.label", ","))
				.list(ConfigUtils.toLinkedHashMap("address", "part.address.value", "part.address.label", ","))
				.list(ConfigUtils.toLinkedHashMap("hobby", "part.hobby.value", "part.hobby.label", ","))
				
				.list(validatorBuilder.getCheckExpression(
						actionBuilder.getActionExpression(),
						FnUtils.seq(Utils.<Fn<? super EEContext, Void, RuntimeException>>
								list(IdUtils.<Void, RuntimeException>convertId(FnUtils.fix("simple_errorValidate")))
								.list(validatorBuilder.getReplaceExpression())
				)))
				
				.list(XmlUtils.<EEContext, Exception>setNode(FnUtils.join(IdUtils.GET_ID, toNode)))
				.list(HtmlUtils.NORMALIZE_CASE)
				.list(XmlUtils.findNode(JXPUtils.list("//*[@id=\"dummy\" or @name=\"dummy\"]"), XmlUtils.REMOVE_THIS))
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@name and (@type=\"radio\" or @type=\"checkbox\")]"),
						HtmlUtils.makeRadio(new OGE("config[node.getAttribute(\"name\")]"))))
				.list(XmlUtils.findNode(JXPUtils.list("//SELECT[@name]"),
						HtmlUtils.makeSelect(new OGE("config[node.getAttribute(\"name\")]"))))
				.list(HtmlUtils.removeExtension("A", "href"))
				.list(HtmlUtils.removeExtension("FORM", "action"))
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@onclick and (@type=\"button\" or @type=\"submit\")]"),
						XmlUtils.updateAttrValue("onclick",
								FnUtils.join(
										XmlUtils.getAttrValue("onclick"),
										ConvertUtils.convertRegExp(Pattern.compile("^([^\\.]+)\\.[^\\.]+$"), "$1")))))
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@name=\"name\"]"), FnUtils.join(FactoryUtils.doEL(new OGE(
						"node.setAttribute(\"sise\", \"40\"),"
						+ "node.setAttribute(\"maxlength\", \"40\")")),
						FnUtils.<Object, Void>fix(null))))
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@name=\"arg\"]"), FnUtils.join(FactoryUtils.doEL(new OGE(
						"node.setAttribute(\"sise\", \"3\"),"
						+ "node.setAttribute(\"maxlength\", \"3\")")),
						FnUtils.<Object, Void>fix(null))))
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@name=\"comment\"]"), FnUtils.join(FactoryUtils.doEL(new OGE(
						"node.setAttribute(\"rows\", \"3\"),"
						+ "node.setAttribute(\"cols\", \"40\")")),
						FnUtils.<Object, Void>fix(null))))
				.list(XmlUtils.findNode(JXPUtils.list("//SCRIPT[@src]"),
						XmlUtils.updateAttrValue("src",
								FnUtils.join(
										XmlUtils.getAttrValue("src"),
										ConvertUtils.convertRegExp(Pattern.compile("^(.*)$"), "/woolpacksample/html/sample/crud/$1")))))

				.list(actionBuilder.getAutoUpdateExpression())
				// age 項目の null 値(マイナス値)を画面から削除します。
				.list(XmlUtils.findNode(JXPUtils.list("//INPUT[@name=\"age\" and @value=\"-1\"]"), XmlUtils.removeAttr("value")))
				.list(FnUtils.ifTrue(
						FnUtils.join(FnUtils.<EEContext, String, RuntimeException>delegate(IdUtils.GET_ID), BoolUtils.checkRegExp(Pattern.compile("^.*Confirm$"))),
						XmlUtils.findNode(JXPUtils.list("//FORM"), HtmlUtils.hiddenAllToChild(new PathEL("input"), Arrays.asList(""))),
						FnUtils.<Object, Void>fix(null)))
				.list(XmlUtils.findNode(JXPUtils.list("//P[@id=\"errorValidate\"]"), HtmlUtils.updateValue(validatorBuilder.getMessagesEL())))
		);
	}

	@Override public void init(final ServletConfig servletConfig) throws ServletException {
		super.init(servletConfig);
		toNode.setFn(FnUtils.join(
				ConvertUtils.convertRegExp(Pattern.compile("^(.*)$"), "/html/sample/crud/$1.html"),
				XmlUtils.nodeFactory(
						FactoryUtils.inputStreamReaderFactory(new ServletInputStreamFactory<IOException>(
						servletConfig.getServletContext()), "UTF-8"),
						XmlTransformerUtils.TRANSFORMER)));
	}

	@Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		final EEContext context = new EEContext();
		context.setId(request.getRequestURI());
		context.setInput(request.getParameterMap());
		context.setRequest(new ServletRequestAttributeMap(request));
		context.setSession(Utils.concurrentMap(new HttpSessionMap(request.getSession()), request.getSession()));
		context.setApplication(Utils.concurrentMap(new ServletContextMap(request.getSession().getServletContext()), request.getSession().getServletContext()));

		try {
			fn.exec(context);
		} catch (final Exception e) {
			throw new ServletException(e);
		}
		
		final Writer w = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), "UTF-8"));
		try {
			final XmlTransformerContext tc = new XmlTransformerContext();
			tc.setSource(new DOMSource(context.getNode()));
			tc.setResult(new StreamResult(w));
			XmlTransformerUtils.TRANSFORMER.exec(tc);
		} catch (final TransformerException e) {
			throw new ServletException(e);
		} finally {
			w.close();
		}
	}
}
