/* * File Input.java * * Copyright (C) 2010 Remco Bouckaert remco@cs.auckland.ac.nz * * This file is part of BEAST2. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership and licensing. * * BEAST 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 2 * of the License, or (at your option) any later version. * * BEAST 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with BEAST; if not, write to the * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ package beast.core; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; import beast.core.parameter.RealParameter; import beast.core.util.Log; /** * Represents input of a BEASTObject class. * Inputs connect BEASTObjects with outputs of other BEASTObjects, * e.g. a Logger can get the result it needs to log from a * BEASTObject that actually performs a calculation. */ public class Input<T> { /** * input name, used for identification when getting/setting values of a plug-in * */ String name = ""; /** * short description of the function of this particular input * */ String tipText = ""; /** * value represented by this input */ T value; /** * Type of T, automatically determined when setting a new value. * Used for type checking. */ protected Class<?> theClass; /** * validation rules * */ public enum Validate { OPTIONAL, REQUIRED, XOR, FORBIDDEN } // (Q2R) I am surprised the default is not required .... Validate rule = Validate.OPTIONAL; /** * used only if validation rule is XOR * */ Input<?> other; public T defaultValue; /** * Possible values for enumerations, e.g. if * an input can be any of "constant", "linear", "quadratic" * this array contains these values. Used for validation and user interfaces. */ public T[] possibleValues; /** * constructors * */ public Input() { } /** * simple constructor, requiring only the input name and tiptext */ public Input(String name, String tipText) { this.name = name; this.tipText = tipText; value = null; checkName(); } // c'tor /** * simple constructor as above but with type pre-specified. * This allows inputs of types that cannot be determined through * introspection, such as template class inputs, e.g. Input<Parameter<?>> */ public Input(String name, String tipText, Class<?> theClass) { this(name, tipText); this.theClass = theClass; } // c'tor /** * constructor for List<> */ public Input(String name, String tipText, T startValue) { this(name, tipText); value = startValue; defaultValue = startValue; } // c'tor /** * constructor for List<> with type specified */ public Input(String name, String tipText, T startValue, Class<?> theClass) { this(name, tipText, startValue); this.theClass = theClass; } // c'tor /** * constructor for List<> with XOR rules */ public Input(String name, String tipText, T startValue, Validate rule, Input<?> other) { this(name, tipText, startValue); if (rule != Validate.XOR) { Log.err.println("Programmer error: input rule should be XOR for this Input constructor"); } this.rule = rule; this.other = other; this.other.other = this; this.other.rule = rule; checkName(); } // c'tor /** * constructor for List<> with XOR rules with type specified */ public Input(String name, String tipText, T startValue, Validate rule, Input<?> other, Class<?> theClass) { this(name, tipText, startValue, rule, other); this.theClass = theClass; } // c'tor /** * Constructor for REQUIRED rules for List-inputs, i.e. lists that require * at least one value to be specified. * If optional (i.e. no value need to be specified), leave the rule out */ public Input(String name, String tipText, T startValue, Validate rule) { this(name, tipText, startValue); /*if (rule != Validate.REQUIRED) { Log.err.println("Programmer error: input rule should be REQUIRED for this Input constructor" + " (" + name + ")"); }*/ this.rule = rule; } // c'tor /** * constructor for REQUIRED rules for List-inputs, with type pre-specified */ public Input(String name, String tipText, T startValue, Validate rule, Class<?> type) { this(name, tipText, startValue, rule); theClass = type; } // c'tor /** * constructor for REQUIRED rules */ public Input(String name, String tipText, Validate rule) { this(name, tipText); if (rule != Validate.REQUIRED) { Log.err.println("Programmer error: input rule should be REQUIRED for this Input constructor" + " (" + name + ")"); } this.rule = rule; } // c'tor /** * constructor for REQUIRED rules, with type pre-specified */ public Input(String name, String tipText, Validate rule, Class<?> type) { this(name, tipText, rule); this.theClass = type; } /** * constructor for XOR rules * */ public Input(String name, String tipText, Validate rule, Input<?> other) { this(name, tipText); if (rule != Validate.XOR) { Log.err.println("Programmer error: input rule should be XOR for this Input constructor"); } this.rule = rule; this.other = other; this.other.other = this; this.other.rule = rule; } // c'tor /** * constructor for XOR rules, with type pre-specified */ public Input(String name, String tipText, Validate rule, Input<?> other, Class<?> type) { this(name, tipText, rule, other); this.theClass = type; } /** * constructor for enumeration. * Typical usage is with an array of possible String values, say ["constant","exponential","lognormal"] * Furthermore, a default value is required (should we have another constructor that could leave * the value optional? When providing a 'no-input' entry in the list and setting that as the default, * that should cover that situation.) */ public Input(String name, String tipText, T startValue, T[] possibleValues) { this.name = name; this.tipText = tipText; value = startValue; defaultValue = startValue; this.possibleValues = possibleValues; checkName(); } // c'tor /** * check name is not one of the reserved ones * */ private void checkName() { if (name.toLowerCase().equals("id") || name.toLowerCase().equals("idref") || name.toLowerCase().equals("spec") || name.toLowerCase().equals("name")) { Log.err.println("Found an input with invalid name: " + name); Log.err.println("'id', 'idref', 'spec' and 'name' are reserved and cannot be used"); System.exit(1); } } /** * various setters and getters */ public String getName() { return name; } public String getTipText() { return tipText; } public String getHTMLTipText() { return "<html>" + tipText.replaceAll("\n", "<br>") + "</html>"; } public String getValueTipText() { if (theClass == Boolean.class) { return ("[true|false]"); } if (theClass == Integer.class) { return ("<integer>"); } if (theClass == Long.class) { return ("<long>"); } if (theClass == Double.class) { return ("<double>"); } if (theClass == Float.class) { return ("<float>"); } if (theClass == String.class) { return "<string>"; } if (theClass == File.class) { return "<filename>"; } if (theClass.isEnum()) { return Arrays.toString(possibleValues).replaceAll(",", "|"); } return ""; } public Class<?> getType() { return theClass; } public void setType(Class<?> theClass) { this.theClass = theClass; } public Validate getRule() { return rule; } public void setRule(final Validate rule) { this.rule = rule; } final public Input<?> getOther() { return other; } /** * Get the value of this input -- not to be called from operators!!! * If this is a StateNode input, instead of returning * the actual value, the current value of the StateNode * is returned. This is defined as the current StateNode * in the State, or itself if it is not part of the state. * * @return value of this input */ public T get() { return value; } /** * As get() but with this difference that the State can manage * whether to make a copy and register the operator. * <p/> * Only Operators should call this method. * Also Operators should never call Input.get(), always Input.get(operator). * * @param operator * @return */ @SuppressWarnings("unchecked") public T get(final Operator operator) { return (T) ((StateNode) value).getCurrentEditable(operator); } /** * Return the dirtiness state for this input. * For a StateNode or list of StateNodes, report whether for any something is dirty, * for a CalcationNode or list of CalculationNodes, report whether any is dirty. * Otherwise, return false. * * */ public boolean isDirty() { final T value = get(); if (value == null) { return false; } if (value instanceof StateNode) { return ((StateNode) value).somethingIsDirty(); } if (value instanceof CalculationNode) { return ((CalculationNode) value).isDirtyCalculation(); } if (value instanceof List<?>) { for (final Object obj : (List<?>) value) { if (obj instanceof CalculationNode && ((CalculationNode) obj).isDirtyCalculation()) { return true; } else if (obj instanceof StateNode && ((StateNode) obj).somethingIsDirty()) { return true; } } } return false; } /** * Sets value to this input. * If class is not determined yet, first determine class of declaration of * this input so that we can do type checking. * If value is of type String, try to parse the value if this input is * Integer, Double or Boolean. * If this input is a List, instead of setting this value, the value is * added to the vector. * Otherwise, m_value is assigned to value. * * @param value * @param beastObject */ @SuppressWarnings("unchecked") public void setValue(final Object value, final BEASTInterface beastObject) { if (value == null) { if (this.value != null) { if (this.value instanceof BEASTInterface) { ((BEASTInterface) this.value).getOutputs().remove(beastObject); } } this.value = null; return; } if (theClass == null) { try { determineClass(beastObject); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Failed to determine class of beastobject id=" + beastObject.getID()); } } if (value instanceof String) { try { setStringValue((String) value, beastObject); } catch (Exception e) { e.printStackTrace(); Log.warning.println("Failed to set the string value to '" + value + "' for beastobject id=" + beastObject.getID()); throw new RuntimeException("Failed to set the string value to '" + value + "' for beastobject id=" + beastObject.getID()); } } else if (this.value != null && this.value instanceof List<?>) { if (theClass.isAssignableFrom(value.getClass())) { @SuppressWarnings("rawtypes") final List vector = (List) this.value; // // don't insert duplicates // RRB: DO insert duplicates: this way CompoundValuable can be set up to // contain rate matrices with dependent variables/parameters. // There does not seem to be an example where a duplicate insertion is a problem... // for (Object o : vector) { // if (o.equals(value)) { // return; // } // } vector.add(value); if (value instanceof BEASTInterface) { ((BEASTInterface) value).getOutputs().add(beastObject); } } else if (value instanceof List<?> && theClass.isAssignableFrom(((List<?>) value).get(0).getClass())) { // add all elements in given list to input list. @SuppressWarnings("rawtypes") final List<Object> vector = (List) this.value; for (Object v : ((List<?>) value)) { vector.add(v); if (v instanceof BEASTInterface) { ((BEASTInterface) v).getOutputs().add(beastObject); } } } else { throw new RuntimeException("Input 101: type mismatch for input " + getName() + ". " + theClass.getName() + ".isAssignableFrom(" + value.getClass() + ")=false"); } } else { if (theClass.isAssignableFrom(value.getClass())) { if (value instanceof BEASTInterface) { if (this.value != null) { ((BEASTInterface) this.value).getOutputs().remove(beastObject); } ((BEASTInterface) value).getOutputs().add(beastObject); } this.value = (T) value; } else { throw new RuntimeException("Input 102: type mismatch for input " + getName()); } } } /** * Call custom input validation. * For an input with name "name", the method canSetName will be invoked, * that is, 'canSet' + the name of the input with first letter capitalised. * The canSetName(Object o) method should have one argument of type Object. * <p/> * It is best for Beauti to throw an Exception from canSetName() with some * diagnostic info when the value cannot be set. */ public boolean canSetValue(Object value, BEASTInterface beastObject) { String inputName = new String(name.charAt(0) + "").toUpperCase() + name.substring(1); try { Method method = beastObject.getClass().getMethod("canSet" + inputName, Object.class); //System.err.println("Calling method " + beastObject.getClass().getName() +"."+ method.getName()); Object o = method.invoke(beastObject, value); return (Boolean) o; } catch (java.lang.NoSuchMethodException e) { return true; } catch (java.lang.reflect.InvocationTargetException e) { Log.warning.println(beastObject.getClass().getName() + "." + getName() + ": " + e.getCause()); if (e.getCause() != null) { throw new RuntimeException(e.getCause().getMessage()); } return false; } catch (IllegalAccessException e) { e.printStackTrace(); throw new RuntimeException("Illegal method access attempted on beastobject id=" + beastObject.getID()); } } /** * Determine class through introspection, * This sets the theClass member of Input<T> to the actual value of T. * If T is a vector, i.e. Input<List<S>>, the actual value of S * is assigned instead * * @param beastObject whose type is to be determined */ public void determineClass(final Object beastObject) { try { final Field[] fields = beastObject.getClass().getFields(); // find this input in the beastObject for (int i = 0; i < fields.length; i++) { if (fields[i].getType().isAssignableFrom(Input.class)) { final Input<?> input = (Input<?>) fields[i].get(beastObject); if (input == this) { // found the input, now determine the type of the input Type t = fields[i].getGenericType(); Type[] genericTypes = ((ParameterizedType) t).getActualTypeArguments(); // check if it is a List // NB: if the List is not initialised, there is no way // to determine the type (that I know of...) if (value != null && value instanceof List<?>) { Type[] genericTypes2 = ((ParameterizedType) genericTypes[0]).getActualTypeArguments(); try { theClass = (Class<?>) genericTypes2[0]; } catch (ClassCastException e) { // can get here with parameterised types, e.g Input<List<Parameter.Base<T>>> theClass = (Class<?>) ((ParameterizedType)genericTypes2[0]).getRawType(); } // getting type of map is not possible?!? //} else if (value != null && value instanceof Map<?,?>) { // Type[] genericTypes2 = ((ParameterizedType) genericTypes[0]).getActualTypeArguments(); // theClass = (Class<?>) genericTypes2[0]; } else { // it is not a list (or if it is, this will fail) try { Object o = genericTypes[0]; if (o instanceof ParameterizedType) { Type rawType = ((ParameterizedType) genericTypes[0]).getRawType(); // Log.warning.println(rawType.getTypeName()); if (rawType.getTypeName().equals("java.util.List")) { // if we got here, value==null throw new RuntimeException("Programming error: Input<List> not initialised"); } } theClass = (Class<?>) o; } catch (Exception e) { // resolve ID String id = ""; Method method = beastObject.getClass().getMethod("getID"); if (method != null) { id = (String) method.invoke(beastObject); } // assemble error message Log.err.println(beastObject.getClass().getName() + " " + id + " failed. " + "Possibly template or abstract BEASTObject used " + "or if it is a list, the list was not initilised???"); Log.err.println("class is " + beastObject.getClass()); e.printStackTrace(System.err); System.exit(1); } } break; } } } } catch (Exception e) { e.printStackTrace(); } } // determineClass /** * Try to parse value of string into Integer, Double or Boolean, * or it this types differs, just assign as string. * * @param stringValue value representation * @throws IllegalArgumentException when all conversions fail */ @SuppressWarnings({"unchecked", "rawtypes"}) private void setStringValue(final String stringValue, final BEASTInterface beastObject) { // figure out the type of T and create object based on T=Integer, T=Double, T=Boolean, T=Valuable if (value instanceof List<?>) { List list = (List) value; list.clear(); // remove start and end spaces String stringValue2 = stringValue.replaceAll("^\\s+", ""); stringValue2 = stringValue2.replaceAll("\\s+$", ""); // split into space-separated bits String[] stringValues = stringValue2.split("\\s+"); for (int i = 0; i < stringValues.length; i++) { if (theClass.equals(Integer.class)) { list.add(new Integer(stringValues[i % stringValues.length])); } else if (theClass.equals(Double.class)) { list.add(new Double(stringValues[i % stringValues.length])); } else if (theClass.equals(Boolean.class)) { String str = stringValues[i % stringValues.length].toLowerCase(); list.add(str.equals("1") || str.equals("true") || str.equals("yes")); } else if (theClass.equals(String.class)) { list.add(new String(stringValues[i % stringValues.length])); } } return; } if (theClass.equals(Integer.class)) { value = (T) new Integer(stringValue); return; } if (theClass.equals(Long.class)) { value = (T) new Long(stringValue); return; } if (theClass.equals(Double.class)) { value = (T) new Double(stringValue); return; } if (theClass.equals(Float.class)) { value = (T) new Float(stringValue); return; } if (theClass.equals(Boolean.class)) { // RRB why the local parsing instead of using the Boolean c'tor? // final String valueString2 = stringValue.toLowerCase(); // if (valueString2.equals("yes") || valueString2.equals("true")) { // value = (T) Boolean.TRUE; // return; // } else if (valueString2.equals("no") || valueString2.equals("false")) { // value = (T) Boolean.FALSE; // return; // } value = (T) new Boolean(stringValue); return; } if (theClass.equals(Function.class)) { final RealParameter param = new RealParameter(); param.initByName("value", stringValue, "upper", 0.0, "lower", 0.0, "dimension", 1); param.initAndValidate(); if (value != null && value instanceof List) { ((List) value).add(param); } else { value = (T) param; } param.getOutputs().add(beastObject); return; } if (theClass.isEnum()) { if (possibleValues == null) { possibleValues = (T[]) theClass.getDeclaringClass().getEnumConstants(); } for (final T t : possibleValues) { if (stringValue.equals(t.toString())) { value = t; return; } } throw new IllegalArgumentException("Input 104: value " + stringValue + " not found. Select one of " + Arrays.toString(possibleValues)); } // call a string constructor of theClass try { Constructor ctor; Object v = stringValue; try { ctor = theClass.getDeclaredConstructor(String.class); } catch (NoSuchMethodException e) { // we get here if there is not String constructor // try integer constructor instead try { if (stringValue.startsWith("0x")) { v = Integer.parseInt(stringValue.substring(2), 16); } else { v = Integer.parseInt(stringValue); } ctor = theClass.getDeclaredConstructor(int.class); } catch (NumberFormatException e2) { // could not parse as integer, try double instead v = Double.parseDouble(stringValue); ctor = theClass.getDeclaredConstructor(double.class); } } ctor.setAccessible(true); final Object o = ctor.newInstance(v); if (value != null && value instanceof List) { ((List) value).add(o); } else { value = (T) o; } if (o instanceof BEASTInterface) { ((BEASTInterface) o).getOutputs().add(beastObject); } } catch (Exception e) { throw new IllegalArgumentException("Input 103: type mismatch, cannot initialize input '" + getName() + "' with value '" + stringValue + "'.\nExpected something of type " + getType().getName() + ". " + (e.getMessage() != null ? e.getMessage() : "")); } } // setStringValue /** * validate input according to validation rule * * */ public void validate() { if (possibleValues != null) { // it is an enumeration, check the value is in the list boolean found = false; for (final T value : possibleValues) { if (value.equals(this.value)) { found = true; } } if (!found) { throw new IllegalArgumentException("Expected one of " + Arrays.toString(possibleValues) + " but got " + this.value); } } switch (rule) { case OPTIONAL: // noting to do break; case REQUIRED: if (get() == null) { throw new IllegalArgumentException("Input '" + getName() + "' must be specified."); } if (get() instanceof List<?>) { if (((List<?>) get()).size() == 0) { throw new IllegalArgumentException("At least one input of name '" + getName() + "' must be specified."); } } break; case XOR: if (get() == null) { if (other.get() == null) { throw new IllegalArgumentException("Either input '" + getName() + "' or '" + other.getName() + "' needs to be specified"); } } else { if (other.get() != null) { throw new IllegalArgumentException("Only one of input '" + getName() + "' and '" + other.getName() + "' must be specified (not both)"); } } // noting to do break; case FORBIDDEN: if (get() instanceof List<?>) { if (((List<?>) get()).size() > 0) { throw new IllegalArgumentException("No input of name '" + getName() + "' must be specified."); } } else if (get() != null) { throw new IllegalArgumentException("Input '" + getName() + "' must not be specified."); } break; } } // validate public String toString() { return String.format("Input(\"%s\")", name); } } // class Input