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