/*
 * 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.adapter;

import java.util.Map;
import java.util.WeakHashMap;

import ognl.MethodFailedException;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.TypeConverter;
import woolpack.el.AbstractEL;
import woolpack.el.ELTargetRuntimeException;
import woolpack.utils.CheckUtils;

/**
 * OGNL を使用した{@link woolpack.el.EL}(Object Graph Expression)。
 * 本クラスはイミュータブルである。 本クラスは OGNL のライブラリを使用する。
 * 適用しているパターン：Adapter。
 * 
 * @author nakamura
 * 
 */
public class OGE extends AbstractEL {
	private static Map<String, Object> cache
	= new WeakHashMap<String, Object>();

	/**
	 * キャッシュに使用する{@link Map}を設定する。 以前に設定されていたキャッシュの{@link Map}は本クラスからから参照されなくなる。
	 * {@link java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)}
	 * を指定した
	 * {@link java.util.LinkedHashMap}を指定するとキャッシュの容量を制御することができる。
	 * {@link java.util.HashMap}などの強参照の{@link Map}を指定すると容量制限のないキャッシュになる。
	 * {@link java.util.WeakHashMap}を委譲先に指定した
	 * {@link woolpack.utils.NewStringKeyMap}を指定すると弱参照によるキャッシュになる。
	 * 本クラスでは初期キャッシュとして{@link WeakHashMap}を使用する。
	 * 本メソッドは同期化されない。
	 * 
	 * @param newCache
	 *            キャッシュに使用する{@link Map}。
	 * @throws NullPointerException
	 *             引数が null の場合。
	 */
	public static void changeCache(final Map<String, Object> newCache) {
		final Map<String, Object> oldCache = cache;
		newCache.putAll(oldCache);
		cache = newCache;
	}

	private final String expression;

	private final TypeConverter typeConverter;

	private final Object tree;

	private static Object registerToMap(final String expression)
			throws OgnlException {
		Object myTree = cache.get(expression);
		if (myTree == null) {
			myTree = Ognl.parseExpression(expression);
			cache.put(expression, myTree);
		}
		return myTree;
	}

	/**
	 * コンストラクタ。
	 * 
	 * @param expression
	 *            OGNL の文字列表現。
	 * @param typeConverter
	 *            OGNL の型変換インタフェース。
	 * @throws NullPointerException
	 *             引数が null の場合。
	 * @throws IllegalArgumentException (
	 *             {@link OgnlException})OGNL の解析に失敗した場合。
	 */
	public OGE(final String expression, final TypeConverter typeConverter) {
		super();
		CheckUtils.checkNotNull(expression);
		CheckUtils.checkNotNull(typeConverter);
		this.expression = expression;
		this.typeConverter = typeConverter;
		try {
			this.tree = registerToMap(expression);
		} catch (final OgnlException exception) {
			throw new IllegalArgumentException(exception);
		}
	}

	public OGE(final String expression) {
		this(expression, new OgnlCollectionTypeConverter(
				OgnlContext.DEFAULT_TYPE_CONVERTER));
	}

	abstract class AbstractOGE {
		AbstractOGE() {
		}
		abstract Object execute(Map context) throws OgnlException;
	}

	private Object execute(final Object root, final AbstractOGE oge2) {
		final Map context = Ognl.createDefaultContext(root);
		final TypeConverter defaultTypeConverter = Ognl
				.getTypeConverter(context);
		Ognl.setTypeConverter(context, typeConverter);
		try {
			return oge2.execute(context);
		} catch (final MethodFailedException e) {
			if (e.getReason() instanceof NoSuchMethodException) {
				throw new UnsupportedOperationException(e);
			} else {
				throw new ELTargetRuntimeException(e.getReason());
			}
		} catch (final OgnlException e) {
			throw new IllegalStateException(e);
		} finally {
			Ognl.setTypeConverter(context, defaultTypeConverter);
		}
	}

	/**
	 * @throws ELTargetRuntimeException
	 *             メソッドがエラーを投げた場合。
	 * @throws UnsupportedOperationException (
	 *             {@link NoSuchMethodException})メソッドが存在しない場合。
	 * @throws ELTargetRuntimeException (
	 *             メソッドが存在しない場合以外で呼出が失敗した場合。
	 * @throws IllegalStateException (
	 *             {@link OgnlException})OGNL の解析に失敗した場合。
	 */
	@Override
	public Object getValue(final Object root, final Class clazz) {
		return execute(root, new AbstractOGE() {
			@Override
			Object execute(final Map context) throws OgnlException {
				if (Object.class.equals(clazz)) {
					return Ognl.getValue(tree, context, root);
				} else {
					return Ognl.getValue(tree, context, root, clazz);
				}
			}
		});
	}

	/**
	 * @throws ELTargetRuntimeException
	 *             メソッドがエラーを投げた場合。
	 * @throws UnsupportedOperationException (
	 *             {@link NoSuchMethodException})メソッドが存在しない場合。
	 * @throws ELTargetRuntimeException (
	 *             メソッドが存在しない場合以外で呼出が失敗した場合。
	 * @throws IllegalStateException (
	 *             {@link OgnlException})OGNL の解析に失敗した場合。
	 */
	@Override
	public void setValue(final Object root, final Object value) {
		execute(root, new AbstractOGE() {
			@Override
			Object execute(final Map context) throws OgnlException {
				Ognl.setValue(tree, context, root, value);
				return null;
			}
		});
	}

	public String getExpression() {
		return expression;
	}

	public TypeConverter getTypeConverter() {
		return typeConverter;
	}
}
