/*
 * JBoss, Home of Professional Open Source
 * Copyright 2008, 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.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.util.license;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.SyncFailedException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A utility which scans all java source files in the cvs tree and validates
 * the license header prior to the package statement for headers that match
 * those declared in thirdparty/licenses/license-info.xml
 *
 * @author Scott.Stark@jboss.org
 * @version $Revision: 7168 $
 */
public class ValidateLicenseHeaders
{
   /**
    * Used to strip out diffs due to copyright date ranges
    */
   static final String COPYRIGHT_REGEX = "copyright\\s(\\(c\\))*\\s*[\\d]+(\\s*,\\s*[\\d]+|\\s*-\\s*[\\d]+)*";

   static final String DEFAULT_HEADER = "/*\n" +
         " * JBoss, Home of Professional Open Source.\n" +
         " * Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors\n" +
         " * as indicated by the @author tags. See the copyright.txt file in the\n" +
         " * distribution for a full listing of individual contributors.\n" +
         " *\n" +
         " * This is free software; you can redistribute it and/or modify it\n" +
         " * under the terms of the GNU Lesser General Public License as\n" +
         " * published by the Free Software Foundation; either version 2.1 of\n" +
         " * the License, or (at your option) any later version.\n" +
         " *\n" +
         " * This software is distributed in the hope that it will be useful,\n" +
         " * but WITHOUT ANY WARRANTY; without even the implied warranty of\n" +
         " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" +
         " * Lesser General Public License for more details.\n" +
         " *\n" +
         " * You should have received a copy of the GNU Lesser General Public\n" +
         " * License along with this software; if not, write to the Free\n" +
         " * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n" +
         " * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n" +
         " */\n";
   static Logger log = Logger.getLogger("ValidateCopyrightHeaders");
   static boolean addDefaultHeader = false;
   static FileFilter dotJavaFilter = new DotJavaFilter();
   /**
    * The term-headers from the thirdparty/license/license-info.xml
    */
   static TreeMap licenseHeaders = new TreeMap();
   /**
    * Java source files with no license header
    */
   static ArrayList noheaders = new ArrayList();
   /**
    * Java source files with a header that does not match one from licenseHeaders
    */
   static ArrayList invalidheaders = new ArrayList();
   /**
    * Total java source files seen
    */
   static int totalCount;
   /**
    * Total out of date jboss headers seen
    */
   static int jbossCount;

   /**
    * ValidateLicenseHeaders jboss-src-root
    *
    * @param args
    */
   public static void main(String[] args)
         throws Exception
   {
      if (args.length == 0 || args[0].startsWith("-h"))
      {
         log.info("Usage: ValidateLicenseHeaders [-addheader] src-root");
         System.exit(1);
      }
      int rootArg = 0;
      if (args.length == 2)
      {
         if (args[0].startsWith("-add"))
            addDefaultHeader = true;
         else
         {
            log.severe("Uknown argument: " + args[0]);
            log.info("Usage: ValidateLicenseHeaders [-addheader] src-root");
            System.exit(1);

         }
         rootArg = 1;
      }

      File jbossSrcRoot = new File(args[rootArg]);
      if (jbossSrcRoot.exists() == false)
      {
         log.info("Src root does not exist, check " + jbossSrcRoot.getAbsolutePath());
         System.exit(1);
      }

//      URL u = Thread.currentThread().getContextClassLoader().getResource("META-INF/services/javax.xml.parsers.DocumentBuilderFactory");
//      System.err.println(u);
//
//      // Load the valid copyright statements for the licenses
//      File licenseInfo = new File(jbossSrcRoot, "thirdparty/licenses/license-info.xml");
//      if( licenseInfo.exists() == false )
//      {
//         log.severe("Failed to find the thirdparty/licenses/license-info.xml under the src root");
//         System.exit(1);
//      }
//      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//      DocumentBuilder db = factory.newDocumentBuilder();
//      Document doc = db.parse(licenseInfo);
//      NodeList licenses = doc.getElementsByTagName("license");
//      for(int i = 0; i < licenses.getLength(); i ++)
//      {
//         Element license = (Element) licenses.item(i);
//         String key = license.getAttribute("id");
//         ArrayList headers = new ArrayList();
//         licenseHeaders.put(key, headers);
//         NodeList copyrights = license.getElementsByTagName("terms-header");
//         for(int j = 0; j < copyrights.getLength(); j ++)
//         {
//            Element copyright = (Element) copyrights.item(j);
//            copyright.normalize();
//            String id = copyright.getAttribute("id");
//            // The id will be blank if there is no id attribute
//            if( id.length() == 0 )
//               continue;
//            String text = getElementContent(copyright);
//            if( text == null )
//               continue;
//            // Replace all duplicate whitespace and '*' with a single space
//            text = text.replaceAll("[\\s*]+", " ");
//            if( text.length() == 1)
//               continue;
//
//            text = text.toLowerCase().trim();
//            // Replace any copyright date0-date1,date2 with copyright ...
//            text = text.replaceAll(COPYRIGHT_REGEX, "...");
//            LicenseHeader lh = new LicenseHeader(id, text);
//            headers.add(lh);
//         }
//      }
      log.fine(licenseHeaders.toString());

      File[] files = jbossSrcRoot.listFiles(dotJavaFilter);
      log.info("Root files count: " + files.length);
      processSourceFiles(files, 0);

      log.info("Processed " + totalCount);
      log.info("Updated jboss headers: " + jbossCount);
      // Files with no headers details
      log.info("Files with no headers: " + noheaders.size());
      FileWriter fw = new FileWriter("NoHeaders.txt");
      for (Iterator iter = noheaders.iterator(); iter.hasNext();)
      {
         File f = (File) iter.next();
         fw.write(f.getAbsolutePath());
         fw.write('\n');
      }
      fw.close();

      // Files with unknown headers details
      log.info("Files with invalid headers: " + invalidheaders.size());
      fw = new FileWriter("InvalidHeaders.txt");
      for (Iterator iter = invalidheaders.iterator(); iter.hasNext();)
      {
         File f = (File) iter.next();
         fw.write(f.getAbsolutePath());
         fw.write('\n');
      }
      fw.close();

      // License usage summary
      log.info("Creating HeadersSummary.txt");
      fw = new FileWriter("HeadersSummary.txt");
      for (Iterator iter = licenseHeaders.entrySet().iterator(); iter.hasNext();)
      {
         Map.Entry entry = (Map.Entry) iter.next();
         String key = (String) entry.getKey();
         fw.write("+++ License type=" + key);
         fw.write('\n');
         List list = (List) entry.getValue();
         Iterator jiter = list.iterator();
         while (jiter.hasNext())
         {
            LicenseHeader lh = (LicenseHeader) jiter.next();
            fw.write('\t');
            fw.write(lh.id);
            fw.write(", count=");
            fw.write("" + lh.count);
            fw.write('\n');
         }
      }
      fw.close();
   }

   /**
    * Get all non-comment content from the element.
    *
    * @param element
    * @return the concatenated text/cdata content
    */
   public static String getElementContent(Element element)
   {
      if (element == null)
         return null;

      NodeList children = element.getChildNodes();
      StringBuffer result = new StringBuffer();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node child = children.item(i);
         if (child.getNodeType() == Node.TEXT_NODE ||
               child.getNodeType() == Node.CDATA_SECTION_NODE)
         {
            result.append(child.getNodeValue());
         }
         else if (child.getNodeType() == Node.COMMENT_NODE)
         {
            // Ignore comment nodes
         }
         else
         {
            result.append(child.getFirstChild());
         }
      }
      return result.toString().trim();
   }

   /**
    * Validate the headers of all java source files
    *
    * @param files
    * @param level
    * @throws IOException
    */
   static void processSourceFiles(File[] files, int level)
         throws IOException
   {
      for (File f : files)
      {
         if (level == 0)
            log.info("processing " + f);
         if (f.isDirectory())
         {
            File[] children = f.listFiles(dotJavaFilter);
            processSourceFiles(children, level + 1);
         }
         else
         {
            parseHeader(f);
         }
      }
   }

   /**
    * Read the first comment upto the package ...; statement
    *
    * @param javaFile
    */
   static void parseHeader(File javaFile)
         throws IOException
   {
      totalCount++;
      RandomAccessFile raf = new RandomAccessFile(javaFile, "rw");
      String line = raf.readLine();
      StringBuffer tmp = new StringBuffer();
      long endOfHeader = 0;
      boolean packageOrImport = false;
      while (line != null)
      {
         long nextEOH = raf.getFilePointer();
         line = line.trim();
         // Ignore any single line comments
         if (line.startsWith("//"))
         {
            line = raf.readLine();
            continue;
         }

         // If this is a package/import/class/interface statement break
         if (line.startsWith("package") || line.startsWith("import")
               || line.indexOf("class") >= 0 || line.indexOf("interface") >= 0)
         {
            packageOrImport = true;
            break;
         }

         // Update the current end of header marker
         endOfHeader = nextEOH;

         if (line.startsWith("/**"))
            tmp.append(line.substring(3));
         else if (line.startsWith("/*"))
            tmp.append(line.substring(2));
         else if (line.startsWith("*"))
            tmp.append(line.substring(1));
         else
            tmp.append(line);
         tmp.append(' ');
         line = raf.readLine();
      }
      raf.close();

      if (tmp.length() == 0 || !packageOrImport)
      {
         addDefaultHeader(javaFile);
         return;
      }


      String text = tmp.toString();
      // Replace all duplicate whitespace with a single space
      text = text.replaceAll("[\\s*]+", " ");
      text = text.toLowerCase().trim();
      // Replace any copyright date0-date1,date2 with copyright ...
      text = text.replaceAll(COPYRIGHT_REGEX, "...");
      if (tmp.length() == 0)
      {
         System.out.println("adding default header for " + javaFile);
         addDefaultHeader(javaFile);
         return;
      }
      else
      {


         if (javaFile.toString().contains("BaseEvictionAlgorithm"))
         {
            System.out.println("Replacing header for " + javaFile);
            System.out.println("Existing header: " + tmp);
            System.out.println("Existing header: " + text);
            System.out.println("EOH: " + endOfHeader);
         }

         replaceHeader(javaFile, endOfHeader);
         return;
      }
      // Search for a matching header
//      boolean matches = false;
//      String matchID = null;
//      Iterator iter = licenseHeaders.entrySet().iterator();
//      escape:
//      while( iter.hasNext() )
//      {
//         Map.Entry entry = (Map.Entry) iter.next();
//         String key = (String) entry.getKey();
//         System.out.println("Considering key " + key);
//         List list = (List) entry.getValue();
//         Iterator jiter = list.iterator();
//         while( jiter.hasNext() )
//         {
//            LicenseHeader lh = (LicenseHeader) jiter.next();
//            System.out.println("Considering hdr " + lh);
//            if( text.startsWith(lh.text) )
//            {
//               matches = true;
//               matchID = lh.id;
//               lh.count ++;
//               lh.usage.add(javaFile);
//               if( log.isLoggable(Level.FINE) )
//                  log.fine(javaFile+" matches copyright key="+key+", id="+lh.id);
//               break escape;
//            }
//         }
//      }
//      text = null;
//      tmp.setLength(0);
//      if(!matches)
//         invalidheaders.add(javaFile);
//      else if( matchID.startsWith("jboss") && !matchID.endsWith("#0"))
//      {
//         // This is a legacy jboss head that needs to be updated to the default
//         replaceHeader(javaFile, endOfHeader);
//         jbossCount ++;
//      }
   }

   /**
    * Replace a legacy jboss header with the current default header
    *
    * @param javaFile    - the java source file
    * @param endOfHeader - the offset to the end of the legacy header
    * @throws IOException - thrown on failure to replace the header
    */
   static void replaceHeader(File javaFile, long endOfHeader)
         throws IOException
   {
      if (log.isLoggable(Level.FINE))
         log.fine("Replacing legacy jboss header in: " + javaFile);
      RandomAccessFile raf = new RandomAccessFile(javaFile, "rw");
      File bakFile = new File(javaFile.getAbsolutePath() + ".bak");
      FileOutputStream fos = new FileOutputStream(bakFile);
      fos.write(DEFAULT_HEADER.getBytes());
      FileChannel fc = raf.getChannel();
      long count = raf.length() - endOfHeader;
      fc.transferTo(endOfHeader, count, fos.getChannel());
      fc.close();
      fos.close();
      raf.close();
      if (!javaFile.delete())
         log.severe("Failed to delete java file: " + javaFile);
      if (!bakFile.renameTo(javaFile))
         throw new SyncFailedException("Failed to replace: " + javaFile);
   }

   /**
    * Add the default jboss lgpl header
    */
   static void addDefaultHeader(File javaFile)
         throws IOException
   {
      if (addDefaultHeader)
      {
         FileInputStream fis = new FileInputStream(javaFile);
         FileChannel fc = fis.getChannel();
         int size = (int) fc.size();
         ByteBuffer contents = ByteBuffer.allocate(size);
         fc.read(contents);
         fis.close();

         ByteBuffer hdr = ByteBuffer.wrap(DEFAULT_HEADER.getBytes());
         FileOutputStream fos = new FileOutputStream(javaFile);
         fos.write(hdr.array());
         fos.write(contents.array());
         fos.close();
      }

      noheaders.add(javaFile);
   }

   /**
    * A class that encapsulates the license id and valid terms header
    */
   static class LicenseHeader
   {
      String id;
      String text;
      int count;
      ArrayList usage = new ArrayList();

      LicenseHeader(String id, String text)
      {
         this.id = id;
         this.text = text;
      }
   }

   /**
    * A filter which accepts files ending in .java (but not _Stub.java), or
    * directories other than gen-src and gen-parsers
    */
   static class DotJavaFilter implements FileFilter
   {
      public boolean accept(File pathname)
      {
         boolean accept = false;
         String name = pathname.getName();
         if (pathname.isDirectory())
         {
            // Ignore the gen-src directories for generated output
            accept = !name.equals("gen-src")
                  && !name.equals("gen-parsers")
                  && !name.equals(".svn");
         }
         else
         {
            accept = !name.endsWith("_Stub.java") && name.endsWith(".java");
         }

         return accept;
      }
   }
}
