/*
 * 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.axis2.AxisFault;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectStreamConstants;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * A SafeObjectInputStream reads data that was written by SafeObjectOutputStream
 * 
 * @see SafeObjectInput
 */
public class SafeObjectInputStream implements ObjectInput, ObjectStreamConstants {

    private static final Log log = LogFactory.getLog(SafeObjectInputStream.class);
    private static final boolean isDebug = log.isDebugEnabled();
    
    // All data is read from the delegated ObjectInput
    ObjectInput in = null;
    final ObjectInput original;
    
    // A buffer is used when Object is persisted as bytes
    private byte[] buffer = null;
    private static final int BUFFER_MIN_SIZE = 4096;
    
    /**
     * Add the SafeObjectInputStream if necessary
     * @param in
     * @return
     */
    public static SafeObjectInputStream install(ObjectInput in) {
        if (in instanceof SafeObjectInputStream) {
            return (SafeObjectInputStream) in;
        }
        return new SafeObjectInputStream(in);
    }
    
    
    /**
     * Intentionally private.  Callers should use the install method to add the SafeObjectInputStream
     * into the stream.
     * @param in
     */
    private SafeObjectInputStream(ObjectInput in) {
        original = in;
        if (log.isDebugEnabled()) {
            this.in = new DebugObjectInput(original);
        } else {
            this.in = original;
        }
    }
    
    //  Delegated Methods
    public int available() throws IOException {
        return in.available();
    }
    public void close() throws IOException {
        in.close();
    }
    public int read() throws IOException {
        return in.read();
    }
    public int read(byte[] b, int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    public int read(byte[] b) throws IOException {
        return in.read(b);
    }
    public boolean readBoolean() throws IOException {
        return in.readBoolean();
    }
    public byte readByte() throws IOException {
        return in.readByte();
    }
    public char readChar() throws IOException {
        return in.readChar();
    }
    public double readDouble() throws IOException {
        return in.readDouble();
    }
    public float readFloat() throws IOException {
        return in.readFloat();
    }
    public void readFully(byte[] b, int off, int len) throws IOException {
        in.readFully(b, off, len);
    }
    public void readFully(byte[] b) throws IOException {
        in.readFully(b);
    }
    public int readInt() throws IOException {
        return in.readInt();
    }
    public String readLine() throws IOException {
        return in.readLine();
    }
    public long readLong() throws IOException {
        return in.readLong();
    }
    public Object readObject() throws ClassNotFoundException, IOException {
        return readObjectOverride();
    }
    public short readShort() throws IOException {
        return in.readShort();
    }
    public int readUnsignedByte() throws IOException {
        return in.readUnsignedByte();
    }
    public int readUnsignedShort() throws IOException {
        return in.readUnsignedShort();
    }
    public String readUTF() throws IOException {
        return in.readUTF();
    }
    public long skip(long n) throws IOException {
        return in.skip(n);
    }
    public int skipBytes(int n) throws IOException {
        return in.skipBytes(n);
    }
    
    // Value Add Methods
    
    /**
     * Read the input stream and place the key/value pairs in a hashmap
     * @return HashMap or null
     * @throws IOException
     * @see SafeObjectOutputStream.writeMap()
     */
    public HashMap readHashMap() throws IOException  {
        HashMap hashMap = new HashMap();
        return (HashMap) readMap(hashMap);
    }
    
    /**
     * Read the input stream and place the key/value pairs in the
     * indicated Map
     * @param map input map
     * @return map or null
     * @throws IOException
     * @see SafeObjectOutputStream.writeMap()
     */
    public Map readMap(Map map) throws IOException {
        boolean isActive = in.readBoolean();
        if (!isActive) {
            return null;
        }
        
        while (in.readBoolean()) {
            Object key = null;
            Object value = null;

            boolean isObjectForm = in.readBoolean();
            try {
                if (isObjectForm) {
                    if (isDebug) {
                        log.debug(" reading using object form");
                    }
                    // Read the key and value directly
                    key = in.readObject();
                    value = in.readObject();
                } else {
                    if (isDebug) {
                        log.debug(" reading using byte form");
                    }
                    // Get the byte stream
                    ByteArrayInputStream bais = getByteStream(in);

                    // Now get the real key and value
                    ObjectInputStream tempOIS = createObjectInputStream(bais);
                    key = tempOIS.readObject();
                    value = tempOIS.readObject();
                    tempOIS.close();
                    bais.close();
                }
                // Put the key and value in the map
                if (isDebug) {
                    log.debug("Read key=" + valueName(key) + " value="+valueName(value));
                }
                map.put(key, value);
            } catch (ClassNotFoundException e) {
                // Swallow the error and try to continue
                log.error(e);
            } catch (IOException e) {
                throw e;
            } catch (Exception e) {
                throw AxisFault.makeFault(e);
            }
        }
        return map;
    }
    
    /**
     * Read the input stream and place objects in an ArrayList
     * @return ArrayList or null
     * @throws IOException
     * @see SafeObjectInputStream.writeList()
     */
    public ArrayList readArrayList() throws IOException {
        List ll = new ArrayList();
        return (ArrayList) readList(ll);
    }
    
    /**
     * Read the input stream and place objects in a LinkedList
     * @return LinkedList or null
     * @throws IOException
     * @see SafeObjectInputStream.writeList()
     */
    public LinkedList readLinkedList() throws IOException {
       List ll = new LinkedList();
       return (LinkedList) readList(ll);
    }
    
    /**
     * Read hte input stream and place objects in the specified List
     * @param list List
     * @return List or null
     * @throws IOException
     * @see SafeObjectInputStream.writeList()
     */
    public List readList(List list) throws IOException {
        boolean isActive = in.readBoolean();
        if (!isActive) {
            return null;
        }
        
        while (in.readBoolean()) {
            Object value;
            boolean isObjectForm = in.readBoolean();
            try {
                if (isObjectForm) {
                    if (isDebug) {
                        log.debug(" reading using object form");
                    }
                    // Read the value directly
                    value = in.readObject();
                } else {
                    if (isDebug) {
                        log.debug(" reading using byte form");
                    }
                    // Get the byte stream
                    ByteArrayInputStream bais = getByteStream(in);

                    // Now get the real key and value
                    ObjectInputStream tempOIS = createObjectInputStream(bais);
                    value = tempOIS.readObject();
                    tempOIS.close();
                    bais.close();
                }
                // Put the key and value in the list
                if (isDebug) {
                    log.debug("Read value="+valueName(value));
                }
                list.add(value);
            } catch (IOException e) {
                throw e;
            } catch (Exception e) {
                throw AxisFault.makeFault(e);
            }
        }
        return list;
    }
    
    /**
     * Reads the object using the same format that was written.
     * 
     * EXPECTED FORMATS
     *   boolean=false
     *   return null Object
     *   
     *   
     *   boolean=true
     *   boolean=true
     *   return Object read from inputStream
     *   
     *   
     *   boolean=true
     *   boolean=false
     *   int=nuber of bytes
     *   bytes
     *   return Object read from bytes
     *   
     * @return Object or null
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private Object readObjectOverride() throws IOException, ClassNotFoundException {
        boolean isActive = in.readBoolean();
        if (!isActive) {
            if (isDebug) {
                log.debug("Read object=null");
            }
            return null;
        }
        Object obj = null;
        boolean isObjectForm = in.readBoolean();
        if (isObjectForm) {
            // Read the object directly
            if (isDebug) {
                log.debug(" reading using object form");
            }
            obj = in.readObject();
        } else {
            if (isDebug) {
                log.debug(" reading using byte form");
            }
            // Get the byte stream
            ByteArrayInputStream bais = getByteStream(in);

            // Now get the real object
            ObjectInputStream tempOIS = createObjectInputStream(bais);
            obj = tempOIS.readObject();
            tempOIS.close();
            bais.close();
        }
        
        if (isDebug) {
            log.debug("Read object=" + valueName(obj));
        }
        return obj;   
        
    }
    
    private String valueName(Object obj) {
        if (obj == null) {
            return "null";
        } else if (obj instanceof String) {
            return (String) obj;
        } else {
            return "Object of class = " + obj.getClass().getName();
        }
    }
    
    /**
     * Get the byte stream for an object that is written using the 
     * byte format.  
     * EXPECTED format
     *     int (number of bytes)
     *     bytes 
     *     
     * @param in
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private ByteArrayInputStream getByteStream(ObjectInput in) throws IOException, ClassNotFoundException {
        
        // Read the length
        int size = in.readInt();
        
        // Fill our temporary buffer
        if (buffer == null || buffer.length < size) {
            int allocSize = (size > BUFFER_MIN_SIZE) ? size : BUFFER_MIN_SIZE;
            buffer = new byte[allocSize];
        }
        in.readFully(buffer, 0, size);
        
        // Return a stream backed by the buffer
        return new ByteArrayInputStream(buffer, 0, size);
    }
    
    private ObjectInputStream createObjectInputStream(InputStream is) throws IOException {
        // The created ObjectInputStream must use the same class/object resolution 
        // code that is used by the original ObjectInput
        return new ObjectInputStreamWithCL(is);
    }
}
