/*
 * 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.axis2.context.externalize;

import org.apache.axiom.om.OMOutputFormat;
import org.apache.axiom.om.impl.builder.StAXBuilder;
import org.apache.axiom.om.util.StAXUtils;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder;
import org.apache.axis2.AxisFault;
import org.apache.axis2.builder.BuilderUtil;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.transport.MessageFormatter;
import org.apache.axis2.transport.TransportUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.stream.XMLStreamReader;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;

/**
 * Utility to read/write the Message of a MessageContext
 * Message Object Format.
 * 
 * <tt>
 * Format := Prolog {DataBlocks} EndBlocks
 * 
 * Prolog :=
 *   NAME      (UTF) 
 *   REVISION  (INT)
 *   ACTIVE    (BOOL)
 *     [OPTIMIZED (BOOL)]  
 *        [OPTIMIZED_CONTENT_TYPE (UTF)]    <--- If OPTIMIZED=TRUE
 *     [CHARSET   (UTF)] 
 *     [NAMESPACE (UTF)]
 *   
 * DataBlock :=
 *   SIZE (INT >0)
 *   DATA (BYTES)
 *   
 * EndBlocks
 *   SIZE (INT)   {0 indicates end -1 indicates failure}
 *     
 *   
 * </tt>
 */
public class MessageExternalizeUtils  implements ExternalizeConstants {
    static final Log log = LogFactory.getLog(MessageExternalizeUtils.class);

    /*
     * @serial Tracks the revision level of a class to identify changes to the
     * class definition that are compatible to serialization/externalization.
     * If a class definition changes, then the serialization/externalization
     * of the class is affected.
     * Refer to the writeExternal() and readExternal() methods.
     */
    // supported revision levels, add a new level to manage compatible changes
    private static final int REVISION_2 = 2;
    // current revision level of this object
    private static final int revisionID = REVISION_2;
    
    
    /**
     * Private Constructor.
     * This class only supports static methods
     */
    private MessageExternalizeUtils() {}
    
    /**
     * Write out the Message
     * @param out
     * @param mc
     * @param correlationIDString
     * @param outputFormat
     * @throws IOException
     */
    public static void writeExternal(ObjectOutput out, 
                                     MessageContext mc,
                                     String correlationIDString,
                                     OMOutputFormat outputFormat) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":writeExternal(): start");
        }
        SOAPEnvelope envelope = mc.getEnvelope();
        if (envelope == null) {
            // Case: No envelope
            out.writeUTF("NULL_ENVELOPE");
            out.writeInt(revisionID);
            out.writeBoolean(EMPTY_OBJECT); // Not Active
            out.writeInt(0);  // EndBlocks
            if (log.isDebugEnabled()) {
                log.debug(correlationIDString + ":writeExternal(): end: msg is Empty");
            }
            return;
        }
        
        // Write Prolog
        String msgClass = envelope.getClass().getName();
        out.writeUTF(msgClass);
        out.writeInt(revisionID);
        out.writeBoolean(ACTIVE_OBJECT);
        if (outputFormat.isOptimized()) {
            out.writeBoolean(true);
            // Write out the contentType.
            out.writeUTF(outputFormat.getContentType());
        } else {
            out.writeBoolean(false);
        }
        out.writeUTF(outputFormat.getCharSetEncoding());
        out.writeUTF(envelope.getNamespace().getNamespaceURI());
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":writeExternal(): " + 
                      "optimized=[" + outputFormat.isOptimized() + "]  " +
                      "optimizedContentType " + outputFormat.getContentType() + "]  " +
                      "charSetEnc=[" + outputFormat.getCharSetEncoding() + "]  " +
                      "namespaceURI=[" + envelope.getNamespace().getNamespaceURI() + "]");
        }
        
        // Write DataBlocks
        // MessageOutputStream writes out the DataBlocks in chunks
        // BufferedOutputStream buffers the data to prevent numerous, small blocks
        MessageOutputStream mos = new MessageOutputStream(out);  
        BufferedOutputStream bos = new BufferedOutputStream(mos);   
        boolean errorOccurred = false;
        try { 
            // Write out the message using the same logic as the 
            // transport layer.
            MessageFormatter msgFormatter = TransportUtils.getMessageFormatter(mc);
            msgFormatter.writeTo(mc, outputFormat, bos, 
                                 true); // Preserve the original message
            
        } catch (IOException e) {
            throw e;
        } catch (Throwable t) {
            throw AxisFault.makeFault(t);
        } finally {
            bos.flush();
            bos.close();
        }
        
        // Write End of Data Blocks
        if (errorOccurred) {
            out.writeInt(-1);
        } else {
            out.writeInt(0);
        }
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":writeExternal(): end");
        }
    }
    
    /**
     * Read the Message
     * @param in
     * @param mc
     * @param correlationIDString
     * @return
     * @throws IOException
     */
    public static SOAPEnvelope readExternal(ObjectInput in,
                                            MessageContext mc,
                                            String correlationIDString) throws IOException, ClassNotFoundException {
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":readExternal(): start");
        }
        SOAPEnvelope envelope = null;
        
        // Read Prolog
        // Read the class name and object state
        String name = in.readUTF();
        int revision = in.readInt();
        
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":readExternal(): name= " + name  +
                      " revision= " + revision);
        }
        // make sure the object data is in a revision level we can handle
        if (revision != REVISION_2) {
            throw new ClassNotFoundException(ExternalizeConstants.UNSUPPORTED_REVID);
        }
        
        
        boolean gotMsg = in.readBoolean();
        if (gotMsg != ACTIVE_OBJECT) {
            if (log.isDebugEnabled()) {
                log.debug(correlationIDString + ":readExternal(): end:" +
                                "no message present");
            }
            in.readInt(); // Read end of data blocks
            return envelope;
        }
        
        // Read optimized, optimized content-type, charset encoding and namespace uri
        boolean optimized= in.readBoolean();
        String optimizedContentType = null;
        if (optimized) {
            optimizedContentType = in.readUTF();
        }
        String charSetEnc = in.readUTF();
        String namespaceURI = in.readUTF();
        if (log.isDebugEnabled()) {
            log.debug(correlationIDString + ":readExternal(): " +
                      "optimized=[" + optimized + "]  " +
                      "optimizedContentType=[" + optimizedContentType + "]  " +
                      "charSetEnc=[" + charSetEnc + "]  " +
                      "namespaceURI=[" + namespaceURI + "]");
        }
        
        MessageInputStream mis = new MessageInputStream(in);
        StAXBuilder  builder = null;
        try {
            if (optimized) {
                boolean isSOAP = true;
                builder =
                    BuilderUtil.getAttachmentsBuilder(mc,
                                                      mis,
                                                      optimizedContentType,
                                                      isSOAP);
                envelope = (SOAPEnvelope) builder.getDocumentElement();
                envelope.buildWithAttachments();
            } else {
                XMLStreamReader xmlreader = StAXUtils.createXMLStreamReader(mis, charSetEnc);
                builder = new StAXSOAPModelBuilder(xmlreader, namespaceURI);
                envelope = (SOAPEnvelope) builder.getDocumentElement();
                envelope.build();
            }
        } catch (Exception ex) {
            // TODO: what to do if can't get the XML stream reader
            // For now, log the event
            log.error(correlationIDString
                      + ":readExternal(): Error when deserializing persisted envelope: ["
                      + ex.getClass().getName() + " : " + ex.getLocalizedMessage() + "]", ex);
            envelope = null;
        } finally {
            if (builder != null) {
                builder.close();
            }
            // Close the message input stream.  This will ensure that the
            // underlying stream is advanced past the message.
            mis.close();
            if (log.isDebugEnabled()) {
                log.debug(correlationIDString + ":readExternal(): end");
            }
        }
        return envelope;
    }
    
    /**
     * MessageOutputStream writes DataBlock chunks to the ObjectOutput.
     */
    private static class MessageOutputStream extends OutputStream {
        ObjectOutput out;
        boolean isDebug;
        MessageOutputStream(ObjectOutput out) {
            this.out = out;
            isDebug = log.isDebugEnabled();
        }
        
         
        public void close() throws IOException {
            // NOOP: ObjectOutput will be closed externally
        }
        
        public void flush() throws IOException {
            out.flush();
        }

        /** 
         * Writes a chunk of data to the ObjectOutput
         */
        public void write(byte[] b, int off, int len) throws IOException {
            if (len > 0) {
                if (isDebug) {
                    log.debug("Write data chunk with len=" + len);
                }
                // Write out the length and the data chunk
                out.writeInt(len);
                out.write(b, off, len);
            }
        }

         
        /** 
         * Writes a chunk of data to the ObjectOutput
         */
        public void write(byte[] b) throws IOException {
            if (b != null &&  b.length > 0) {
                if (isDebug) {
                    log.debug("Write data chunk with size=" + b.length);
                }
                // Write out the length and the data chunk
                out.writeInt(b.length);
                out.write(b);
            }
        }

         
        /** 
         * Writes a single byte chunk of data to the ObjectOutput
         */
        public void write(int b) throws IOException {
            if (isDebug) {
                log.debug("Write one byte data chunk");
            }
            // Write out the length and the data chunk
            out.writeInt(1);
            out.write(b);
        }
    }
       
    /**
     * Provides a InputStream interface over ObjectInput.
     * MessageInputStream controls the reading of the DataBlock chunks
     *
     */
    private static class MessageInputStream extends InputStream {
        
        ObjectInput in;
        boolean isDebug;
        int chunkAvail = 0;
        boolean isEOD = false;
        
        /**
         * Constructor
         * @param in
         */
        MessageInputStream(ObjectInput in) {
            this.in = in;
            isDebug = log.isDebugEnabled();
        }

         
        /**
         * Read a single logical byte
         */
        public int read() throws IOException {
            if (isDebug) {
                log.debug("invoking read()");
            }
            // Determine how many bytes are left in the current data chunk
            updateChunkAvail();
            int ret = 0;
            if (isEOD) {
                ret = -1;
            } else {
                chunkAvail--;
                ret = in.readByte();
            }
            log.debug("returning " + ret);
            return ret;
        }

         
        /**
         * Read an array of logical bytes
         */
        public int read(byte[] b, int off, int len) throws IOException {
            if (isDebug) {
                log.debug("invoking read with off=" + off + " and len=" + len);
            }
            if (isEOD) {
                if (isDebug) {
                    log.debug("EOD returning -1");
                }
                return -1;
            }
            int bytesRead = 0;
            while ((len >0 && !isEOD)) {
                // Determine how many bytes are left in the current data chunk
                updateChunkAvail();
                if (!isEOD) {
                    // Read the amount of bytes requested or the number of bytes available in the current chunk
                    int readLength = len < chunkAvail ? len : chunkAvail;
                    int br = in.read(b, off, readLength);
                    if (br < 0) {
                        throw new IOException("End of File encountered");
                    }
                    // Update state with the number of bytes read
                    off += br;
                    len -= br;
                    chunkAvail -= br;
                    bytesRead += br;
                }
            }
            if (isDebug) {
                log.debug("bytes read = " + bytesRead);
            }
            return bytesRead;
        }

         
        public int read(byte[] b) throws IOException {
            return read(b, 0, b.length);
        }
        
        public void close() throws IOException {
            if (isDebug) {
                log.debug("start close");
            }
            // Keep reading chunks until EOD
            if (!isEOD) {
                byte[] tempBuffer = new byte[4 * 1024];
                while (!isEOD) {
                    read(tempBuffer);
                }
            }
            if (isDebug) {
                log.debug("end close");
            }
        }
        
        /**
         * updateChunkAvail updates the chunkAvail field with the
         * amount of data in the chunk.
         * @throws IOException
         */
        private void updateChunkAvail() throws IOException {
            
            // If there are no more bytes in the current chunk,
            // read the size of the next datablock
            if (chunkAvail == 0 && !isEOD) {
                chunkAvail = in.readInt();
                if (isDebug) {
                    log.debug("New DataBlock with size=" + chunkAvail);
                }
                if (chunkAvail <= 0) {
                    if (isDebug) {
                        log.debug("End of data");
                    }
                    isEOD = true;
                    chunkAvail = 0;
                }
            }
        }
    }
}
