/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.resource.metadata.impl;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.uima.UIMAFramework;
import org.apache.uima.UIMARuntimeException;
import org.apache.uima.UIMA_IllegalArgumentException;
import org.apache.uima.UIMA_UnsupportedOperationException;
import org.apache.uima.UimaContext;
import org.apache.uima.UimaContextHolder;
import org.apache.uima.internal.util.XMLUtils;
import org.apache.uima.resource.metadata.AllowedValue;
import org.apache.uima.resource.metadata.MetaDataObject;
import org.apache.uima.util.ConcurrentHashMapWithProducer;
import org.apache.uima.util.InvalidXMLException;
import org.apache.uima.util.Level;
import org.apache.uima.util.NameClassPair;
import org.apache.uima.util.XMLParser;
import org.apache.uima.util.XMLSerializer;
import org.apache.uima.util.XMLSerializer.CharacterValidatingContentHandler;
import org.apache.uima.util.XMLizable;
import org.apache.uima.util.impl.Settings_impl;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * Abstract base class for all MetaDataObjects in the reference implementation. Provides basic
 * support for getting and setting property values given their names, using bean introspection and
 * reflection.
 * <p>
 * Also provides the ability to write objects to XML and build objects from their DOM
 * representation, as required to implement the {@link XMLizable} interface, which is a
 * superinterface of {@link MetaDataObject}. In future versions, this could be replaced by a
 * non-proprietary XML binding solution such as JAXB or EMF.
 * <p>
 * 
 * The implementation for getting and setting property values uses the JavaBeans introspection API.
 * Therefore subclasses of this class must be valid JavaBeans and either use the standard naming
 * conventions for getters and setters. BeanInfo augmentation is ignored; the implementation here
 * uses the flag IGNORE_ALL_BEANINFO. See
 * <a href="http://java.sun.com/docs/books/tutorial/javabeans/"> The Java Beans Tutorial</a> for
 * more information.
 * 
 * To support XML Comments, which can occur between any sub-elements, including array values, the
 * "data" for all objects is stored in a pair of ArrayLists; one holds the "name" of the slot, the
 * other the value; comments are interspersed within this list where they occur.
 * 
 * To the extent possible, this should be the *only* data storage used for the xml element.
 * Subclasses should access these elements on demand. Data will be read into / written from this
 * representation; Cloning will copy this information.
 * 
 * For getters that need to do some special initial processing, a global flag will be set whenever
 * this base code changes the underlying value.
 */
public abstract class MetaDataObject_impl implements MetaDataObject {

  static final long serialVersionUID = 5876728533863334480L;

  private static String PROP_NAME_SOURCE_URL = "sourceUrl";
  private static String PROP_NAME_INFOSET = "infoset";

  // note: AttributesImpl is just a "getter" for attributes, has no setter methods,
  // see javadocs for Attributes (sax)
  private static final Attributes EMPTY_ATTRIBUTES = new AttributesImpl();

//@formatter:off
  /*
   * Cache for Java Bean lookup
   * 
   *   Key: the Class object of the MetaDataObject, corresponds to some XML element
   *   Value: a list of objects, one for each "attribute", holding:
   *     the attribute name
   *     the reader (a Method)
   *     the writer (a Method)
   *     the java Class of the data type of this attribute <converted to a wrapper class for primitives>
   */
//@formatter:on

  public static class MetaDataAttr {
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      MetaDataAttr other = (MetaDataAttr) obj;
      if (clazz == null) {
        if (other.clazz != null)
          return false;
      } else if (!clazz.equals(other.clazz))
        return false;
      if (name == null) {
        if (other.name != null)
          return false;
      } else if (!name.equals(other.name))
        return false;
      return true;
    }

    final String name;
    final Method reader;
    final Method writer;
    final Class clazz;

    public MetaDataAttr(String name, Method reader, Method writer, Class clazz) {
      this.name = name;
      this.reader = reader;
      this.writer = writer;
      this.clazz = clazz;
    }
  }

  private static final List<MetaDataAttr> EMPTY_ATTRIBUTE_LIST = Collections.emptyList();

  // Cache for Java Bean info lookup
  // Class level cache (static) for introspection - 30x speedup in CDE for large descriptor
  // holds "filtered" set of Java Bean Info
  private static final transient ConcurrentHashMapWithProducer<Class<? extends MetaDataObject_impl>, MetaDataAttr[]> class2attrsMap = new ConcurrentHashMapWithProducer<>();
  // holds the unfiltered set of Java Bean Info
  private static final transient ConcurrentHashMapWithProducer<Class<? extends MetaDataObject_impl>, MetaDataAttr[]> class2attrsMapUnfiltered = new ConcurrentHashMapWithProducer<>();

  /**
   * methods used for serializing
   *
   */
  public interface Serializer {
    void outputStartElement(Node node, String nameSpace, String localName, String qname,
            Attributes attributes) throws SAXException;

    void outputEndElement(Node node, String aNamespace, String localname, String qname)
            throws SAXException;

    void outputStartElementForArrayElement(Node node, String nameSpace, String localName,
            String qname, Attributes attributes) throws SAXException;

    void outputEndElementForArrayElement(Node node, String aNamespace, String localname,
            String qname) throws SAXException;

    void insertNl();

    boolean shouldBeSkipped(PropertyXmlInfo propInfo, Object val, MetaDataObject_impl mdo);

    boolean startElementProperty();

    void deleteNodeStore();

    boolean indentChildElements(XmlizationInfo info, MetaDataObject_impl mdo);

    void saveAndAddNodeStore(Node infoset);

    void addNodeStore();

    void writeDelayedStart(String name) throws SAXException;

    void writeSimpleValue(Object val) throws SAXException;

    void writeSimpleValueWithTag(String className, Object value, Node node) throws SAXException;

    boolean shouldEncloseInArrayElement(Class propClass);

    boolean isArrayHasIndentableElements(Object array);

    void maybeStartArraySymbol() throws SAXException;

    void maybeEndArraySymbol() throws SAXException;

    Node findMatchingSubElement(String elementName);
  }

  /**
   * Information, kept globally (by thread) for one serialization
   * 
   * Inherited by some custom impls, e.g. TypeOrFeature_impl
   */
  public static class SerialContext {

    final public ContentHandler ch;
    final public Serializer serializer;

    public SerialContext(ContentHandler ch, Serializer serializer) {
      this.ch = ch;
      this.serializer = serializer;
    }
  }

//@formatter:off
  /**
   * Keeps the serialContext by thread
   *   set when starting to serialize
   *   cleared at the end (in finally clause) to prevent memory leaks
   *   Inherited by some custom impls, e.g. TypeOrFeature_impl
  */
//@formatter:on
  public static final ThreadLocal<SerialContext> serialContext = new ThreadLocal<>();

  public static SerialContext getSerialContext(ContentHandler ch) {
    SerialContext sc = serialContext.get();
    if (null == serialContext.get()) {
      sc = new SerialContext(ch, getSerializerFromContentHandler(ch));
      serialContext.set(sc);
    }
    return sc;
  }

  private transient URL mSourceUrl;

  // This is only used if we are capturing comments and ignorable whitespace in the XML
  private transient Node infoset = null; // by default, set to null

  public void setInfoset(Node infoset) {
    this.infoset = infoset;
  }

  public Node getInfoset() {
    return infoset;
  }

  /**
   * Creates a new <code>MetaDataObject_impl</code> with null attribute values
   */
  public MetaDataObject_impl() {
  }

  /**
   * Override this method to include additional attributes
   * 
   * @return additional attributes
   */
  public List<MetaDataAttr> getAdditionalAttributes() {
    return EMPTY_ATTRIBUTE_LIST;
  }

  /**
   * Like getAttributes, but doesn't filter the attributes. Design is only for backwards
   * compatibility. Unfiltered version used only by getAttributeValue and setAttributeValue
   * 
   * @return an unfiltered array of Attribute objects associated with this class
   */
  MetaDataAttr[] getUnfilteredAttributes() {
    final Class<? extends MetaDataObject_impl> clazz = this.getClass();
    MetaDataAttr[] attrs = class2attrsMapUnfiltered.get(clazz);
    if (null == attrs) {
      getAttributesFromBeans(clazz);
    }
    return class2attrsMapUnfiltered.get(clazz);
  }

  /**
   * On first call, looks up the information using JavaBeans introspection, but then caches the
   * result for subsequent calls.
   * 
   * Any class which wants to add additional parameters needs to implement / override
   * getAdditionalParameters.
   * 
   * @return an array of Attribute objects associated with this class
   */
  MetaDataAttr[] getAttributes() {
    final Class<? extends MetaDataObject_impl> clazz = this.getClass();
    MetaDataAttr[] attrs = class2attrsMap.get(clazz);
    if (null == attrs) {
      getAttributesFromBeans(clazz);
    }
    return class2attrsMap.get(clazz);
  }

  private void getAttributesFromBeans(final Class<? extends MetaDataObject_impl> clazz) {
    PropertyDescriptor[] pds;
    try {
      pds = Introspector.getBeanInfo(clazz, Introspector.IGNORE_ALL_BEANINFO)
              .getPropertyDescriptors();
    } catch (IntrospectionException e) {
      throw new UIMARuntimeException(e);
    }
    ArrayList<MetaDataAttr> resultList = new ArrayList<>(pds.length);
    ArrayList<MetaDataAttr> resultListUnfiltered = new ArrayList<>(pds.length);
    for (PropertyDescriptor pd : pds) {
      String propName = pd.getName();
      Class<?> propClass = pd.getPropertyType();
      // translate primitive types (int, boolean, etc.) to wrapper classes
      if (null != propClass && propClass.isPrimitive()) {
        propClass = getWrapperClass(propClass);
      }
      MetaDataAttr mda = new MetaDataAttr(propName, pd.getReadMethod(), pd.getWriteMethod(),
              propClass);
      resultListUnfiltered.add(mda);
      // only include properties with read and write methods,
      // and don't include the SourceUrl property, which is for
      // internal bookkeeping and shouldn't affect object equality
      // and don't include infoset, which is for internal bookkeeping
      // related to comments and whitespace
      if (pd.getReadMethod() != null && pd.getWriteMethod() != null
              && !pd.getName().equals(PROP_NAME_SOURCE_URL)
              && !pd.getName().equals(PROP_NAME_INFOSET)) {
        resultList.add(mda);
      }
    }
    resultList.addAll(getAdditionalAttributes());
    resultListUnfiltered.addAll(getAdditionalAttributes());
    MetaDataAttr[] attrs = resultList.toArray(new MetaDataAttr[resultList.size()]);
    MetaDataAttr[] otherAttrs = class2attrsMap.putIfAbsent(clazz, attrs);

    attrs = resultListUnfiltered.toArray(new MetaDataAttr[resultListUnfiltered.size()]);
    otherAttrs = class2attrsMapUnfiltered.putIfAbsent(clazz, attrs);
    attrs = (otherAttrs != null) ? otherAttrs : attrs;
  }

  /**
   * Returns a list of <code>NameClassPair</code> objects indicating the attributes of this object
   * and the String names of the Classes of the attributes' values. For primitive types, the wrapper
   * classes will be returned (e.g. <code>java.lang.Integer</code> instead of int).
   * 
   * Several subclasses override this, to add additional items to the list.
   * 
   * @see org.apache.uima.resource.metadata.MetaDataObject#listAttributes()
   * @deprecated - use getAttributes
   */
  @Override
  @Deprecated
  public List<NameClassPair> listAttributes() {

    try {
      PropertyDescriptor[] props = getPropertyDescriptors();
      List<NameClassPair> resultList = new ArrayList<>(props.length);
      for (int i = 0; i < props.length; i++) {
        // only list properties with read and write methods,
        // and don't include the SourceUrl property, which is for
        // internal bookkeeping and shouldn't affect object equality
        // and don't include infoset, which is for internal bookkeeping
        // related to comments and whitespace
        if (props[i].getReadMethod() != null && props[i].getWriteMethod() != null
                && !props[i].getName().equals(PROP_NAME_SOURCE_URL)
                && !props[i].getName().equals(PROP_NAME_INFOSET)) {
          String propName = props[i].getName();
          Class propClass = props[i].getPropertyType();
          // translate primitive types (int, boolean, etc.) to wrapper classes
          if (propClass.isPrimitive()) {
            propClass = getWrapperClass(propClass);
          }
          resultList.add(new NameClassPair(propName, propClass.getName()));
        }
      }
      return resultList;
    } catch (IntrospectionException e) {
      throw new UIMARuntimeException(e);
    }
  }

  private Object getAttributeValue(MetaDataAttr attr) {
    final Method reader = attr.reader;
    if (reader != null) {
      try {
        return reader.invoke(this);
      } catch (Exception e) {
        throw new UIMARuntimeException(e);
      }
    }
    return null;
  }

  /**
   * @see org.apache.uima.resource.metadata.MetaDataObject#getAttributeValue(String)
   */
  @Override
  public Object getAttributeValue(String aName) {
    try {
      MetaDataAttr[] attrs = getUnfilteredAttributes();
      for (MetaDataAttr attr : attrs) {
        if (attr.name.equals(aName)) {
          Method reader = attr.reader;
          if (reader != null) {
            return reader.invoke(this);
          }
        }
      }
      return null;
    } catch (Exception e) {
      throw new UIMARuntimeException(e);
    }
  }

  /**
   * Gets the Class of the given attribute's value. For primitive types, the wrapper classes will be
   * returned (e.g. <code>java.lang.Integer</code> instead of int).
   * 
   * @param aName
   *          name of an attribute
   * 
   * @return Class of value that may be assigned to the named attribute. Returns <code>null</code>
   *         if there is no attribute with the given name.
   */
  public Class getAttributeClass(String aName) {
    MetaDataAttr[] attrList = getAttributes();
    for (MetaDataAttr attr : attrList) {
      if (attr.name.equals(aName)) {
        return attr.clazz;
      }
    }
    return null;
  }

  /**
   * Returns whether this object is modifiable. MetaDataObjects are modifiable by default.
   * 
   * @see org.apache.uima.resource.metadata.MetaDataObject#isModifiable()
   */
  @Override
  public boolean isModifiable() {
    return true;
  }

  private void setAttributeValue(MetaDataAttr attr, Object aValue) {
    Method writer = attr.writer;
    if (writer != null) {
      try {
        writer.invoke(this, aValue);
      } catch (IllegalArgumentException e) {
        throw new UIMA_IllegalArgumentException(
                UIMA_IllegalArgumentException.METADATA_ATTRIBUTE_TYPE_MISMATCH,
                new Object[] { aValue, attr.name }, e);
      } catch (Exception e) {
        throw new UIMARuntimeException(e);
      }
    }
  }

  /**
   * @see org.apache.uima.resource.metadata.MetaDataObject#setAttributeValue(String, Object)
   */
  @Override
  public void setAttributeValue(String aName, Object aValue) {
    try {
      MetaDataAttr[] attrs = getUnfilteredAttributes();
      for (MetaDataAttr attr : attrs) {
        if (attr.name.equals(aName)) {
          Method writer = attr.writer;
          if (writer != null) {
            try {
              writer.invoke(this, new Object[] { aValue });
            } catch (IllegalArgumentException e) {
              throw new UIMA_IllegalArgumentException(
                      UIMA_IllegalArgumentException.METADATA_ATTRIBUTE_TYPE_MISMATCH,
                      new Object[] { aValue, aName }, e);
            }
          } else {
            throw new UIMA_UnsupportedOperationException(
                    UIMA_UnsupportedOperationException.NOT_MODIFIABLE,
                    new Object[] { aName, this.getClass().getName() });
          }
        }
      }
    } catch (UIMA_IllegalArgumentException e) {
      throw e;
    } catch (Exception e) {
      throw new UIMARuntimeException(e);
    }
  }

  /**
   * Gets the relative path base used to resolve imports. This is equal to the sourceUrl of this
   * object, if known (i.e. if the object was parsed from an XML file or if setSourceUrl was
   * explicitly called). If the source URL is not known, the value of the user.dir System property
   * is returned.
   * 
   * @return the base URL for resolving relative paths in this object
   */
  public URL getRelativePathBase() {
    if (mSourceUrl != null) {
      return mSourceUrl;
    } else {
      try {
        return new File(System.getProperty("user.dir")).toURL();
      } catch (MalformedURLException e) {
        try {
          return new URL("file:/");
        } catch (MalformedURLException e1) {
          // assert false;
          return null;
        }
      }
    }
  }

  /**
   * Gets the URL from which this object was parsed. When this object is parsed from an XML file,
   * this is set by the parser to the URL of the source file XML file. If the object has been
   * created by some other method, the source URL will not be known, and this method will return
   * null.
   * <p>
   * This setting is used to resolve imports and is also included in exception messages to indicate
   * the source of the problem.
   * 
   * @return the source URL from which this object was parsed
   */
  @Override
  public URL getSourceUrl() {
    return mSourceUrl;
  }

  /**
   * If the sourceURL of this object is non-null, returns its string representation. If it is null,
   * returns "&lt;unknown&gt;". Useful for error messages.
   * 
   * @return the source URL as a string, or "&lt;unknown&gt;"
   */
  @Override
  public String getSourceUrlString() {
    return mSourceUrl != null ? mSourceUrl.toString() : "<unknown>";
  }

  /**
   * Sets the source URL of this object, only if that URL is currently set to null. This is used
   * internally to update null relative base paths before doing import resolution, without
   * overriding user-specified settings.
   * 
   * @param aUrl
   *          the location of the XML file from which this object was parsed
   */
  public void setSourceUrlIfNull(URL aUrl) {
    if (mSourceUrl == null) {
      setSourceUrl(aUrl);
    }
  }

  /**
   * Sets the URL from which this object was parsed. Typically only the XML parser sets this. This
   * recursively sets the source URL of all descendants of this object.
   * 
   * Recursion doesn't happen for sub arrays/maps of maps or arrays.
   * 
   * @param aUrl
   *          the location of the XML file from which this object was parsed
   */
  @Override
  public void setSourceUrl(URL aUrl) {
    mSourceUrl = aUrl;

    // set recursively on subobjects
    MetaDataAttr[] attrs = getAttributes();
    for (MetaDataAttr attr : attrs) {
      Object val = getAttributeValue(attr);
      if (val instanceof MetaDataObject_impl) {
        ((MetaDataObject_impl) val).setSourceUrl(aUrl);
      } else if (val != null && val.getClass().isArray()) {
        Object[] arrayVal = (Object[]) val;
        for (Object item : arrayVal) {
          if (item instanceof MetaDataObject_impl) {
            ((MetaDataObject_impl) item).setSourceUrl(aUrl);
          }
        }
      } else if (val instanceof Map) {
        Collection values = ((Map) val).values();
        for (Object value : values) {
          if (value instanceof MetaDataObject_impl) {
            ((MetaDataObject_impl) value).setSourceUrl(aUrl);
          }
        }
      }
    }
  }

  /**
   * @see org.apache.uima.resource.metadata.MetaDataObject#clone() multi-core: could be cloning
   *      while another thread is modifying?
   */
  @Override
  public Object clone() {
    // System.out.println("MetaDataObject_impl: clone");
    MetaDataObject_impl clone = null;
    try {
      // do the default cloning
      clone = (MetaDataObject_impl) super.clone();
    } catch (CloneNotSupportedException e) {
      // assert false : "All MetaDataObjects are Cloneable";
      throw new UIMARuntimeException(e);
    }

    // now clone all values that are MetaDataObjects
    MetaDataAttr[] attrs = getAttributes();
    for (MetaDataAttr attr : attrs) {
      // String attrName = ncp.name;
      Object val = getAttributeValue(attr);
      if (val instanceof MetaDataObject) {
        Object clonedVal = ((MetaDataObject) val).clone();
        clone.setAttributeValue(attr, clonedVal);
      } else if (val != null && val.getClass().isArray()) {
        // clone the array, and clone any MetaDataObjects in the array
        Class componentType = val.getClass().getComponentType();
        int length = Array.getLength(val);
        Object arrayClone = Array.newInstance(componentType, length);
        for (int j = 0; j < length; j++) {
          Object component = Array.get(val, j);
          if (component instanceof MetaDataObject) {
            component = ((MetaDataObject) component).clone();
          }
          Array.set(arrayClone, j, component);
        }
        clone.setAttributeValue(attr, arrayClone);
      }
    }
    return clone;
  }

  /**
   * Dump this metadata object's attributes and values to a String. This is useful for debugging.
   */
  @Override
  public String toString() {
    StringBuffer buf = new StringBuffer();
    buf.append(getClass().getName()).append(": \n");
    MetaDataAttr[] attrList = getAttributes();
    for (MetaDataAttr attr : attrList) {
      buf.append(attr.name + " = ");
      Object val = getAttributeValue(attr);
      if (val == null)
        buf.append("NULL");
      else if (val instanceof Object[]) {
        Object[] array = (Object[]) val;
        buf.append("Array{");
        for (int j = 0; j < array.length; j++) {
          buf.append(j).append(": ").append(array[j].toString()).append('\n');
        }
        buf.append("}\n");
      } else
        buf.append(val.toString());
      buf.append('\n');
    }
    return buf.toString();
  }

  /**
   * Determines if this object is equal to another. Two MetaDataObjects are equivalent if they share
   * the same attributes and the same values for those attributes.
   * 
   * @param aObj
   *          object with which to compare this object
   * 
   * @return true if and only if this object is equal to <code>aObj</code>
   */
  @Override
  public boolean equals(Object aObj) {
    if (!(aObj instanceof MetaDataObject_impl)) {
      return false;
    }
    MetaDataObject_impl mdo = (MetaDataObject_impl) aObj;
    // get the attributes (and classes) for the two objects

    MetaDataAttr[] theseAttrs = this.getAttributes();
    MetaDataAttr[] thoseAttrs = mdo.getAttributes();
    // attribute lists must be same length
    if (theseAttrs.length != thoseAttrs.length) {
      return false;
    }
    // iterate through all attributes in this object
    List<MetaDataAttr> thoseAttrsAsList = Arrays.asList(thoseAttrs);
    for (MetaDataAttr attr : theseAttrs) {
      // other object must contain this attribute
      if (!thoseAttrsAsList.contains(attr)) {
        return false;
      }
      // get values and test equivalency
      Object val1 = this.getAttributeValue(attr);
      Object val2 = mdo.getAttributeValue(attr);
      if (!valuesEqual(val1, val2)) {
        return false;
      }
    }
    // if we get this far, objects are equal
    return true;
  }

//@formatter:off
  /**
   * Compare 2 values for equality.  
   * Reason val1.equals(val2) is not used:
   *   If val1 is of type Object[], the equal test is object identity equality, not
   *      element by element identity.
   *   So we use Arrays.equals or deepEquals instead.
   * 
   * @param val1
   * @param val2
   * @return true if equal
   */
//@formatter:on
  private boolean valuesEqual(Object val1, Object val2) {
    if (val1 == null) {
      return val2 == null;
    }

    if (val1.getClass().isArray()) {
      if (val2.getClass() != val1.getClass()) {
        return false;
      }
      // some of this may not be necessary - it depends what kind of array values are actually used
      // The "if" statements below are in guessed order of frequency of occurance
      if (val1 instanceof String[])
        return Arrays.equals((String[]) val1, (String[]) val2);
      // deepEquals handles arrays whose elements are arrays
      if (val1 instanceof Object[])
        return Arrays.deepEquals((Object[]) val1, (Object[]) val2);
      if (val1 instanceof int[])
        return Arrays.equals((int[]) val1, (int[]) val2);
      if (val1 instanceof float[])
        return Arrays.equals((float[]) val1, (float[]) val2);
      if (val1 instanceof double[])
        return Arrays.equals((double[]) val1, (double[]) val2);
      if (val1 instanceof boolean[])
        return Arrays.equals((boolean[]) val1, (boolean[]) val2);
      if (val1 instanceof byte[])
        return Arrays.equals((byte[]) val1, (byte[]) val2);
      if (val1 instanceof short[])
        return Arrays.equals((short[]) val1, (short[]) val2);
      if (val1 instanceof long[])
        return Arrays.equals((long[]) val1, (long[]) val2);
      return Arrays.equals((char[]) val1, (char[]) val2);
    }

    if (val1 instanceof Map) {// only need this to handle Maps w/ array vals
      if (!(val2 instanceof Map)) {
        return false;
      }
      if (((Map) val1).size() != ((Map) val2).size()) {
        return false;
      }

      if (val1.getClass() != val2.getClass()) {
        return false;
      }

      Set entrySet1 = ((Map) val1).entrySet();
      Iterator it = entrySet1.iterator();
      while (it.hasNext()) {
        Map.Entry entry = (Map.Entry) it.next();
        Object subval1 = ((Map) val1).get(entry.getKey());
        Object subval2 = ((Map) val2).get(entry.getKey());
        if (!valuesEqual(subval1, subval2)) {
          return false;
        }
      }
      // for Map values, get here if all values in the map are equal
      return true;
    }
    // not an instance of Map
    return val1.equals(val2);
  }

  /**
   * Gets the hash code for this object. The hash codes of two NameClassPairs <code>x</code> and
   * <code>y</code> must be equal if <code>x.equals(y)</code> returns true;
   * 
   * @return the hash code for this object
   */
  @Override
  public int hashCode() {
    int hashCode = 0;

    // add the hash codes of all attributes
    MetaDataAttr[] attrs = getAttributes();
    for (MetaDataAttr attr : attrs) {
      // String attrName = attr.name;
      Object val = getAttributeValue(attr);
      if (val != null) {
        if (val instanceof Object[]) {
          hashCode += Arrays.hashCode((Object[]) val);
        } else if (val instanceof Map) {// only need to do this to handle Maps w/ array vals
          // note doesn't handle maps whose values are maps.
          Set<Map.Entry> entrySet = ((Map) val).entrySet();
          for (Map.Entry entry : entrySet) {
            hashCode += entry.getKey().hashCode();
            Object subval = entry.getValue();
            if (subval instanceof Object[]) {
              hashCode += Arrays.hashCode((Object[]) subval);
            } else {
              hashCode += subval.hashCode();
            }
          }
        } else {
          hashCode += val.hashCode();
        }
      }
    }
    return hashCode;
  }

  /**
   * Writes out this object's XML representation.
   * 
   * @param aWriter
   *          a Writer to which the XML string will be written
   */
  @Override
  public void toXML(Writer aWriter) throws SAXException, IOException {
    toXML(new XMLSerializer(aWriter));
  }

  /**
   * Writes out this object's XML representation.
   * 
   * @param aOutputStream
   *          an OutputStream to which the XML string will be written
   */
  @Override
  public void toXML(OutputStream aOutputStream) throws SAXException, IOException {
    toXML(new XMLSerializer(aOutputStream));
  }

  private void toXML(XMLSerializer sax2xml) throws SAXException, IOException {
    ContentHandler contentHandler = sax2xml.getContentHandler();
    contentHandler.startDocument();
    toXML(contentHandler, true); // no reason to create a new content handler
    contentHandler.endDocument();
  }

  /**
   * This is called internally, also for JSon serialization
   * 
   * @see org.apache.uima.util.XMLizable#toXML(ContentHandler)
   */
  @Override
  public void toXML(ContentHandler aContentHandler) throws SAXException {
    toXML(aContentHandler, false);
  }

  /**
   * @see org.apache.uima.util.XMLizable#toXML(org.xml.sax.ContentHandler, boolean)
   * 
   *      This is called internally, also for JSon serialization If this is the first call to
   *      serialize, create a serialContext (and clean up afterwards) Other callers (e.g. JSON) must
   *      set the serialContext first before calling
   */
  @Override
  public void toXML(ContentHandler aContentHandler, boolean aWriteDefaultNamespaceAttribute)
          throws SAXException {
    if (null == serialContext.get()) {
      getSerialContext(aContentHandler);
      try {
        toXMLcommon(aWriteDefaultNamespaceAttribute);
      } finally {
        serialContext.remove();
      }
    } else {
      toXMLcommon(aWriteDefaultNamespaceAttribute);
    }
  }

  private static Serializer getSerializerFromContentHandler(ContentHandler aContentHandler) {
    return (aContentHandler instanceof CharacterValidatingContentHandler)
            ? new MetaDataObjectSerializer_indent(
                    (CharacterValidatingContentHandler) aContentHandler)
            : new MetaDataObjectSerializer_plain(aContentHandler);
  }

  private void toXMLcommon(boolean aWriteDefaultNamespaceAttribute) throws SAXException {
    XmlizationInfo inf = getXmlizationInfo();
    final Serializer serializer = serialContext.get().serializer;

    // write the element's start tag
    // get attributes (can be provided by subclasses)
    AttributesImpl attrs = getXMLAttributes();

    if (aWriteDefaultNamespaceAttribute && inf.namespace != null) {
      // attrs.addAttribute("", "xmlns", "xmlns", "xs:string", inf.namespace); // NOTE: Saxon
      // appears to ignore this ??
      // this is the way to add a default namespace, correctly. Works with Saxon and non-Saxon
      ((MetaDataObjectSerializer_plain) serializer).startPrefixMapping("", inf.namespace);
    }

    // start element
    serializer.outputStartElement(infoset, inf.namespace, inf.elementTagName, inf.elementTagName,
            attrs);
    serializer.saveAndAddNodeStore(infoset); // https://issues.apache.org/jira/browse/UIMA-3477

    // write child elements
    try {
      final boolean insertNl = serializer.indentChildElements(inf, this);
      for (PropertyXmlInfo propInf : inf.propertyInfo) {
        if (insertNl) {
          serializer.insertNl();
        }
        writePropertyAsElement(propInf, inf.namespace);
      }
    } finally {
      serializer.deleteNodeStore();
    }

    // end element
    serializer.outputEndElement(infoset, inf.namespace, inf.elementTagName, inf.elementTagName);
  }

  /**
   * Called by the {@link #toXML(ContentHandler, boolean)} method to get the XML attributes that
   * will be written as part of the element's tag. By default this method returns an empty
   * Attributes object. Subclasses may override it in order to write attributes to the XML.
   * 
   * @return an object defining the attributes to be written to the XML
   */
  protected AttributesImpl getXMLAttributes() {
    return new AttributesImpl();
  }

  /**
   * To be implemented by subclasses to return information describing how to represent this object
   * in XML.
   * 
   * @return information defining this object's XML representation
   */
  protected abstract XmlizationInfo getXmlizationInfo();

  /**
   * Looks in this class's XmlizationInfo for a property with the given XML element name.
   * 
   * @param aXmlElementName
   *          the unqualified name of an XML element
   * 
   * @return information on the property that corresponds to the given element name,
   *         <code>null</code> if none.
   */
  protected PropertyXmlInfo getPropertyXmlInfo(String aXmlElementName) {
    PropertyXmlInfo[] inf = getXmlizationInfo().propertyInfo;
    for (int i = 0; i < inf.length; i++) {
      if (aXmlElementName.equals(inf[i].xmlElementName))
        return inf[i];
    }
    return null;
  }

  public boolean valueIsNullOrEmptyArray(Object val) {
    return (val == null) || (val.getClass().isArray() && ((Object[]) val).length == 0);
  }

  /**
   * Utility method used to write a property out as an XML element.
   * 
   * @param aPropInfo
   *          information on how to represent the property in XML
   * @param aNamespace
   *          XML namespace URI for this object representation
   * @throws SAXException
   *           -
   */
  protected void writePropertyAsElement(PropertyXmlInfo aPropInfo, String aNamespace)
          throws SAXException {
    final SerialContext sc = serialContext.get();
    final Serializer serializer = sc.serializer;
    // get value of property
    Object val = getAttributeValue(aPropInfo.propertyName);
    // if null or empty array, check to see if we're supposed to omit it
    if (serializer.shouldBeSkipped(aPropInfo, val, this)) {
      return;
    }

    // if XML element name was supplied, write a tag
    final String elementName = aPropInfo.xmlElementName;
    Node elementNode = null;

    if (null != elementName) { // can be null in this case:
      // <fixedFlow> <== after outputting this,
      // there is no <array> conntaining:
      // <node>A</node>
      // <node>B</node>

      // elementNode = findMatchingSubElement(aContentHandler, aPropInfo.xmlElementName);
      elementNode = getMatchingNode(sc, aPropInfo.xmlElementName);
      if (serializer.startElementProperty()) { // skip for JSON
        serializer.outputStartElement(elementNode, aNamespace, aPropInfo.xmlElementName,
                aPropInfo.xmlElementName, EMPTY_ATTRIBUTES);
      }
      serializer.addNodeStore();
    }

    // get class of property
    Class propClass = getAttributeClass(aPropInfo.propertyName);

    try {
      // if value is null then write nothing
      if (val != null) {
        if (aPropInfo.xmlElementName != null) {
          serializer.writeDelayedStart(aPropInfo.xmlElementName);
        }

        // if value is an array then we have to treat that specially
        if (val.getClass().isArray()) {
          writeArrayPropertyAsElement(aPropInfo.propertyName, propClass, val,
                  aPropInfo.arrayElementTagName, aNamespace);
        } else {
          // if value is an XMLizable object, call its toXML method
          if (val instanceof XMLizable) {
            ((XMLizable) val).toXML(sc.ch);
          }
          // else, if property's class is java.lang.Object, attempt to write
          // it as a primitive
          else if (propClass == Object.class) {
            writePrimitiveValue(val);
          } else {
            // assume attribute's class is known (e.g. String, Integer), so it
            // is not necessary to write the class name to the XML. Just write
            // the string representation of the object
            // XMLUtils.writeNormalizedString(val.toString(), aWriter, true);

            serializer.writeSimpleValue(val);
          }
        }
      }
    } finally {
      if (null != elementName) {
        serializer.deleteNodeStore();
        // if XML element name was supplied, end the element that we started
        if (serializer.startElementProperty()) { // skip for JSON
          serializer.outputEndElement(elementNode, aNamespace, aPropInfo.xmlElementName,
                  aPropInfo.xmlElementName);
        }
      }
    }
  }

  /**
   * Utility method used to write an array property out as an XML element.
   * 
   * @param aPropName
   *          name of the attribute
   * @param aPropClass
   *          class of the attribute
   * @param aValue
   *          value (guaranteed to be an array and non-null)
   * @param aArrayElementTagName
   *          name of tag to be assigned to each element of the array. May be <code>null</code>, in
   *          which case each element will be assigned a value appropriate to its class.
   * @param aNamespace
   *          the XML namespace URI for this object
   * @throws SAXException
   *           -
   */
  protected void writeArrayPropertyAsElement(String aPropName, Class aPropClass, Object aValue,
          String aArrayElementTagName, String aNamespace) throws SAXException {
    final SerialContext sc = serialContext.get();
    final Serializer serializer = sc.serializer;

    // if aPropClass is generic Object, reader won't know whether to expect
    // an array, so we tell it be writing an "array" element here.

    Node arraySubElement = null;
    if (serializer.shouldEncloseInArrayElement(aPropClass)) {
      arraySubElement = getMatchingNode(sc, "array");
      serializer.outputStartElement(arraySubElement, aNamespace, "array", "array",
              EMPTY_ATTRIBUTES);
      serializer.addNodeStore();
    }

    try {
      // iterate through elements of the array (at this point we don't allow
      // nested arrays here
      serializer.maybeStartArraySymbol(); // for JSON
      if (serializer.isArrayHasIndentableElements(aValue)) {
        serializer.insertNl(); // for JSON
      }

      for (final Object curElem : ((Object[]) aValue)) {
        Node matchingArrayElement = getMatchingNode(sc, aArrayElementTagName);

        // if a particular array element tag has been specified, write it
        // (skipped if JSON)
        serializer.outputStartElementForArrayElement(matchingArrayElement, aNamespace,
                aArrayElementTagName, aArrayElementTagName, EMPTY_ATTRIBUTES);

        if (curElem instanceof AllowedValue) {
          ((XMLizable) curElem).toXML(sc.ch);
        } else if (curElem instanceof XMLizable) {
          // if attribute's value is an XMLizable object, call its toXML method
          serializer.insertNl();
          ((XMLizable) curElem).toXML(sc.ch);

        } else if (aArrayElementTagName == null) { // else, attempt to write it as a primitive
          writePrimitiveValue(curElem); // write <tag>value</tag> or {"tag" : "value"}
        } else {
          serializer.writeSimpleValue(curElem); // don't include the type - just write the value
        }

        serializer.outputEndElementForArrayElement(matchingArrayElement, aNamespace,
                aArrayElementTagName, aArrayElementTagName);
      } // end of for loop over all elements of array

      serializer.maybeEndArraySymbol(); // for JSON
    } finally {
      if (serializer.shouldEncloseInArrayElement(aPropClass)) {
        serializer.deleteNodeStore();
        serializer.outputEndElement(arraySubElement, aNamespace, "array", "array");
      }
    }
  }

  /**
   * Utility method for writing to XML an property whose value is a <code>Map</code> with
   * <code>String</code> keys and <code>XMLizable</code> values.
   * 
   * @param aPropName
   *          name of the property to write to XML
   * @param aXmlElementName
   *          name of the XML element for the property, <code>null</code> if none
   * @param aKeyXmlAttribute
   *          name of the XML attribute for the key
   * @param aValueTagName
   *          XML element tag name to use for each entry in the Map
   * @param aOmitIfNull
   *          if true, null or empty map will not be written at all, if false, null or empty map
   *          will be written as an empty element
   * @param aNamespace
   *          namespace for this object
   * @throws SAXException
   *           passthru
   */
  protected void writeMapPropertyToXml(String aPropName, String aXmlElementName,
          String aKeyXmlAttribute, String aValueTagName, boolean aOmitIfNull, String aNamespace)
          throws SAXException {
    final SerialContext sc = serialContext.get();
    final Serializer serializer = sc.serializer;
    // get map
    @SuppressWarnings("unchecked")
    Map<String, Object> theMap = (Map<String, Object>) getAttributeValue(aPropName);
    Node matchingNode = getMatchingNode(sc, aXmlElementName);

    // if map is empty handle appropriately
    if (theMap == null || theMap.isEmpty()) {
      if (!aOmitIfNull && aXmlElementName != null) {
        serializer.outputStartElement(matchingNode, aNamespace, aXmlElementName, aXmlElementName,
                EMPTY_ATTRIBUTES);
        serializer.outputEndElement(matchingNode, aNamespace, aXmlElementName, aXmlElementName);
      }
    } else { // map is not empty
      // write start tag for attribute if desired
      serializer.outputStartElement(matchingNode, aNamespace, aXmlElementName, aXmlElementName,
              EMPTY_ATTRIBUTES);
      serializer.addNodeStore();

      try {
        // iterate over entries in the Map
        for (Map.Entry<String, Object> curEntry : theMap.entrySet()) {
          String key = curEntry.getKey();

          // write a tag for the value, with a "key" attribute
          AttributesImpl attrs = new AttributesImpl();
          attrs.addAttribute("", aKeyXmlAttribute, aKeyXmlAttribute, "", key); // nulls not OK -
                                                                               // must use ""
          Node innerMatchingNode = getMatchingNode(sc, aValueTagName);
          serializer.outputStartElement(innerMatchingNode, aNamespace, aValueTagName, aValueTagName,
                  attrs);

          // write the value (must be XMLizable or an array of XMLizable)
          Object val = curEntry.getValue();
          if (val.getClass().isArray()) {
            Object[] arr = (Object[]) val;
            for (int j = 0; j < arr.length; j++) {
              XMLizable elem = (XMLizable) arr[j];
              elem.toXML(sc.ch);
            }
          } else {
            serializer.addNodeStore();
            try {
              ((XMLizable) val).toXML(sc.ch);
            } finally {
              serializer.deleteNodeStore();
            }
          }

          // write end tag for the value
          serializer.outputEndElement(innerMatchingNode, aNamespace, aValueTagName, aValueTagName);
        }
      } finally {
        serializer.deleteNodeStore();
        // if we wrote start tag for attribute, now write end tag
        serializer.outputEndElement(matchingNode, aNamespace, aXmlElementName, aXmlElementName);
      }
    }
  }

  /**
   * Initializes this object from its XML DOM representation. This method is typically called from
   * the {@link XMLParser}.
   * 
   * @param aElement
   *          the XML element that represents this object.
   * @param aParser
   *          a reference to the UIMA <code>XMLParser</code>. The
   *          {@link XMLParser#buildObject(Element)} method can be used to construct sub-objects.
   * 
   * @throws InvalidXMLException
   *           if the input XML element does not specify a valid object
   */
  @Override
  public final void buildFromXMLElement(Element aElement, XMLParser aParser)
          throws InvalidXMLException {
    buildFromXMLElement(aElement, aParser, new XMLParser.ParsingOptions(true));
  }

  /**
   * Initializes this object from its XML DOM representation. This method is typically called from
   * the {@link XMLParser}.
   * 
   * It is overridden by specific Java impl classes to provide additional defaulting (e.g. see
   * AnalysisEngineDescription_impl)
   * 
   * @param aElement
   *          the XML element that represents this object.
   * @param aParser
   *          a reference to the UIMA <code>XMLParser</code>. The
   *          {@link XMLParser#buildObject(Element)} method can be used to construct sub-objects.
   * @param aOptions
   *          option settings
   * 
   * @throws InvalidXMLException
   *           if the input XML element does not specify a valid object
   */
  @Override
  public void buildFromXMLElement(Element aElement, XMLParser aParser,
          XMLParser.ParsingOptions aOptions) throws InvalidXMLException {
    // check element type
    if (!aElement.getTagName().equals(getXmlizationInfo().elementTagName))
      throw new InvalidXMLException(InvalidXMLException.INVALID_ELEMENT_TYPE,
              new Object[] { getXmlizationInfo().elementTagName, aElement.getTagName() });

    if (aOptions.preserveComments) {
      infoset = aElement;
    }

    // get child elements, each of which represents a property
    List<String> foundProperties = new ArrayList<>();
    NodeList childNodes = aElement.getChildNodes();
    for (int i = 0; i < childNodes.getLength(); i++) {
      Node curNode = childNodes.item(i);
      if (curNode instanceof Element) {
        Element curElem = (Element) curNode;
        String elemName = curElem.getTagName();

        // look up this name in this class's XmlizationInfo
        PropertyXmlInfo propXmlInfo = getPropertyXmlInfo(elemName);
        if (propXmlInfo != null) {
          // read the property's value from this element
          readPropertyValueFromXMLElement(propXmlInfo, curElem, aParser, aOptions);
          foundProperties.add(elemName);
        } else {
          // There is no property that matches the XML element.
          // We have a couple of special cases to check before concluding that
          // the XML is invalid.

          // (1) if this class has only one property, we do not require
          // specifying the property name as an XML element.
          if (getXmlizationInfo().propertyInfo.length == 1) {
            readPropertyValueFromXMLElement(getXmlizationInfo().propertyInfo[0], aElement, aParser,
                    aOptions);
          } else {
            // (2) if an object can be constructed from the unknown element,
            // attempt to assign this object to any null-valued property
            // that can accept it.
            readUnknownPropertyValueFromXMLElement(curElem, aParser, aOptions, foundProperties);
          }
        }
      }
    }
  }

  /**
   * Utility method to read an attribute's value from its DOM representation.
   * 
   * @param aPropXmlInfo
   *          information about the property to read
   * @param aElement
   *          DOM element to read from
   * @param aParser
   *          parser to use to construct complex values
   * @param aOptions
   *          option settings
   * @throws InvalidXMLException
   *           -
   */
  protected void readPropertyValueFromXMLElement(PropertyXmlInfo aPropXmlInfo, Element aElement,
          XMLParser aParser, XMLParser.ParsingOptions aOptions) throws InvalidXMLException {
    String propName = aPropXmlInfo.propertyName;
    // get class of the property
    Class propClass = getAttributeClass(propName);
    // if we expect an array, treat that specially
    if (propClass.isArray()) {
      readArrayPropertyValueFromXMLElement(aPropXmlInfo, propClass, aElement, aParser, aOptions);
    } else if (propClass == String.class) // special processing to handle env vars
    {
      String text = XMLUtils.getText(aElement, aOptions.expandEnvVarRefs);
      setAttributeValue(propName, text);
    } else {
      // attempt to get the first child element, which represents the object
      // that is the value of this attribute (there may not be one for a
      // primitive type)
      Element objElem = XMLUtils.getFirstChildElement(aElement);
      if (objElem != null) {
        // is it an array? (This can happen if aAttrClass is generic Object)
        if (objElem.getTagName().equals("array")) {
          readArrayPropertyValueFromXMLElement(aPropXmlInfo, Object.class, objElem, aParser,
                  aOptions);
        } else {
          setAttributeValue(propName, aParser.buildObjectOrPrimitive(objElem, aOptions));
        }
      } else {
        // get text of the element and check for null
        String text = XMLUtils.getText(aElement, aOptions.expandEnvVarRefs);
        if (!text.equals("")) {
          try {
            // not null - attempt to construct an appropriate object from this String
            // class must have a constructor that takes a String parameter
            Constructor constructor = propClass.getConstructor(new Class[] { String.class });
            // construct the object
            Object val = constructor.newInstance(new Object[] { text });
            setAttributeValue(propName, val);
          } catch (Exception e) {
            throw new InvalidXMLException(InvalidXMLException.UNKNOWN_ELEMENT,
                    new Object[] { aElement.getTagName() }, e);
          }
        }
      }
    }
  }

  /**
   * Utility method to read an array property's value from its DOM representation.
   * 
   * @param aPropXmlInfo
   *          information about the property to read
   * @param aPropClass
   *          class of the property's value
   * @param aElement
   *          DOM element representing the entire array
   * @param aParser
   *          parser to use to construct complex values
   * @param aOptions
   *          option settings
   * @throws InvalidXMLException
   *           -
   */
  protected void readArrayPropertyValueFromXMLElement(PropertyXmlInfo aPropXmlInfo,
          Class aPropClass, Element aElement, XMLParser aParser, XMLParser.ParsingOptions aOptions)
          throws InvalidXMLException {
    Constructor primitiveElementStringConstructor = null; // may be used to build objects from
    // strings

    // get all child nodes (note not all may be elements)
    NodeList elems = aElement.getChildNodes();
    int numChildren = elems.getLength();

    // iterate through children, and for each element construct a value,
    // adding it to a list
    List valueList = new ArrayList();
    for (int i = 0; i < numChildren; i++) {
      Node curNode = elems.item(i);
      if (curNode instanceof Element) {
        Element curElem = (Element) curNode;

        // does the PropertyXmlInfo specify the expected tag name?
        if (aPropXmlInfo.arrayElementTagName != null) {
          if (curElem.getTagName().equals(aPropXmlInfo.arrayElementTagName)) {
            // get text of element
            String elemText = XMLUtils.getText(curElem, aOptions.expandEnvVarRefs);

            // create appropriate primitive value
            try {
              // must have a constructor that takes a String parameter
              if (primitiveElementStringConstructor == null) {
                primitiveElementStringConstructor = aPropClass.getComponentType()
                        .getConstructor(new Class[] { String.class });
              }
              // construct the object and add to list
              valueList.add(
                      primitiveElementStringConstructor.newInstance(new Object[] { elemText }));
            } catch (Exception e) {
              throw new InvalidXMLException(e);
            }
          } else
            // element type does not match
            throw new InvalidXMLException(InvalidXMLException.INVALID_ELEMENT_TYPE,
                    new Object[] { aPropXmlInfo.arrayElementTagName, curElem.getTagName() });
        } else {
          // array element type is not specified, try defaults
          valueList.add(aParser.buildObjectOrPrimitive(curElem, aOptions));
        }
      }
    }

    // initialize an appropriate array of the same length as the valueList,
    // and copy the values
    Class componentType = Object.class;
    if (!(aPropClass == Object.class)) {
      componentType = aPropClass.getComponentType();
      // verify that objects are of appropriate type
      Iterator i = valueList.iterator();
      while (i.hasNext()) {
        Object curObj = i.next();
        if (!componentType.isAssignableFrom(curObj.getClass())) {
          throw new InvalidXMLException(InvalidXMLException.INVALID_CLASS,
                  new Object[] { componentType, curObj.getClass() });
        }
      }
    } else {
      // attribute class is generic object, so we don't know what type of
      // array to create. Try to infer it from the values - if they are all
      // of the same type, use that, else use Object[].
      Iterator i = valueList.iterator();
      while (i.hasNext()) {
        Object curObj = i.next();
        if (componentType == Object.class) {
          componentType = curObj.getClass();
        } else if (componentType != curObj.getClass()) {
          componentType = Object.class;
          break;
        }
      }
    }

    Object array = Array.newInstance(componentType, valueList.size());
    valueList.toArray((Object[]) array);

    // assign this array as the value of the attribute
    setAttributeValue(aPropXmlInfo.propertyName, array);
  }

  /**
   * Utility method that attempts to read a property value from an XML element even though it is not
   * known to which property the value should be assigned. If an object can be constructed from the
   * XML element, it will be assigned to any unasigned property that can accept it.
   * 
   * @param aElement
   *          DOM element to read from
   * @param aParser
   *          parser to use to construct complex values
   * @param aOptions
   *          -
   * @param aKnownPropertyNames
   *          List of propertiees that we've already values for (these values will not be
   *          overwritten)
   * 
   * @throws InvalidXMLException
   *           if no acceptable object is described by aElement
   */
  protected void readUnknownPropertyValueFromXMLElement(Element aElement, XMLParser aParser,
          XMLParser.ParsingOptions aOptions, List<String> aKnownPropertyNames)
          throws InvalidXMLException {
    boolean success = false;
    try {
      Object valueObj = aParser.buildObjectOrPrimitive(aElement, aOptions);

      if (valueObj != null) {
        // find a property that can accept this type, which we have not already
        // read a value for
        PropertyXmlInfo[] props = getXmlizationInfo().propertyInfo;
        for (int i = 0; i < props.length; i++) {
          String propName = props[i].propertyName;
          Class<?> propClass = getAttributeClass(propName);
          if (propClass.isAssignableFrom(valueObj.getClass())) {
            // check if we have already read a value for this attribute
            if (!aKnownPropertyNames.contains(propName)) {
              // set value
              setAttributeValue(propName, valueObj);
              success = true;
              break;
            }
          } else if (propClass.isArray()
                  && propClass.getComponentType().isAssignableFrom(valueObj.getClass())) {
            // it's an array - get current value and append
            Object curVal = getAttributeValue(propName);
            int curLen = curVal == null ? 0 : Array.getLength(curVal);
            Object newVal = Array.newInstance(propClass.getComponentType(), curLen + 1);
            if (curLen > 0) {
              System.arraycopy(curVal, 0, newVal, 0, curLen);
            }
            Array.set(newVal, curLen, valueObj);
            // set value
            setAttributeValue(propName, newVal);
            success = true;
            break;
          }
        }
      }
    } catch (Exception e) {
      if (e instanceof InvalidXMLException) {
        throw (InvalidXMLException) e;
      } else {
        throw new InvalidXMLException(e);
      }
    }

    // throw exception if we did not succeed
    if (!success) {
      throw new InvalidXMLException(InvalidXMLException.UNKNOWN_ELEMENT,
              new Object[] { aElement.getTagName() });
    }
  }

  /**
   * Utility method for reading from XML an attribute whose value is a <code>Map</code> with
   * <code>String</code> keys and <code>XMLizable</code> (or an array of these) values.
   * 
   * @param aPropName
   *          name of the property to read from XML
   * @param aElement
   *          element to read from
   * @param aKeyXmlAttribute
   *          XML attribute for the key
   * @param aValueTagName
   *          XML element tag name for each entry in the map
   * @param aParser
   *          parser to use to build sub-objects
   * @param aOptions
   *          parsing option settings
   * @param aValueIsArray
   *          true if the value of the map entires is an array. This method only supports
   *          homogeneous arrays.
   * @throws InvalidXMLException
   *           -
   */
  protected void readMapPropertyFromXml(String aPropName, Element aElement, String aKeyXmlAttribute,
          String aValueTagName, XMLParser aParser, XMLParser.ParsingOptions aOptions,
          boolean aValueIsArray) throws InvalidXMLException {
    // get the Map to which we add entries (it should already exist)
    Map<String, Object> theMap = (Map<String, Object>) getAttributeValue(aPropName);

    // get all child nodes
    NodeList childNodes = aElement.getChildNodes();
    for (int i = 0; i < childNodes.getLength(); i++) {
      Node curNode = childNodes.item(i);
      if (curNode instanceof Element) {
        Element curElem = (Element) curNode;
        // check element tag name
        if (!curElem.getTagName().equals(aValueTagName)) {
          throw new InvalidXMLException(InvalidXMLException.INVALID_ELEMENT_TYPE,
                  new Object[] { aValueTagName, curElem.getTagName() });
        }
        // get the key attribute
        String key = curElem.getAttribute(aKeyXmlAttribute);
        if (key.equals("")) {
          throw new InvalidXMLException(InvalidXMLException.REQUIRED_ATTRIBUTE_MISSING,
                  new Object[] { "key", aValueTagName });
        }
        // build value object
        Object val = null;
        if (!aValueIsArray) {
          Element valElem = XMLUtils.getFirstChildElement(curElem);
          if (valElem == null) {
            throw new InvalidXMLException(InvalidXMLException.ELEMENT_NOT_FOUND,
                    new Object[] { "(any)", aValueTagName });
          }
          val = aParser.buildObject(valElem, aOptions);
        } else // array
        {
          ArrayList<XMLizable> vals = new ArrayList<>();
          NodeList arrayNodes = curElem.getChildNodes();
          for (int j = 0; j < arrayNodes.getLength(); j++) {
            Node curArrayNode = arrayNodes.item(j);
            if (curArrayNode instanceof Element) {
              Element valElem = (Element) curArrayNode;
              vals.add(aParser.buildObject(valElem));
            }
          }
          if (!vals.isEmpty()) {
            val = Array.newInstance(vals.get(0).getClass(), vals.size());
            vals.toArray((Object[]) val);
          }
        }

        // add object to the map
        theMap.put(key, val);
      }
    }
  }

  /**
   * Gets the wrapper class corresponding to the given primitive type. For example,
   * <code>java.lang.Integer</code> is the wrapper class for the primitive type <code>int</code>.
   * 
   * @param aPrimitiveType
   *          <code>Class</code> object representing a primitive type
   * 
   * @return <code>Class</code> object representing the wrapper type for <code>PrimitiveType</code>.
   *         If <code>aPrimitiveType</code> is not a primitive type, it is itself returned.
   */
  protected static Class getWrapperClass(Class aPrimitiveType) {
    if (Integer.TYPE.equals(aPrimitiveType))
      return Integer.class;
    else if (Short.TYPE.equals(aPrimitiveType))
      return Short.class;
    else if (Long.TYPE.equals(aPrimitiveType))
      return Long.class;
    else if (Byte.TYPE.equals(aPrimitiveType))
      return Byte.class;
    else if (Character.TYPE.equals(aPrimitiveType))
      return Character.class;
    else if (Float.TYPE.equals(aPrimitiveType))
      return Float.class;
    else if (Double.TYPE.equals(aPrimitiveType))
      return Double.class;
    else if (Boolean.TYPE.equals(aPrimitiveType))
      return Boolean.class;
    else
      return aPrimitiveType;
  }

  /**
   * Utility method that introspects this bean and returns a list of
   * <code>PropertyDescriptor</code>s for its properties.
   * <p>
   * The JavaBeans introspector is used, with the IGNORE_ALL_BEANINFO flag. This saves on
   * initialization time by preventing the introspector from searching for nonexistent BeanInfo
   * classes for all the MetaDataObjects.
   * 
   * Caching needed, this method is called for every access to a field, and introspection doesn't
   * cache (from observation... although the javadocs say otherwise (as of Java6 10/2011 - both IBM
   * and Sun)
   * 
   * @return the <code>PropertyDescriptors</code> for all properties introduced by subclasses of
   *         <code>MetaDataObject_impl</code>.
   * 
   * @throws IntrospectionException
   *           if introspection fails
   * @deprecated - use getAttributes instead
   */
  @Deprecated
  // never called, we hope. No longer caches anything.
  protected PropertyDescriptor[] getPropertyDescriptors() throws IntrospectionException {
    return Introspector.getBeanInfo(this.getClass(), Introspector.IGNORE_ALL_BEANINFO)
            .getPropertyDescriptors();
  }

  // This next method moved here from XMLUtils, but left there because it's public.
  // The version here handles comments and ignorablewhitespace
  /**
   * Writes a standard XML representation of the specified Object, in the form:<br>
   * <code>&lt;className&gt;string value%lt;/className%gt;</code>
   * <p>
   * where <code>className</code> is the object's java class name without the package and made
   * lowercase, e.g. "string","integer", "boolean" and <code>string value</code> is the result of
   * <code>Object.toString()</code>.
   * <p>
   * This is intended to be used for Java Strings and wrappers for primitive value classes (e.g.
   * Integer, Boolean).
   * 
   * For JSON, the value is output as { java-class name : value } unless the value is just a string,
   * in which case we output just the string
   * 
   * @param aObj
   *          the object to write
   * @param aContentHandler
   *          the SAX ContentHandler to which events will be sent
   * 
   * @throws SAXException
   *           if the ContentHandler throws an exception
   */
  private void writePrimitiveValue(Object aObj) throws SAXException {
    // final Attributes EMPTY_ATTRIBUTES = new AttributesImpl();
    final SerialContext sc = serialContext.get();
    final Serializer serializer = sc.serializer;

    String className = aObj.getClass().getName();
    int lastDotIndex = className.lastIndexOf(".");
    if (lastDotIndex > -1)
      className = className.substring(lastDotIndex + 1).toLowerCase();
    Node node = getMatchingNode(sc, className);

    serializer.writeSimpleValueWithTag(className, aObj, node);
  }

  protected Node getMatchingNode(SerialContext serialContext, String name) {
    return (infoset == null) ? null : serialContext.serializer.findMatchingSubElement(name);
  }

  /*
   * UIMA-5274 Resolve any ${variable} entries in the string. Returns null if the expansion fails,
   * i.e. a missing variable, or if no settings have been loaded. Logs a warning if settings have
   * been loaded but an entry is missing.
   */
  protected String resolveSettings(String text) {
    UimaContext uimaContext = UimaContextHolder.getContext();
    if (uimaContext != null) {
      Settings_impl settings = (Settings_impl) uimaContext.getExternalOverrides();
      if (settings != null) {
        try {
          return settings.resolve(text);
        } catch (Exception e) {
          UIMAFramework.getLogger(this.getClass()).log(Level.WARNING, e.toString());
        }
      }
    }
    return null;
  }

  // /*****************************************
  // * JSON support *
  // *
  // * The main API takes a JsonContentHandlerJacksonWrapper instance
  // *
  // * The other APIs take combinations of - the output(Writer, File, or OutputStream) - the
  // prettyprint flag
  // *
  // * @throws IOException
  // *
  // * ***************************************/
  //
  // public void toJSON(Writer aWriter) throws SAXException {
  // toJSON(aWriter, false);
  // }
  //
  // public void toJSON(Writer aWriter, boolean isFormattedOutput) throws SAXException {
  // try {
  // JsonGenerator jg = new JsonFactory().createGenerator(aWriter);
  // toJSON(jg, isFormattedOutput);
  // } catch (IOException e) {
  // throw new SAXException(e);
  // }
  //
  // }
  //
  // public void toJSON(JsonGenerator jg, boolean isFormattedOutput) throws SAXException {
  // JsonContentHandlerJacksonWrapper jch = new JsonContentHandlerJacksonWrapper(jg,
  // isFormattedOutput);
  // jch.withoutNl();
  // toXML(jch);
  // try {
  // jg.flush();
  // } catch (IOException e) {
  // throw new SAXException(e);
  // }
  // }
  //
  // /**
  // * Writes out this object's JSON representation.
  // *
  // * @param aOutputStream
  // * an OutputStream to which the XML string will be written
  // * @throws SAXException
  // */
  // public void toJSON(OutputStream aOutputStream) throws SAXException {
  // toJSON(aOutputStream, false);
  // }
  //
  // public void toJSON(OutputStream aOutputStream, boolean isFormattedOutput) throws SAXException {
  // try {
  // JsonGenerator jg = new JsonFactory().createGenerator(aOutputStream);
  // toJSON(jg, isFormattedOutput);
  // } catch (IOException e) {
  // throw new SAXException(e);
  // }
  // }
  //
  // public void toJSON(File file) throws SAXException {
  // toJSON(file, false);
  // }
  //
  // public void toJSON(File file, boolean isFormattedOutput) throws SAXException {
  // try {
  // JsonGenerator jg = new JsonFactory().createGenerator(file, JsonEncoding.UTF8);
  // toJSON(jg, isFormattedOutput);
  // jg.close();
  // } catch (IOException e) {
  // throw new SAXException(e);
  // }
  // }

  // public void toJSON(ContentHandler aCh) throws SAXException {
  // XmlizationInfo inf = getXmlizationInfo();
  // final boolean isJson = aCh instanceof JsonContentHandlerJacksonWrapper;
  // final JsonContentHandlerJacksonWrapper jch = isJson ? (JsonContentHandlerJacksonWrapper) aCh :
  // null;
  // final JsonGenerator jg = isJson ? jch.getJsonGenerator() : null;
  //
  //
  // // write the element's start tag
  // // get attributes (can be provided by subclasses)
  // AttributesImpl attrs = getXMLAttributes();
  //
  // if (valueIsEmpty(inf, attrs)) {
  // return;
  // }
  //
  // // start element
  //
  // try {
  // outputStartElement(aCh, infoset, inf.namespace, inf.elementTagName, inf.elementTagName, attrs);
  // } catch (SAXException e) {
  // throw new RuntimeException(e); // should never happen
  // }
  //
  // // write child elements
  // try {
  // if (isJson) {
  // if (inf.propertyInfo.length > 1) {
  // jch.writeNlJustBeforeNext();
  // }
  // }
  // for (int i = 0; i < inf.propertyInfo.length; i++) {
  // PropertyXmlInfo propInf = inf.propertyInfo[i];
  // writePropertyAsElement(propInf, inf.namespace, aCh);
  // }
  // } catch (SAXException e) {
  // throw new RuntimeException(e); // should never happen
  // }
  //
  // // end element
  // outputEndElement(jch, infoset, inf.namespace, inf.elementTagName, inf.elementTagName);
  // }

  // private boolean valueIsEmpty(XmlizationInfo inf, AttributesImpl attrs) {
  // for (PropertyXmlInfo propInf : inf.propertyInfo) {
  // Object val = getAttributeValue(propInf.propertyName);
  // if (!valueIsNullOrEmptyArray(val)) {
  // return false;
  // }
  // }
  // for (int i = 0; i < attrs.getLength(); i++) {
  // String val = attrs.getValue(i);
  // if (val != null && (!val.equals(""))) {
  // return false;
  // }
  // }
  // return true;
  // }

  // private String maybeMakeJsonString(Object aObj, boolean isJson) {
  // if (isJson && aObj instanceof String) {
  // return JSONSerializer.makeQuotedString((String) aObj);
  // }
  // return aObj.toString();
  // }

}
