/**
 * Copyright 2010-2014 Jingjing Li.
 *
 * This file is part of jplot2d.
 *
 * jplot2d is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 *
 * jplot2d is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with jplot2d. If not, see .
 */
package org.jplot2d.env;

import java.beans.IntrospectionException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.jplot2d.annotation.Hierarchy;
import org.jplot2d.annotation.HierarchyOp;
import org.jplot2d.annotation.Property;
import org.jplot2d.annotation.PropertyGroup;

/**
 * This class extract properties from method declaration, rather than using java introspector. The property annotation
 * is defined on getter. if a sub-interface override a getter, the overriding method will take effect. The setter method
 * type must match the getter's return type. setter methods with same name but different argument type are not allowed.
 * The indexed getter and setter are not considered.
 * 
 * @author Jingjing Li
 * 
 */
public class InterfaceInfo {

	private static final String IS_PREFIX = "is";
	private static final String GET_PREFIX = "get";
	private static final String SET_PREFIX = "set";

	private static Map, InterfaceInfo> interfaceInfoCache = new HashMap, InterfaceInfo>();

	/**
	 * property name to PropertyInfo map. It contains all properties no matter if it has @property annotation.
	 */
	private final Map piMap;

	private final Map propReadMap = new HashMap();

	private final Map propWriteMap = new HashMap();

	/** listener get add remove Methods */
	private final Collection listenerGarMethods = new HashSet();

	/**
	 * Hierarchy annotated Method to HierarchyOp map
	 */
	private final Map hierachyMethodMap = new HashMap();

	private final Map pisGroupMap = new LinkedHashMap();

	/**
	 * Load interface info for the given interface and its parents.The subclass may hide the info of its superclass. For
	 * example, if a sub-interface only redeclare a getter, the info will report the property is read-only.
	 * 
	 * @param interfaceClass
	 */
	public static synchronized InterfaceInfo loadInterfaceInfo(Class interfaceClass) {
		InterfaceInfo iinfo = interfaceInfoCache.get(interfaceClass);
		if (iinfo == null) {
			iinfo = new InterfaceInfo(interfaceClass);
			interfaceInfoCache.put(interfaceClass, iinfo);
		}
		return iinfo;
	}

	private InterfaceInfo(Class interfaceClass) {
		// get an array of all the public methods at all level
		Method methods[] = interfaceClass.getMethods();

		try {
			piMap = getPropertyInfo(methods);
		} catch (IntrospectionException e) {
			throw new Error(e);
		}

		loadGroup(interfaceClass);
		load(interfaceClass);

		for (Method method : methods) {
			Hierarchy hierAnn = method.getAnnotation(Hierarchy.class);
			if (hierAnn != null) {
				hierachyMethodMap.put(method, hierAnn.value());
			}
		}

	}

	private void loadGroup(Class interfaceClass) {
		// load parents
		for (Class superClass : interfaceClass.getInterfaces()) {
			loadGroup(superClass);
		}
		// load this interface
		PropertyGroup pg = interfaceClass.getAnnotation(PropertyGroup.class);
		if (pg != null) {
			pisGroupMap.put(pg.value(), null);
		}
	}

	private void load(Class beanClass) {

		Map> pilGroupMap = new HashMap>();

		for (PropertyInfo p : piMap.values()) {
			Method readMethod = p.getReadMethod();
			Method writeMethod = p.getWriteMethod();
			if (readMethod != null) {
				Property pann = readMethod.getAnnotation(Property.class);
				if (pann != null) {
					p.setDisplayName(pann.displayName());
					p.setDisplayDigits(pann.displayDigits());
					p.setShortDescription(pann.description());
					p.setReadOnly(pann.readOnly());
					p.setOrder(pann.order());
					// only writable property can be in profile
					p.setStyleable(pann.styleable() && writeMethod != null);
					PropertyGroup pg = readMethod.getDeclaringClass().getAnnotation(PropertyGroup.class);
					if (pg != null) {
						List pis = pilGroupMap.get(pg.value());
						if (pis == null) {
							pis = new ArrayList();
							pilGroupMap.put(pg.value(), pis);
						}
						pis.add(p);
					}
				}

				propReadMap.put(readMethod, p);
				if (writeMethod != null) {
					propWriteMap.put(writeMethod, p);
				}
			}
		}

		// sort value array of pisGroupMap
		Iterator> itr = pisGroupMap.entrySet().iterator();
		while (itr.hasNext()) {
			Map.Entry me = itr.next();
			List gpdList = pilGroupMap.get(me.getKey());
			if (gpdList != null) {
				PropertyInfo[] gpds = gpdList.toArray(new PropertyInfo[gpdList.size()]);
				Arrays.sort(gpds);
				me.setValue(gpds);
			} else {
				itr.remove();
			}
		}

	}

	public PropertyInfo getPropertyInfo(String pname) {
		return piMap.get(pname);
	}

	public Map getPropertyInfoGroupMap() {
		return pisGroupMap;
	}

	public Map getProfilePropertyInfoGroupMap() {
		Map result = new LinkedHashMap();

		for (Map.Entry me : pisGroupMap.entrySet()) {
			String group = me.getKey();
			List ppl = new ArrayList();
			for (PropertyInfo pi : me.getValue()) {
				if (pi.isStyleable()) {
					ppl.add(pi);
				}
			}
			if (ppl.size() > 0) {
				PropertyInfo[] pps = ppl.toArray(new PropertyInfo[ppl.size()]);
				result.put(group, pps);
			}
		}

		return result;
	}

	/**
	 * Returns true if the given property is writable.
	 * 
	 * @param pname
	 *            The property name
	 * @return true if the given property is writable
	 */
	public boolean isWritableProp(String pname) {
		if (piMap.containsKey(pname)) {
			return piMap.get(pname).getWriteMethod() != null;
		}
		return false;
	}

	/**
	 * Returns true if the given method is a property getter.
	 * 
	 * @param method
	 * @return
	 */
	protected boolean isPropReadMethod(Method method) {
		return propReadMap.containsKey(method);
	}

	/**
	 * Returns true if the given method is a property setter.
	 * 
	 * @param method
	 * @return
	 */
	protected boolean isPropWriteMethod(Method method) {
		return propWriteMap.containsKey(method);
	}

	/**
	 * Returns true if the given setter is disabled by property annotation.
	 * 
	 * @param method
	 * @return
	 */
	protected boolean isPropWriteDisabled(Method method) {
		PropertyInfo pinfo = propWriteMap.get(method);
		if (pinfo == null) {
			throw new IllegalArgumentException("The method " + method.getName() + " is not a property writter.");
		}
		return pinfo.isReadOnly();
	}

	/**
	 * Returns the getter of the given setter.
	 * 
	 * @param method
	 * @return
	 */
	protected Method getPropReadMethodByWriteMethod(Method method) {
		PropertyInfo pinfo = propWriteMap.get(method);
		if (pinfo == null) {
			throw new IllegalArgumentException("The method " + method.getName() + " is not a property writter.");
		}
		return pinfo.getReadMethod();
	}

	protected boolean isListenerMethod(Method method) {
		return listenerGarMethods.contains(method);
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isGetCompMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.GET;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isGetCompArrayMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.GETARRAY;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isAddCompMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.ADD;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isRemoveCompMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.REMOVE;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isJoinElementMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.JOIN;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isRefElementMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.REF;
	}

	/**
	 * @param method
	 * @return
	 */
	protected boolean isRef2ElementMethod(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.REF2;
	}

	protected boolean isAddRef2Method(Method method) {
		HierarchyOp hop = hierachyMethodMap.get(method);
		return hop != null && hop == HierarchyOp.ADD_REF2;
	}

	/**
	 * Extract an array of PropertyDescriptor from the given methods.
	 * 
	 * @return An array of PropertyDescriptors, with the order of getter declared.
	 * @throws IntrospectionException
	 */
	private static Map getPropertyInfo(Method[] methods) throws IntrospectionException {

		Map pdReaderMap = new LinkedHashMap();
		Map pdWriterMap = new HashMap();

		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method == null) {
				continue;
			}
			// skip static methods.
			int mods = method.getModifiers();
			if (Modifier.isStatic(mods)) {
				continue;
			}
			String name = method.getName();
			// Optimization. Don't bother with invalid propertyNames.
			if (name.length() <= 3 && !name.startsWith(IS_PREFIX)) {
				continue;
			}

			Class[] argTypes = method.getParameterTypes();
			Class resultType = method.getReturnType();
			int argCount = argTypes.length;

			if (argCount == 0) {
				String pname = null;
				if (name.startsWith(GET_PREFIX)) {
					// Simple getter
					pname = decapitalize(name.substring(3));
				} else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
					// Boolean getter
					pname = decapitalize(name.substring(2));
				}
				if (pname != null) {
					// replace the overridden getter method
					Method pre = pdReaderMap.get(pname);
					if (pre == null) {
						pdReaderMap.put(pname, method);
					} else if (pre.getReturnType().isAssignableFrom(method.getReturnType())) {
						pdReaderMap.put(pname, method);
					} else if (method.getReturnType().isAssignableFrom(pre.getReturnType())) {
						// the return type of exist getter is more precise
					} else {
						throw new IntrospectionException("getter name conflict: " + name);
					}
				}
			} else if (argCount == 1) {
				if (resultType == void.class && name.startsWith(SET_PREFIX)) {
					// Simple setter
					String pname = decapitalize(name.substring(3));
					if (pdWriterMap.containsKey(pname)) {
						throw new IntrospectionException("setter name conflict: " + name);
					}
					pdWriterMap.put(pname, method);
				}
			}
		}

		Map pimap = new HashMap();
		for (Map.Entry me : pdReaderMap.entrySet()) {
			String pname = me.getKey();
			Method reader = me.getValue();
			Method writer = pdWriterMap.get(pname);
			PropertyInfo pd = new PropertyInfo(pname, reader, writer);
			pimap.put(pname, pd);
		}

		return pimap;
	}

	/**
	 * Utility method to take a string and convert it to normal Java variable name capitalization. This normally means
	 * converting the first character from upper case to lower case, but in the (unusual) special case when there is
	 * more than one character and both the first and second characters are upper case, we leave it alone.
	 * 

* Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays as "URL". * * @param name * The string to be decapitalized. * @return The decapitalized version of the string. */ private static String decapitalize(String name) { if (name == null || name.length() == 0) { return name; } if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) { return name; } char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } }

Ads help maintain this website.