/*
 * 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.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serializable;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.Assignable;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.jboss.shrinkwrap.api.serialization.ZipSerializableView;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.impl.base.Validate;
import org.jboss.shrinkwrap.impl.base.io.IOUtil;
import org.jboss.shrinkwrap.spi.Identifiable;

/**
 * Implementation of a {@link Serializable} view of {@link Archive}s, backed by ZIP en/decoding the contents during
 * serialization/deserialization. Defines the wire protocol and must remain backwards-compatible.
 *
 * @author <a href="mailto:andrew.rubinger@jboss.org">ALR</a>
 * @version $Revision: $
 */
public class ZipSerializableViewImpl implements ZipSerializableView {

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

    /**
     * serialVersionUID
     */
    private static final long serialVersionUID = 1L;

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

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

    /**
     * Name of the archive; to be serialized
     */
    private final String name;

    /**
     * String form of the ID
     */
    private transient String id;

    /**
     * Underlying archive. Won't be directly serialized; instead we'll encode it as ZIP and send that
     */
    private transient Archive<?> archive;

    // -------------------------------------------------------------------------------------||
    // Constructor ------------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Creates a new instance, wrapping the specified {@link Archive}
     */
    public ZipSerializableViewImpl(final Archive<?> archive) {
        Validate.notNull(archive, "Archive must be specified");
        final String name = archive.getName();
        Validate.notNullOrEmpty(name, "Name of archive must be specified");
        this.archive = archive;
        this.name = name;
        this.id = archive.getId();
    }

    // -------------------------------------------------------------------------------------||
    // Required Implementations -----------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * {@inheritDoc}
     *
     * @see org.jboss.shrinkwrap.api.Assignable#as(java.lang.Class)
     */
    @Override
    public <TYPE extends Assignable> TYPE as(final Class<TYPE> clazz) {
        return archive.as(clazz);
    }

    // -------------------------------------------------------------------------------------||
    // Serialization ----------------------------------------------------------------------||
    // -------------------------------------------------------------------------------------||

    /**
     * Serializes the invocation with a custom form
     *
     * @serialData After all non-transient fields are written, we send the {@link Archive} contents encoded as ZIP.
     */
    private void writeObject(final ObjectOutputStream out) throws IOException {
        // Default write of non-transient fields
        out.defaultWriteObject();

        // Write as ZIP
        final InputStream in = archive.as(ZipExporter.class).exportAsInputStream();
        try {
            IOUtil.copy(in, out); // Don't close the outstream
            // Write the ID explicitly
            out.writeObject(id);
        } finally {
            // In case we get an InputStream type that supports closing
            in.close();
        }

        // Log
        if (log.isLoggable(Level.FINER)) {
            log.finer("Wrote archive: " + archive.toString());
        }
    }

    /**
     * Deserializes according to the custom form defined by {@link ZipSerializableImpl#writeObject(ObjectOutputStream)}
     */
    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
        // Get default form
        in.defaultReadObject();

        // Create new Archive
        final String name = this.name;
        final ZipImporter archive = ShrinkWrap.create(ZipImporter.class, name);

        // Read in
        archive.importFrom(in);

        // Set
        this.archive = archive.as(JavaArchive.class);

        // Log
        if (log.isLoggable(Level.FINER)) {
            log.finer("Read in archive: " + archive.toString());
        }

        /*
         * Leave this bit here.
         *
         * After reading in the ZIP stream contents, we need to also get to the EOF marker (which is not read in by the
         * ZIP import process because it's the ZIP header, not part of the true contents. Putting this loop here ensures
         * we reach the marker, which is *not* the true end of the stream. Object data may be read again after here via
         * something like:
         *
         * in.readObject();
         *
         * Without this loop we'll get an OptionalDataException when trying to read more objects in from the stream. In
         * the future we may add state which needs to be part of the serialization protocol, and things need to stay in
         * order, so they'll be added *after* the archive ZIP contents. Thus we must be able to read them.
         */
        while (in.read() != -1) {
        }

        // We've now added an ID to archives, so add this into the wire protocol
        try {
            final String id = (String) in.readObject();
            this.archive.as(Identifiable.class).setId(id);
        } catch (final OptionalDataException ode) {
            /*
             * Swallow; this occurs when an older wire format (without notion of ID) attempts to send an archive to a
             * new format. The new archive will have the ID assigned to it at creation time, and we'll ignore that we
             * can't read in the remote one from this stream (as the remote one from the old version does not have an ID
             * to send
             */
            if (log.isLoggable(Level.FINER)) {
                log.finer("Detected an older version of the archive sent over the wire; no ID was sent.  "
                    + "Ignoring and using the default ID for this archive: " + archive.toString());
            }
        }

    }
}
