/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.wicket.core.util.lang;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.wicket.Application;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.convert.ConversionException;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class parses expressions to lookup or set a value on the object that is given. <br/>
 * The supported expressions are:
 * <dl>
 * <dt>"property"</dt>
 * <dd>
 * This could be a bean property with getter and setter. Or if a map is given as
 * an object it will be lookup with the expression as a key when there is not getter for that
 * property.
 * </dd>
 * <dt>"property1.property2"</dt>
 * <dd>
 * Both properties are looked up as described above. If property1 evaluates to
 * null then if there is a setter (or if it is a map) and the Class of the property has a default
 * constructor then the object will be constructed and set on the object.
 * </dd>
 * <dt>"method()"</dt>
 * <dd>
 * The corresponding method is invoked.
 * </dd>
 * <dt>"property.index" or "property[index]"</dt>
 * <dd>
 * If the property is a List or Array then the following expression can be a index on
 * that list like: 'mylist.0'. The list will grow automatically if the index is greater than the size.<p>
 * This expression will also map on indexed properties, i.e. {@code getProperty(index)} and {@code setProperty(index,value)}
 * methods.
 * </dd>
 * <dt>"property.key" or "property[key]"</dt>
 * <dd>
 * If the property is a Map then the following expression can be a key in that map like: 'myMap.key'.
 * </dd>
 * </dl>
 * <strong>Note that the {@link DefaultPropertyLocator} by default provides access to private members
 * and methods. If guaranteeing encapsulation of the target objects is a big concern, you should consider
 * using an alternative implementation.</strong>
 * <p>
 * <strong>Note: If a property evaluates to an instance of {@link org.apache.wicket.model.IModel} then
 * the expression should use '.object' to work with its value.</strong>
 *
 * @author jcompagner
 * @author svenmeier
 */
public final class PropertyResolver
{
	/** Log. */
	private static final Logger log = LoggerFactory.getLogger(PropertyResolver.class);

	private final static int RETURN_NULL = 0;
	private final static int CREATE_NEW_VALUE = 1;
	private final static int RESOLVE_CLASS = 2;

	private final static ConcurrentHashMap<Object, IPropertyLocator> applicationToLocators = Generics.newConcurrentHashMap(2);

	private static final String GET = "get";
	private static final String IS = "is";
	private static final String SET = "set";

	/**
	 * Looks up the value from the object with the given expression. If the expression, the object
	 * itself or one property evaluates to null then a null will be returned.
	 *
	 * @param expression
	 *            The expression string with the property to be lookup.
	 * @param object
	 *            The object which is evaluated.
	 * @return The value that is evaluated. Null something in the expression evaluated to null.
	 */
	public static Object getValue(final String expression, final Object object)
	{
		if (expression == null || expression.equals("") || object == null)
		{
			return object;
		}

		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RETURN_NULL);
		if (objectWithGetAndSet == null)
		{
			return null;
		}

		return objectWithGetAndSet.getValue();
	}

	/**
	 * Set the value on the object with the given expression. If the expression can't be evaluated
	 * then a WicketRuntimeException will be thrown. If a null object is encountered then it will
	 * try to generate it by calling the default constructor and set it on the object.
	 *
	 * The value will be tried to convert to the right type with the given converter.
	 *
	 * @param expression
	 *            The expression string with the property to be set.
	 * @param object
	 *            The object which is evaluated to set the value on.
	 * @param value
	 *            The value to set.
	 * @param converter
	 *            The converter to convert the value if needed to the right type.
	 * @throws WicketRuntimeException
	 */
	public static void setValue(final String expression, final Object object,
		final Object value, final PropertyResolverConverter converter)
	{
		if (Strings.isEmpty(expression))
		{
			throw new WicketRuntimeException("Empty expression setting value: " + value +
				" on object: " + object);
		}
		if (object == null)
		{
			throw new WicketRuntimeException(
				"Attempted to set property value on a null object. Property expression: " +
					expression + " Value: " + value);
		}

		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, CREATE_NEW_VALUE);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("Null object returned for expression: " + expression +
				" for setting value: " + value + " on: " + object);
		}
		objectWithGetAndSet.setValue(value, converter == null ? new PropertyResolverConverter(Application.get()
			.getConverterLocator(), Session.get().getLocale()) : converter);
	}

	/**
	 * @param expression
	 * @param object
	 * @return class of the target property object
	 * @throws WicketRuntimeException if the cannot be resolved
	 */
	public static Class<?> getPropertyClass(final String expression, final Object object)
	{
		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("Null object returned for expression: " + expression +
				" for getting the target class of: " + object);
		}
		return objectWithGetAndSet.getTargetClass();
	}

	/**
	 * @param <T>
	 * @param expression
	 * @param clz
	 * @return class of the target Class property expression
	 * @throws WicketRuntimeException if class cannot be resolved
	 */
	@SuppressWarnings("unchecked")
	public static <T> Class<T> getPropertyClass(final String expression, final Class<?> clz)
	{
		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, null, RESOLVE_CLASS, clz);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("No Class returned for expression: " + expression +
				" for getting the target class of: " + clz);
		}
		return (Class<T>)objectWithGetAndSet.getTargetClass();
	}

	/**
	 * @param expression
	 * @param object
	 * @return Field for the property expression
	 * @throws WicketRuntimeException if there is no such field
	 */
	public static Field getPropertyField(final String expression, final Object object)
	{
		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("Null object returned for expression: " + expression +
				" for getting the target class of: " + object);
		}
		return objectWithGetAndSet.getField();
	}

	/**
	 * @param expression
	 * @param object
	 * @return Getter method for the property expression
	 * @throws WicketRuntimeException if there is no getter method
	 */
	public static Method getPropertyGetter(final String expression, final Object object)
	{
		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("Null object returned for expression: " + expression +
				" for getting the target class of: " + object);
		}
		return objectWithGetAndSet.getGetter();
	}

	/**
	 * @param expression
	 * @param object
	 * @return Setter method for the property expression
	 * @throws WicketRuntimeException if there is no setter method
	 */
	public static Method getPropertySetter(final String expression, final Object object)
	{
		ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS);
		if (objectWithGetAndSet == null)
		{
			throw new WicketRuntimeException("Null object returned for expression: " + expression +
				" for getting the target class of: " + object);
		}
		return objectWithGetAndSet.getSetter();
	}

	/**
	 * Just delegating the call to the original getObjectAndGetSetter passing the object type as
	 * parameter.
	 *
	 * @param expression
	 * @param object
	 * @param tryToCreateNull
	 * @return {@link ObjectWithGetAndSet}
	 */
	private static ObjectWithGetAndSet getObjectWithGetAndSet(final String expression,
		final Object object, int tryToCreateNull)
	{
		return getObjectWithGetAndSet(expression, object, tryToCreateNull, object.getClass());
	}

	/**
	 * Receives the class parameter also, since this method can resolve the type for some
	 * expression, only knowing the target class.
	 *
	 * @param expression property expression
	 * @param object root object
	 * @param tryToCreateNull how should null values be handled
	 * @param clz owning clazz
	 * @return final getAndSet and the target to apply it on, or {@code null} if expression results in an intermediate null
	 */
	private static ObjectWithGetAndSet getObjectWithGetAndSet(final String expression, final Object object, final int tryToCreateNull, Class<?> clz)
	{
		String expressionBracketsSeperated = Strings.replaceAll(expression, "[", ".[").toString();
		int index = getNextDotIndex(expressionBracketsSeperated, 0);
		while (index == 0 && expressionBracketsSeperated.startsWith("."))
		{
			// eat dots at the beginning of the expression since they will confuse
			// later steps
			expressionBracketsSeperated = expressionBracketsSeperated.substring(1);
			index = getNextDotIndex(expressionBracketsSeperated, 0);
		}
		int lastIndex = 0;
		Object value = object;
		String exp = expressionBracketsSeperated;
		while (index != -1)
		{
			exp = expressionBracketsSeperated.substring(lastIndex, index);
			if (exp.length() == 0)
			{
				exp = expressionBracketsSeperated.substring(index + 1);
				break;
			}

			IGetAndSet getAndSet;
			try
			{
				getAndSet = getGetAndSet(exp, clz);
			}
			catch (WicketRuntimeException ex)
			{
				// expression by itself can't be found. try combined with the following
				// expression (e.g. for a indexed property);
				int temp = getNextDotIndex(expressionBracketsSeperated, index + 1);
				if (temp == -1)
				{
					exp = expressionBracketsSeperated.substring(lastIndex);
					break;
				} else {
					index = temp;
					continue;
				}
			}
			Object nextValue = null;
			if (value != null)
			{
				nextValue = getAndSet.getValue(value);
			}
			if (nextValue == null)
			{
				if (tryToCreateNull == CREATE_NEW_VALUE)
				{
					nextValue = getAndSet.newValue(value);
					if (nextValue == null)
					{
						return null;
					}
				}
				else if (tryToCreateNull == RESOLVE_CLASS)
				{
					clz = getAndSet.getTargetClass();
				}
				else
				{
					return null;
				}
			}
			value = nextValue;
			if (value != null)
			{
				// value can be null if we are in the RESOLVE_CLASS
				clz = value.getClass();
			}

			lastIndex = index + 1;
			index = getNextDotIndex(expressionBracketsSeperated, lastIndex);
			if (index == -1)
			{
				exp = expressionBracketsSeperated.substring(lastIndex);
				break;
			}
		}
		IGetAndSet getAndSet = getGetAndSet(exp, clz);
		return new ObjectWithGetAndSet(getAndSet, value);
	}

	/**
	 *
	 * @param expression
	 * @param start
	 * @return next dot index
	 */
	private static int getNextDotIndex(final String expression, final int start)
	{
		boolean insideBracket = false;
		for (int i = start; i < expression.length(); i++)
		{
			char ch = expression.charAt(i);
			if (ch == '.' && !insideBracket)
			{
				return i;
			}
			else if (ch == '[')
			{
				insideBracket = true;
			}
			else if (ch == ']')
			{
				insideBracket = false;
			}
		}
		return -1;
	}

	private static IGetAndSet getGetAndSet(String exp, final Class<?> clz)
	{
		IPropertyLocator locator = getLocator();
		
		IGetAndSet getAndSet = locator.get(clz, exp);
		if (getAndSet == null) {
			throw new WicketRuntimeException(
					"Property could not be resolved for class: " + clz + " expression: " + exp);
		}
		
		return getAndSet;
	}

	/**
	 * Utility class: instantiation not allowed.
	 */
	private PropertyResolver()
	{
	}

	/**
	 * @author jcompagner
	 *
	 */
	private final static class ObjectWithGetAndSet
	{
		private final IGetAndSet getAndSet;
		private final Object value;

		/**
		 * @param getAndSet
		 * @param value
		 */
		public ObjectWithGetAndSet(IGetAndSet getAndSet, Object value)
		{
			this.getAndSet = getAndSet;
			this.value = value;
		}

		/**
		 * @param value
		 * @param converter
		 */
		public void setValue(Object value, PropertyResolverConverter converter)
		{
			getAndSet.setValue(this.value, value, converter);
		}

		/**
		 * @return The value
		 */
		public Object getValue()
		{
			return getAndSet.getValue(value);
		}

		/**
		 * @return class of property value
		 */
		public Class<?> getTargetClass()
		{
			return getAndSet.getTargetClass();
		}

		/**
		 * @return Field or null if no field exists for expression
		 */
		public Field getField()
		{
			return getAndSet.getField();
		}

		/**
		 * @return Getter method or null if no getter exists for expression
		 */
		public Method getGetter()
		{
			return getAndSet.getGetter();
		}

		/**
		 * @return Setter method or null if no setter exists for expression
		 */
		public Method getSetter()
		{
			return getAndSet.getSetter();
		}
	}

	/**
	 * A property to get and set.
	 * 
	 * @author jcompagner
	 */
	public interface IGetAndSet
	{
		/**
		 * @param object
		 *            The object where the value must be taken from.
		 *
		 * @return The value of this property
		 */
		Object getValue(final Object object);

		/**
		 * @return The target class of the object that as to be set.
		 */
		Class<?> getTargetClass();

		/**
		 * @param object
		 *            The object where the new value must be set on.
		 *
		 * @return The new value for the property that is set back on that object.
		 */
		Object newValue(Object object);

		/**
		 * @param object
		 * @param value
		 * @param converter
		 */
		void setValue(final Object object, final Object value,
			PropertyResolverConverter converter);

		/**
		 * @return Field or null if there is no field
		 */
		Field getField();

		/**
		 * @return Getter method or null if there is no getter
		 */
		Method getGetter();

		/**
		 * @return Setter of null if there is no setter
		 */
		Method getSetter();
	}

	public static abstract class AbstractGetAndSet implements IGetAndSet
	{
		/**
		 * {@inheritDoc}
		 */
		@Override
		public Field getField()
		{
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Method getGetter()
		{
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Method getSetter()
		{
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return null;
		}
	}

	private static final class MapGetAndSet extends AbstractGetAndSet
	{
		private final String key;

		MapGetAndSet(String key)
		{
			this.key = key;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(final Object object)
		{
			return ((Map<?, ?>)object).get(key);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		@SuppressWarnings("unchecked")
		public void setValue(final Object object, final Object value,
			final PropertyResolverConverter converter)
		{
			((Map<String, Object>)object).put(key, value);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(final Object object)
		{
			// Map can't make a newValue or should it look what is more in the
			// map and try to make one of the class if finds?
			return null;
		}
	}

	private static final class ListGetAndSet extends AbstractGetAndSet
	{
		final private int index;

		ListGetAndSet(int index)
		{
			this.index = index;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(final Object object)
		{
			if (((List<?>)object).size() <= index)
			{
				return null;
			}
			return ((List<?>)object).get(index);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		@SuppressWarnings("unchecked")
		public void setValue(final Object object, final Object value,
			final PropertyResolverConverter converter)
		{
			List<Object> lst = (List<Object>)object;

			if (lst.size() > index)
			{
				lst.set(index, value);
			}
			else if (lst.size() == index)
			{
				lst.add(value);
			}
			else
			{
				while (lst.size() < index)
				{
					lst.add(null);
				}
				lst.add(value);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(Object object)
		{
			// List can't make a newValue or should it look what is more in the
			// list and try to make one of the class if finds?
			return null;
		}
	}

	private static final class ArrayGetAndSet extends AbstractGetAndSet
	{
		private final int index;
		private final Class<?> clzComponentType;

		ArrayGetAndSet(Class<?> clzComponentType, int index)
		{
			this.clzComponentType = clzComponentType;
			this.index = index;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(Object object)
		{
			if (Array.getLength(object) > index)
			{
				return Array.get(object, index);
			}
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setValue(Object object, Object value, PropertyResolverConverter converter)
		{
			value = converter.convert(value, clzComponentType);
			Array.set(object, index, value);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(Object object)
		{
			Object value = null;
			try
			{
				value = clzComponentType.getDeclaredConstructor().newInstance();
				Array.set(object, index, value);
			}
			catch (Exception e)
			{
				log.warn("Cannot set new value " + value + " at index " + index +
					" for array holding elements of class " + clzComponentType, e);
			}
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return clzComponentType;
		}
	}

	private static final class ArrayLengthGetAndSet extends AbstractGetAndSet
	{
		ArrayLengthGetAndSet()
		{
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(final Object object)
		{
			return Array.getLength(object);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setValue(final Object object, final Object value,
			final PropertyResolverConverter converter)
		{
			throw new WicketRuntimeException("You can't set the length on an array:" + object);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(final Object object)
		{
			throw new WicketRuntimeException("Can't get a new value from a length of an array: " +
				object);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return int.class;
		}
	}

	private static final class IndexedPropertyGetAndSet extends AbstractGetAndSet
	{
		final private Integer index;
		final private Method getMethod;
		private Method setMethod;

		IndexedPropertyGetAndSet(final Method method, final int index)
		{
			this.index = index;
			getMethod = method;
			getMethod.setAccessible(true);
		}

		private static Method findSetter(final Method getMethod, final Class<?> clz)
		{
			String name = getMethod.getName();
			name = SET + name.substring(3);
			try
			{
				return clz.getMethod(name, new Class[] { int.class, getMethod.getReturnType() });
			}
			catch (Exception e)
			{
				log.debug("Can't find setter method corresponding to " + getMethod);
			}
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(Object object)
		{
			Object ret;
			try
			{
				ret = getMethod.invoke(object, index);
			}
			catch (InvocationTargetException ex)
			{
				throw new WicketRuntimeException("Error calling index property method: " +
					getMethod + " on object: " + object, ex.getCause());
			}
			catch (Exception ex)
			{
				throw new WicketRuntimeException("Error calling index property method: " +
					getMethod + " on object: " + object, ex);
			}
			return ret;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setValue(final Object object, final Object value,
			final PropertyResolverConverter converter)
		{
			if (setMethod == null)
			{
				setMethod = findSetter(getMethod, object.getClass());
			}
			if (setMethod != null)
			{
				setMethod.setAccessible(true);
				Object converted = converter.convert(value, getMethod.getReturnType());
				if (converted == null && value != null)
				{
					throw new ConversionException("Can't convert value: " + value + " to class: " +
						getMethod.getReturnType() + " for setting it on " + object);
				}
				try
				{
					setMethod.invoke(object, index, converted);
				}
				catch (InvocationTargetException ex)
				{
					throw new WicketRuntimeException("Error index property calling method: " +
						setMethod + " on object: " + object, ex.getCause());
				}
				catch (Exception ex)
				{
					throw new WicketRuntimeException("Error index property calling method: " +
						setMethod + " on object: " + object, ex);
				}
			}
			else
			{
				throw new WicketRuntimeException("No set method defined for value: " + value +
					" on object: " + object);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return getMethod.getReturnType();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(Object object)
		{
			if (setMethod == null)
			{
				setMethod = findSetter(getMethod, object.getClass());
			}

			if (setMethod == null)
			{
				log.warn("Null setMethod");
				return null;
			}

			Class<?> clz = getMethod.getReturnType();
			Object value = null;
			try
			{
				value = clz.getDeclaredConstructor().newInstance();
				setMethod.invoke(object, index, value);
			}
			catch (Exception e)
			{
				log.warn("Cannot set new value " + value + " at index " + index, e);
			}
			return value;
		}
	}

	private static final class MethodGetAndSet extends AbstractGetAndSet
	{
		private final Method getMethod;
		private final Method setMethod;
		private final Field field;

		MethodGetAndSet(Method getMethod, Method setMethod, Field field)
		{
			this.getMethod = getMethod;
			this.getMethod.setAccessible(true);
			this.field = field;
			this.setMethod = setMethod;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public final Object getValue(final Object object)
		{
			Object ret;
			try
			{
				ret = getMethod.invoke(object, (Object[])null);
			}
			catch (InvocationTargetException ex)
			{
				throw new WicketRuntimeException("Error calling method: " + getMethod +
					" on object: " + object, ex.getCause());
			}
			catch (Exception ex)
			{
				throw new WicketRuntimeException("Error calling method: " + getMethod +
					" on object: " + object, ex);
			}
			return ret;
		}

		/**
		 * @param object
		 * @param value
		 * @param converter
		 */
		@Override
		public final void setValue(final Object object, final Object value,
			PropertyResolverConverter converter)
		{
			Class<?> type = null;
			if (setMethod != null)
			{
				type = setMethod.getParameterTypes()[0];
			}
			else if (field != null)
			{
				type = field.getType();
			}

			Object converted = null;
			if (type != null)
			{
				converted = converter.convert(value, type);
				if (converted == null)
				{
					if (value != null)
					{
						throw new ConversionException("Method [" + getMethod +
							"]. Can't convert value: " + value + " to class: " +
							type + " for setting it on " + object);
					}
					else if (setMethod != null && type.isPrimitive())
					{
						throw new ConversionException("Method [" + setMethod +
							"]. Can't convert null value to a primitive class: " +
							type + " for setting it on " + object);
					}
				}
			}

			if (setMethod != null)
			{
				try
				{
					setMethod.invoke(object, converted);
				}
				catch (InvocationTargetException ex)
				{
					throw new WicketRuntimeException("Error calling method: " + setMethod +
						" on object: " + object, ex.getCause());
				}
				catch (Exception ex)
				{
					throw new WicketRuntimeException("Error calling method: " + setMethod +
						" on object: " + object, ex);
				}
			}
			else if (field != null)
			{
				try
				{
					field.set(object, converted);
				}
				catch (Exception ex)
				{
					throw new WicketRuntimeException("Error setting field: " + field +
						" on object: " + object, ex);
				}
			}
			else
			{
				throw new WicketRuntimeException("no set method defined for value: " + value +
					" on object: " + object + " while respective getMethod being " +
					getMethod.getName());
			}
		}

		private static Method findSetter(Method getMethod, Class<?> clz)
		{
			String name = getMethod.getName();
			if (name.startsWith(GET))
			{
				name = SET + name.substring(3);
			}
			else
			{
				name = SET + name.substring(2);
			}
			try
			{
				Method method = clz.getMethod(name, getMethod.getReturnType());
				if (method != null)
				{
					method.setAccessible(true);
				}
				return method;
			}
			catch (NoSuchMethodException e)
			{
				Method[] methods = clz.getMethods();
				for (Method method : methods)
				{
					if (method.getName().equals(name))
					{
						Class<?>[] parameterTypes = method.getParameterTypes();
						if (parameterTypes.length == 1)
						{
							if (parameterTypes[0].isAssignableFrom(getMethod.getReturnType()))
							{
								return method;
							}
						}
					}
				}
				log.debug("Cannot find setter corresponding to " + getMethod, e);
			}
			catch (Exception e)
			{
				log.debug("Cannot find setter corresponding to " + getMethod, e);
			}
			return null;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(Object object)
		{
			if (setMethod == null)
			{
				log.warn("Null setMethod");
				return null;
			}

			Class<?> clz = getMethod.getReturnType();
			Object value = null;
			try
			{
				value = clz.getDeclaredConstructor().newInstance();
				setMethod.invoke(object, value);
			}
			catch (Exception e)
			{
				log.warn("Cannot set new value " + value, e);
			}
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return getMethod.getReturnType();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Method getGetter()
		{
			return getMethod;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Method getSetter()
		{
			return setMethod;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Field getField()
		{
			return field;
		}
	}

	/**
	 * @author jcompagner
	 */
	private static class FieldGetAndSet extends AbstractGetAndSet
	{
		private final Field field;

		/**
		 * Construct.
		 *
		 * @param field
		 */
		public FieldGetAndSet(final Field field)
		{
			super();
			this.field = field;
			this.field.setAccessible(true);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object getValue(final Object object)
		{
			try
			{
				return field.get(object);
			}
			catch (Exception ex)
			{
				throw new WicketRuntimeException("Error getting field value of field " + field +
					" from object " + object, ex);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Object newValue(final Object object)
		{
			Class<?> clz = field.getType();
			Object value = null;
			try
			{
				value = clz.getDeclaredConstructor().newInstance();
				field.set(object, value);
			}
			catch (Exception e)
			{
				log.warn("Cannot set field " + field + " to " + value, e);
			}
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setValue(final Object object, Object value,
			final PropertyResolverConverter converter)
		{
			value = converter.convert(value, field.getType());
			try
			{
				field.set(object, value);
			}
			catch (Exception ex)
			{
				throw new WicketRuntimeException("Error setting field value of field " + field +
					" on object " + object + ", value " + value, ex);
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Class<?> getTargetClass()
		{
			return field.getType();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Field getField()
		{
			return field;
		}
	}

	/**
	 * Clean up cache for this app.
	 *
	 * @param application
	 */
	public static void destroy(Application application)
	{
		applicationToLocators.remove(application);
	}

	/**
	 * Get the current {@link IPropertyLocator}.
	 * 
	 * @return locator for the current {@link Application} or a general one if no current application is present
	 * @see Application#get()
	 */
	public static IPropertyLocator getLocator()
	{
		Object key;
		if (Application.exists())
		{
			key = Application.get();
		}
		else
		{
			key = PropertyResolver.class;
		}
		IPropertyLocator result = applicationToLocators.get(key);
		if (result == null)
		{
			IPropertyLocator tmpResult = applicationToLocators.putIfAbsent(key, result = new CachingPropertyLocator(new DefaultPropertyLocator()));
			if (tmpResult != null)
			{
				result = tmpResult;
			}
		}
		return result;
	}

	/**
	 * Set a locator for the given application.
	 * 
	 * @param application application, may be {@code null}
	 * @param locator locator
	 */
	public static void setLocator(final Application application, final IPropertyLocator locator)
	{
		if (application == null)
		{
			applicationToLocators.put(PropertyResolver.class, locator);
		}
		else
		{
			applicationToLocators.put(application, locator);
		}
	}

	/**
	 * A locator of properties.
	 * 
	 * @see <a href="https://issues.apache.org/jira/browse/WICKET-5623">WICKET-5623</a>
	 */
	public interface IPropertyLocator
	{
		/**
		 * Get {@link IGetAndSet} for a property.
		 * 
		 * @param clz owning class
		 * @param exp identifying the property
		 * @return getAndSet or {@code null} if non located
		 */
		IGetAndSet get(Class<?> clz, String exp);
	}

	/**
	 * A wrapper for another {@link IPropertyLocator} that caches results of {@link #get(Class, String)}.
	 */
	public static class CachingPropertyLocator implements IPropertyLocator
	{
		private final ConcurrentHashMap<String, IGetAndSet> map = Generics.newConcurrentHashMap(16);
		
		/**
		 * Special token to put into the cache representing no located {@link IGetAndSet}. 
		 */
		private IGetAndSet NONE = new AbstractGetAndSet() {

			@Override
			public Object getValue(Object object) {
				return null;
			}

			@Override
			public Object newValue(Object object) {
				return null;
			}

			@Override
			public void setValue(Object object, Object value, PropertyResolverConverter converter) {
			}
		};

		private IPropertyLocator locator;

		public CachingPropertyLocator(IPropertyLocator locator) {
			this.locator = locator;
		}

		@Override
		public IGetAndSet get(Class<?> clz, String exp) {
			String key = clz.getName() + "#" + exp;
			
			IGetAndSet located = map.get(key);
			if (located == null) {
				located = locator.get(clz, exp);
				if (located == null) {
					located = NONE;
				}
				map.put(key, located);
			}
			
			if (located == NONE) {
				located = null;
			}
			
			return located;
		}
	}

	/**
	 * Default locator supporting <em>Java Beans</em> properties, maps, lists and method invocations.
	 */
	public static class DefaultPropertyLocator implements IPropertyLocator
	{
		@Override
		public IGetAndSet get(Class<?> clz, String exp) {
			IGetAndSet getAndSet = null;
			
			Method method = null;
			Field field;
			if (exp.startsWith("["))
			{
				// if expression begins with [ skip method finding and use it as
				// a key/index lookup on a map.
				exp = exp.substring(1, exp.length() - 1);
			}
			else if (exp.endsWith("()"))
			{
				// if expression ends with (), don't test for setters just skip
				// directly to method finding.
				method = findMethod(clz, exp);
			}
			else
			{
				method = findGetter(clz, exp);
			}
			if (method == null)
			{
				if (List.class.isAssignableFrom(clz))
				{
					try
					{
						int index = Integer.parseInt(exp);
						getAndSet = new ListGetAndSet(index);
					}
					catch (NumberFormatException ex)
					{
						// can't parse the exp as an index, maybe the exp was a
						// method.
						method = findMethod(clz, exp);
						if (method != null)
						{
							getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter(
								method, clz), null);
						}
						else
						{
							field = findField(clz, exp);
							if (field != null)
							{
								getAndSet = new FieldGetAndSet(field);
							}
							else
							{
								throw new WicketRuntimeException(
									"The expression '" +
										exp +
										"' is neither an index nor is it a method or field for the list " +
										clz);
							}
						}
					}
				}
				else if (Map.class.isAssignableFrom(clz))
				{
					getAndSet = new MapGetAndSet(exp);
				}
				else if (clz.isArray())
				{
					try
					{
						int index = Integer.parseInt(exp);
						getAndSet = new ArrayGetAndSet(clz.getComponentType(), index);
					}
					catch (NumberFormatException ex)
					{
						if (exp.equals("length") || exp.equals("size"))
						{
							getAndSet = new ArrayLengthGetAndSet();
						}
						else
						{
							throw new WicketRuntimeException("Can't parse the expression '" + exp +
								"' as an index for an array lookup");
						}
					}
				}
				else
				{
					field = findField(clz, exp);
					if (field == null)
					{
						method = findMethod(clz, exp);
						if (method == null)
						{
							int index = exp.indexOf('.');
							if (index != -1)
							{
								String propertyName = exp.substring(0, index);
								String propertyIndex = exp.substring(index + 1);
								try
								{
									int parsedIndex = Integer.parseInt(propertyIndex);
									// if so then it could be a getPropertyIndex(int)
									// and setPropertyIndex(int, object)
									String name = Character.toUpperCase(propertyName.charAt(0)) +
										propertyName.substring(1);
									method = clz.getMethod(GET + name, int.class);
									getAndSet = new IndexedPropertyGetAndSet(method, parsedIndex);
								}
								catch (Exception e)
								{
									throw new WicketRuntimeException(
										"No get method defined for class: " + clz +
											" expression: " + propertyName);
								}
							}
						}
						else
						{
							getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter(
								method, clz), null);
						}
					}
					else
					{
						getAndSet = new FieldGetAndSet(field);
					}
				}
			}
			else
			{
				field = findField(clz, exp);
				getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter(method, clz),
					field);
			}
			
			return getAndSet;
		}
		
		/**
		 * @param clz
		 * @param expression
		 * @return introspected field
		 */
		private Field findField(final Class<?> clz, final String expression)
		{
			Field field = null;
			try
			{
				field = clz.getField(expression);
			}
			catch (Exception e)
			{
				Class<?> tmp = clz;
				while (tmp != null && tmp != Object.class)
				{
					Field[] fields = tmp.getDeclaredFields();
					for (Field aField : fields)
					{
						if (aField.getName().equals(expression))
						{
							aField.setAccessible(true);
							return aField;
						}
					}
					tmp = tmp.getSuperclass();
				}
				log.debug("Cannot find field " + clz + "." + expression);
			}
			return field;
		}

		/**
		 * @param clz
		 * @param expression
		 * @return The method for the expression null if not found
		 */
		private Method findGetter(final Class<?> clz, final String expression)
		{
			String name = Character.toUpperCase(expression.charAt(0)) + expression.substring(1);
			Method method = null;
			try
			{
				method = clz.getMethod(GET + name, (Class[])null);
			}
			catch (Exception ignored)
			{
			}
			if (method == null)
			{
				try
				{
					method = clz.getMethod(IS + name, (Class[])null);
				}
				catch (Exception e)
				{
					log.debug("Cannot find getter " + clz + "." + expression);
				}
			}
			return method;
		}

		private Method findMethod(final Class<?> clz, String expression)
		{
			if (expression.endsWith("()"))
			{
				expression = expression.substring(0, expression.length() - 2);
			}
			Method method = null;
			try
			{
				method = clz.getMethod(expression, (Class[])null);
			}
			catch (Exception e)
			{
				log.debug("Cannot find method " + clz + "." + expression);
			}
			return method;
		}
	}
}
