package org.limewire.util; 

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Converts older package and class names to the new equivalent name and is useful 
 * in code refactoring. For example, if packages or classes are renamed, 
 * <code>ObjectInputStream</code> fails to find 
 * the older name. <code>ConverterObjectInputStream</code> looks up the old name 
 * and if a new name exists, <code>ConverterObjectInputStream</code> internally
 * updates any references to the new name without the old name needing to be in the
 * classpath (and as long as serialVersionUID is the same; see {@link Serializable}
 * for more information).
 * <p>
 * <code>ConverterObjectInputStream</code> comes with a pre-set package and class 
 * mapping between the old and new name, and you can add more lookups with 
 * {@link #addLookup(String, String)}.
 * <p>
 * Pre-set package and class mapping:
 * <table cellpadding="5">
 * <tr>
 * <td><b>Old Name</b></td>
 * <td><b>New Name</b></td>
 * </tr><tr>
 * <td>com.limegroup.gnutella.util.FileComparator</td>
 * <td>org.limewire.collection.FileComparator</td>
 * </tr>
 * <tr>
 * <td>com.limegroup.gnutella.downloader.Interval</td>
 * <td>org.limewire.collection.Interval</td>
 * </tr>
 * <tr>
 * <td>com.limegroup.gnutella.util.IntervalSet</td>
 * <td> org.limewire.collection.IntervalSet</td>
 * </tr>
 * <tr>
 * <td>com.limegroup.gnutella.util.Comparators$CaseInsensitiveStringComparator</td>
 * <td> org.limewire.collection.Comparators$CaseInsensitiveStringComparator</td>
 * </tr><tr>
 * <td>com.limegroup.gnutella.util.StringComparator</td>
 * <td> org.limewire.collection.StringComparator</td>
 * </tr>
 * <tr>
 * <td>com.sun.java.util.collections</td>
 * <td>java.util</td>
 * </tr>
 * </table>
 * None of the earlier forms of the class need to exist in the classpath.
 */

public class ConverterObjectInputStream extends ObjectInputStream { 
    
    private static final Log LOG = LogFactory.getLog(ConverterObjectInputStream.class);
    
    private Map<String, String> lookups = new HashMap<String, String>(8);

    /**
     * Constructs a new <code>ConverterObjectInputStream</code> wrapping the 
     * specified <code>InputStream</code>.
     */     
    public ConverterObjectInputStream(InputStream in) throws IOException { 
        super(in); 
        createLookups();
    } 
    
    /**
     * Erases any lookups that were added using {@link #addLookup(String, String)}.
     */
    public void revertToDefault() {
        lookups.clear();
        createLookups();
    }
    
    /** Adds all internal lookups. */
    private void createLookups() {
        lookups.put("com.limegroup.gnutella.util.FileComparator", "org.limewire.collection.FileComparator");
        lookups.put("com.limegroup.gnutella.util.Comparators$CaseInsensitiveStringComparator", "org.limewire.collection.Comparators$CaseInsensitiveStringComparator");
        lookups.put("com.limegroup.gnutella.util.StringComparator", "org.limewire.collection.StringComparator");
        lookups.put("com.sun.java.util.collections", "java.util");
        lookups.put("com.limegroup.gnutella.MediaType", "org.limewire.util.MediaType");
    }

    /**
     * Adds a mapping between an old package or class name to a new name.
     * @param oldName the name of the old package or class
     * @param newName the name of the new package or class
     */
    public void addLookup(String oldName, String newName) {
        lookups.put(oldName, newName);
    }
     
    /** 
     * Overridden to manually alter the class descriptors. 
     * Note this does NOT require the original class to be loadable.
     * <p>
     * Lookup works as follows:
     * <ul>
     * <li>The serialized (old) class name is looked up, if a corresponding new
     * class name exists the <code>ObjectStreamClass</code> object for it is returned.</li>
     * <li>The package name of the serialized class name is extracted and
     * looked up if a new package name exists, it is prepended to the name of
     * the class the corresponding class is loaded.</li>
     * <li>Otherwise the original ObjectStreamClass is returned.</li> 
     * <ul>
     */ 
    @Override
    protected ObjectStreamClass readClassDescriptor() throws 
      IOException, ClassNotFoundException { 
        ObjectStreamClass read = super.readClassDescriptor(); 
        String className = read.getName();        

        if(LOG.isDebugEnabled())
            LOG.debug("Looking up class: " + className);
        
        boolean array = className.startsWith("[L") && className.endsWith(";");
        if(array) {
            className = className.substring(2, className.length()-1);
            if(LOG.isDebugEnabled())
                LOG.debug("Stripping array form off, resulting in: " + className);
        }
        
        
        ObjectStreamClass clazzToReturn = null; 
        String newName = lookups.get(className);
        if (newName != null) {
            clazzToReturn = ObjectStreamClass.lookup(Class.forName(newName));
        } else if(className.contains("$")) {
            // If it's an inner class, replace the parent.
            int index = className.indexOf("$");
            String newParent = lookups.get(className.substring(0, index));
            if(newParent != null) {
                clazzToReturn = ObjectStreamClass.lookup(Class.forName(newParent + className.substring(index)));
            }
        }
        
        if(clazzToReturn == null) {
            int index = className.lastIndexOf('.');
            // use "" as lookup key for default package
            String oldPackage = index != -1 ? className.substring(0, index) : "";
            String newPackage = lookups.get(oldPackage);
            if (newPackage != null) {
                if (newPackage.length() == 0) {
                    // mapped to default package
                    clazzToReturn = ObjectStreamClass.lookup(Class.forName(className.substring(index + 1)));
                } else {
                    clazzToReturn = ObjectStreamClass.lookup(Class.forName(newPackage + '.' + className.substring(index + 1)));
                }
            } else {
                // Nothing it maps to -- must be the real name!
                clazzToReturn = read;
            }
        }
        
        if(LOG.isDebugEnabled() && clazzToReturn != read)
            LOG.debug("Located substitute class: " + clazzToReturn.getName());
        
        // If it's an array, and we modified we we read off disk, convert
        // to array form.
        if(array && read != clazzToReturn) {
            clazzToReturn = ObjectStreamClass.lookup(Class.forName("[L" + clazzToReturn.getName() + ";"));
            if(LOG.isDebugEnabled())
                LOG.debug("Re-added array wrapper, for class: " + clazzToReturn.getName());
        }
        
        return clazzToReturn;
    } 
}
