/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual 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 org.jboss.shrinkwrap.impl.base.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamConstants;
import java.io.ObjectStreamField;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.logging.Logger;

import junit.framework.TestCase;

import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.serialization.SerializableView;
import org.jboss.shrinkwrap.api.serialization.ZipSerializableView;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 * Ensures that serialization of Archives is possible via the {@link SerializableView}s.
 *
 * SHRINKWRAP-178
 *
 * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
 * @version $Revision: $
 */
public class SerializationTestCase {

    // -------------------------------------------------------------------------------------||
    // Class Members -----------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Logger
     */
    private static final Logger log = Logger.getLogger(SerializationTestCase.class.getName());

    /**
     * Name of the payload archive used in testing serialization
     */
    private static final String NAME_PAYLOAD_ARCHIVE = "serializedArchive.jar";

    // -------------------------------------------------------------------------------------||
    // Instance Members --------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * A populated archive to be used in testing serialization
     */
    private JavaArchive payload;

    // -------------------------------------------------------------------------------------||
    // Lifecycle ---------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Creates a payload archive to be used in serialization tests
     */
    @Before
    public void createPayload() {
        payload = ShrinkWrap.create(JavaArchive.class, NAME_PAYLOAD_ARCHIVE).addClasses(SerializationTestCase.class,
            JavaArchive.class);
    }

    // -------------------------------------------------------------------------------------||
    // Tests -------------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Ensures we may serialize an {@link Archive} as {@link ZipSerializableView} and preserve contents as expected
     */
    @Test
    public void zipSerializableView() throws Exception {
        this.testSerializableView(ZipSerializableView.class);
    }

    /**
     * Ensures we may serialize an {@link Archive} as {@link SerializableView} and preserve contents as expected
     */
    @Test
    public void serializableView() throws Exception {
        this.testSerializableView(SerializableView.class);
    }

    /**
     * Tests that the payload archive may be serialized as the specified {@link SerializableView} type and contents of
     * the roundtrip are as expected.
     *
     * @param <S>
     * @param serializableView
     * @throws Exception
     */
    private <S extends SerializableView> void testSerializableView(final Class<S> serializableView) throws Exception {
        // Define the initial archive
        log.info("Before: " + payload.toString(true));

        // Serialize
        final JavaArchive roundtrip = serializeAndDeserialize(payload.as(serializableView)).as(JavaArchive.class);
        log.info("After: " + roundtrip.toString(true));

        // Ensure contents are as expected
        this.testCurrentFields(payload, roundtrip);
    }

    /**
     * Ensures that the current serialization protocol is compatible with the version initially released. We accomplish
     * this by mocking {@link ZipSerializableOriginalImpl} and redefining its class name via
     * {@link SerializationTestCase#serializeAndDeserialize(ZipSerializableView, Class)}, which uses the
     * {@link SpoofingObjectOutputStream}.
     *
     * @throws Exception
     */
    @Test
    public void zipWireProtocolCurrentToOriginal() throws Exception {
        final SerializableView currentWireFormat = this.payload.as(SerializableView.class);
        final SerializableView roundtrip = this.testWireProtocol(currentWireFormat, ZipSerializableOriginalImpl.class);
        this.testOriginalFields(payload, roundtrip.as(JavaArchive.class));
    }

    /**
     * Ensures that the original serialization protocol is compatible with the current version. We accomplish this by
     * mocking {@link ZipSerializableOriginalImpl} and redefining its class name via
     * {@link SerializationTestCase#serializeAndDeserialize(ZipSerializableView, Class)}, which uses the
     * {@link SpoofingObjectOutputStream}.
     *
     * @throws Exception
     */
    @Test
    public void zipWireProtocolOriginalToCurrent() throws Exception {
        final SerializableView originalWireFormat = new ZipSerializableOriginalImpl(payload);
        final SerializableView roundtrip = this.testWireProtocol(originalWireFormat, ZipSerializableViewImpl.class);
        this.testOriginalFields(payload, roundtrip.as(JavaArchive.class));
    }

    // -------------------------------------------------------------------------------------||
    // Internal Helper Methods -------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Ensures that the specified client object may be serialized to the specified type
     *
     * @param clientObject
     *            The object to be serialized
     * @param targetType
     *            The type we should be represented as
     * @return The roundtrip view
     * @throws IOException
     */
    private SerializableView testWireProtocol(final SerializableView clientObject,
        final Class<? extends SerializableView> targetType) throws IOException {
        // Roundtrip the object, now representing as the target type
        final SerializableView roundtrip = serializeAndDeserialize(clientObject, targetType);

        // The type of the object put through roundtrip serialization must be of the type specified
        TestCase.assertEquals(targetType, roundtrip.getClass());

        // Return
        return roundtrip;
    }

    private void testOriginalFields(final Archive<?> payload, final Archive<?> roundtrip) {
        final Map<ArchivePath, Node> originalContents = payload.getContent();
        final Map<ArchivePath, Node> roundtripContents = roundtrip.getContent();
        Assert.assertEquals("Contents after serialization were not as expected", originalContents, roundtripContents);
        Assert.assertEquals("Name of original archive was not as expected", NAME_PAYLOAD_ARCHIVE, payload.getName());
        Assert.assertEquals("Name not as expected after serialization", payload.getName(), roundtrip.getName());
    }

    private void testCurrentFields(final Archive<?> payload, final Archive<?> roundtrip) {
        this.testOriginalFields(payload, roundtrip);
        Assert.assertEquals("ID not as expected after serialization", payload.getId(), roundtrip.getId());
    }

    /**
     * Roundtrip serializes/deserializes the specified {@link Archive}
     *
     * @param archive
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static SerializableView serializeAndDeserialize(final SerializableView archive) throws IOException,
        ClassNotFoundException {
        assert archive != null : "Archive must be specified";
        final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        final ObjectOutputStream out = new ObjectOutputStream(byteOut);
        out.writeObject(archive);
        out.flush();
        out.close();
        final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
        final SerializableView roundtrip = (SerializableView) in.readObject();
        in.close();
        return roundtrip;
    }

    /**
     * Roundtrip serializes/deserializes the specified {@link SerializableView} and reconsitutes/redefines as the
     * specified target type
     *
     * @param archive
     *            The original {@link SerializableView} instance
     * @param The
     *            new type we should cast to after deserialization
     * @see http://crazybob.org/2006/01/unit-testing-serialization-evolution.html
     * @see http://crazybob.org/2006/01/unit-testing-serialization-evolution_13.html
     * @see http://www.theserverside.com/news/thread.tss?thread_id=38398
     * @author Bob Lee
     */
    private static <S extends SerializableView> S serializeAndDeserialize(final SerializableView archive,
        final Class<S> targetType) throws IOException {
        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
        final ObjectOutputStream oout = new SpoofingObjectOutputStream(bout, archive.getClass(), targetType);
        oout.writeObject(archive);
        oout.flush();
        oout.close();
        final ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        final ObjectInputStream oin = new ObjectInputStream(bin);
        try {
            final Object obj = oin.readObject();
            oin.close();
            log.info("Original type " + archive.getClass().getName() + " now represented as "
                + obj.getClass().getName());
            return targetType.cast(obj);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    // -------------------------------------------------------------------------------------||
    // Internal Helper Classes -------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * SpoofingObjectOutputStream
     *
     * ObjectOutputStream which will replace a class name with one explicitly given
     *
     * @see http://crazybob.org/2006/01/unit-testing-serialization-evolution_13.html
     * @author Bob Lee
     * @version $Revision: $
     */
    static class SpoofingObjectOutputStream extends ObjectOutputStream {

        String oldName;

        String newName;

        public SpoofingObjectOutputStream(OutputStream out, Class<?> oldClass, Class<?> newClass) throws IOException {
            super(out);
            this.oldName = oldClass.getName();
            this.newName = newClass.getName();
        }

        @Override
        protected void writeClassDescriptor(ObjectStreamClass descriptor) throws IOException {
            Class<?> clazz = descriptor.forClass();

            boolean externalizable = Externalizable.class.isAssignableFrom(clazz);
            boolean serializable = Serializable.class.isAssignableFrom(clazz);
            boolean hasWriteObjectData = hasWriteObjectMethod(clazz);
            boolean isEnum = Enum.class.isAssignableFrom(clazz);

            writeUTF(replace(descriptor.getName()));
            writeLong(descriptor.getSerialVersionUID());
            byte flags = 0;
            if (externalizable) {
                flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
                flags |= ObjectStreamConstants.SC_BLOCK_DATA;
            } else if (serializable) {
                flags |= ObjectStreamConstants.SC_SERIALIZABLE;
            }
            if (hasWriteObjectData) {
                flags |= ObjectStreamConstants.SC_WRITE_METHOD;
            }
            if (isEnum) {
                flags |= ObjectStreamConstants.SC_ENUM;
            }
            writeByte(flags);

            ObjectStreamField[] fields = descriptor.getFields();
            writeShort(fields.length);
            for (ObjectStreamField field : fields) {
                writeByte(field.getTypeCode());
                writeUTF(field.getName());
                if (!field.isPrimitive()) {
                    writeObject(replace(field.getTypeString()));
                }
            }
        }

        String replace(String className) {
            if (className.equals(newName)) {
                throw new RuntimeException("Found instance of " + className + "." + " Expected instance of " + oldName
                    + ".");
            }
            return className == oldName ? newName : className;
        }

        boolean hasWriteObjectMethod(Class<?> clazz) {
            try {
                Method method = clazz.getDeclaredMethod("writeObject", ObjectOutputStream.class);
                int modifiers = method.getModifiers();
                return method.getReturnType() == Void.TYPE && !Modifier.isStatic(modifiers)
                    && Modifier.isPrivate(modifiers);
            } catch (NoSuchMethodException e) {
                return false;
            }
        }
    }
}
