/* Fields.java

	Purpose:
		
	Description:
		
	History:
		Thu Oct 28 14:40:50     2004, Created by tomyeh

Copyright (C) 2004 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.lang.reflect;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.AccessibleObject;
import java.util.Map;

import org.zkoss.lang.Classes;
import org.zkoss.lang.SystemException;

/**
 * Utilities to access fields.
 *
 * @author tomyeh
 */
public class Fields {
	/** Changes the accessibility without throwing any exception.
	 * @since 3.0.3
	 */
	public static final void setAccessible(AccessibleObject f, boolean accessible) {
		try {
			f.setAccessible(accessible);
		} catch (Throwable t) {
		}
	}
	/** Returns the value of the specified public field or public
	 * method of the object.
	 *
	 * <p>If getByCompound(obj, "a.b.c") is called and obj.getA() or
	 * obj.getA().getB() returns null, the result is null.
	 * However, NullPointerException is thrown if obj is null.
	 *
	 * @param name the field name. It can be in form of "a.b.c", but cannot
	 * be an expression.
	 * It must be a public field or public method (prefixed with set).
	 * @exception NoSuchMethodException if no corresponding field.
	 */
	public static final Object getByCompound(Object obj, String name)
	throws NoSuchMethodException {
		for (;;) {
			final int j = name.indexOf('.');
			if (j < 0)
				return get(obj, name);

			obj = get(obj, name.substring(0, j));
			if (obj == null)
				return obj;
			name = name.substring(j + 1);
		}
	}
	/** Sets the value of the specified public field or public
	 * method in the object.
	 *
	 * @param autoCoerce whether to automatically convert val to the proper
	 * class that matches the argument of method or field.
	 */
	public static final
	void setByCompound(Object obj, String name, Object val,
	boolean autoCoerce) throws NoSuchMethodException {
		for (;;) {
			final int j = name.indexOf('.');
			if (j < 0) {
				set(obj, name, val, autoCoerce);
				return;
			}

			obj = get(obj, name.substring(0, j));
			//Unlike getByCompound, obj==null is considered error here
			name = name.substring(j + 1);
		}
	}
	/** Sets the value of the specified public field  or public method
	 * in the object, without converting the specified val.
	 *
	 * <p>It is a shortcut of setByCompound(obj, name, val, false).
	 *
	 * @param name the field name. It can be in form of "a.b.c", but cannot
	 * be an expression.
	 * It must be a public field or public method (prefixed with set).
	 */
	public static final
	void setByCompound(Object obj, String name, Object val)
	throws NoSuchMethodException {
		setByCompound(obj, name, val, false);
	}

	/** Returns the value of the specified public field or public method
	 * in the object.
	 * Unlike {@link #getByCompound}, name cannot contain '.'.
	 * In other words, it must be the name of a public field
	 * or method (prefixed with get).
	 */
	public static final Object get(Object obj, String name)
	throws NoSuchMethodException {
		try {
			final AccessibleObject acs = Classes.getAccessibleObject(
				obj.getClass(), name, null,
				Classes.B_GET|Classes.B_PUBLIC_ONLY);
			return 	acs instanceof Method ?
				((Method)acs).invoke(obj): ((Field)acs).get(obj);
		} catch (NoSuchMethodException ex) {
			if (obj instanceof Map) {
				return ((Map) obj).get(name);
			}
			throw ex;
		} catch (Exception ex) {
			throw SystemException.Aide.wrap(ex, "Not found: " + name);
		}
	}
	/** Sets the value of the specified public filed or public method
	 * in the object.
	 * Unlike {@link #setByCompound}, name cannot contain '.'.
	 * In other words, it must be the name of a public field
	 * or method (prefixed with set).
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static final void set(Object obj, String name, Object val,
	boolean autoCoerce) throws NoSuchMethodException {
		try {
			AccessibleObject acs;
			try {
				acs = Classes.getAccessibleObject(
					obj.getClass(),
					name, new Class[] {val != null ? val.getClass(): null},
					Classes.B_SET|Classes.B_PUBLIC_ONLY);
			} catch (NoSuchMethodException ex) {
				if (!autoCoerce || val == null)
					throw ex;

				//retry without specifying any argument type
				acs = Classes.getAccessibleObject(
					obj.getClass(), name, new Class[] {null},
					Classes.B_SET|Classes.B_PUBLIC_ONLY);
			}
			if (acs instanceof Method) {
				final Method mtd = (Method)acs;
				mtd.invoke(obj,
					autoCoerce ? Classes.coerce(mtd.getParameterTypes()[0], val): val);
			} else {
				final Field fld = (Field)acs;
				fld.set(obj,
					autoCoerce ? Classes.coerce(fld.getType(), val): val);
			}
		} catch (NoSuchMethodException ex) {
			if (obj instanceof Map) {
				((Map) obj).put(name, val);
				return;
			}
			throw ex;
		} catch (Exception ex) {
			throw SystemException.Aide.wrap(ex, "Not found: " + name);
		}
	}
}