/*
 * 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.LinkedHashMap;
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.EL;
import woolpack.el.ELAdapter;
import woolpack.el.ELTargetRuntimeException;

/**
 * OGNL を使用した{@link EL}(Object Graph Expression)。
 * 本クラスはイミュータブルである。
 * 本クラスは OGNL のライブラリを使用する。
 * 適用しているパターン：Adapter。
 * @author nakamura
 *
 */
public class OGE extends ELAdapter{
	private static Map<String, Object> cache = new WeakHashMap<String, Object>();
	
	/**
	 * キャッシュに使用する{@link Map}を設定する。
	 * 以前に設定されていたキャッシュの{@link Map}は本クラスからから参照されなくなる。
	 * {@link LinkedHashMap#removeEldestEntry(java.util.Map.Entry)}を指定した{@link LinkedHashMap}を指定するとキャッシュの容量を制御することができる。
	 * {@link java.util.HashMap}などの強参照の{@link Map}を指定すると容量制限のないキャッシュになる。
	 * {@link java.util.WeakHashMap}を指定すると弱参照によるキャッシュになる。
	 * 本クラスでは初期キャッシュとして{@link WeakHashMap}を使用する。
	 * 本メソッドは同期化されない。
	 * @param newCache キャッシュに使用する{@link Map}。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static void changeCache(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(new String(expression), myTree);
		}
		return myTree;
	}
	
	/**
	 * コンストラクタ。
	 * @param expression OGNL の文字列表現。
	 * @param typeConverter OGNL の型変換インタフェース。
	 * @throws NullPointerException 引数が null の場合。
	 * @throws RuntimeException ({@link OgnlException})OGNL の解析に失敗した場合。
	 */
	public OGE(final String expression, final TypeConverter typeConverter){
		expression.getClass();
		typeConverter.getClass();
		this.expression = expression;
		this.typeConverter = typeConverter;
		try {
			this.tree = registerToMap(expression);
		} catch (final OgnlException exception) {
			throw new RuntimeException(exception);
		}
	}

	public OGE(final String expression){
		this(expression, new OgnlCollectionTypeConverter(OgnlContext.DEFAULT_TYPE_CONVERTER));
	}
	
	abstract class OGE2{
		abstract Object execute(Map context) throws MethodFailedException, OgnlException;
	}
	
	private Object execute(final Object root, final OGE2 oge2) throws ELTargetRuntimeException{
		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 RuntimeException(e);
			}else{
				throw new ELTargetRuntimeException(e.getReason());
			}
		} catch (final OgnlException e) {
			throw new RuntimeException(e);
		}finally{
			Ognl.setTypeConverter(context, defaultTypeConverter);
		}
	}

	/**
	 * @throws ELTargetRuntimeException メソッドがエラーを投げた場合。
	 * @throws RuntimeException ({@link OgnlException})OGNL の解析に失敗した場合。
	 */
	@Override public Object getValue(final Object root, final Class clazz) throws ELTargetRuntimeException{
		return execute(root, new OGE2(){
			@Override 
			Object execute(final Map context) throws MethodFailedException, OgnlException{
				if(Object.class.equals(clazz)){
					return Ognl.getValue(tree, context, root);
				}else{
					return Ognl.getValue(tree, context, root, clazz);
				}
			}
		});
	}

	/**
	 * @throws ELTargetRuntimeException メソッドがエラーを投げた場合。
	 * @throws RuntimeException ({@link OgnlException})OGNL の解析に失敗した場合。
	 */
	@Override public void setValue(final Object root, final Object value) throws ELTargetRuntimeException{
		execute(root, new OGE2(){
			@Override 
			Object execute(final Map context) throws MethodFailedException, OgnlException{
				Ognl.setValue(tree, context, root, value);
				return null;
			}
		});
	}
	
	public String getExpression(){
		return expression;
	}

	public TypeConverter getTypeConverter() {
		return typeConverter;
	}
}
