package com.google.sitebricks;


import com.google.inject.matcher.Matcher;
import net.jcip.annotations.Immutable;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;


/**
 * Utility class that finds all the classes in a given package.
 * (based on a similar utility in TestNG)
 * <p/>
 * Created on Feb 24, 2006
 *
 * @author <a href="mailto:cedric@beust.com">Cedric Beust</a>
 * @author Dhanji R. Prasanna (dhanji@gmail.com)
 * @see org.testng.internal.PackageUtils
 */
@Immutable
class Classes {

  private final Matcher<? super Class<?>> matcher;
  private final Logger log = Logger.getLogger(Classes.class.getName());

  private Classes(Matcher<? super Class<?>> matcher) {
    this.matcher = matcher;
  }

  /**
   * @param pack A list of packages to scan, recursively.
   * @return A set of all the classes inside this package
   * @throws PackageScanFailedException Thrown when error reading from disk or jar.
   * @throws IllegalStateException      Thrown when something very odd is
   *                                    happening with classloaders.
   */
  @NotNull
  public Set<Class<?>> in(Package pack) {
    String packageName = pack.getName();
    String packageOnly = pack.getName();

    final boolean recursive = true;

    Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
    String packageDirName = packageOnly.replace('.', '/');

    Enumeration<URL> dirs;
    try {
      dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);

    } catch (IOException e) {
      throw new PackageScanFailedException(
          "Could not read from package directory: " + packageDirName, e);
    }

    while (dirs.hasMoreElements()) {
      URL url = dirs.nextElement();
      String protocol = url.getProtocol();

      if ("file".equals(protocol)) {
        try {
          findClassesInDirPackage(packageOnly,
              URLDecoder.decode(url.getFile(), "UTF-8"),
              recursive,
              classes);
        } catch (UnsupportedEncodingException e) {
          throw new PackageScanFailedException("Could not read from file: " + url, e);
        }
      } else if ("jar".equals(protocol)) {
        JarFile jar;

        try {
          jar = ((JarURLConnection) url.openConnection()).getJarFile();
        } catch (IOException e) {
          throw new PackageScanFailedException("Could not read from jar url: " + url, e);
        }

        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
          JarEntry entry = entries.nextElement();
          String name = entry.getName();
          if (name.charAt(0) == '/') {
            name = name.substring(1);
          }
          if (name.startsWith(packageDirName)) {
            int idx = name.lastIndexOf('/');
            if (idx != -1) {
              packageName = name.substring(0, idx).replace('/', '.');
            }

            if ((idx != -1) || recursive) {
              //it's not inside a deeper dir
              if (name.endsWith(".class") && !entry.isDirectory()) {
                String className = name.substring(packageName.length() + 1, name.length() - 6);
                //include this class in our results

                /* Issue #26 - package-info causes ISE during package scan. This ia a bit of a hack for just
                 * package-info. TODO Determine better handling of unexpected classloader issues.
                 */
                if (!"package-info".equalsIgnoreCase(className)) {
                  add(packageName, classes, className);
                }
//                  vResult.add();
              }
            }
          }
        }
      }
    }

    return classes;
  }

  private void add(String packageName, Set<Class<?>> classes, String className) {

    Class<?> clazz;
    try {
      clazz = Class.forName(packageName + '.' + className);
    } catch (ClassNotFoundException e) {
      log.severe("A class discovered by the scanner could not be found by the ClassLoader, " +
          "something very odd has happened with the classloading (see root cause): " +
          e.toString());

      throw new IllegalStateException(
          "A class discovered by the scanner could not be found by the ClassLoader", e);
    }

    if (matcher.matches(clazz))
      classes.add(clazz);
  }

  private void findClassesInDirPackage(String packageName,
                                       String packagePath,
                                       final boolean recursive,
                                       Set<Class<?>> classes) {
    File dir = new File(packagePath);

    if (!dir.exists() || !dir.isDirectory()) {
      return;
    }

    File[] dirfiles = dir.listFiles(new FileFilter() {
      public boolean accept(File file) {
        return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
      }
    });

    for (File file : dirfiles) {
      if (file.isDirectory()) {
        findClassesInDirPackage(packageName + "." + file.getName(),
            file.getAbsolutePath(),
            recursive,
            classes);
      } else {
        String className = file.getName().substring(0, file.getName().length() - 6);
        //include class
        add(packageName, classes, className);
      }
    }
  }

  public static Classes matching(Matcher<? super Class<?>> matcher) {
    return new Classes(matcher);
  }

}

@@ -230,53 +230,8 @@
 on;%0A
-import java.io.UnsupportedEncodingException;%0A
 impo
@@ -285,36 +285,8 @@
 RL;%0A
-import java.net.URLDecoder;%0A
 impo
@@ -2089,32 +2089,16 @@
 col)) %7B%0A
-        try %7B%0A  
         
@@ -2137,262 +2137,42 @@
 nly,
-%0A              URLDecoder.decode(url.getFile(), %22UTF-8%22),%0A              recursive,%0A              classes);%0A        %7D catch (UnsupportedEncodingException e) %7B%0A          throw new PackageScanFailedException(%22Could not read from file: %22 + url, e);%0A        %7D
+ toPath(url), recursive, classes);
 %0A   
@@ -5429,10 +5429,520 @@
 );%0A  %7D%0A%0A
+  private static String toPath(final URL url) %7B%0A    String path = url.getPath();%0A    StringBuilder buf = new StringBuilder();%0A    for ( int i = 0, length = path.length(); i %3C length; i++ ) %7B%0A      char c = path.charAt( i );%0A      if ( '/' == c ) %7B%0A        buf.append( File.separatorChar );%0A      %7D else if ( '%25' == c && i %3C length - 2 ) %7B%0A        buf.append( (char) Integer.parseInt( path.substring( ++i, ++i + 1 ), 16 ) );%0A      %7D else %7B%0A        buf.append( c );%0A      %7D%0A    %7D%0A    return buf.toString();%0A  %7D%0A%0A
 %7D%0A

Use of URLDecoder messes up Jetty7 webapp paths on OS X http://code.google.com/p/google-sitebricks/issues/detail?id=68 Submitted By: Stuart McCulloch