/*-
* Copyright © 2009 Diamond Light Source Ltd.
*
* This file is part of GDA.
*
* GDA is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License version 3 as published by the Free
* Software Foundation.
*
* GDA 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 Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with GDA. If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.gda.doe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.beanutils.BeanMap;
/**
* IMPORTANT NOTE: All beans used with this class must have hashCode and equals
* implemented correctly. All, including children and beans in collections.
*/
public class DOEUtils {
private static final int MAX_RANGE_SIZE = 1000;
/**
* Gets the RangeInfo for the objects passed in by constructing a
* recursive method with the objects in order first in list, outtermost.
* @param obs
* @return list
* @throws Exception
*/
public static List<RangeInfo> getInfoFromList(final List<Object> obs) throws Exception {
final List<RangeInfo> exp = new ArrayList<RangeInfo>(31);
getInfoFromList(obs, 0, exp);
return exp;
}
private static void getInfoFromList(List<Object> obs, int index, List<RangeInfo> exp) throws Exception {
if (index>=obs.size()) return;
final Object bean = obs.get(index);
if(bean != null)
{
final List<RangeInfo> runs = DOEUtils.getInfo(bean);
for (RangeInfo rangeInfo : runs) {
if (!rangeInfo.isEmpty()) exp.add(rangeInfo);
getInfoFromList(obs, index+1, exp);
}
}
else
{
getInfoFromList(obs, index+1, exp);
}
}
/**
* Reads the fields defined with a DOEField annotation and
* returns a list of objects used to describe the range.
*
* Each RangeInfo represents on experiment with the
*
* @param bean
* @return list of expanded
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public static List<RangeInfo> getInfo(final Object bean) throws Exception {
final List<Collection<FieldContainer>> weightedFields = new ArrayList<Collection<FieldContainer>>(11);
for (int i = 0; i < 11; i++) weightedFields.add(new LinkedHashSet<FieldContainer>(7));
readAnnotations(null, bean, weightedFields, -1);
List<FieldContainer> expandedFields = expandFields(weightedFields);
final List<RangeInfo> ret = new ArrayList<RangeInfo>(31);
getInfo(new RangeInfo(), expandedFields, 0, ret);
return ret;
}
/**
*
* @param info
* @param orderedFields
* @param index
* @param ret
* @throws Exception
*/
protected static void getInfo(final RangeInfo info,
final List<FieldContainer> orderedFields,
final int index,
final List<RangeInfo> ret) throws Exception {
if (index>=orderedFields.size()) {
// NOTE: You must implement hashCode and equals
// on all beans. These are used to avoid adding
// repeats.
final RangeInfo clone = (RangeInfo) deepClone(info);
ret.add(clone);
return;
}
final FieldContainer field = orderedFields.get(index);
final Object originalObject = field.getOriginalObject();
final String stringValue = (String) getBeanValue(originalObject, field.getName());
if (stringValue==null) {
getInfo(info, orderedFields, index+1, ret);
return;
}
final String range = stringValue.toString();
final List<? extends Number> vals = DOEUtils.expand(range, field.getAnnotation().type());
for (Number value : vals) {
if (vals.size()>1) info.set(new FieldValue(field.getOriginalObject(), field.getName(), value.toString()));
getInfo(info, orderedFields, index+1, ret);
}
}
/**
* Reads the fields defined with a DOEField annotation and
* returns a list of expanded objects of the passed in type.
*
* Uses BeanUtils to clone beans.
*
* @param bean
* @return list of expanded
* @throws IllegalAccessException
* @throws IllegalArgumentException
*/
public static List<? extends Object> expand(final Object bean) throws Exception {
final List<Collection<FieldContainer>> weightedFields = new ArrayList<Collection<FieldContainer>>(11);
for (int i = 0; i < 11; i++) weightedFields.add(new LinkedHashSet<FieldContainer>(7));
readAnnotations(null, bean, weightedFields, -1);
List<FieldContainer> expandedFields = expandFields(weightedFields);
final List<Object> ret = new ArrayList<Object>(31);
Object clone = deepClone(bean);
expand(clone, expandedFields, 0, ret);
return ret;
}
/**
* Makes a 1D list from the weightedFields.
* @param weightedFields
* @return list
*/
private static List<FieldContainer> expandFields(final List<Collection<FieldContainer>> weightedFields) {
final List<FieldContainer> ret = new ArrayList<FieldContainer>(31);
for (Collection<FieldContainer> fields : weightedFields) {
for (FieldContainer fc : fields) {
if (!ret.contains(fc)) ret.add(0,fc);
}
}
return ret;
}
/**
* Recursive method reads the annotations of all non-null fields.
*
* This algorithm is not perfect and there is probably a simpler one
* that deals with more cases. All the test cases are in @see DOETest.
*
* The complexity comes with dealing with fields which are lists of beans
* that may have fields which are ranges.
*
*
* @param fieldObject
* @param weightedFields
* @throws Exception
*/
protected static void readAnnotations(final FieldContainer parent,
final Object fieldObject,
final List<Collection<FieldContainer>> weightedFields,
final int index) throws Exception {
// A few common fields that we can rule out as objects which have DOEField fields.
if (fieldObject.getClass().getName().startsWith("java.lang.")) return;
Field[] ff = null;
if (fieldObject instanceof List<?>) {
final List<?> vals = (List<?>)fieldObject;
if (!vals.isEmpty()) {
ff = vals.get(0).getClass().getDeclaredFields();
}
} else {
ff = fieldObject.getClass().getDeclaredFields();
}
if (ff == null) return;
final List<Field> controlledFields = getControlledFields(fieldObject, ff);
for (int i = 0; i < ff.length; i++) {
final Field f = ff[i];
if (controlledFields!=null&&controlledFields.contains(f)) continue;
final DOEField doe = f.getAnnotation(DOEField.class);
FieldContainer fc = new FieldContainer();
fc.setField(f);
fc.setOriginalObject(fieldObject);
fc.setParent(parent);
fc.setListIndex(index);
fc.setAnnotation(doe);
if (doe!=null) {
final Collection<FieldContainer> list = weightedFields.get(doe.value());
if (fieldObject instanceof List<?>) {
final List<?> values = (List<?>)fieldObject;
for (int j = 0; j < values.size(); j++) {
list.add(fc.clone(values.get(j), j));
}
} else {
list.add(fc);
}
} else {
try {
if (fieldObject instanceof List<?>) {
final List<?> values = (List<?>)fieldObject;
for (int j = 0; j < values.size(); j++) {
readAnnotations(fc, values.get(j), weightedFields, j);
}
} else {
final Object value = getBeanValue(fieldObject, f.getName());
if (value!=null) readAnnotations(fc, value, weightedFields, -1);
}
} catch (Throwable ignored) {
}
}
}
}
private static List<Field> getControlledFields(Object fieldObject, Field[] ff) throws Exception {
if (fieldObject instanceof List<?>) return null;
final List<Field> controlled = new ArrayList<Field>(7);
for (int i = 0; i < ff.length; i++) {
final Field f = ff[i];
final DOEControl control = f.getAnnotation(DOEControl.class);
if (control!=null) {
final Object value = getBeanValue(fieldObject, f.getName());
if (value!=null) {
final String[] vals = control.values();
if (!Arrays.asList(vals).contains(value)) continue;
final String[] ffs = control.fields();
for (int j = 0; j < vals.length; j++) {
if (vals[j].equals(value)) continue; // why this line?
controlled.add(fieldObject.getClass().getDeclaredField(ffs[j]));
}
}
}
}
return controlled;
}
/**
* Recursive method which expands out all the simulations into
* a 1D list from the ranges specified. This reads the annotation
* weightings to construct the loops based on parameter weighting.
*
* For instance temperature might be in an outer loop to process all
* experiments at a given temperature together.
*
* @param clone
* @param orderedFields
* @param index
* @param ret
* @throws Exception
*/
protected static void expand( Object clone,
final List<FieldContainer> orderedFields,
final int index,
final List<Object> ret) throws Exception {
if (index>=orderedFields.size()) {
// NOTE: You must implement hashCode and equals
// on all beans. These are used to avoid adding
// repeats.
if (!ret.contains(clone)) ret.add(clone);
return;
}
final FieldContainer field = orderedFields.get(index);
final Object originalObject = field.getOriginalObject();
final String stringValue = (String) getBeanValue(originalObject, field.getName());
if (stringValue==null) {
expand(clone, orderedFields, index+1, ret);
return;
}
final String range = stringValue.toString();
final List<? extends Number> vals = DOEUtils.expand(range, field.getAnnotation().type());
for (Number value : vals) {
clone = deepClone(clone);
setBeanValue(clone, field, value.toString(), field.getListIndex());
expand(clone, orderedFields, index+1, ret);
}
}
protected static boolean setBeanValue(final Object clone, final FieldContainer field, final String value, final int index) throws Exception {
final List<FieldContainer> fieldPath = new ArrayList<FieldContainer>(3);
FieldContainer f = field.getParent();
while (f!=null) {
fieldPath.add(0,f);
f = f.getParent();
}
Object cloneObject = clone;
for (FieldContainer fc : fieldPath) {
if (cloneObject instanceof List<?>) {
final int listIndex = field.getParent().getListIndex();
final List<?> cloneList = (List<?>)cloneObject;
if (listIndex>-1) {
cloneObject = cloneList.get(listIndex);
} else {
return false;
}
} else {
cloneObject = getBeanValue(cloneObject, fc.getName());
}
}
if (cloneObject instanceof List<?> && index>-1) {
cloneObject = ((List<?>)cloneObject).get(index);
}
if (value != null && value.equals(getBeanValue(cloneObject, field.getName()))) {
return false;
}
setBeanValue(cloneObject, field.getName(), value);
return true;
}
/**
* Deep copy using serialization. All objects in the graph must serialize to use this method or an exception will be thrown.
*
* @param fromBean
* @return deeply cloned bean
*/
public static Object deepClone(final Object fromBean) throws Exception {
return deepClone(fromBean, fromBean.getClass().getClassLoader());
}
/**
* Creates a clone of any serializable object. Collections and arrays may be cloned if the entries are serializable. Caution super class members are not
* cloned if a super class is not serializable.
*/
public static Object deepClone(Object toClone, final ClassLoader classLoader) throws Exception {
if (null == toClone)
return null;
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
ObjectOutputStream oOut = new ObjectOutputStream(bOut);
oOut.writeObject(toClone);
oOut.close();
ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray());
bOut.close();
ObjectInputStream oIn = new ObjectInputStream(bIn) {
/**
* What we are saying with this is that either the class loader or any of the beans added using extension points classloaders should be able to find
* the class.
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
try {
return Class.forName(desc.getName(), false, classLoader);
} catch (Exception ne) {
ne.printStackTrace();
}
return null;
}
};
bIn.close();
// the whole idea is to create a clone, therefore the readObject must
// be the same type in the toClone, hence of T
@SuppressWarnings("unchecked")
Object copy = oIn.readObject();
oIn.close();
return copy;
}
/**
* Changes a value on the given bean using reflection
*
* @param bean
* @param fieldName
* @param value
* @throws Exception
*/
public static void setBeanValue(final Object bean, final String fieldName, final Object value) throws Exception {
final String setterName = getSetterName(fieldName);
try {
final Method method;
if (value != null) {
method = bean.getClass().getMethod(setterName, value.getClass());
} else {
method = bean.getClass().getMethod(setterName, Object.class);
}
method.invoke(bean, value);
} catch (NoSuchMethodException ne) {
// Happens when UI and bean types are not the same, for instance Text editing a double field,
// or label showing a double field.
final BeanMap properties = new BeanMap(bean);
properties.put(fieldName, value);
}
}
/**
* Translates a doe string encoded for the possible range types
* into a list of Double values.
*
* @param range
* @return expanded values
*/
public static List<? extends Number> expand(final String range) {
return expand(range, (String)null);
}
/**
* Expand values defined in a range
* @param range
* @param clazz
* @return list of double values
*/
public static <T extends Number> List<T> expand(final String range, Class<T> clazz) {
return expand(range,null,clazz);
}
/**
* Expand values defined in a range
* @param range
* @param unit
* @return list of double values
*/
public static List<? extends Number> expand(final String range, final String unit) {
return expand(range,unit,Double.class);
}
/**
*
* @param range
* @param unit
* @param clazz
* @return list of values
*/
private static <T extends Number> List<T> expand(String range, String unit, Class<T> clazz) {
final List<T> ret = new ArrayList<T>(7);
if (DOEUtils.isList(range, unit)) {
final String value = DOEUtils.removeUnit(range, unit);
final String[] items = value.split(",");
for (String val : items) ret.add(getValue(val.trim(), clazz));
} else if (DOEUtils.isRange(range, unit)) {
final double[] ran = DOEUtils.getRange(range, unit);
if (ran[0]>ran[1]) {
for (double i = ran[0]; i >= ran[1]; i-=ran[2]) {
if (ret.size()>MAX_RANGE_SIZE) break;
ret.add(getValue(i, clazz));
}
} else {
for (double i = ran[0]; i <= ran[1]; i+=ran[2]) {
if (ret.size()>MAX_RANGE_SIZE) break;
ret.add(getValue(i, clazz));
}
}
} else {
final String value = DOEUtils.removeUnit(range, unit);
ret.add(getValue(value.trim(), clazz));
}
return ret;
}
public static double[] getRange(String range, String unit) {
if (!DOEUtils.isRange(range, unit)) return null;
final String value = DOEUtils.removeUnit(range, unit);
final String[] item = value.split(";");
final double start = Double.parseDouble(item[0].trim());
final double end = Double.parseDouble(item[1].trim());
double inc = Double.parseDouble(item[2].trim());
return new double[]{start,end,inc};
}
/**
* There must be a better way of doing this
* @param val
* @param clazz
* @return number
*/
private static <T extends Number> T getValue(String val, Class<T> clazz) {
return getValue(new Double(val), clazz);
}
/**
*
* @param <T>
* @param val
* @param clazz
* @return number
*/
@SuppressWarnings("unchecked")
private static <T extends Number> T getValue(double val, Class<T> clazz) {
if (clazz==Integer.class) {
return (T)new Integer(Math.round(Math.round(val)));
} else if (clazz==Double.class) {
return (T)new Double(val);
}
throw new ClassCastException("DOEUtils cannot expand with class "+clazz+" yet.");
}
/**
* Used to test a value to see if it is legal syntax for a doe value.
* @param value
* @return true if doe value
*/
public static boolean isDOE(final String value) {
return isRange(value, null) || isList(value, null);
}
/**
* Returns true if the value is a range of numbers. The decimal
* places must be eight or less.
*
* @param value
* @param unit - may be null
* @return true of the value is a list of values
*/
public static boolean isRange(final String value, final String unit) {
return isRange(value, 8, unit);
}
/**
*
* @param value
* @param decimalPlaces
* @param unit
* @return true of the value is a list of values
*/
public static boolean isRange(String value, int decimalPlaces, String unit) {
final Pattern rangePattern = getRangePattern(decimalPlaces, unit);
return rangePattern.matcher(value.trim()).matches();
}
/**
* A regular expression to match a range.
* @param decimalPlaces for numbers matched
* @param unit - may be null if no unit in the list.
* @return Pattern
*/
public static Pattern getRangePattern(final int decimalPlaces, final String unit) {
final String ndec = decimalPlaces>0
? "\\.?\\d{0,"+decimalPlaces+"})"
: ")";
final String digitExpr = "(\\-?\\d+"+ndec;
final String rangeExpr = "("+digitExpr+";\\ ?"+digitExpr+";\\ ?"+digitExpr+")";
if (unit==null) {
return Pattern.compile(rangeExpr);
}
return Pattern.compile(rangeExpr+"\\ {1}\\Q"+unit+"\\E");
}
/**
* Returns true if the value is a list of numbers. The decimal
* places must be eight or less.
*
* @param value
* @param unit - may be null
* @return true of the value is a list of values
*/
public static boolean isList(final String value, final String unit) {
return isList(value,8,unit);
}
/**
*
* @param value
* @param decimalPlaces
* @param unit
* @return true of the value is a list of values
*/
public static boolean isList(String value, int decimalPlaces, String unit) {
final Pattern listPattern = getListPattern(decimalPlaces, unit);
return listPattern.matcher(value.trim()).matches();
}
/**
* A regular expression to match a
* @param decimalPlaces for numbers matched
* @param unit - may be null if no unit in the list.
* @return Pattern
*/
public static Pattern getListPattern(final int decimalPlaces, final String unit) {
final String ndec = decimalPlaces>0
? "\\.?\\d{0,"+decimalPlaces+"})"
: ")";
final String digitExpr = "(\\-?\\d+"+ndec;
final String listExpr = "(("+digitExpr+",\\ ?)+"+digitExpr+")";
if (unit==null) {
return Pattern.compile(listExpr);
}
return Pattern.compile(listExpr+"\\ {1}\\Q"+unit+"\\E");
}
/**
* Strips the unit, should only be called on strings that are known to match a
* value pattern.
*
* @param value
* @param unit
* @return value without unit.
*/
public static String removeUnit(String value, String unit) {
if (unit==null) return value;
final Pattern pattern = Pattern.compile("(.+)\\ ?\\Q"+unit+"\\E");
final Matcher matcher = pattern.matcher(value);
if (matcher.matches()) return matcher.group(1);
return value;
}
/**
* Method gets value out of bean using reflection.
*
* @param bean
* @param fieldName
* @return value
* @throws Exception
*/
public static Object getBeanValue(final Object bean, final String fieldName) throws Exception {
final String getterName = getGetterName(fieldName);
final Method method = bean.getClass().getMethod(getterName);
return method.invoke(bean);
}
/**
* There must be a smarter way of doing this i.e. a JDK method I cannot find. However it is one line of Java so after spending some time looking have coded
* self.
*
* @param fieldName
* @return String
*/
public static String getSetterName(final String fieldName) {
if (fieldName == null)
return null;
return getName("set", fieldName);
}
/**
* There must be a smarter way of doing this i.e. a JDK method I cannot find. However it is one line of Java so after spending some time looking have coded
* self.
*
* @param fieldName
* @return String
*/
public static String getGetterName(final String fieldName) {
if (fieldName == null)
return null;
return getName("get", fieldName);
}
public static String getFieldWithUpperCaseFirstLetter(final String fieldName) {
return fieldName.substring(0, 1).toUpperCase(Locale.US) + fieldName.substring(1);
}
private static String getName(final String prefix, final String fieldName) {
return prefix + getFieldWithUpperCaseFirstLetter(fieldName);
}
}