CLOVER🍀

That was when it all began.

Scalaで特定のパッケージ配下のクラスを検索する

先の日記は、同様の趣旨のプログラムをJavaで書いたのですが、今度はScalaで書いてみました。

パッケージの取得と配下のクラスファイルの走査方法自体は前回で書いているので、今回は割愛。とりあえず、結果のソースを載せてみます。

なお、動作的にScala版がJava版と異なるところは、サポートしていないプロトコルやファイルフォーマットからクラスがロードされていた場合、例外ではなくNilを返却するところですかね。

import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer

import java.io.File
import java.net.{JarURLConnection, URL}
import java.util.jar.{JarEntry, JarFile}

object ClassFinder {
  def main(args: Array[String]): Unit =
    new ClassFinder().findClasses(args(0)).foreach(println)
}

class ClassFinder(private val classLoader: ClassLoader) {
  def this() = this(Thread.currentThread.getContextClassLoader)

  private def pathToClassName(path: String): String = path.substring(0, path.size - ".class".size)

  private def isClassFile(entry: JarEntry): Boolean = isClassFile(entry.getName)
  private def isClassFile(file: File): Boolean = file.isFile && isClassFile(file.getName)
  private def isClassFile(filePath: String): Boolean = filePath.endsWith(".class")

  private def resourceNameToClassName(resourceName: String): String =
    pathToClassName(resourceNameToPackageName(resourceName))
  private def resourceNameToPackageName(resourceName: String): String =
    resourceName.replace('/', '.')
  private def packageNameToResourceName(packageName: String): String =
    packageName.replace('.', '/')

  private val finderFunction: PartialFunction[URL, String => List[Class[_]]] =
    findClassesWithFile orElse
    findClassesWithJarFile orElse
    findClassesWithNone

  def findClasses(rootPackageName: String): List[Class[_]] = {
    val resourceName = packageNameToResourceName(rootPackageName)
    classLoader.getResource(resourceName) match {
      case null => Nil
      case url => finderFunction(url)(rootPackageName)
    }
  }

  def findClassesWithFile: PartialFunction[URL, String => List[Class[_]]] = {
    case url if url.getProtocol == "file" =>
      val classes = new ListBuffer[Class[_]]

      def findClassesWithFileInner(packageName: String, dir: File): List[Class[_]] = {
        dir.list.foreach { path =>
          new File(dir, path) match {
            case file if isClassFile(file) =>
              classes += classLoader.loadClass(packageName + "." + pathToClassName(file.getName))
            case directory if directory.isDirectory =>
              findClassesWithFileInner(packageName + "." + directory.getName, directory)
            case _ =>
          }
        }

        classes toList
      }

      findClassesWithFileInner(_: String, new File(url.getFile))
  }

  def findClassesWithJarFile: PartialFunction[URL, String => List[Class[_]]] = {
    case url if url.getProtocol == "jar" =>
      def manageJar[T](jarFile: JarFile)(body: JarFile => T): T = try {
        body(jarFile)
      } finally {
        jarFile.close()
      }

      def findClassesWithJarFileInner(packageName: String): List[Class[_]] =
        url openConnection match {
          case jarURLConnection: JarURLConnection =>
            manageJar(jarURLConnection.getJarFile) { jarFile =>
              jarFile.entries.asScala.toList.collect {
                case jarEntry if resourceNameToPackageName(jarEntry.getName).startsWith(packageName) &&
                                  isClassFile(jarEntry) =>
                                    classLoader.loadClass(resourceNameToClassName(jarEntry.getName))
              }
            }
        }

      findClassesWithJarFileInner
  }

  def findClassesWithNone: PartialFunction[URL, String => List[Class[_]]] = {
    case _ => packageName => Nil
  }
}

今回のテーマは、部分関数の関数合成です。クラスを検索するメソッドとして、3つ用意しました。
ファイルシステムから検索するメソッド

  def findClassesWithFile: PartialFunction[URL, String => List[Class[_]]]

JARファイルから検索するメソッド

  def findClassesWithJarFile: PartialFunction[URL, String => List[Class[_]]]

何もせず、Nilを返すメソッド

  def findClassesWithNone: PartialFunction[URL, String => List[Class[_]]] = {
    case _ => packageName => Nil
  }

これらのメソッドを、PartialFunction#orElseで合成します。

  private val finderFunction: PartialFunction[URL, String => List[Class[_]]] =
    findClassesWithFile orElse
    findClassesWithJarFile orElse
    findClassesWithNone

まあ、PartialFunctionに与えている型パラメータが

PartialFunction[URL, String => List[Class[_]]]

なので、PartialFunction#applyの結果、さらに関数が返ってくるところがちょっと注意ですね。こういうの、かえって読みにくくなるだけかなぁ…?

あとは、このフィールドに対してapplyメソッドを呼び出し、その結果返ってきた関数に対して検索処理を指示します。

  def findClasses(rootPackageName: String): List[Class[_]] = {
    val resourceName = packageNameToResourceName(rootPackageName)
    classLoader.getResource(resourceName) match {
      case null => Nil
      case url => finderFunction(url)(rootPackageName)
    }
  }

この部分ですね。

      case url => finderFunction(url)(rootPackageName)

ファイルシステムやJARファイル以外に増えても、メソッドを増やしてfinderFunctionに合成すれば大丈夫!最後にNilを返す関数を合成しているのは、ちょっと面倒くさがりだからですかね…。

最近はNettyの写経ばっかりしていたので、Scalaの関数型オブジェクトを使ったプログラミングが楽しいです♪

もちろん、Java版と同じように動作します。
展開されたクラスファイルに対して走査する場合。

$ scala -cp ../java/classes ClassFinder root.sub
class root.sub.SubPackageClass1$InnerClass1
class root.sub.SubPackageClass1
class root.sub.SubPackageClass2

続いて、JARファイル内のクラスを走査する場合。

$ scala -cp ../java/classes.jar ClassFinder root.sub
class root.sub.SubPackageClass1$InnerClass1
class root.sub.SubPackageClass1
class root.sub.SubPackageClass2