CLOVER🍀

That was when it all began.

Javaのプロパティファイルを比較するスクリプト

前回のエントリのScala版です。勢いで書いてみました。

スクリプトの動作仕様には、変更ありません。

  • 比較したプロパティファイルに差分があれば、diffコマンドっぽく出力する
  • 出力結果がUnicodeエスケープされていた形だと見づらくてしかたがないので、java.util.Propertiesを使って元の形に戻す
  • キーの出現順は、Propertiesを使用するので、保障しない
  • ファイル名が「.utf8」で終わっていた場合は、勝手にnative2asciiコマンドをかけた上で比較する

書いたスクリプトは、こんな感じ。
properties_file_comparator.scala

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection.immutable.TreeMap
import scala.sys.process._

import java.io.ByteArrayInputStream
import java.nio.file.{Files, Paths}
import java.util.Properties

val Native2AsciiCommand = "native2ascii -encoding %s %s"
val PropertiesFileEncoding = "UTF-8"
val PropertiesFileSuffix = ".utf8"

val path1 = args(0)
val path2 = args(1)

val propertiesFile1 = autoNative2Ascii(path1)
val propertiesFile2 = autoNative2Ascii(path2)

compareProperties(propertiesFile1, propertiesFile2)

@tailrec
def compareProperties(properties1: TreeMap[String, String],
                      properties2: TreeMap[String, String]): Unit = {
  (properties1.nonEmpty, properties2.nonEmpty) match {
    case (true, true) =>
      val key1 = properties1.firstKey
      val key2 = properties2.firstKey

      if (key1 > key2) {
        printDifference(property2 = Property(Option(key2), properties2.get(key2)))
        compareProperties(properties1, properties2 - key2)
      } else if (key1 < key2) {
        printDifference(property1 = Property(Option(key1), properties1.get(key1)))
        compareProperties(properties1 - key1, properties2)
      } else {
        (properties1.get(key1), properties2.get(key2)) match {
          case (Some(value1), Some(value2)) if value1 == value2 =>
          case (v1 @ Some(value1), v2 @ Some(value2)) =>
            printDifference(Property(Option(key1), v1), Property(Option(key2), v2))
          case other =>
        }
        compareProperties(properties1 - key1, properties2 - key2)        
      }
    case (false, _) =>
      properties2 foreach { case (k, v) => printDifference(property2 = Property(Option(k), Option(v))) }
    case (_, false) =>
      properties1 foreach { case (k, v) => printDifference(property1 = Property(Option(k), Option(v))) }
  }
}

def printDifference(property1: Property = Property(None, None),
                    property2: Property = Property(None, None)): Unit = {
  property1.key.foreach { key =>
    println("< %s = %s".format(key, property1.printableValue))
  }

  property2.key.foreach { key =>
    println("> %s = %s".format(key, property2.printableValue))
  }

  println("---")
}

def requireNative2Ascii(fileName: String): Boolean =
  PropertiesFileSuffix != null && fileName.endsWith(PropertiesFileSuffix)

def autoNative2Ascii(fileName: String): TreeMap[String, String] =
  if (requireNative2Ascii(fileName)) {
    val result = Native2AsciiCommand.format(PropertiesFileEncoding, fileName) !!

    val properties = new Properties
    properties.load(new ByteArrayInputStream(result.getBytes))
    new TreeMap[String, String] ++ properties.asScala
  } else {
    val properties = new Properties
    properties.load(new ByteArrayInputStream(Files.readAllBytes(Paths.get(fileName))))
    new TreeMap[String, String] ++ properties.asScala
  }

case class Property(key: Option[String], value: Option[String]) {
  def printableValue: String =
    value.getOrElse("") match {
      case null => ""
      case v => v.map {
        case '\n' => "\\n"
        case '\r' => "\\r"
        case '\t' => "\\t"
        case other => other
      }.mkString
    }
}

TreeMapを使った基本コンセプトは変わらない…というか、単にScalaに読み替えただけ、かな…。

比較対象も変更なく。

<<a.properties>>
foo=bar
hoge=\u65e5\u672c\u8a9e
fuga=var
propKey=propValue

<<a.properties.utf8>>
foo=bar
hoge=日本語
fuga=var
propKey=propValue

<<b.properties>>
foo=boo
hoge=\u82f1\u8a9e
propKey=propValue
bar=hello

<<b.properties.utf8>>
foo=boo
hoge=英語
propKey=propValue
bar=hello

実行結果も変わりなく。

$ scala properties_file_comparator.scala a.properties b.properties
> bar = hello
---
< foo = bar
> foo = boo
---
< fuga = var
---
< hoge = 日本語
> hoge = 英語
---

$ scala properties_file_comparator.scala a.properties.utf8 b.properties.utf8 
> bar = hello
---
< foo = bar
> foo = boo
---
< fuga = var
---
< hoge = 日本語
> hoge = 英語
---

なお、Scala版のディレクトリ内も比較可能なバージョンは、ちょっとマジメに頑張って書いてみました。でもまあ、もうちょっと整理して書いた方がよかったかなぁ…。

これはただ足跡的に貼っているだけですので…。

build.sbt

name := "properties-file-comparator"

version := "0.0.1"

scalaVersion := "2.9.2"

organization := "littlewings"

src/main/scala/PropertiesFileComparator.scala

import scala.annotation.tailrec
import scala.collection.SortedMap

import java.nio.file.{Files, Path, Paths}

object PropertiesFileComparator {
  def main(args: Array[String]): Unit = {
    val (leftTarget, rightTarget) = args toList match {
      case left :: right :: Nil => (left, right)
      case other => usage()
    }

    val leftPath = Paths get leftTarget match {
      case left if Files.exists(left) => left
      case left => noSuchFileOrDirectoryExit(left)
    }

    val rightPath = Paths get rightTarget match {
      case right if Files.exists(right) => right
      case right => noSuchFileOrDirectoryExit(right)
    }

    val propertiesFileComparator =
      (Files.isDirectory(leftPath) && Files.isDirectory(rightPath)) ||
      (!Files.isDirectory(leftPath) && !Files.isDirectory(leftPath)) match {
        case true => new PropertiesFileComparator(leftPath, rightPath)
        case false => fileTypeNotMatchExit(leftPath, rightPath)
      }

    propertiesFileComparator.compare.foreach {
      case Equals(_, _) =>
      case d @ Difference(leftPath, rightPath, _) =>
        printf("Found Difference %s <-> %s%n", leftPath, rightPath)
        println(d.message)
      case of @ OnlyFile(_) => println(of.message)
    }
  }

  private def usage(): Nothing = {
    val message = """|This Tool Required 2 Arguments
                     |  1: Compare Properties File or Directory
                     |  2: Compare Propertirs File or Directory""".stripMargin
    println(message)
    sys.exit(0)
  }

  private def noSuchFileOrDirectoryExit(path: Path): Nothing = {
    printf("No Such File or Directory [%s]%n", path)
    sys.exit(1)
  }

  private def fileTypeNotMatchExit(left: Path, right: Path): Nothing = {
    val leftMessage =
                  if (Files.isDirectory(left)) "Directory"
                  else "File"
    val rightMessage =
                  if (Files.isDirectory(right)) "Directory"
                  else "File"

    printf("%s is %s, But %s is %s%n", left, leftMessage, right, rightMessage)
    sys.exit(1)
  }
}

class PropertiesFileComparator(leftPath: Path, rightPath: Path) {
  def compare(): List[CompareResult] = {
    val propertiesFileFinder = new PropertiesFileFinder
    (propertiesFileFinder.find(leftPath), propertiesFileFinder.find(rightPath)) match {
      case (leftPropertiesFile :: Nil, rightPropertiesFile :: Nil) =>
        List(compareProperties(leftPropertiesFile, rightPropertiesFile))
      case (leftPropertiesFiles, rightPropertiesFiles) =>
        compareFiles(leftPropertiesFiles, rightPropertiesFiles, Nil)
    }
  }

  @tailrec
  private def compareFiles(leftFiles: List[PropertiesFile],
                           rightFiles: List[PropertiesFile],
                           results: List[CompareResult]): List[CompareResult] = {
    (leftFiles, rightFiles) match {
      case (leftFile :: leftRest, rightFile :: rightRest)
        if leftFile.path.toFile.getName > rightFile.path.toFile.getName =>
        compareFiles(leftFiles, rightRest, OnlyFile(rightFile.path) :: results)
      case (leftFile :: leftRest, rightFile :: rightRest)
        if leftFile.path.toFile.getName < rightFile.path.toFile.getName =>
        compareFiles(leftRest, rightFiles, OnlyFile(leftFile.path) :: results)
      case (leftFile :: leftRest, rightFile :: rightRest) =>
        compareFiles(leftRest, rightRest, compareProperties(leftFile, rightFile) :: results)
      case (Nil, Nil) => results reverse
      case (leftFile :: leftRest, Nil) => compareFiles(Nil, Nil, OnlyFile(leftFile.path) :: results)
      case (Nil, rightFile :: rightRest) => compareFiles(Nil, Nil, OnlyFile(rightFile.path) :: results)
    }
  }

  private def compareProperties(leftFile: PropertiesFile, rightFile: PropertiesFile): CompareResult = {
    val buffer = new DifferenceBuffer(leftFile.path, rightFile.path)

    @tailrec
    def comparePropertiesInner(leftProperties: SortedMap[String, String],
                             rightProperties: SortedMap[String, String]): CompareResult = {
      if (leftProperties.nonEmpty && rightProperties.nonEmpty) {
        val leftKey = leftProperties.firstKey
        val rightKey = rightProperties.firstKey
        val lkOption = Some(leftKey)
        val rkOption = Some(rightKey)

        if (leftKey > rightKey) {
          buffer += DifferencePart(None, None, rkOption, rightProperties.get(rightKey))
          comparePropertiesInner(leftProperties, rightProperties - rightKey)
        } else if (leftKey < rightKey) {
          buffer += DifferencePart(lkOption, leftProperties.get(leftKey), None, None)
          comparePropertiesInner(leftProperties - leftKey, rightProperties)
        } else { // (leftKey == rightKey) {
          (leftProperties.get(leftKey), rightProperties.get(rightKey)) match {
            case (Some(lv), Some(rv)) if lv == rv =>
            case (l @ Some(lv), r @ Some(rv)) if lv != rv =>
              buffer += DifferencePart(lkOption, l, rkOption, r)
            case other =>
            /*
            case (l @ None, r @ Some(_)) =>
              buffer += DifferencePart(lkOption, l, rkOption, r)
            case (l @ Some(_), r @ None) =>
              buffer += DifferencePart(lkOption, l, rkOption, r)
            case (None, None) =>
            */
          }
          comparePropertiesInner(leftProperties - leftKey, rightProperties - rightKey)
        }
      } else if (leftProperties.isEmpty) {
        if (rightProperties.nonEmpty) {
          for ((k, v) <- rightProperties)
            buffer += DifferencePart(None, None, Some(k), Option(v))
        }

        buffer toDifferenceOrEquals
      } else if (rightProperties.isEmpty) {
        if (leftProperties.nonEmpty) {
          for ((k, v) <- leftProperties)
            buffer += DifferencePart(Some(k), Option(v), None, None)
        }

        buffer toDifferenceOrEquals
      } else {
        buffer toDifferenceOrEquals
      }
    }

    comparePropertiesInner(leftFile.properties, rightFile.properties)
  }
}

case class PropertiesFile(path: Path, properties: SortedMap[String, String]

src/main/scala/CompareResult.scala

import scala.collection.mutable.ListBuffer

import java.nio.file.Path

trait CompareResult {
  def message: String

  protected def escapeValue(value: String): String =
    value match {
      case null => ""
      case other =>
        other.map {
          case '\n' => "\\n"
          case '\r' => "\\r"
          case '\t' => "\\t"
          case other => other
        }.mkString
    }
}

class DifferenceBuffer(leftPath: Path, rightPath: Path) {
  private[this] val differencePartBuffer: ListBuffer[DifferencePart] = new ListBuffer

  def +=(difference: DifferencePart): this.type = {
    differencePartBuffer += difference
    this
  }

  def toDifferenceOrEquals: CompareResult = differencePartBuffer toList match {
    case Nil => Equals(leftPath, rightPath)
    case differences => Difference(leftPath, rightPath, differences)
  }
}

case class Equals(leftPath: Path, rightPath: Path) extends CompareResult {
  def message: String = ""
}

case class Difference(leftPath: Path, rightPath: Path, differences: List[DifferencePart]) extends CompareResult {
  def message: String =
    differences
      .map(_.message)
      .mkString(System.lineSeparator + "---" + System.lineSeparator)
}

case class DifferencePart(leftKey: Option[String], leftValue: Option[String],
                          rightKey: Option[String], rightValue: Option[String]) extends CompareResult {
  def message: String =
    List((leftKey, leftValue), (rightKey, rightValue)).zip(List("< %s = %s", "> %s = %s"))
      .filter { case (c, _) => c._1.isDefined }
      .map { case (c, f) => f.format(c._1.get, escapeValue(c._2.getOrElse(""))) }
      .mkString(System.lineSeparator)
}

case class OnlyFile(path: Path) extends CompareResult {
  def message: String =
    "Onln in %s: %s".format(path.getParent, path.getFileName)
}

src/main/scala/PropertiesFileFinder.scala

import scala.collection.SortedMap
import scala.collection.JavaConverters._
import scala.collection.immutable.TreeMap
import scala.collection.mutable.ListBuffer
import scala.sys.process._

import java.io.ByteArrayInputStream
import java.nio.file.{Files, FileVisitResult, Path, Paths, SimpleFileVisitor}
import java.nio.file.attribute.BasicFileAttributes
import java.util.Properties

object PropertiesFileFinder {
  val Native2AsciiCommand = "native2ascii -encoding %s %s"
}

class PropertiesFileFinder(encoding: Option[String] = Some("UTF-8"),
                           suffix: Option[String] = Some(".utf8"),
                           autoNative2Ascii: Boolean = true) {
  def find(path: Path): List[PropertiesFile] = {
    val visitor = new PropertiesFileVisitor(path, toProperties)
    Files.walkFileTree(path, visitor)
    visitor.propertiesFiles
  }

  private def toProperties(file: Path): PropertiesFile = {
    val applyNative2Ascii = suffix exists { s =>
      file.toString.endsWith(s) && autoNative2Ascii
    }

    val props = new Properties
    val bytes = applyNative2Ascii match {
      case true =>
        val enc = encoding.getOrElse(throw new IllegalArgumentException("Not Set Encoding!"))
        val result =
                PropertiesFileFinder.Native2AsciiCommand.format(enc, file) !!  // execute command

        result.getBytes(enc)
      case false => Files.readAllBytes(file)
    }

    props.load(new ByteArrayInputStream(bytes))

    if (applyNative2Ascii) {
      val fileName = file.toFile.getName
      val parent = Option(file.getParent)
      val newFile = Paths.get(fileName.substring(0, fileName.size - suffix.get.size))
      PropertiesFile(
        parent.getOrElse(Paths.get("")).resolve(newFile),
        new TreeMap[String, String] ++ props.asScala
      )
    } else {
      PropertiesFile(file, new TreeMap[String, String] ++ props.asScala)
    }
  }
}

private class PropertiesFileVisitor(path: Path, toProperties: (Path => PropertiesFile))
        extends SimpleFileVisitor[Path] {
  private[this] val propertiesFilesBuffer: ListBuffer[PropertiesFile] = new ListBuffer

  override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult =
    file toFile match {
      case f =>
      // case f if f.getName.indexOf(".properties") != -1 =>
        val propertiesFile = toProperties(file)
        propertiesFilesBuffer += PropertiesFile(propertiesFile.path, propertiesFile.properties)
        FileVisitResult.CONTINUE
      // case other => FileVisitResult.CONTINUE
    }

  def propertiesFiles: List[PropertiesFile] = propertiesFilesBuffer toList
}