/*
 * Copyright (C) 2007-2010 Institute for Computational Biomedicine,
 *               Weill Medical College of Cornell University
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package edu.cornell.med.icb.io;

import edu.cornell.med.icb.util.ICBStringUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class is a very glorified {@link TSVReader}. This class provides
 * a lot of extra functionality, such as
 * <ul>
 * <li> Know the number of field (columns) you have and want to name those
 *   fields and access the fields by name.
 * <li> Fields are sepeareted by ConditionsParser.fieldSeparator
 *   (default is the tab character).
 * <li> Commented and blank lines are skipped automatically
 * <li> A field of type ConditionField.FieldType.VALUE can
 *   contain a single value, which can be retrieved as a
 *   String, int, double, or boolean. Alternatively, if
 *   ConditionField.list is set to true,
 *   the field can contain a list of values (individual values
 *   separated by ConditionField.listSeparator, default is ',')
 *   and returned as an array of String, int, double, or boolean.
 *   Access via
 *   <ul>
 *   <li> {@link #parseFieldValueString}
 *   <li> {@link #parseFieldValueStringArray}
 *   <li> {@link #parseFieldValueInt}
 *   <li> {@link #parseFieldValueIntArray}
 *   <li> {@link #parseFieldValueDouble}
 *   <li> {@link #parseFieldValueDoubleArray}
 *   <li> {@link #parseFieldValueBoolean}
 *   <li> {@link #parseFieldValueBooleanArray}
 *   </ul>
 * <li> A field of type ConditionField.FieldType.MAP,
 *   ConditionField.keyValueSeparator (default is '=')
 *   will be used to specify key/value pairs seperated by
 *   ConditionField.listSeparator (default is ',').
 *   Access via {@link #parseFieldMap}
 * <li> Alternatively, when a field is of type
 *   ConditionField.FieldType.MAP, you can use
 *   {@link #parseFieldBean} to provide extra functionality,
 *   which is to use the bean setters on the
 *   targetObject to set the properties of the
 *   keys to the specified values.
 *   If one of the key/value pairs in the map is
 *   ConditionField.classnameKey (default is
 *   "_CLASSNAME_") and targetObject isn't specified (null),
 *   the object will be created and the bean properties
 *   set.
 * </ul>
 * @author Kevin Dorff (Nov 21, 2007)
 */
public class ConditionsParser {

    /**
     * The condition fields.
     */
    private final List<ConditionField> fields;

    /**
     * Default field separator (tab).
     */
    private static final char FIELD_SEPARATOR_DEFAULT = '\t';

    /**
     * Default comment prefix.
     */
    private static final String COMMENT_PREFIX_DEFAULT = "#";

    /**
     * Default escape character.
     */
    private static final char ESCAPE_CHAR_DEFAULT = '\\';

    /**
     * The field separator that is being used.
     */
    private char fieldSeparator = FIELD_SEPARATOR_DEFAULT;

    /**
     * The comment prefix that is being used.
     */
    private String commentPrefix = COMMENT_PREFIX_DEFAULT;

    /**
     * The escape character that is being used.
     */
    private Character escapeChar = ESCAPE_CHAR_DEFAULT;

    /**
     * The TSV reader that is being used.
     */
    private TSVReader tsvReader;

    /**
     * True if hasNext.
     */
    private boolean hasNext;

    /**
     * The current line number.
     */
    private int lineNumber;

    /**
     * A map of the field name to the field number.
     */
    private Map<String, Integer> nameToFieldNumberMap;

    /**
     * Create a new ConditionsParser with no fields
     * specified. This is provided assuming addField will
     * be called later to add fields.
     */
    public ConditionsParser() {
        this(new ArrayList<ConditionField>());
    }

    /**
     * Create a new ConditionsParser with a List[ConditionField]
     * of fields.
     * @param fieldsVal the ConditionFields to use in the parsing.
     */
    public ConditionsParser(final List<ConditionField> fieldsVal) {
        super();
        this.fields = new ArrayList<ConditionField>(fieldsVal.size());
        this.fields.addAll(fieldsVal);
        this.tsvReader = null;
        this.hasNext = false;
    }

    /**
     * Add a new ConditionField to the parser.
     * @param field the ConditionField to add to the parser
     * @return this ConditionsParser object, so configuration can be
     * chained.
     */
    public ConditionsParser addField(final ConditionField field) {
        fields.add(field);
        return this;
    }

    /**
     * Clear the fields for the parser.
     * @return this ConditionsParser object, so configuration can be
     * chained.
     */
    public ConditionsParser clearFields() {
        fields.clear();
        return this;
    }

    /**
     * Set the fieldSeparator, the char value that will
     * separate multiple fields in the input. The default
     * is '\t' (tab). Such as with a MAP "123\t456"
     * will define two fields, the first field has a value of
     * "123" and the second field has a value of "456".
     * This is NOT a regex.
     * @param fieldSeparatorVal the char which is used to separate
     * multiple fields in the input
     * @return this ConditionsParser object, so configuration can be
     * chained.
     */
    public ConditionsParser setFieldSeparator(final char fieldSeparatorVal) {
        this.fieldSeparator = fieldSeparatorVal;
        return this;
    }

    /**
     * Get the fieldSeparator char.
     * @return the fieldSeparator char
     */
    public char getFieldSeparator() {
        return this.fieldSeparator;
    }

    /**
     * Get the number of fields the parser
     * will try to retrieve from the file.
     * @return the the number of fields
     */
    public int getFieldsCount() {
        if (fields == null) {
            return 0;
        } else {
            return fields.size();
        }
    }

    /**
     * Get the list of fields the parser
     * will use when parsing the file.
     * @return the list of fields
     */
    public List<ConditionField> getFields() {
        return fields;
    }

    /**
     * Set the commentPrefix, the String value (normally
     * one or two characters) that specify the line
     * is a comment and should be ignored, treated like
     * a blank line. The default is "#". The line
     * will not be treated as a comment unless this
     * String is the VERY FIRST THING on the line,
     * no extra whitespace, etc. will be tolerated.
     * This is NOT a regex.
     * @param commentPrefixVal the String which specify a line
     * is a comment if the line starts with this String
     * @return this ConditionsParser object, so configuration can be
     * chained.
     */
    public ConditionsParser setCommentPrefix(final String commentPrefixVal) {
        this.commentPrefix = commentPrefixVal;
        return this;
    }

    /**
     * Get the commentPrefix String.
     * @return the commentPrefix String
     */
    public String getCommentPrefix() {
        return this.commentPrefix;
    }

    /**
     * Set the escape character that will be used when
     * parsing.
     * @param escapeCharVal which escape char to use, or null
     * to not use any escape char
     * @return this ConditionsParser object, so configuration can be
     * chained.
     */
    public ConditionsParser setEscapeChar(final Character escapeCharVal) {
        this.escapeChar = escapeCharVal;
        return this;
    }

    /**
     * Get the escape char to use when parsing.
     * @return the escape char to use when parsing
     */
    public Character getEscapeChar() {
        return this.escapeChar;
    }

    /**
     * This method is called first to begin parsing
     * condition data.
     * @param sourceReader the reader for the condition data
     * to parse
     */
    public void beginParse(final Reader sourceReader) {
        tsvReader = new TSVReader(sourceReader, fieldSeparator);
        hasNext = false;
        lineNumber = 0;
        tsvReader.setCommentPrefix(commentPrefix);
        tsvReader.setEscapeChar(escapeChar);
        tsvReader.setUnescapeResults(false);
        nameToFieldNumberMap = new HashMap<String, Integer>(fields.size());
        int pos = 0;
        for (final ConditionField field : fields) {
            final Integer current = nameToFieldNumberMap.get(field.getFieldName());
            if (current == null) {
                // Only use the name for ONE field, the first one
                nameToFieldNumberMap.put(field.getFieldName(), pos);
            }
            pos++;
        }
    }

    /**
     * This does complete parsing of the entire file into objects.
     * All fields should have ConditionField.valueBeanProperty
     * set or else they will get ignored.
     * @param sourceReader the reader for the condition data
     * to parse
     * @param templateClass the template object class. Each item in
     * the return list will start as a new object of this
     * class type. The class must have an empty constructor.
     * @param valuesMap this OPTIONAL map can be provided
     * and all values that are set on the bean will be
     * placed in this map. It is fine to pass null for this
     * paramter.
     * @param <T> the generic type in question
     * @throws IOException Error reading the source file
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data). Also thrown if there is
     * a catastrphic BeanUtils problem or a problem creating
     * an object.
     * @return List of objects, each item represents
     * one (non comment) line in the input file.
     */
    public <T> List<T> beginParseAllToBeans(final Reader sourceReader,
                                            final Class<T> templateClass,
                                            final Map<String, String> valuesMap)
            throws IOException, ConditionsParsingException {
        final List<T> resultObjs = new ArrayList<T>();
        beginParse(sourceReader);
        while (hasNext()) {
            final T object;
            try {
                object = templateClass.newInstance();
            } catch (IllegalAccessException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + templateClass.getName()
                    + " - IllegalAccessException for line " + lineNumber, e);
            } catch (InstantiationException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + templateClass.getName()
                    + " - InstantiationException for line " + lineNumber, e);
            }
            resultObjs.add(object);
            parseAllFieldsBean(object, valuesMap);
        }
        return resultObjs;
    }

    /**
     * This does complete parsing of the entire file into objects.
     * All fields should have ConditionField.valueBeanProperty
     * set or else they will get ignored.
     * @param <T> the generic type in question
     * @param sourceReader the reader for the condition data
     * to parse
     * @param templateObject the template Object. Each item in
     * the return list will start as a property-for-property
     * copy of this object (each property must be in the
     * bean pattern). The template object must have an
     * empty constructor. If a getter throws an exception
     * during the property-for-property copy, this will
     * cause this method to fail. The close the
     * templateObject is to simple bean better the
     * @param valuesMap this OPTIONAL map can be provided
     * and all values that are set on the bean will be
     * placed in this map. It is fine to pass null for this
     * paramter.
     * @throws IOException Error reading the source file
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data). Also thrown if there is
     * a catastrphic BeanUtils problem or a problem creating
     * an object.
     * @return List of objects, each item represents
     * one (non comment) line in the input file.
     */
    @SuppressWarnings("unchecked")
    public <T> List<T> beginParseAllToBeans(final Reader sourceReader,
                                            final T templateObject,
                                            final Map<String, String> valuesMap)
            throws IOException, ConditionsParsingException {
        if (templateObject == null) {
            throw new ConditionsParsingException(
                    "templateObject cannot be is null when calling parseAllFieldsBean");
        }
        final List<T> resultObjs = new ArrayList<T>();
        beginParse(sourceReader);
        while (hasNext()) {
            final T object;
            try {
                object = (T) BeanUtils.cloneBean(templateObject);
            } catch (IllegalAccessException e) {
                throw new ConditionsParsingException("Could not create new object "
                        + "for classname " + templateObject.getClass().getName()
                        + " - IllegalAccessException for line " + lineNumber, e);
            } catch (InvocationTargetException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + templateObject.getClass().getName()
                    + " - InvocationTargetException for line " + lineNumber, e);
            } catch (NoSuchMethodException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + templateObject.getClass().getName()
                    + " - NoSuchMethodException for line " + lineNumber, e);
            } catch (InstantiationException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + templateObject.getClass().getName()
                    + " - InstantiationException for line " + lineNumber, e);
            }
            resultObjs.add(object);
            parseAllFieldsBean(object, valuesMap);
        }
        return resultObjs;
    }

    /**
     * Determine if a next condition line is available. This should
     * ONLY be called once per line, if you call it multiple times you
     * will be skipping lines. This will automatically skip over
     * blank or comment lines.
     * @return true if a next line is available.
     * @throws IOException error reading condition file.
     * @throws ConditionsParsingException when tsvReader doesn't exist,
     * such as beginParse() not called before hasNext()
     */
    public boolean hasNext() throws IOException, ConditionsParsingException {
        if (tsvReader == null) {
            throw new ConditionsParsingException(
                    "No tsvReader, did you call beginParse(...)?");
        }

        for (final ConditionField field : fields) {
            // Reset back to "defaults"
            field.resetValueToDefault();
        }

        while (true) {
            hasNext = tsvReader.hasNext();
            if (hasNext) {
                lineNumber++;
                if (tsvReader.isCommentLine() || tsvReader.isEmptyLine()) {
                    // Do nothing, this is a comment or empty line
                    tsvReader.skip();
                } else {
                    tsvReader.next();
                    final int numFields = tsvReader.numTokens();
                    if (numFields > fields.size()) {
                        // Too many fields. This is a problem.
                        tsvReader.close();
                        tsvReader = null;
                        hasNext = false;
                        throw new ConditionsParsingException(
                                "Source file has too many fields on line "
                                + lineNumber);
                    }
                    for (int i = 0; i < numFields; i++) {
                        fields.get(i).setCurrentValue(tsvReader.getString());
                    }
                    break;
                }
            } else {
                tsvReader.close();
                tsvReader = null;
                break;
            }
        }
        return hasNext;
    }

    /**
     * Get the current line number.
     * @return the current line number
     */
    public int getLineNumber() {
        return lineNumber;
    }

    /**
     * Return the field number for the given field name.
     * @param fieldName the field name
     * @return the field number for that field name
     * @throws ConditionsParsingException the field name
     * wasn't specified in the configuration of
     * this object.
     */
    private int getFieldNumber(final String fieldName)
            throws ConditionsParsingException {
        final Integer fieldNumber = nameToFieldNumberMap.get(fieldName);
        if (fieldNumber == null) {
            throw new ConditionsParsingException("Field name "
                    + fieldName + " does not exist in specified fields");
        }
        return fieldNumber;
    }

    /**
     * Return the String value of the field named with fieldName
     * or the default if that field wasn't in the conditions file.
     * If isList is set for this field, the first value is
     * returned, otherwise the entire value is returned
     * @param fieldName the field name
     * @return the string value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public String parseFieldValueString(final String fieldName)
            throws ConditionsParsingException {
        if (!hasNext) {
            throw new ConditionsParsingException(
                    "No line to parse. Did you call beginParse(...) and hasNext()?");
        }
        final int fieldNumber = getFieldNumber(fieldName);
        final ConditionField field = fields.get(fieldNumber);
        String wholeValue = field.getCurrentValue();
        if (wholeValue == null) {
            wholeValue = "";
        }
        if (field.isList()) {
            // Only return the first one
            final String[] listVals = ICBStringUtils.split(wholeValue,
                    field.getListSeparator(), escapeChar);
            if (listVals.length == 0) {
                return "";
            } else {
                return ICBStringUtils.unescape(listVals[0],
                        escapeChar);
            }
        } else {
            return ICBStringUtils.unescape(field.getCurrentValue(),
                    escapeChar);
        }
    }

    /**
     * Return the String[] values of the field named with fieldName
     * or the default if that field wasn't in the conditions file.
     * If isList this will return the data as split by
     * ConditionField.listSeparator, otherwise a single element
     * string array will be returned containing the single value.
     * @param fieldName the field name
     * @return the string array value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public String[] parseFieldValueStringArray(final String fieldName)
            throws ConditionsParsingException {
        if (!hasNext) {
            throw new ConditionsParsingException(
                    "No line to parse. Did you call beginParse(...) and hasNext()?");
        }
        final int fieldNumber = getFieldNumber(fieldName);
        final ConditionField field = fields.get(fieldNumber);
        String wholeValue = field.getCurrentValue();
        if (wholeValue == null) {
            wholeValue = "";
        }
        if (field.isList()) {
            // Return all
            String[] listVals = ICBStringUtils.split(wholeValue,
                    field.getListSeparator(), escapeChar);
            listVals = ICBStringUtils.unescape(listVals, escapeChar);
            return listVals;
        } else {
            // Make an array with the single element
            final String[] listVals = new String[1];
            listVals[0] = ICBStringUtils.unescape(field.getCurrentValue(),
                    escapeChar);
            return listVals;
        }
    }

    /**
     * Return the int value of the field named with fieldName
     * or the default (converted to an int) if that field wasn't
     * in the conditions file. If isList is set for this
     * field, the first value is returned, otherwise the entire
     * value is returned. Note that if the data isn't parsable
     * to an int this will throw a NumberFormatException.
     * @param fieldName the field name
     * @return the int value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data).
     */
    public int parseFieldValueInt(final String fieldName)
            throws ConditionsParsingException {
        final String fieldValue = parseFieldValueString(fieldName);
        return Integer.parseInt(fieldValue);
    }

    /**
     * Return the int[] values of the field named with fieldName
     * or the default if that field wasn't in the conditions file.
     * If isList this will return the data as split by
     * ConditionField.listSeparator, otherwise a single element
     * int array will be returned containing the single value.
     * Note that if the data isn't parsable to a int this
     * will throw a NumberFormatException.
     * @param fieldName the field name
     * @return the int array value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public int[] parseFieldValueIntArray(final String fieldName)
            throws ConditionsParsingException {
        final String[] fieldValues = parseFieldValueStringArray(fieldName);
        final int[] outValues = new int[fieldValues.length];
        for (int i = 0; i < fieldValues.length; i++) {
            outValues[i] = Integer.parseInt(fieldValues[i]);
        }
        return outValues;
    }

    /**
     * Return the double value of the field named with fieldName
     * or the default (converted to an double) if that field wasn't
     * in the conditions file. If isList is set for this
     * field, the first value is returned, otherwise the entire
     * value is returned. Note that if the data isn't parsable
     * to an double this will throw a NumberFormatException.
     * @param fieldName the field name
     * @return the int value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data).
     */
    public double parseFieldValueDouble(final String fieldName)
            throws ConditionsParsingException {
        final String fieldValue = parseFieldValueString(fieldName);
        return Double.parseDouble(fieldValue);
    }

    /**
     * Return the double[] values of the field named with fieldName
     * or the default if that field wasn't in the conditions file.
     * If isList this will return the data as split by
     * ConditionField.listSeparator, otherwise a single element
     * double array will be returned containing the single value.
     * Note that if the data isn't parsable to a double this
     * will throw a NumberFormatException.
     * @param fieldName the field name
     * @return the double array value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public double[] parseFieldValueDoubleArray(final String fieldName)
            throws ConditionsParsingException {
        final String[] fieldValues = parseFieldValueStringArray(fieldName);
        final double[] outValues = new double[fieldValues.length];
        for (int i = 0; i < fieldValues.length; i++) {
            outValues[i] = Double.parseDouble(fieldValues[i]);
        }
        return outValues;
    }

    /**
     * Return the boolean value of the field named with fieldName
     * or the default (converted to an boolean) if that field wasn't
     * in the conditions file. If isList is set for this
     * field, the first value is returned, otherwise the entire
     * value is returned. Note that if the data isn't equal
     * to (ignoring case) "true", this will return false.
     * @param fieldName the field name
     * @return the int value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data).
     */
    public boolean parseFieldValueBoolean(final String fieldName)
            throws ConditionsParsingException {
        final String fieldValue = parseFieldValueString(fieldName);
        return Boolean.parseBoolean(fieldValue);
    }

    /**
     * Return the boolean[] values of the field named with fieldName
     * or the default if that field wasn't in the conditions file.
     * If isList this will return the data as split by
     * ConditionField.listSeparator, otherwise a single element
     * boolean array will be returned containing the single value.
     * Note that if the values don't parse to (ignoring case)
     * true, the value of false will be set.
     * @param fieldName the field name
     * @return the boolean array value for that field
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public boolean[] parseFieldValueBooleanArray(final String fieldName)
            throws ConditionsParsingException {
        final String[] fieldValues = parseFieldValueStringArray(fieldName);
        final boolean[] outValues = new boolean[fieldValues.length];
        for (int i = 0; i < fieldValues.length; i++) {
            outValues[i] = Boolean.parseBoolean(fieldValues[i]);
        }
        return outValues;
    }

    /**
     * Parse the field to a MAP. MAP entries are specified by
     * ConditionField.listSeparator (default is ",") and key=value
     * (the "=") are separated by ConditionField.valueSeparator.
     * The Map is returned in a Map[String,String].
     * Default will be used if that field wasn't in the
     * conditions file.
     * @param fieldName the field name
     * @return the Map for the field name
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data)
     */
    public Map<String, String> parseFieldMap(final String fieldName)
            throws ConditionsParsingException {
        final String[] fieldValues = parseFieldValueStringArray(fieldName);
        final ConditionField field = fields.get(getFieldNumber(fieldName));
        final char valueSeparator = field.getKeyValueSeparator();
        final Map<String, String> outValues = new HashMap<String, String>();
        for (final String fieldValue : fieldValues) {
            final int pos = fieldValue.indexOf(valueSeparator);
            if (pos == -1) {
                // No "=". Just place an empty string
                outValues.put(fieldValue,  "");
                continue;
            }
            final String left = fieldValue.substring(0, pos);
            final String right = fieldValue.substring(pos + 1, fieldValue.length());
            outValues.put(left, right);
        }
        return outValues;
    }

    /**
     * This is a special case that allows ALL fields to be
     * parsed at once as bean. This will attempt to parse
     * any field which has ConditionField.valueBeanProperty
     * set. This is equivalent to calling parseFieldBean
     * for each field individually. This requires that
     * targetObject be not null..
     * @param targetObject Required. The object to set
     * the bean values
     * @param valuesMap this OPTIONAL map can be provided
     * and all values that are set on the bean will be
     * placed in this map. It is fine to pass null for this
     * paramter.
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data). Also thrown if there is
     * a catastrphic BeanUtils problem or a problem creating
     * an object.
     */
    public void parseAllFieldsBean(final Object targetObject,
                                 final Map<String, String> valuesMap)
            throws ConditionsParsingException {
        if (targetObject == null) {
            throw new ConditionsParsingException(
                    "targetObject cannot be is null when calling parseAllFieldsBean");
        }
        for (final ConditionField field : fields) {
            if (StringUtils.isNotBlank(field.getValueBeanProperty())) {
                parseFieldBean(field.getFieldName(), targetObject, valuesMap);
            }
        }
    }

    /**
     * Parse the field (should be FieldType of MAP) to bean
     * names and values to be set on targetObject.
     * Multiple bean key=values are specified
     * by ConditionField.listSeparator (default of ",")
     * and key=value (the "=") are separated by
     * ConditionField.valueSeparator. Note that if a property for
     * a key is not available on the targetObject this will
     * just do nothing (not set any value) - this is done
     * silently. If targetObject is null and a key of
     * ConditionField.classnameKey (default of "_CLASSNAME_")
     * is provided in the field, a new object will be created
     * and the properties set on that object.
     * @param fieldName the field name
     * @param targetObject the object to set the bean values on
     * or null to create a new object using class defined
     * in the key that matches ConditionField.classnameKey.
     * @param valuesMap this OPTIONAL map can be provided
     * and all values that are set on the bean will be
     * placed in this map. It is fine to pass null for this
     * paramter.
     * @return The object which the beans properties were set
     * on. If targetObject is not null, this will be object,
     * if targetObject IS null and a key was provided
     * equal to ConditionField.classnameKey, the value associated
     * with that key will be the object that is created,
     * beans properties set, and the object returned.
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data). This can also be returned
     * if there is no object to set the bean properties on, ie,
     * targetObject is null and ConditionField.classnameKey is no
     * specified in the field. Also thrown if there is
     * a catastrphic BeanUtils problem or a problem creating
     * an object.
     */
    public Object parseFieldBean(final String fieldName,
                                 final Object targetObject,
                                 final Map<String, String> valuesMap)
            throws ConditionsParsingException {

        final int fieldNumber = getFieldNumber(fieldName);
        final ConditionField field = fields.get(fieldNumber);

        if (field.getFieldType() == ConditionField.FieldType.VALUE) {
            if (targetObject != null && StringUtils.isNotBlank(field.getValueBeanProperty())) {
                parseFieldBeanValue(field, targetObject, valuesMap);
            }
            return targetObject;
        }

        final Map<String, String> fieldValues = parseFieldMap(fieldName);
        final String classname = fieldValues.get(field.getClassnameKey());

        String prependObjectBean = "";
        if (StringUtils.isNotBlank(field.getValueBeanProperty())
                && StringUtils.isNotBlank(classname)) {
            prependObjectBean = field.getValueBeanProperty();
        }

        Object object = targetObject;
        if (object == null || prependObjectBean.length() > 0) {
            if (classname == null) {
                throw new ConditionsParsingException("targetObject is null and "
                    + field.getClassnameKey()
                    + " is not specified for line " + lineNumber);
            }
            try {
                final Object newObject = Class.forName(classname).newInstance();
                if (prependObjectBean.length() > 0) {
                    BeanUtils.setProperty(object, prependObjectBean, newObject);
                    prependObjectBean = prependObjectBean + ".";
                } else {
                    object = newObject;
                }
            } catch (InvocationTargetException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + classname
                    + " - InvocationTargetException for line " + lineNumber, e);
            } catch (InstantiationException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + classname
                    + " - InstantiationException for line " + lineNumber, e);
            } catch (IllegalAccessException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + classname
                    + " - IllegalAccessException for line " + lineNumber, e);
            } catch (ClassNotFoundException e) {
                throw new ConditionsParsingException("Could not create new object "
                    + "for classname " + classname
                    + " - ClassNotFoundException for line " + lineNumber, e);
            }
        }

        for (final String key : fieldValues.keySet()) {
            if (key.equals(field.getClassnameKey())) {
                continue;
            }
            final String value = fieldValues.get(key);
            try {
                BeanUtils.setProperty(object, prependObjectBean + key, value);
                if (valuesMap != null) {
                    valuesMap.put(key, value);
                }
            } catch (IllegalAccessException e) {
                throw new ConditionsParsingException(
                    "Could not create new object "
                    + "for classname " + classname
                    + " - IllegalAccessException for line " + lineNumber
                    + " key = " + prependObjectBean + key
                    + " / value = " + value, e);
            } catch (InvocationTargetException e) {
                throw new ConditionsParsingException(
                    "Could not create new object "
                    + "for classname " + classname
                    + " - InvocationTargetException for line " + lineNumber
                    + " key = " + prependObjectBean + key
                    + " / value = " + value, e);
            }
        }
        return object;
    }

    /**
     * When parseFieldBean is called and the field in question has
     * ConditionField.FieldType of VALUE and
     * ConditionField.valueBeanProperty is defined, this method
     * will be used to assign the value. Not only does this work
     * with values (ConditionField.list is false),
     * it also works arrays (ConditionField.list is true). The
     * The caveat is that the target object (bean) should define
     * the property to be an array, not a collection/list/set.
     * @param field the field (not fieldName as seen elsewhere)
     * we are setting the bean value for
     * @param targetObject the object to set the bean values on
     * or null to create a new object using class defined
     * in the key that matches ConditionField.classnameKey.
     * @param valuesMap this OPTIONAL map can be provided
     * and all values that are set on the bean will be
     * placed in this map. It is fine to pass null for this
     * paramter.
     * @throws ConditionsParsingException no data to parse,
     * beginParse or hasNext probably wasn't called (or called
     * passed the end of the data). Also thrown if there is
     * a catastrphic BeanUtils problem or a problem creating
     * an object.
     */
    private void parseFieldBeanValue(final ConditionField field,
                                     final Object targetObject,
                                 final Map<String, String> valuesMap)
            throws ConditionsParsingException {
        final Object fieldValue;
        boolean isClassname = false;
        if (field.isList()) {
            fieldValue = parseFieldValueStringArray(field.getFieldName());
        } else {
            if (field.isClassname()) {
                isClassname = true;
            }
            fieldValue = parseFieldValueString(field.getFieldName());
        }
        try {
            if (isClassname) {
                final Object newObject =
                        Class.forName((String) fieldValue).newInstance();
                BeanUtils.setProperty(targetObject,
                        field.getValueBeanProperty(), newObject);
            } else {
                BeanUtils.setProperty(targetObject,
                        field.getValueBeanProperty(), fieldValue);
                if (valuesMap != null) {
                        valuesMap.put(field.getValueBeanProperty(),
                                ArrayUtils.toString(fieldValue));
                }
            }
        } catch (InvocationTargetException e) {
            throw new ConditionsParsingException("BeanUtils problem "
                + " - InvocationTargetException for line " + lineNumber
                + " key = " + field.getValueBeanProperty()
                + " / value = " + fieldValue, e);
        } catch (InstantiationException e) {
            throw new ConditionsParsingException("BeanUtils problem "
                + " - InstantiationException for line " + lineNumber
                + " key = " + field.getValueBeanProperty()
                + " / value = " + fieldValue, e);
        } catch (IllegalAccessException e) {
            throw new ConditionsParsingException("BeanUtils problem "
                + " - IllegalAccessException for line " + lineNumber
                + " key = " + field.getValueBeanProperty()
                + " / value = " + fieldValue, e);
        } catch (ClassNotFoundException e) {
            throw new ConditionsParsingException("Could not create new object "
                + " - ClassNotFoundException for line " + lineNumber
                + " key = " + field.getValueBeanProperty()
                + " / value = " + fieldValue, e);
        }
    }

}
