/*
 * Copyright (C) 2004, 2005 Joe Walnes.
 * Copyright (C) 2006, 2007, 2015, 2021 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 * 
 * Created on 23. August 2004 by Joe Walnes
 */
package com.thoughtworks.acceptance;

import com.thoughtworks.acceptance.objects.Hardware;
import com.thoughtworks.acceptance.objects.Software;
import com.thoughtworks.acceptance.objects.StandardObject;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;
import java.io.Serializable;

import com.thoughtworks.acceptance.objects.Hardware;
import com.thoughtworks.acceptance.objects.Software;
import com.thoughtworks.acceptance.objects.StandardObject;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;


public class CustomSerializationTest extends AbstractAcceptanceTest {

    public static class ObjectWithCustomSerialization extends StandardObject implements Serializable {

        private int a;
        private transient int b;
        private transient String c;
        private transient Object d;
        private transient Software e;

        public ObjectWithCustomSerialization() {
        }

        public ObjectWithCustomSerialization(int a, int b, String c, Software e) {
            this.a = a;
            this.b = b;
            this.c = c;
            this.e = e;
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            b = in.readInt();
            in.defaultReadObject();
            c = (String) in.readObject();
            d = in.readObject();
            e = (Software) in.readObject();
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.writeInt(b);
            out.defaultWriteObject();
            out.writeObject(c);
            out.writeObject(d);
            out.writeObject(e);
        }

    }

    public void testWritesCustomFieldsToStream() {
        ObjectWithCustomSerialization obj = new ObjectWithCustomSerialization(1, 2, "hello", new Software("tw", "xs"));
        xstream.alias("custom", ObjectWithCustomSerialization.class);
        xstream.alias("software", Software.class);

        String expectedXml = ""
                + "<custom serialization=\"custom\">\n"
                + "  <custom>\n"
                + "    <int>2</int>\n"
                + "    <default>\n"
                + "      <a>1</a>\n"
                + "    </default>\n"
                + "    <string>hello</string>\n"
                + "    <null/>\n"
                + "    <software>\n"
                + "      <vendor>tw</vendor>\n"
                + "      <name>xs</name>\n"
                + "    </software>\n"
                + "  </custom>\n"
                + "</custom>";

        assertBothWays(obj, expectedXml);
    }

    public static class Parent extends StandardObject implements Serializable {

        private transient int parentA;
        private int parentB;
        private transient int parentC;

        public Parent() {
        }

        public Parent(int parentA, int parentB, int parentC) {
            this.parentA = parentA;
            this.parentB = parentB;
            this.parentC = parentC;
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            parentA = in.readInt();
            in.defaultReadObject();
            parentC = in.readInt();
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.writeInt(parentA);
            out.defaultWriteObject();
            out.writeInt(parentC);
        }
    }

    public static class Child extends Parent {

        private transient int childA;
        private int childB;
        private transient int childC;

        public Child() {
        }

        public Child(int parentA, int parentB, int parentC, int childA, int childB, int childC) {
            super(parentA, parentB, parentC);
            this.childA = childA;
            this.childB = childB;
            this.childC = childC;
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            childA = in.readInt();
            in.defaultReadObject();
            childC = in.readInt();
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.writeInt(childA);
            out.defaultWriteObject();
            out.writeInt(childC);
        }
    }

    public void testIncludesCompleteClassHierarchyWhenParentAndChildHaveSerializationMethods() {
        Child child = new Child(1, 2, 3, 10, 20, 30);
        xstream.alias("child", Child.class);
        xstream.alias("parent", Parent.class);

        String expectedXml = ""
                + "<child serialization=\"custom\">\n"
                + "  <parent>\n"
                + "    <int>1</int>\n"
                + "    <default>\n"
                + "      <parentB>2</parentB>\n"
                + "    </default>\n"
                + "    <int>3</int>\n"
                + "  </parent>\n"
                + "  <child>\n"
                + "    <int>10</int>\n"
                + "    <default>\n"
                + "      <childB>20</childB>\n"
                + "    </default>\n"
                + "    <int>30</int>\n"
                + "  </child>\n"
                + "</child>";

        assertBothWays(child, expectedXml);
    }

    public static class Child2 extends Parent {

        private int childA;

        public Child2(int parentA, int parentB, int parentC, int childA) {
            super(parentA, parentB, parentC);
            this.childA = childA;
        }

    }

    public void testIncludesCompleteClassHierarchyWhenOnlyParentHasSerializationMethods() {
        Child2 child = new Child2(1, 2, 3, 20);
        xstream.alias("child2", Child2.class);
        xstream.alias("parent", Parent.class);

        String expectedXml = ""
                + "<child2 serialization=\"custom\">\n"
                + "  <parent>\n"
                + "    <int>1</int>\n"
                + "    <default>\n"
                + "      <parentB>2</parentB>\n"
                + "    </default>\n"
                + "    <int>3</int>\n"
                + "  </parent>\n"
                + "  <child2>\n"
                + "    <default>\n"
                + "      <childA>20</childA>\n"
                + "    </default>\n"
                + "  </child2>\n"
                + "</child2>";

        assertBothWays(child, expectedXml);
    }

    static class Pair {
        protected Object first;
        protected Object second;

        protected Pair() {
        }

        public Pair(final Object first, final Object second) {
            this.first = first;
            this.second = second;
        }
    }

    static class SerializablePair extends Pair implements Serializable {
        private static final long serialVersionUID = 20201214L;

        public SerializablePair(final Object first, final Object second) {
            super(first, second);
        }

        private void writeObject(final ObjectOutputStream out) throws IOException {
            out.writeObject(first);
            out.writeObject(second);
        }

        private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
            first = in.readObject();
            second = in.readObject();
        }
    }

    public void testCustomSerializationWithoutDefaultReadAndWriteObject() {
        xstream = new XStream(new PureJavaReflectionProvider());
        setupSecurity(xstream);
        xstream.alias("pair", Pair.class);
        xstream.alias("serpair", SerializablePair.class);

        final String expectedXml = ""
            + "<serpair serialization=\"custom\">\n"
            + "  <unserializable-parents>\n"
            + "    <first class=\"int\">42</first>\n"
            + "    <second class=\"string\">fourty-two</second>\n"
            + "  </unserializable-parents>\n"
            + "  <serpair>\n"
            + "    <int>42</int>\n"
            + "    <string>fourty-two</string>\n"
            + "  </serpair>\n"
            + "</serpair>";

        final Pair pair = (Pair)new SerializablePair(new Integer(42), "fourty-two");
        assertBothWays(pair, expectedXml.replace('\'', '"'));
    }

    static class MyDate extends java.util.Date {
        public MyDate(int time) {
            super(time);
        }
    }

    static class MyHashtable extends java.util.Hashtable {
        private String name;

        public MyHashtable(String name) {
            this.name = name;
        }

        public synchronized boolean equals(Object o) {
            return super.equals(o) && ((MyHashtable)o).name.equals(name);
        }
    }

    public void testSupportsSubclassesOfClassesThatAlreadyHaveConverters() {
        MyDate in = new MyDate(1234567890);
        String xml = xstream.toXML(in);
        assertObjectsEqual(in, xstream.fromXML(xml));

        MyHashtable in2 = new MyHashtable("hi");
        in2.put("cheese", "curry");
        in2.put("apple", new Integer(3));
        String xml2 = xstream.toXML(in2);
        assertObjectsEqual(in2, xstream.fromXML(xml2));
    }

    public static class ObjectWithNamedFields extends StandardObject implements Serializable {

        private String name;
        private int number;
        private Software someSoftware;
        private Object polymorphic;
        private Object nothing;

        private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("theName", String.class),
            new ObjectStreamField("theNumber", int.class),
            new ObjectStreamField("theSoftware", Software.class),
            new ObjectStreamField("thePolymorphic", Object.class),
            new ObjectStreamField("theNothing", Object.class)
        };

        private void writeObject(ObjectOutputStream out) throws IOException {
            // don't call defaultWriteObject()
            ObjectOutputStream.PutField fields = out.putFields();
            fields.put("theName", name);
            fields.put("theNumber", number);
            fields.put("theSoftware", someSoftware);
            fields.put("thePolymorphic", polymorphic);
            fields.put("theNothing", nothing);
            out.writeFields();
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            // don't call defaultReadObject()
            ObjectInputStream.GetField fields = in.readFields();
            name = (String) fields.get("theName", "unknown");
            number = fields.get("theNumber", -1);
            someSoftware = (Software) fields.get("theSoftware", null);
            polymorphic = fields.get("thePolymorphic", null);
            nothing = fields.get("theNothing", null);
        }

    }

    public void testAllowsNamedFields() {
        ObjectWithNamedFields obj = new ObjectWithNamedFields();
        obj.name = "Joe";
        obj.number = 99;
        obj.someSoftware = new Software("tw", "xs");
        obj.polymorphic = new Hardware("small", "ipod");
        obj.nothing = null;

        xstream.alias("with-named-fields", ObjectWithNamedFields.class);
        xstream.alias("software", Software.class);

        String expectedXml = ""
                + "<with-named-fields serialization=\"custom\">\n"
                + "  <with-named-fields>\n"
                + "    <default>\n"
                + "      <theName>Joe</theName>\n"
                + "      <theNumber>99</theNumber>\n"
                + "      <theSoftware>\n"
                + "        <vendor>tw</vendor>\n"
                + "        <name>xs</name>\n"
                + "      </theSoftware>\n"
                + "      <thePolymorphic class=\"com.thoughtworks.acceptance.objects.Hardware\">\n"
                + "        <arch>small</arch>\n"
                + "        <name>ipod</name>\n"
                + "      </thePolymorphic>\n"
                + "    </default>\n"
                + "  </with-named-fields>\n"
                + "</with-named-fields>";

        assertBothWays(obj, expectedXml);
    }

    public void testUsesDefaultIfNamedFieldNotFound() {
        xstream.alias("with-named-fields", ObjectWithNamedFields.class);
        xstream.alias("software", Software.class);

        String inputXml = ""
                + "<with-named-fields serialization=\"custom\">\n"
                + "  <with-named-fields>\n"
                + "    <default>\n"
                + "      <theSoftware>\n"
                + "        <vendor>tw</vendor>\n"
                + "        <name>xs</name>\n"
                + "      </theSoftware>\n"
                + "      <thePolymorphic class=\"com.thoughtworks.acceptance.objects.Hardware\">\n"
                + "        <arch>small</arch>\n"
                + "        <name>ipod</name>\n"
                + "      </thePolymorphic>\n"
                + "    </default>\n"
                + "  </with-named-fields>\n"
                + "</with-named-fields>";

        ObjectWithNamedFields result = (ObjectWithNamedFields) xstream.fromXML(inputXml);
        assertEquals(-1, result.number);
        assertEquals("unknown", result.name);
        assertEquals(new Software("tw", "xs"), result.someSoftware);
    }

    public void testCustomStreamWithNestedCustomStream() {
        ObjectWithNamedFields outer = new ObjectWithNamedFields();
        outer.name = "Joe";
        outer.someSoftware = new Software("tw", "xs");
        outer.nothing = null;

        ObjectWithNamedFields inner = new ObjectWithNamedFields();
        inner.name = "Thing";

        outer.polymorphic = inner;

        xstream.alias("with-named-fields", ObjectWithNamedFields.class);
        xstream.alias("software", Software.class);

        String expectedXml = ""
                + "<with-named-fields serialization=\"custom\">\n"
                + "  <with-named-fields>\n"
                + "    <default>\n"
                + "      <theName>Joe</theName>\n"
                + "      <theNumber>0</theNumber>\n"
                + "      <theSoftware>\n"
                + "        <vendor>tw</vendor>\n"
                + "        <name>xs</name>\n"
                + "      </theSoftware>\n"
                + "      <thePolymorphic class=\"with-named-fields\" serialization=\"custom\">\n"
                + "        <with-named-fields>\n"
                + "          <default>\n"
                + "            <theName>Thing</theName>\n"
                + "            <theNumber>0</theNumber>\n"
                + "          </default>\n"
                + "        </with-named-fields>\n"
                + "      </thePolymorphic>\n"
                + "    </default>\n"
                + "  </with-named-fields>\n"
                + "</with-named-fields>";

        assertBothWays(outer, expectedXml);
    }

    public static class NoDefaultFields extends StandardObject implements Serializable {

        private transient int something;

        public NoDefaultFields() {
        }

        public NoDefaultFields(int something) {
            this.something = something;
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            something = in.readInt();
        }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(something);
        }

    }

    public void testObjectWithCallToDefaultWriteButNoDefaultFields() {
        xstream.alias("x", NoDefaultFields.class);

        String expectedXml = ""
                + "<x serialization=\"custom\">\n"
                + "  <x>\n"
                + "    <default/>\n"
                + "    <int>77</int>\n"
                + "  </x>\n"
                + "</x>";
        assertBothWays(new NoDefaultFields(77), expectedXml);
    }

    public void testMaintainsBackwardsCompatabilityWithXStream1_1_0FieldFormat() {
        ObjectWithNamedFields outer = new ObjectWithNamedFields();
        outer.name = "Joe";
        outer.someSoftware = new Software("tw", "xs");
        outer.nothing = null;

        ObjectWithNamedFields inner = new ObjectWithNamedFields();
        inner.name = "Thing";

        outer.polymorphic = inner;

        xstream.alias("with-named-fields", ObjectWithNamedFields.class);
        xstream.alias("software", Software.class);

        String oldFormatOfXml = ""
                + "<with-named-fields serialization=\"custom\">\n"
                + "  <with-named-fields>\n"
                + "    <fields>\n"
                + "      <field name=\"theName\" class=\"string\">Joe</field>\n"
                + "      <field name=\"theNumber\" class=\"int\">0</field>\n"
                + "      <field name=\"theSoftware\" class=\"software\">\n"
                + "        <vendor>tw</vendor>\n"
                + "        <name>xs</name>\n"
                + "      </field>\n"
                + "      <field name=\"thePolymorphic\" class=\"with-named-fields\" serialization=\"custom\">\n"
                + "        <with-named-fields>\n"
                + "          <fields>\n"
                + "            <field name=\"theName\" class=\"string\">Thing</field>\n"
                + "            <field name=\"theNumber\" class=\"int\">0</field>\n"
                + "          </fields>\n"
                + "        </with-named-fields>\n"
                + "      </field>\n"
                + "    </fields>\n"
                + "  </with-named-fields>\n"
                + "</with-named-fields>";


        assertEquals(outer, xstream.fromXML(oldFormatOfXml));
    }

    public static class ObjectWithNamedThatMatchRealFields extends StandardObject implements Serializable {

        private String name;
        private int number;

        private void writeObject(ObjectOutputStream out) throws IOException {
            ObjectOutputStream.PutField fields = out.putFields();
            fields.put("name", name.toUpperCase());
            fields.put("number", number * 100);
            out.writeFields();
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            ObjectInputStream.GetField fields = in.readFields();
            name = ((String) fields.get("name", "unknown")).toLowerCase();
            number = fields.get("number", 10000) / 100;
        }

    }

    public void testSupportsWritingFieldsForObjectsThatDoNotExplicitlyDefineThem() {
        xstream.alias("an-object", ObjectWithNamedThatMatchRealFields.class);

        ObjectWithNamedThatMatchRealFields input = new ObjectWithNamedThatMatchRealFields();
        input.name = "a name";
        input.number = 5;

        String expectedXml = ""
                + "<an-object serialization=\"custom\">\n"
                + "  <an-object>\n"
                + "    <default>\n"
                + "      <name>A NAME</name>\n"
                + "      <number>500</number>\n"
                + "    </default>\n"
                + "  </an-object>\n"
                + "</an-object>";

        assertBothWays(input, expectedXml);
    }

    public static class ObjectThatReadsCustomFieldsButDoesNotWriteThem extends StandardObject implements Serializable {

        private String name;
        private int number;

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            ObjectInputStream.GetField fields = in.readFields();
            name = ((String) fields.get("name", "unknown"));
            number = fields.get("number", 10000);
        }

    }

    public void testSupportsGetFieldsWithoutPutFields() {
        xstream.alias("an-object", ObjectThatReadsCustomFieldsButDoesNotWriteThem.class);

        ObjectThatReadsCustomFieldsButDoesNotWriteThem input = new ObjectThatReadsCustomFieldsButDoesNotWriteThem();
        input.name = "a name";
        input.number = 5;

        String expectedXml = ""
                + "<an-object serialization=\"custom\">\n"
                + "  <an-object>\n"
                + "    <default>\n"
                + "      <number>5</number>\n"
                + "      <name>a name</name>\n"
                + "    </default>\n"
                + "  </an-object>\n"
                + "</an-object>";

        assertBothWays(input, expectedXml);
    }
    
    public static class ObjectThatWritesCustomFieldsButDoesNotReadThem extends StandardObject implements Serializable {

        private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("number", int.class),
            new ObjectStreamField("name", String.class),
        };

        private void writeObject(ObjectOutputStream out) throws IOException {
            ObjectOutputStream.PutField fields = out.putFields();
            fields.put("name", "test");
            fields.put("number", 42);
            out.writeFields();
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
        }

    }

    public void testSupportsPutFieldsWithoutGetFields() {
        xstream.alias("an-object", ObjectThatWritesCustomFieldsButDoesNotReadThem.class);

        ObjectThatWritesCustomFieldsButDoesNotReadThem input = new ObjectThatWritesCustomFieldsButDoesNotReadThem();

        String expectedXml = ""
                + "<an-object serialization=\"custom\">\n"
                + "  <an-object>\n"
                + "    <default>\n"
                + "      <name>test</name>\n"
                + "      <number>42</number>\n"
                + "    </default>\n"
                + "  </an-object>\n"
                + "</an-object>";

        assertBothWays(input, expectedXml);
    }
}
