XmlValue.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project 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.
 *
 * Waarp 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
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.common.xml;

import org.waarp.common.exception.InvalidArgumentException;
import org.waarp.common.utility.WaarpStringUtils;

import java.io.InvalidObjectException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * XmlValue base element
 */
public class XmlValue {

  private static final String CAN_NOT_CONVERT_VALUE = "Can not convert value ";

  private static final String TO_TYPE = " to type ";

  private static final String CAN_NOT_CONVERT_VALUE_FROM =
      "Can not convert value from ";

  private final XmlDecl decl;

  private Object value;

  private List<?> values;

  private XmlValue[] subXml;

  public XmlValue(final XmlDecl decl) {
    this.decl = decl;
    if (this.decl.isSubXml()) {
      if (this.decl.isMultiple()) {
        value = null;
        values = new ArrayList<XmlValue>();
        subXml = null;
        return;
      }
      final int len = this.decl.getSubXmlSize();
      final XmlDecl[] newDecls = this.decl.getSubXml();
      subXml = new XmlValue[len];
      for (int i = 0; i < len; i++) {
        subXml[i] = new XmlValue(newDecls[i]);
      }
      value = null;
      values = null;
      return;
    }
    if (this.decl.isMultiple()) {
      value = null;
      switch (getType()) {
        case BOOLEAN:
        case STRING:
        case TIMESTAMP:
        case SQLDATE:
        case SHORT:
        case DOUBLE:
        case LONG:
        case BYTE:
        case CHARACTER:
        case FLOAT:
        case INTEGER:
          values = new ArrayList<Boolean>();
          break;
        case XVAL:
        case EMPTY:
        default:
          break;
      }
    }
  }

  @SuppressWarnings("unchecked")
  public XmlValue(final XmlValue from) {
    this(from.decl);
    if (decl.isSubXml()) {
      if (decl.isMultiple()) {
        final List<XmlValue[]> subvalues = (List<XmlValue[]>) from.values;
        for (final XmlValue[] xmlValues : subvalues) {
          final XmlValue[] newValues = new XmlValue[xmlValues.length];
          for (int i = 0; i < xmlValues.length; i++) {
            newValues[i] = new XmlValue(xmlValues[i]);
          }
          ((Collection<XmlValue[]>) values).add(newValues);
        }
      } else {
        for (int i = 0; i < from.subXml.length; i++) {
          subXml[i] = new XmlValue(from.subXml[i]);
        }
      }
    } else if (decl.isMultiple()) {
      final List<Object> subvalues = (List<Object>) from.values;
      for (final Object object : subvalues) {
        try {
          addValue(getCloneValue(getType(), object));
        } catch (final InvalidObjectException e) {
          // nothing
        }
      }
    } else {
      try {
        setValue(from.getCloneValue());
      } catch (final InvalidObjectException e) {
        // Nothing
      }
    }
  }

  /**
   * @return the decl
   */
  public final XmlDecl getDecl() {
    return decl;
  }

  /**
   * Get Java field name
   *
   * @return the field name
   */
  public final String getName() {
    return decl.getName();
  }

  /**
   * @return the type
   */
  public final Class<?> getClassType() {
    return decl.getClassType();
  }

  /**
   * @return the type
   */
  public final XmlType getType() {
    return decl.getType();
  }

  /**
   * @return the xmlPath
   */
  public final String getXmlPath() {
    return decl.getXmlPath();
  }

  /**
   * @return True if this Value is a subXml
   */
  public final boolean isSubXml() {
    return decl.isSubXml();
  }

  /**
   * @return the associated SubXML with the XmlValue (might be null if
   *     singleton
   *     or Multiple)
   */
  public final XmlValue[] getSubXml() {
    return subXml;
  }

  /**
   * @return True if the Value are list of values
   */
  public final boolean isMultiple() {
    return decl.isMultiple();
  }

  /**
   * @return the associated list with the XmlValues (might be null if
   *     singleton
   *     or SubXml)
   */
  public final List<?> getList() {
    return values;
  }

  /**
   * Add a value into the Multiple values from the String (not compatible with
   * subXml)
   *
   * @param valueOrig
   *
   * @throws InvalidObjectException
   * @throws InvalidArgumentException
   */
  @SuppressWarnings("unchecked")
  public final void addFromString(final String valueOrig)
      throws InvalidObjectException, InvalidArgumentException {
    final String valueNew = XmlUtil.getExtraTrimed(valueOrig);
    switch (getType()) {
      case BOOLEAN:
        ((Collection<Boolean>) values).add(
            (Boolean) convert(getClassType(), valueNew));
        break;
      case INTEGER:
        ((Collection<Integer>) values).add(
            (Integer) convert(getClassType(), valueNew));
        break;
      case FLOAT:
        ((Collection<Float>) values).add(
            (Float) convert(getClassType(), valueNew));
        break;
      case CHARACTER:
        ((Collection<Character>) values).add(
            (Character) convert(getClassType(), valueNew));
        break;
      case BYTE:
        ((Collection<Byte>) values).add(
            (Byte) convert(getClassType(), valueNew));
        break;
      case LONG:
        ((Collection<Long>) values).add(
            (Long) convert(getClassType(), valueNew));
        break;
      case DOUBLE:
        ((Collection<Double>) values).add(
            (Double) convert(getClassType(), valueNew));
        break;
      case SHORT:
        ((Collection<Short>) values).add(
            (Short) convert(getClassType(), valueNew));
        break;
      case SQLDATE:
        ((Collection<Date>) values).add(
            (Date) convert(getClassType(), valueNew));
        break;
      case TIMESTAMP:
        ((Collection<Timestamp>) values).add(
            (Timestamp) convert(getClassType(), valueNew));
        break;
      case STRING:
        ((Collection<String>) values).add(
            (String) convert(getClassType(), valueNew));
        break;
      case XVAL:
        throw new InvalidObjectException(
            "XVAL cannot be assigned from String directly");
        // ((List<XmlValue>) this.values).add((XmlValue) value)
      case EMPTY:
        throw new InvalidObjectException("EMPTY cannot be assigned");
    }
  }

  /**
   * Add a value into the Multiple values from the Object
   *
   * @param value
   *
   * @throws InvalidObjectException
   */
  @SuppressWarnings("unchecked")
  public final void addValue(final Object value) throws InvalidObjectException {
    if (getType().isNativelyCompatible(value)) {
      switch (getType()) {
        case BOOLEAN:
          ((Collection<Boolean>) values).add((Boolean) value);
          break;
        case INTEGER:
          ((Collection<Integer>) values).add((Integer) value);
          break;
        case FLOAT:
          ((Collection<Float>) values).add((Float) value);
          break;
        case CHARACTER:
          ((Collection<Character>) values).add((Character) value);
          break;
        case BYTE:
          ((Collection<Byte>) values).add((Byte) value);
          break;
        case LONG:
          ((Collection<Long>) values).add((Long) value);
          break;
        case DOUBLE:
          ((Collection<Double>) values).add((Double) value);
          break;
        case SHORT:
          ((Collection<Short>) values).add((Short) value);
          break;
        case SQLDATE:
          if (Date.class.isAssignableFrom(value.getClass())) {
            ((Collection<Date>) values).add((Date) value);
          } else if (java.util.Date.class.isAssignableFrom(value.getClass())) {
            ((Collection<Date>) values).add(
                new Date(((java.util.Date) value).getTime()));
          }
          break;
        case TIMESTAMP:
          ((Collection<Timestamp>) values).add((Timestamp) value);
          break;
        case STRING:
          ((Collection<String>) values).add(
              XmlUtil.getExtraTrimed((String) value));
          break;
        case XVAL:
          ((Collection<XmlValue[]>) values).add((XmlValue[]) value);
          break;
        default:
          throw new InvalidObjectException(
              CAN_NOT_CONVERT_VALUE_FROM + value.getClass() + TO_TYPE +
              getClassType());
      }
    } else {
      throw new InvalidObjectException(
          CAN_NOT_CONVERT_VALUE_FROM + value.getClass() + TO_TYPE +
          getClassType());
    }
  }

  /**
   * @return the value as Object (might be null if multiple)
   */
  public final Object getValue() {
    return value;
  }

  /**
   * Utility function to get a clone of a value
   *
   * @param type
   * @param value
   *
   * @return the clone Object
   *
   * @throws InvalidObjectException
   */
  public static Object getCloneValue(final XmlType type, final Object value)
      throws InvalidObjectException {
    if (value == null) {
      throw new InvalidObjectException(
          "Can not convert value from null to type " + type.classType);
    }
    switch (type) {
      case BOOLEAN:
        return value;
      case INTEGER:
        return value;
      case FLOAT:
        return value;
      case CHARACTER:
        return value;
      case BYTE:
        return value;
      case LONG:
        return value;
      case DOUBLE:
        return value;
      case SHORT:
        return value;
      case SQLDATE:
        return new Date(((Date) value).getTime());
      case TIMESTAMP:
        return new Timestamp(((Timestamp) value).getTime());
      case STRING:
        return value;
      case XVAL:
        return new XmlValue((XmlValue) value);
      case EMPTY:
      default:
        throw new InvalidObjectException(
            CAN_NOT_CONVERT_VALUE_FROM + value.getClass() + TO_TYPE +
            type.classType);
    }
  }

  /**
   * @return a clone of the value as Object (might be null if multiple)
   *
   * @throws InvalidObjectException
   */
  public final Object getCloneValue() throws InvalidObjectException {
    if (getType() == XmlType.EMPTY) {
      return new XmlValue(decl);
    }
    return getCloneValue(getType(), value);
  }

  /**
   * @return the value as a string
   */
  public final String getString() {
    if (getType().isString()) {
      return XmlUtil.getExtraTrimed((String) value);
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type String");
  }

  /**
   * @return the value as an integer
   */
  public final int getInteger() {
    if (getType().isInteger()) {
      return (Integer) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Integer");
  }

  /**
   * @return the value as a boolean
   */
  public final boolean getBoolean() {
    if (getType().isBoolean()) {
      return (Boolean) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Boolean");
  }

  /**
   * @return the value as a long
   */
  public final long getLong() {
    if (getType().isLong()) {
      return (Long) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Long");
  }

  /**
   * @return the value as a float
   */
  public final float getFloat() {
    if (getType().isFloat()) {
      return (Float) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Float");
  }

  /**
   * @return the value as a float
   */
  public final char getCharacter() {
    if (getType().isCharacter()) {
      return (Character) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() +
        " to type Character");
  }

  /**
   * @return the value as a float
   */
  public final byte getByte() {
    if (getType().isByte()) {
      return (Byte) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Byte");
  }

  /**
   * @return the value as a float
   */
  public final double getDouble() {
    if (getType().isDouble()) {
      return (Double) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Double");
  }

  /**
   * @return the value as a float
   */
  public final short getShort() {
    if (getType().isShort()) {
      return (Short) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Short");
  }

  /**
   * @return the value as a float
   */
  public final Date getDate() {
    if (getType().isDate()) {
      return (Date) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() + " to type Date");
  }

  /**
   * @return the value as a float
   */
  public final Timestamp getTimestamp() {
    if (getType().isTimestamp()) {
      return (Timestamp) value;
    }
    throw new IllegalArgumentException(
        CAN_NOT_CONVERT_VALUE_FROM + decl.getClassType() +
        " to type Timestamp");
  }

  /**
   * Set a value from String
   *
   * @param value
   *
   * @throws InvalidArgumentException
   */
  public final void setFromString(final String value)
      throws InvalidArgumentException {
    this.value = convert(getClassType(), XmlUtil.getExtraTrimed(value));
  }

  /**
   * Test if the Value is empty. If it is a SubXml or isMultiple, check if
   * subnodes are present but not if those
   * nodes are empty.
   *
   * @return True if the Value is Empty
   */
  public final boolean isEmpty() {
    if (isSubXml()) {
      if (isMultiple()) {
        return values.isEmpty();
      } else {
        return subXml.length == 0;
      }
    }
    if (isMultiple()) {
      return values.isEmpty();
    } else {
      return value == null;
    }
  }

  /**
   * Get a value into a String
   *
   * @return the value in String format
   */
  public final String getIntoString() {
    if (!isMultiple() && !isSubXml()) {
      if (value != null) {
        return value.toString();
      } else {
        return "";
      }
    } else {
      throw new IllegalArgumentException(
          "Cannot convert Multiple values to single String");
    }
  }

  /**
   * @param value the value to set
   *
   * @throws InvalidObjectException
   * @throws NumberFormatException
   */
  @SuppressWarnings("unchecked")
  public final void setValue(final Object value) throws InvalidObjectException {
    if (getType().isNativelyCompatible(value)) {
      switch (getType()) {
        case BOOLEAN:
        case TIMESTAMP:
        case SHORT:
        case DOUBLE:
        case LONG:
        case BYTE:
        case CHARACTER:
        case FLOAT:
        case INTEGER:
          this.value = value;
          break;
        case SQLDATE:
          if (Date.class.isAssignableFrom(value.getClass())) {
            this.value = value;
          } else if (java.util.Date.class.isAssignableFrom(value.getClass())) {
            this.value = new Date(((java.util.Date) value).getTime());
          }
          break;
        case STRING:
          this.value = XmlUtil.getExtraTrimed((String) value);
          break;
        case XVAL:
          final XmlValue[] newValue = (XmlValue[]) value;
          if (isSubXml()) {
            // should check also internal XmlDecl equality but
            // can only check size
            if (decl.getSubXmlSize() != newValue.length) {
              throw new InvalidObjectException(
                  "XmlDecl are not compatible from Array of XmlValue" +
                  TO_TYPE + getClassType());
            }
            if (isMultiple()) {
              ((List<XmlValue[]>) values).add(newValue);
            } else {
              subXml = newValue;
            }
          } else {
            throw new InvalidObjectException(
                "Can not convert value from Array of XmlValue" + TO_TYPE +
                getClassType());
          }
          break;
        default:
          throw new InvalidObjectException(
              CAN_NOT_CONVERT_VALUE_FROM + value.getClass() + TO_TYPE +
              getClassType());
      }
    } else {
      throw new InvalidObjectException(
          CAN_NOT_CONVERT_VALUE_FROM + value.getClass() + TO_TYPE +
          getClassType());
    }
  }

  /**
   * Convert String value to the specified type. Throws
   * InvalidArgumentException
   * if type is unrecognized.
   *
   * @throws InvalidArgumentException
   */
  protected static Object convert(final Class<?> type, final String value)
      throws InvalidArgumentException {
    try {
      // test from specific to general
      //
      if (String.class.isAssignableFrom(type)) {
        return value;
      }
      // primitives
      //
      else if (type.equals(Boolean.TYPE)) {
        if ("1".equals(value)) {
          return Boolean.TRUE;
        }
        return Boolean.valueOf(value);
      } else if (type.equals(Integer.TYPE)) {
        return Integer.valueOf(value);
      } else if (type.equals(Float.TYPE)) {
        return Float.valueOf(value);
      } else if (type.equals(Character.TYPE)) {
        return Character.valueOf(value.charAt(0));
      } else if (type.equals(Byte.TYPE)) {
        return Byte.valueOf(value);
      } else if (type.equals(Long.TYPE)) {
        return Long.valueOf(value);
      } else if (type.equals(Double.TYPE)) {
        return Double.valueOf(value);
      } else if (type.equals(Short.TYPE)) {
        return Short.valueOf(value);
      }
      // primitive wrappers
      //
      else if (Boolean.class.isAssignableFrom(type)) {
        if ("true".equalsIgnoreCase(value)) {
          return Boolean.TRUE;
        } else {
          return Boolean.FALSE;
        }
      } else if (Character.class.isAssignableFrom(type)) {
        if (value.length() == 1) {
          return Character.valueOf(value.charAt(0));
        } else {
          throw new IllegalArgumentException(
              CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
        }
      } else if (Number.class.isAssignableFrom(type)) {
        if (Double.class.isAssignableFrom(type)) {
          return Double.valueOf(value);
        } else if (Float.class.isAssignableFrom(type)) {
          return Float.valueOf(value);
        } else if (Integer.class.isAssignableFrom(type)) {
          return Integer.valueOf(value);
        } else if (Long.class.isAssignableFrom(type)) {
          return Long.valueOf(value);
        } else if (Short.class.isAssignableFrom(type)) {
          return Short.valueOf(value);
        }
        // other primitive-like classes
        //
        else if (BigDecimal.class.isAssignableFrom(type)) {
          throw new IllegalArgumentException("Can not use type " + type);
        } else if (BigInteger.class.isAssignableFrom(type)) {
          throw new IllegalArgumentException("Can not use type " + type);
        } else {
          throw new IllegalArgumentException(
              CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
        }
      }
      //
      // Time and date. We stick close to the JDBC representations
      // for time and date, but add the "GMT" timezone so XML files
      // can be transferred across timezones without ambiguity. See
      // java.sql.Date.toString() and java.sql.Timestamp.toString().
      //
      else if (Date.class.isAssignableFrom(type)) {
        return new Date(
            WaarpStringUtils.getDateFormat().parse(value).getTime());
      } else if (Timestamp.class.isAssignableFrom(type)) {
        final int dotIndex = value.indexOf('.');
        final int spaceIndex = value.indexOf(' ', dotIndex);
        if (dotIndex < 0 || spaceIndex < 0) {
          throw new IllegalArgumentException(
              CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
        }
        final Timestamp ts = new Timestamp(WaarpStringUtils.getTimestampFormat()
                                                           .parse(
                                                               value.substring(
                                                                   0, dotIndex))
                                                           .getTime());
        final int nanos =
            Integer.parseInt(value.substring(dotIndex + 1, spaceIndex));
        ts.setNanos(nanos);

        return ts;
      } else if (java.util.Date.class.isAssignableFrom(type)) {
        // Should not be
        return new Date(
            WaarpStringUtils.getTimeFormat().parse(value).getTime());
      } else {
        throw new IllegalArgumentException(
            CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
      }
    } catch (final NumberFormatException e) {
      throw new InvalidArgumentException(
          CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
    } catch (final IllegalArgumentException e) {
      throw new InvalidArgumentException(
          CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type, e);
    } catch (final ParseException e) {
      throw new InvalidArgumentException(
          CAN_NOT_CONVERT_VALUE + value + TO_TYPE + type);
    }
  }

  @Override
  public String toString() {
    return "Val: " + (isMultiple()? values.size() + " elements" :
        value != null? value.toString() :
            subXml != null? "subXml" : "no value") + ' ' + decl;
  }

  public final String toFullString() {
    final StringBuilder detail = new StringBuilder("Val: " + (isMultiple()?
        values.size() + " elements" : value != null? value.toString() :
        subXml != null? "subXml" : "no value") + ' ' + decl);
    if (decl.isSubXml()) {
      if (isMultiple()) {
        detail.append('[');
        for (final Object obj : values) {
          if (obj instanceof XmlValue) {
            detail.append(((XmlValue) obj).toFullString()).append(", ");
          } else {
            detail.append('[');
            for (final XmlValue obj2 : (XmlValue[]) obj) {
              detail.append(obj2.toFullString()).append(", ");
            }
            detail.append("], ");
          }
        }
        detail.append(']');
      } else {
        detail.append('[');
        for (final XmlValue obj : subXml) {
          detail.append(obj.toFullString()).append(", ");
        }
        detail.append(']');
      }
    }
    return detail.toString();
  }
}