CLOVER🍀

That was when it all began.

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

前に、仕事でJavaのプロパティファイルを比較するのに、Groovyで簡単なスクリプトを作りました。

プロパティファイルにいろいろ環境に依存した設定が書かれていて、なおかつその内容がExcelとかで管理されていたので実際のソースコードとその内容が一致しているかどうかを確認したかった、というのが作った動機です。

そもそも、Excelで管理されていたんなら自動生成すればいいんじゃあ…?とかいうツッコミもあろうかとは思いますが、いったん置いておきまして…。

スクリプトに求めたのは、以下の挙動です。

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

とすると、こんなスクリプトになりました。
properties_file_comparator.groovy

native2AsciiCommand = 'native2ascii -encoding %s %s'
propertiesFileEncoding = 'UTF-8'
propertiesFileSuffix = '.utf8'

def path1 = args[0]
def path2 = args[1]

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

compareProperties(propertiesFile1.clone(), propertiesFile2.clone())

def compareProperties(properties1, properties2) {
    if (properties1.isEmpty()) {
        for (entry2 in properties2) {
            def prop2 = new Property(key: entry2.key, value: properties2.remove(entry2.key))
            printDifference(new Property(), prop2)
        }
    } else if (properties2.isEmpty()) {
        for (entry1 in properties1) {
            def prop1 = new Property(key: entry1.key, value: properties1.remove(entry1.key))
            printDifference(prop1, new Property())
        }
    } else {
        def key1 = properties1.firstKey()
        def key2 = properties2.firstKey()

        if (key1 > key2) {
            def prop2 = new Property(key: key2, value: properties2.remove(key2))
            printDifference(new Property(), prop2)
            compareProperties(properties1, properties2)
        } else if (key1 < key2) {
            def prop1 = new Property(key: key1, value: properties1.remove(key1))
            printDifference(prop1, new Property())
            compareProperties(properties1, properties2)
        } else {
            def value1 = properties1.remove(key1)
            def value2 = properties2.remove(key2)

            if (value1 != value2) {
                def prop1 = new Property(key: key1, value: value1)
                def prop2 = new Property(key: key2, value: value2)
                printDifference(prop1, prop2)
            }

            compareProperties(properties1, properties2)
        }
    }
}

def printDifference(prop1, prop2) {
    if (prop1.hasKey()) {
        println("< $prop1.key = ${prop1.printableValue()}")
    }

    if (prop2.hasKey()) {
        println("> $prop2.key = ${prop2.printableValue()}")
    }

    println("---")
}

def requireNative2Ascii(fileName) {
    propertiesFileSuffix != null && fileName.endsWith(propertiesFileSuffix)
}

def autoNative2Ascii(fileName) {
    if (requireNative2Ascii(fileName)) {
        def command = String.format(native2AsciiCommand, propertiesFileEncoding, fileName)
        def proc = command.execute()
        def outputStream = new ByteArrayOutputStream()
        proc.consumeProcessOutput(outputStream, new ByteArrayOutputStream())

        def retVal = proc.waitFor()

        if (retVal != 0) {
            throw new IllegalArgumentException("native2ascii ret = [${retVal}]")
        }

        def properties = new Properties()
        new ByteArrayInputStream(outputStream.toByteArray()).withStream { properties.load(it) }
        new TreeMap(properties)
    } else {
        def properties = new Properties()
        new File(fileName).withInputStream { properties.load(it) }
        new TreeMap(properties)
    }
}

class Property {
    def key
    def value

    def hasKey() { key != null }
    def printableValue() {
        if (value == null) {
            ""
        } else {
            def builder = new StringBuilder()
            for (v in value) {
                switch (v) {
                    case '\n':
                        builder.append("\\n")
                        break
                    case '\r':
                        builder.append("\\r")
                        break
                    case '\t':
                        builder.append("\\t")
                        break
                    default:
                        builder.append(v)
                        break
                }
            }
            builder.toString()
        }
    }
}

あれ、貼ってみると、思ったより長い…。キーごとの比較を簡単にするために、TreeMapを使いました。

動作確認のために、以下のようなプロパティファイルを用意。

<<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

実行すると、それぞれこんな結果になります。

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

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

一致しているものは出力されていないので、意図通り動いています。

なお、上記のスクリプトは今日即興で作ったもので、実際に仕事で使ったものはサブディレクトリ内も含めた形で比較できるように作ってました。

properties_file_comparator.groovy

import groovy.io.FileType

autoNative2ascii = System.getProperty('auto.n2a', 'true').toBoolean()
native2ascii = System.getProperty('n2a.path', 'native2ascii')
propertiesEncoding = System.getProperty('n2a.encoding', 'UTF-8')
propertiesEncodeSuffix = System.getProperty('n2a.suffix', '.utf8')
tabFormatEnable = System.getProperty('tab.format', 'false').toBoolean()
consoleEncoding = System.getProperty('console.encoding', 'UTF-8')

if (args.size() != 2) {
    println("Required 2 Arguments: dir1 dir2")
    System.exit(1)
}

dirOrFile1 = new File(args[0])
dirOrFile2 = new File(args[1])

requiredExists(dirOrFile1)
requiredExists(dirOrFile2)

def dir1Properties = new TreeMap()
def dir2Properties = new TreeMap()

traverseDir(dir1Properties, dirOrFile1)
traverseDir(dir2Properties, dirOrFile2)

def dir1PropertiesClone = dir1Properties.clone()
def dir2PropertiesClone = dir2Properties.clone()

messageBuffer = []

if (dir1PropertiesClone.size() == 1 && dir2PropertiesClone.size() == 1) {
    def prop1Path = dir1PropertiesClone.firstKey()
    def prop2Path = dir2PropertiesClone.firstKey()
    compareProperties(prop1Path, dir1PropertiesClone.remove(prop1Path),
                      prop2Path, dir2PropertiesClone.remove(prop2Path))
    flushMessageBuffer(prop1Path, prop2Path)
} else {
    comparePropertiesMap(dir1PropertiesClone, dir2PropertiesClone)
}

def comparePropertiesMap(prop1Map, prop2Map) {
    if (prop1Map.isEmpty()) {
        if (!prop2Map.isEmpty()) {
            prop2Map.each { key, value ->
                printOnlyFileRight(dirOrFile2, key)
            }
        }

        return
    } else if (prop2Map.isEmpty()) {
        if (!prop1Map.isEmpty()) {
            prop1Map.each { key, value ->
                printOnlyFileLeft(dirOrFile1, key)
            }
        }

        return
    }

    def key1 = prop1Map.firstKey()
    def key2 = prop2Map.firstKey()

    if (key1 == key2) {
        def prop1Path = dirOrFile1.path + key1
        def prop2Path = dirOrFile2.path + key2
        compareProperties(prop1Path, prop1Map.remove(key1), prop2Path, prop2Map.remove(key2))
        flushMessageBuffer(prop1Path, prop2Path)
    } else if (key1 < key2) {
        printOnlyFileLeft(dirOrFile1, key1)
        prop1Map.remove(key1)
    } else if (key2 < key1) {
        printOnlyFileRight(dirOrFile2, key2)
        prop2Map.remove(key2)
    }

    comparePropertiesMap(prop1Map, prop2Map)
}

def compareProperties(prop1Path, prop1, prop2Path, prop2) {
    if (prop1.isEmpty()) {
        if (!prop2.isEmpty()) {
            prop2.each { key, value ->
                printOnlyContentsRight(prop2Path, ["key": key, "value": value])
            }
        }

        return
    } else if (prop2.isEmpty()) {
        if (!prop1.isEmpty()) {
            prop1.each { key, value ->
                printOnlyContentsLeft(prop1Path, ["key": key, "value": value])
            }
        }

        return
    }

    def key1 = prop1.firstKey()
    def key2 = prop2.firstKey()

    if (key1 == key2) {
        def value1 = prop1.remove(key1)
        def value2 = prop2.remove(key2)

        if (value1 != value2) {
            printDifferContents(prop1Path, ["key": key1, "value": value1], prop2Path, ["key": key2, "value": value2])
        }
    } else if (key1 < key2) {
        printOnlyContentsLeft(prop1Path, ["key": key1, "value": prop1.remove(key1)])
    } else if (key1 > key2) {
        printOnlyContentsRight(prop2Path, ["key": key2, "value": prop2.remove(key2)])
    }

    compareProperties(prop1Path, prop1, prop2Path, prop2)
}

def flushMessageBuffer(prop1Path, prop2Path) {
    if (!messageBuffer.isEmpty()) {
        if (tabFormatEnable == false) {
            println("--------------- ファイル[${prop1Path}] と ファイル[${prop2Path}] に差異があります ---------------")
        } else {
            //print("${prop1Path}\t${prop2Path}\t")
        }

        messageBuffer.each {
            println(it)
        }

        messageBuffer = []
    }
}

def printOnlyFileLeft(dir, filePath) {
    if (tabFormatEnable == false) {
        printOnlyFile(dir, filePath)
    } else {
        println(dir.path + filePath + "\t\t\t\t\t")
    }
}

def printOnlyFileRight(dir, filePath) {
    if (tabFormatEnable == false) {
        printOnlyFile(dir, filePath)
    } else {
        println("\t" + dir.path + filePath + "\t\t\t\t")
    }
}

def printOnlyFile(dir, filePath) {
    println("--------------- ${dir} のみに存在するファイル => ${filePath} ---------------")
}

def printDifferContents(prop1Path, prop1, prop2Path, prop2) {
    if (tabFormatEnable == false) {
        messageBuffer.add("****** 相違 *****")
        messageBuffer.add("< ${prop1.key} = ${escapePropertyValue(prop1.value)}")
        messageBuffer.add("> ${prop2.key} = ${escapePropertyValue(prop2.value)}")
    } else {
        messageBuffer.add("${prop1Path}\t${prop2Path}\t${prop1.key}\t${escapePropertyValue(prop1.value)}\t${escapePropertyValue(prop2.value)}")

    }
}

def printOnlyContentsLeft(propPath, prop) {
    if (tabFormatEnable == false) {
        printOnlyContents(propPath, prop)
    } else {
        messageBuffer.add("${propPath}\t\t${prop.key}\t${escapePropertyValue(prop.value)}\t")
    }
}

def printOnlyContentsRight(propPath, prop) {
    if (tabFormatEnable == false) {
        printOnlyContents(propPath, prop)
    } else {
        messageBuffer.add("\t${propPath}\t${prop.key}\t\t${prop.value}")
    }
}

def printOnlyContents(propPath, prop) {
    messageBuffer.add("***** ${propPath} のみに存在 *****")
    messageBuffer.add("${prop.key} = ${escapePropertyValue(prop.value)}")
}

def traverseDir(propertiesMap, targetDirOrFile) {
    def entryProps = { dir, file ->
        def inputStream
        def fileKey

        if (autoNative2ascii &&
            file.path.endsWith("${propertiesEncodeSuffix}")) {
            def command = "${native2ascii} -encoding ${propertiesEncoding} ${file.path}"
            def proc = command.execute()
            def outStream = new ByteArrayOutputStream()
            proc.consumeProcessOutput(outStream, new ByteArrayOutputStream())

            def returnCode = proc.waitFor()
            if (returnCode != 0) {
                println("[${command}] return code = [${returnCode}]")
            }

            inputStream = new ByteArrayInputStream(outStream.toByteArray())
            def path = relativePath(dir, file)
            fileKey = path.substring(0, path.length() - propertiesEncodeSuffix.length())
        } else {
            inputStream = file.newInputStream()
            fileKey = relativePath(dir, file)
        }

        inputStream.withStream {
            def props = new Properties()
            props.load(it)
            propertiesMap.put(fileKey, new TreeMap(props))
        }
    }

    def traverseProps
    traverseProps = { dir, file ->
        file.eachDir(traverseProps.curry(dir))
        file.eachFileMatch(FileType.FILES, ~/.*\.properties(\.[^.]+)?$/, entryProps.curry(dir))
    }

    if (targetDirOrFile.directory) {
        traverseProps(targetDirOrFile, targetDirOrFile)
    } else {
        entryProps(null, targetDirOrFile)
    }
}

def escapePropertyValue(value) {
    if (value == null) {
        null
    } else {
        def builder = new StringBuilder()
        for (v in value) {
            switch (v) {
                case '\n':
                    builder.append("\\n")
                    break
                case '\r':
                    builder.append("\\r")
                    break
                case '\t':
                    builder.append("\\t")
                    break
                //case '\\':
                //    builder.append("\\")
                //    break
                default:
                    builder.append(v)
                    break
            }
        }
        builder.toString()
    }
}

def relativePath(baseDir, targetFile) {
    if (baseDir == null) {
        targetFile.name
    } else {
        targetFile.path.substring(baseDir.path.length())
    }
}

def requiredExists(dirOrFile) {
    if (!dirOrFile.exists()) {
        println("[${dirOrFile}]が存在しません")
        System.exit(1)
    }
}

こちらは、業務都合で出力形式とかに細工がしてあります(笑)。