/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.tools;

import java.beans.XMLEncoder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.TreeMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.net.URLClassLoader;
import java.net.MalformedURLException;
import java.net.URL;

/** A tool that builds a databaase of serialVersionUIDs for a given set
 *  of classes.
 * 
 * @author Scott.Stark@jboss.org
 */
public class SerialVersionUID
{
   static Logger log = Logger.getLogger("SerialVersionUID");

   static LinkedHashSet libdirList = new LinkedHashSet();

   static boolean outputXML = false;

   static void parsePropertyList(String listValue, Set dst)
   {
      String elements[] = listValue.split(",");

      for (int i = 0; i < elements.length; i++)
      {
         dst.add(elements[i].trim());
      }
   }

   public static void processProperties()
   {
      Properties fallbackProps = new Properties();
      // A list of directories to scan for .jar files.
      fallbackProps.setProperty("libdir.list", "lib");
      fallbackProps.setProperty("output.xml", "true");

      Properties props = new Properties(fallbackProps);
      String propName, propValue;
      for (Enumeration e = fallbackProps.propertyNames(); e.hasMoreElements();)
      {
         propName = (String) e.nextElement();
         propValue = System.getProperty(propName);
         if (propValue != null)
         {
            props.setProperty(propName, propValue);
         }
      }

      String tmpStr;
      tmpStr = props.getProperty("libdir.list");
      if (tmpStr.length() == 0)
      {
         tmpStr = fallbackProps.getProperty("libdir.list");
      }
      parsePropertyList(tmpStr, libdirList);

      tmpStr = props.getProperty("output.xml");
      if (tmpStr.length() == 0)
      {
         tmpStr = fallbackProps.getProperty("output.xml");
      }
      tmpStr.trim();
      if (tmpStr == "yes" || tmpStr == "true")
      {
         outputXML = true;
      }
   }

   static void buildJarSet(File dir, LinkedHashSet jarFiles)
      throws MalformedURLException
   {
      File[] files = dir.listFiles();
      for(int n = 0; n < files.length; n ++)
      {
         File child = files[n];
         if(child.isDirectory())
            buildJarSet(child, jarFiles);
         else if( child.getName().endsWith(".jar") )
            jarFiles.add(child.toURL());
      }
   }

   /**
    * Build a TreeMap of the class name to ClassVersionInfo
    * @param jar
    * @param classVersionMap TreeMap<String, ClassVersionInfo> for serializable
    *    classes
    * @param cl - the class loader to use
    * @throws IOException thrown if the jar cannot be opened
    */ 
   static void generateJarSerialVersionUIDs(URL jar, TreeMap classVersionMap,
      ClassLoader cl, String pkgPrefix) throws IOException
   {
      String jarName = jar.getFile();
      JarFile jf = new JarFile(jarName);
      Enumeration entries = jf.entries();
      while( entries.hasMoreElements() )
      {
         JarEntry entry = (JarEntry) entries.nextElement();
         String name = entry.getName();
         if( name.endsWith(".class") && name.startsWith(pkgPrefix) )
         {
            name = name.substring(0, name.length() - 6);
            String classname = name.replace('/', '.');
            try
            {
               log.fine("Creating ClassVersionInfo for: "+classname);
               ClassVersionInfo cvi = new ClassVersionInfo(classname, cl);
               if( cvi.getSerialVersion() != 0 )
               {
                  ClassVersionInfo prevCVI = (ClassVersionInfo)
                     classVersionMap.put(classname, cvi);
                  if( prevCVI != null )
                  {
                     if( prevCVI.getSerialVersion() != cvi.getSerialVersion() )
                     {
                        log.severe("Found inconsistent classes, "
                           +prevCVI+" != "+cvi+", jar: "+jarName);
                     }
                  }
                  if( cvi.getHasExplicitSerialVersionUID() == false )
                  {
                     log.warning("No explicit serialVersionUID: "+cvi);
                  }
               }
            }
            catch(OutOfMemoryError e)
            {
               log.log(Level.SEVERE, "Check the MaxPermSize", e);
            }
            catch(Throwable e)
            {
               log.log(Level.FINE, "While loading: "+name, e);
            }
         }
      }
      jf.close();
   }

   /**
    * Create a Map<String, ClassVersionInfo> for the dist jars.
    * 
    * @return Map<String, ClassVersionInfo>
    * @throws IOException
 .   */ 
   public static Map processJars() throws IOException
   {
      LinkedHashSet jarFiles = new LinkedHashSet();

      File dir;
      Iterator libdirIterator = libdirList.iterator();
      while (libdirIterator.hasNext())
      {
         dir = new File((String) libdirIterator.next());
         buildJarSet(dir, jarFiles);
      }

      URL[] cp = new URL[jarFiles.size()];
      jarFiles.toArray(cp);
      ClassLoader parent = Thread.currentThread().getContextClassLoader();
      URLClassLoader completeClasspath = new URLClassLoader(cp, parent);

      TreeMap classVersionMap = new TreeMap();
      Iterator jarIter = jarFiles.iterator();
      while( jarIter.hasNext() )
      {
         URL jar = (URL) jarIter.next();
         try
         {
            generateJarSerialVersionUIDs(jar, classVersionMap, completeClasspath, "");
         }
         catch(IOException e)
         {
            log.info("Failed to process jar: "+jar);
         }
      }

      return classVersionMap;
   }

   /**
    * Generate a mapping of the serial version UIDs for the serializable classes
    * under the dist directory (as specified by the dist.dir property).
    * @throws Exception
    */ 
   public static void main(String[] args) throws Exception
   {
      processProperties();

      Map classVersionMap = processJars();
      log.info(classVersionMap.size() + " serialVersionUIDs stored into the database");

      FileOutputStream fos = new FileOutputStream("serialuid.ser");
      if (outputXML == false)
      {
         // Write the map out the object file
         ObjectOutputStream oos = new ObjectOutputStream(fos);
         oos.writeObject(classVersionMap);
         oos.close();
      }
      else
      {
         XMLEncoder xml = new XMLEncoder(fos);
         Iterator mapIterator = classVersionMap.values().iterator();
         while (mapIterator.hasNext())
         {
            xml.writeObject(mapIterator.next());
         }
         xml.close();
      }
   }
}
