Java tutorial
/*- ******************************************************************************* * Copyright (c) 2011, 2014 Diamond Light Source Ltd. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Matthew Gerring - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.dawnsci.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.io.Serializable; 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 = 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 Serializable 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); Serializable 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(Serializable 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; } /** * 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 int getSize(String range, String unit) { if (range == null) return -1; final double[] arange = getRange(range, unit); if (arange == null) return -1; return (int) Math.round((arange[1] - arange[0]) / arange[2]); } public static double[] getRange(String range, String unit) { if (range == null) return null; if (!DOEUtils.isRange(range, unit)) return null; final Pattern colPattern = getColonRangePattern(8, null); Matcher matcher = colPattern.matcher(range); if (matcher.matches()) { final double start = Double.parseDouble(matcher.group(1)); final double end = Double.parseDouble(matcher.group(2)); double inc = 1; return new double[] { start, end, inc }; } 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 colPattern = getColonRangePattern(decimalPlaces, unit); if (colPattern.matcher(value.trim()).matches()) return true; final Pattern rangePattern = getScanRangePattern(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 */ private static Pattern getColonRangePattern(final int decimalPlaces, final String unit) { final String ndec = decimalPlaces > 0 ? "\\.?\\d{0," + decimalPlaces + "})" : ")"; final String digitExpr = "(\\-?\\d+" + ndec; final String rangeExpr = digitExpr + " ?: ?" + digitExpr; if (unit == null) { return Pattern.compile(rangeExpr); } return Pattern.compile(rangeExpr + "\\ {1}\\Q" + unit + "\\E"); } /** * 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 */ private static Pattern getScanRangePattern(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; } /** * 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 <T extends Serializable> T deepClone(final T 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 <T extends Serializable> T deepClone(T 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") T copy = (T) 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); } } /** * 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); } }