// Copyright 2008-2012 severally by the contributors
//
// Licensed 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 net.sf.practicalxml.converter.bean;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import net.sf.practicalxml.DomUtil;
import net.sf.practicalxml.OutputUtil;
import net.sf.practicalxml.XmlUtil;
import net.sf.practicalxml.converter.ConversionException;
import net.sf.practicalxml.converter.ConversionConstants;
import net.sf.practicalxml.converter.bean.Bean2XmlConverter;
import net.sf.practicalxml.converter.bean.Bean2XmlOptions;
import net.sf.practicalxml.converter.testinternals.InaccessibleClass;
import net.sf.practicalxml.junit.DomAsserts;


public class TestBean2XmlConverter
extends AbstractBeanConverterTestCase
{
    public TestBean2XmlConverter(String name)
    {
        super(name);
    }


//----------------------------------------------------------------------------
//  Support Code / Assertions
//----------------------------------------------------------------------------

    private void assertSingleChild(
            Element parent,
            String childName,
            String expectedType,
            String expectedValue,
            boolean isNil)
    {
        List<Element> children = DomUtil.getChildren(parent, childName);
        assertEquals(childName + " count", 1, children.size());
        assertPrimitiveElement(
                childName, children.get(0),
                childName, expectedType, expectedValue, isNil);
    }


    private void assertJavaXsiType(String message, Element elem, Object obj)
    {
        assertType(message, elem, "java:" + obj.getClass().getName());
    }


    private void assertConversionFailure(String message, Bean2XmlConverter driver, Object data)
    {
        try
        {
            driver.convert(data, "test");
            fail(message);
        }
        catch (ConversionException ee)
        {
//            System.out.println(ee);
        }
    }


//----------------------------------------------------------------------------
//  Test Cases
//----------------------------------------------------------------------------

    public void testConvertNullDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        Element root = driver.convert(null, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 0);
        assertPrimitiveElement("", root, "test", "", null, false);
    }


    public void testConvertNullAsEmpty() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.NULL_AS_EMPTY);

        Element root = driver.convert(null, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        NodeList children = root.getChildNodes();
        assertEquals(1, children.getLength());

        Node child = children.item(0);
        assertTrue(child instanceof Text);
        assertEquals("", child.getNodeValue());
    }


    public void testConvertNullAsXsiNil() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.NULL_AS_XSI_NIL);

        Element root = driver.convert(null, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 0);
        assertPrimitiveElement("", root, "test", "", null, true);
    }


    public void testConvertPrimitivesDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();
        for (int idx = 0 ; idx < PRIMITIVE_VALUES.length ; idx++)
        {
            PrimitiveValue value = PRIMITIVE_VALUES[idx];
            Element root = driver.convert(PRIMITIVE_VALUES[idx].getValue(), "test");
    //        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

            assertPrimitiveElement(
                    "value[" + idx + "]", root,
                    "test", "", value.getDefaultText(), false);
        }
    }


    public void testConvertPrimitivesWithXsdFormatting() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.XSD_FORMAT);
        for (int idx = 0 ; idx < PRIMITIVE_VALUES.length ; idx++)
        {
            PrimitiveValue value = PRIMITIVE_VALUES[idx];
            Element root = driver.convert(PRIMITIVE_VALUES[idx].getValue(), "test");
    //        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

            assertPrimitiveElement(
                    "value[" + idx + "]", root,
                    "test", "", value.getXsdText(), false);
        }
    }


    public void testConvertPrimitivesWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);
        for (int idx = 0 ; idx < PRIMITIVE_VALUES.length ; idx++)
        {
            PrimitiveValue value = PRIMITIVE_VALUES[idx];
            Element root = driver.convert(PRIMITIVE_VALUES[idx].getValue(), "test");
    //        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

            assertPrimitiveElement(
                    "value[" + idx + "]", root,
                    "test", value.getXsdType(), value.getXsdText(), false);
        }
    }


    public void testConvertEnumDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        Element root = driver.convert(MyEnum.FOO, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));
        assertNameTypeValue(root, "test", "", "FOO");
    }


    public void testConvertEnumWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        Element root = driver.convert(MyEnum.FOO, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));
        assertNameTypeValue(root, "test", "java:" + MyEnum.class.getName(), "FOO");
    }


    public void testConvertEnumWithStringValue() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.ENUM_AS_NAME_AND_VALUE);

        Element root = driver.convert(MyEnum.FOO, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));
        assertNameTypeValue(root, "test", "", "foo");   // note lower case
        assertAttribute(root, "name", "FOO");
    }


    public void testConvertPrimitiveArrayDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        int[] data = new int[] { 1, 2, 3 };
        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());
        assertNameTypeValue("child 1", children.get(0), "data", "", "1");
        assertNameTypeValue("child 2", children.get(1), "data", "", "2");
        assertNameTypeValue("child 3", children.get(2), "data", "", "3");

        assertAttribute(children.get(0), "index", "");
        assertAttribute(children.get(1), "index", "");
        assertAttribute(children.get(2), "index", "");
    }


    public void testConvertArrayWithSequenceNumbers() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_INDEX_ATTR);

        int[] data = new int[] { 1, 2, 3 };
        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());
        assertNameTypeValue("child 1", children.get(0), "data", "", "1");
        assertNameTypeValue("child 2", children.get(1), "data", "", "2");
        assertNameTypeValue("child 3", children.get(2), "data", "", "3");

        assertAttribute(children.get(0), "index", "0");
        assertAttribute(children.get(1), "index", "1");
        assertAttribute(children.get(2), "index", "2");
    }


    public void testConvertArrayWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        Object[] data = new Object[] { Integer.valueOf(1), new BigDecimal("2"), "3" };
        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "xsd:int", "1");
        assertNameTypeValue("child 2", children.get(1), "data", "xsd:decimal", "2");
        assertNameTypeValue("child 3", children.get(2), "data", "xsd:string", "3");
    }


    public void testConvertArrayWithSimpleParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        String[] data = new String[] {"foo", "bar", "baz"};

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "test", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "test", "", "baz");
    }


    public void testConvertArrayWithDepluralizedParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        String[] data = new String[] {"foo", "bar", "baz"};

        Element root = driver.convert(data, "tests");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "test", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "test", "", "baz");
    }


    public void testConvertListDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        List<String> data = Arrays.asList("foo", "bar", "baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "data", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "data", "", "baz");

        assertAttribute(children.get(0), "index", "");
        assertAttribute(children.get(1), "index", "");
        assertAttribute(children.get(2), "index", "");
    }


    public void testConvertListWithSequenceNumbers() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_INDEX_ATTR);

        List<String> data = Arrays.asList("foo", "bar", "baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "data", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "data", "", "baz");

        assertAttribute(children.get(0), "index", "0");
        assertAttribute(children.get(1), "index", "1");
        assertAttribute(children.get(2), "index", "2");
    }


    public void testConvertListWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        List<String> data = Arrays.asList("foo", "bar", "baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "xsd:string", "foo");
        assertNameTypeValue("child 2", children.get(1), "data", "xsd:string", "bar");
        assertNameTypeValue("child 3", children.get(2), "data", "xsd:string", "baz");

        assertAttribute(children.get(0), "index", "");
        assertAttribute(children.get(1), "index", "");
        assertAttribute(children.get(2), "index", "");
    }


    public void testConvertListWithSimpleParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        List<String> data = Arrays.asList("foo", "bar", "baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "test", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "test", "", "baz");
    }


    public void testConvertListWithDepluralizedParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        List<String> data = Arrays.asList("foo", "bar", "baz");

        Element root = driver.convert(data, "tests");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "foo");
        assertNameTypeValue("child 2", children.get(1), "test", "", "bar");
        assertNameTypeValue("child 3", children.get(2), "test", "", "baz");
    }


    public void testConvertSetDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        // TreeSet will order output
        Set<String> data = new TreeSet<String>();
        data.add("foo");
        data.add("bar");
        data.add("baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "", "bar");
        assertNameTypeValue("child 2", children.get(1), "data", "", "baz");
        assertNameTypeValue("child 3", children.get(2), "data", "", "foo");

        assertAttribute(children.get(0), "index", "");
        assertAttribute(children.get(1), "index", "");
        assertAttribute(children.get(2), "index", "");
    }


    public void testConvertSetWithSequenceNumbers() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_INDEX_ATTR);

        // TreeSet will order output
        Set<String> data = new TreeSet<String>();
        data.add("foo");
        data.add("bar");
        data.add("baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "", "bar");
        assertNameTypeValue("child 2", children.get(1), "data", "", "baz");
        assertNameTypeValue("child 3", children.get(2), "data", "", "foo");

        assertAttribute(children.get(0), "index", "0");
        assertAttribute(children.get(1), "index", "1");
        assertAttribute(children.get(2), "index", "2");
    }


    public void testConvertSetWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        // TreeSet will order output
        Set<String> data = new TreeSet<String>();
        data.add("foo");
        data.add("bar");
        data.add("baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "data", "xsd:string", "bar");
        assertNameTypeValue("child 2", children.get(1), "data", "xsd:string", "baz");
        assertNameTypeValue("child 3", children.get(2), "data", "xsd:string", "foo");

        assertAttribute(children.get(0), "index", "");
        assertAttribute(children.get(1), "index", "");
        assertAttribute(children.get(2), "index", "");
    }


    public void testConvertSetWithSimpleParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        // TreeSet will order output
        Set<String> data = new TreeSet<String>();
        data.add("foo");
        data.add("bar");
        data.add("baz");

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "bar");
        assertNameTypeValue("child 2", children.get(1), "test", "", "baz");
        assertNameTypeValue("child 3", children.get(2), "test", "", "foo");
    }


    public void testConvertSetWithDepluralizedParentName() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_NAMED_BY_PARENT);

        // TreeSet will order output
        Set<String> data = new TreeSet<String>();
        data.add("foo");
        data.add("bar");
        data.add("baz");

        Element root = driver.convert(data, "tests");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        assertNameTypeValue("child 1", children.get(0), "test", "", "bar");
        assertNameTypeValue("child 2", children.get(1), "test", "", "baz");
        assertNameTypeValue("child 3", children.get(2), "test", "", "foo");
    }


    public void testConvertMapDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        // LinkedHashMap should ensure ordered output
        Map<String,Integer> data = new LinkedHashMap<String,Integer>();
        data.put("foo", new Integer(123));
        data.put("bar", new Integer(456));
        data.put("b&<z>", new Integer(789));     // must be escaped

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 3, children.size());

        Element child1 = children.get(0);
        assertEquals("child1 name", "foo", child1.getAttribute(ConversionConstants.AT_MAP_KEY));
        assertEquals("child1 value", "123", child1.getTextContent());

        Element child2 = children.get(1);
        assertEquals("child2 name", "bar", child2.getAttribute(ConversionConstants.AT_MAP_KEY));
        assertEquals("child2 value", "456", child2.getTextContent());

        Element child3 = children.get(2);
        assertEquals("child3 name", "b&<z>", child3.getAttribute(ConversionConstants.AT_MAP_KEY));
        assertEquals("child3 value", "789", child3.getTextContent());
    }


    public void testConvertMapDefaultWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        // TreeMap means that the data will be re-ordered
        Map<String,Integer> data = new TreeMap<String,Integer>();
        data.put("foo", new Integer(123));
        data.put("bar", new Integer(456));

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 2, children.size());

        assertNameTypeValue(children.get(0), "data", "xsd:int", "456");
        assertAttribute(children.get(0), "key", "bar");

        assertNameTypeValue(children.get(1), "data", "xsd:int", "123");
        assertAttribute(children.get(1), "key", "foo");
    }


    public void testConvertMapIntrospectWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME, Bean2XmlOptions.USE_TYPE_ATTR);

        // TreeMap means that the data will be re-ordered
        Map<String,Integer> data = new TreeMap<String,Integer>();
        data.put("foo", new Integer(123));
        data.put("bar", new Integer(456));

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 2, children.size());

        assertNameTypeValue(children.get(0), "bar", "xsd:int", "456");
        assertAttribute(children.get(0), "key", "");

        assertNameTypeValue(children.get(1), "foo", "xsd:int", "123");
        assertAttribute(children.get(1), "key", "");
    }


    public void testConvertMapWithinMapDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        // LinkedHashMap ensures ordering for child entries
        Map<String,String> childMap1 = new LinkedHashMap<String,String>();
        childMap1.put("foo", "bar");
        Map<String,String> childMap2 = new LinkedHashMap<String,String>();
        childMap2.put("argle", "bargle");

        // regression test: one of the parent keys can't be used as element name
        Map<String,Map<String,String>> parentMap = new LinkedHashMap<String,Map<String,String>>();
        parentMap.put("abcd", childMap1);
        parentMap.put("b&<gus>", childMap2);

        Element root = driver.convert(parentMap, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 2, children.size());

        Element child1 = children.get(0);
        assertEquals("child1 elem", ConversionConstants.EL_COLLECTION_ITEM, child1.getNodeName());
        assertEquals("child1 name", "abcd", child1.getAttribute(ConversionConstants.AT_MAP_KEY));

        List<Element> grandChildren1 = DomUtil.getChildren(child1);
        assertEquals("grandchildren1 count", 1, grandChildren1.size());

        Element grandchild1 = grandChildren1.get(0);
        assertEquals("grandchild1 elem", ConversionConstants.EL_COLLECTION_ITEM, grandchild1.getNodeName());
        assertEquals("grandchild1 name", "foo", grandchild1.getAttribute(ConversionConstants.AT_MAP_KEY));
        assertEquals("grandchild1 value", "bar", grandchild1.getTextContent());

        Element child2 = children.get(1);
        assertEquals("child2 elem", ConversionConstants.EL_COLLECTION_ITEM, child2.getNodeName());
        assertEquals("child2 name", "b&<gus>", child2.getAttribute(ConversionConstants.AT_MAP_KEY));

        List<Element> grandChildren2 = DomUtil.getChildren(child2);
        assertEquals("grandchildren2 count", 1, grandChildren2.size());

        Element grandchild2 = grandChildren2.get(0);
        assertEquals("grandchild2 elem", ConversionConstants.EL_COLLECTION_ITEM, grandchild2.getNodeName());
        assertEquals("grandchild2 name", "argle", grandchild2.getAttribute(ConversionConstants.AT_MAP_KEY));
        assertEquals("grandchild2 value", "bargle", grandchild2.getTextContent());
    }


    public void testFailMapIntrospectWithInvalidKey() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.MAP_KEYS_AS_ELEMENT_NAME);

        Map<String,Integer> data = new TreeMap<String,Integer>();
        data.put("%key1%", new Integer(123));
        data.put("%key2%", new Integer(456));

        assertConversionFailure("converted map with invalid key under INTROSPECT_MAPS",
                                driver, data);
    }


    public void testConvertSimpleBeanDefault() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        SimpleBean bean = new SimpleBean("zippy", 123, new BigDecimal("456.78"), true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 4);
        assertSingleChild(root, "sval", "", "zippy", false);
        assertSingleChild(root, "ival", "", "123", false);
        assertSingleChild(root, "dval", "", "456.78", false);
        assertSingleChild(root, "bval", "", "true", false);
    }


    public void testConvertSimpleBeanWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        SimpleBean bean = new SimpleBean("zippy", 123, new BigDecimal("456.78"), true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, bean);

        assertChildCount(root, 4);
        assertSingleChild(root, "sval", "xsd:string", "zippy", false);
        assertSingleChild(root, "ival", "xsd:int", "123", false);
        assertSingleChild(root, "dval", "xsd:decimal", "456.78", false);
        assertSingleChild(root, "bval", "xsd:boolean", "true", false);
    }


    public void testConvertSimpleBeanDefaultNullHandling() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        SimpleBean bean = new SimpleBean(null, 123, null, true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 2);
        assertSingleChild(root, "ival", "", "123", false);
        assertSingleChild(root, "bval", "", "true", false);
    }


    public void testConvertSimpleBeanXsiNil() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.NULL_AS_XSI_NIL);

        SimpleBean bean = new SimpleBean(null, 123, null, true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 4);
        assertSingleChild(root, "sval", "", null, true);
        assertSingleChild(root, "ival", "", "123", false);
        assertSingleChild(root, "dval", "", null, true);
        assertSingleChild(root, "bval", "", "true", false);
    }


    public void testConvertSimpleBeanXsiNilAndXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(
                                            Bean2XmlOptions.NULL_AS_XSI_NIL,
                                            Bean2XmlOptions.USE_TYPE_ATTR);

        SimpleBean bean = new SimpleBean(null, 123, null, true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 4);
        assertSingleChild(root, "sval", "xsd:string", null, true);
        assertSingleChild(root, "ival", "xsd:int", "123", false);
        assertSingleChild(root, "dval", "xsd:decimal", null, true);
        assertSingleChild(root, "bval", "xsd:boolean", "true", false);
    }


    public void testConvertSimpleBeanNullAsEmpty() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.NULL_AS_EMPTY);

        SimpleBean bean = new SimpleBean(null, 123, null, true);
        Element root = driver.convert(bean, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertChildCount(root, 4);
        assertSingleChild(root, "sval", "", "", false);
        assertSingleChild(root, "ival", "", "123", false);
        assertSingleChild(root, "dval", "", "", false);
        assertSingleChild(root, "bval", "", "true", false);
    }


    public void testConvertBeanArrayWithXsiType() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.USE_TYPE_ATTR);

        SimpleBean bean1 = new SimpleBean("foo", 123, new BigDecimal("456.789"), true);
        SimpleBean bean2 = new SimpleBean("bar", 456, new BigDecimal("0.0"), false);
        SimpleBean[] data = new SimpleBean[] { bean1, bean2 };

        Element root = driver.convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertJavaXsiType("root", root, data);

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 2, children.size());

        assertJavaXsiType("child1", children.get(0), bean1);
        assertJavaXsiType("child2", children.get(1), bean2);

        // no need to run through every field ... I think
        DomAsserts.assertEquals("foo", root, "//test/data[1]/sval");
        DomAsserts.assertEquals("123", root, "//test/data[1]/ival");
        DomAsserts.assertEquals("bar", root, "//test/data[2]/sval");
        DomAsserts.assertEquals("456", root, "//test/data[2]/ival");
    }


    public void testConvertCompoundBeanDefault() throws Exception
    {
        CompoundBean data = new CompoundBean(
                new SimpleBean("zippy", 123, null, true),
                new int[] { 1, 2, 3 },
                Arrays.asList("foo", null, "baz"));

        // at this point, I'm convinced the type output works, so we'll do default
        // output and then use XPath for all assertions ... but note nulls in data
        Element root = new Bean2XmlConverter()
                       .convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        DomAsserts.assertEquals("zippy",    root, "/test/simple/sval");
        DomAsserts.assertEquals("123",      root, "/test/simple/ival");
        DomAsserts.assertEquals("true",     root, "/test/simple/bval");

        DomAsserts.assertEquals("1",        root, "/test/primArray/data[1]");
        DomAsserts.assertEquals("2",        root, "/test/primArray/data[2]");
        DomAsserts.assertEquals("3",        root, "/test/primArray/data[3]");

        DomAsserts.assertEquals("foo",      root, "/test/stringList/data[1]");
        DomAsserts.assertEquals("baz",      root, "/test/stringList/data[2]");
    }


    public void testConvertSequenceAsRepeatedElements() throws Exception
    {
        // since we can't just pass an array into this conversion (because it
        // would try to make multiple root elements), we'll use a compound bean
        // and validate its components (and we'll leave a null in to verify
        // that we ignore it)

        CompoundBean data = new CompoundBean(
                null,
                new int[] { 1, 2, 3 },
                Arrays.asList("foo", null, "baz"));

        Element root = new Bean2XmlConverter(
                            Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS)
                       .convert(data, "test");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        DomAsserts.assertEquals("1",        root, "/test/primArray[1]");
        DomAsserts.assertEquals("2",        root, "/test/primArray[2]");
        DomAsserts.assertEquals("3",        root, "/test/primArray[3]");

        DomAsserts.assertEquals("foo",      root, "/test/stringList[1]");
        DomAsserts.assertEquals("baz",      root, "/test/stringList[2]");
    }


    public void testFailSequenceAsRepeatedElementsAtRoot() throws Exception
    {
        int[] data = new int[] { 1, 2, 3 };

        try
        {
            new Bean2XmlConverter(Bean2XmlOptions.SEQUENCE_AS_REPEATED_ELEMENTS)
                .convert(data, "test");
        }
        catch (ConversionException ee)
        {
            // success
        }
    }


    public void testNamespacedConversion() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();

        // need to use something with child elements so we can verify namespace
        // inheritance
        SimpleBean bean1 = new SimpleBean("foo", 123, new BigDecimal("123"), false);
        Element root = driver.convert(bean1, TEST_NAMESPACE, "argle:bargle");
//        System.out.println(OutputUtil.compactString(root.getOwnerDocument()));

        assertEquals(TEST_NAMESPACE, root.getNamespaceURI());
        assertEquals("argle:bargle", root.getNodeName());
        assertEquals("argle",        root.getPrefix());
        assertEquals("bargle",       root.getLocalName());

        List<Element> children = DomUtil.getChildren(root);
        assertEquals("child count", 4, children.size());

        Element child = children.get(0);
        assertEquals(TEST_NAMESPACE, child.getNamespaceURI());
        assertEquals("argle", child.getPrefix());
        // the order of elements is not guaranteed, so an ugly assert is needed
        assertTrue(DomUtil.getLocalName(child).equals("sval") ||
                   DomUtil.getLocalName(child).equals("ival") ||
                   DomUtil.getLocalName(child).equals("dval") ||
                   DomUtil.getLocalName(child).equals("bval"));
    }


    // note: this was causing a NullPointerException prior to 1.1.10
    public void testBeanWithDifferentGetterAndSetter() throws Exception
    {
        Bean2XmlConverter driver = new Bean2XmlConverter();
        Element root = driver.convert(new BeanWithDifferentGetterAndSetter(123), "root");
        DomAsserts.assertEquals("123", root, "//value");
    }


    public void testCircularReferences() throws Exception
    {
        CircularBean1 bean1 = new CircularBean1("this is the parent");
        CircularBean2 bean2 = new CircularBean2("this is the child");
        bean1.setRef(bean2);
        bean2.setRef(bean1);

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SKIP_CIRCULAR_REFERENCES);
        Element root = driver.convert(bean1, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertExists("parent should have data field",          root, "/root/parentData");
        DomAsserts.assertExists("child should have data field",           root, "/root/ref/childData");
        DomAsserts.assertNotExists("child should not have ref to parent", root, "/root/ref/ref");
    }


    public void testCircularReferencesNoSkip() throws Exception
    {
        CircularBean1 bean1 = new CircularBean1("this is the parent");
        CircularBean2 bean2 = new CircularBean2("this is the child");
        bean1.setRef(bean2);
        bean2.setRef(bean1);

        Bean2XmlConverter driver = new Bean2XmlConverter();
        try
        {
            Element root = driver.convert(bean1, "root");
            fail("able to convert wth circular reference:\n" + OutputUtil.indentedString(root.getOwnerDocument(), 4));
        }
        catch (ConversionException ex)
        {
            assertTrue("message did not explain exception: " + ex.getMessage(),
                       ex.getMessage().contains("circular"));
        }
    }


    public void testSelfReferences() throws Exception
    {
        SelfReferencingBean bean = new SelfReferencingBean("somedata");

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SKIP_CIRCULAR_REFERENCES);
        Element root = driver.convert(bean, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertExists("should have data field",       root, "/root/data");
        DomAsserts.assertNotExists("should not have ref field", root, "/root/ref");
    }


    public void testInaccessibleClass() throws Exception
    {
        InaccessibleClass bean = InaccessibleClass.newInstance("somedata");

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.SET_ACCESSIBLE);
        Element root = driver.convert(bean, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertEquals("getValue()", "somedata", root, "/root/value");
    }


    public void testDeferredExceptions() throws Exception
    {
        ThrowingBean bean = new ThrowingBean("my data");

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.DEFER_EXCEPTIONS);
        Element root = driver.convert(bean, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertCount("number of data values", 1, root, "/root/*");
        DomAsserts.assertExists("non-throwing getter",  root, "/root/data");

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 2, exes.size());

        for (ConversionException ex : exes)
        {
            Throwable cause = ex.getCause();
            if (cause instanceof InvocationTargetException)
                cause = cause.getCause();

            if (ex.getField().contains("throw1"))
                assertEquals("throw1 cause", IllegalArgumentException.class, cause.getClass());
            else if (ex.getField().contains("throw2"))
                assertEquals("throw2 cause", IllegalStateException.class, cause.getClass());
            else
                fail("unexpected exception: ex");
        }
    }


    public void testCalendarXsdFormat() throws Exception
    {
        Date theDate = new Date(1377731547000L);
        Calendar cal = GregorianCalendar.getInstance();
        cal.setTime(theDate);
        cal.setTimeZone(TimeZone.getTimeZone("GMT"));

        CalendarBean bean = new CalendarBean(cal);

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.XSD_FORMAT);
        Element root = driver.convert(bean, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertCount("number of data values",     5,                                          root, "/root/value/*");
        DomAsserts.assertEquals("date",                     XmlUtil.formatXsdDatetime(theDate),         root, "/root/value/date");
        DomAsserts.assertEquals("millis",                   "1377731547000",                            root, "/root/value/millis");
        DomAsserts.assertEquals("timezone",                 "GMT",                                      root, "/root/value/timezone");
        DomAsserts.assertEquals("firstDayOfWeek",           String.valueOf(cal.getFirstDayOfWeek()),    root, "/root/value/firstDayOfWeek");
        DomAsserts.assertEquals("minimumDaysInFirstWeek",   String.valueOf(cal.getMinimalDaysInFirstWeek()), root, "/root/value/minimumDaysInFirstWeek");
    }


    public void testDateSubclassesXsdFormat() throws Exception
    {
        // using XSD format means that we can simplify the test, as we don't need to
        // worry about local timezone; we'll defer toString output/parsing to the
        // round-trip tests in TestBeanConverter

        Date theDate = new Date(1377731547000L);
        String strDate = XmlUtil.formatXsdDatetime(theDate);

        DateBean bean = new DateBean(theDate);

        Bean2XmlConverter driver = new Bean2XmlConverter(Bean2XmlOptions.XSD_FORMAT);
        Element root = driver.convert(bean, "root");
//        System.out.println(OutputUtil.indentedString(root.getOwnerDocument(), 4));

        DomAsserts.assertCount("number of data values", 5,       root, "/root/*");
        DomAsserts.assertEquals("java.util.Date",       strDate, root, "/root/date");
        DomAsserts.assertEquals("java.sql.Date",        strDate, root, "/root/sqlDate");
        DomAsserts.assertEquals("java.sql.Time",        strDate, root, "/root/sqlTime");
        DomAsserts.assertEquals("java.sqlTimestamp",    strDate, root, "/root/timestamp");
        DomAsserts.assertEquals("custom subclass",      strDate, root, "/root/myDate");
    }

}
