commit a9f6b02adc3f2c2bd17e9d04d5cf1c621e42a9dd Author: danijoo Date: Thu Sep 24 00:16:00 2015 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18dd732 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +.idea +gradlew +gradlew.bat +*.iml +.gradle +gradle \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6f3bd4a --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +group 'net.headlezz.resdiff' +version '1.0' + +apply plugin: 'kotlin' +apply plugin: 'application' + +sourceCompatibility = 1.6 +mainClassName = "net.headlezz.resdiff.ResdiffPackage" + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.13.1514' + } +} + +repositories { + mavenCentral() +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'org.jetbrains.kotlin:kotlin-stdlib:0.13.1514' + compile 'org.jetbrains.kotlin:kotlin-reflect:0.13.1514' + compile 'commons-cli:commons-cli:1.3.1' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +sourceSets { + test.kotlin.srcDirs += 'src/test/kotlin' +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.7' +} diff --git a/libs/btc-ascii-table-1.0.jar b/libs/btc-ascii-table-1.0.jar new file mode 100644 index 0000000..59c0d72 Binary files /dev/null and b/libs/btc-ascii-table-1.0.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8a6680a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'resdiff' + diff --git a/src/main/kotlin/net/headlezz/resdiff/CLOptions.kt b/src/main/kotlin/net/headlezz/resdiff/CLOptions.kt new file mode 100644 index 0000000..45729f1 --- /dev/null +++ b/src/main/kotlin/net/headlezz/resdiff/CLOptions.kt @@ -0,0 +1,30 @@ +package net.headlezz.resdiff + +import org.apache.commons.cli.CommandLine +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Options + +public class CLOptions(args: Array) { + + private val options = Options() + private val clOptions: CommandLine + + init { + options.addOption("h", "help", false, "Show this screen") + options.addOption("t", "type", true, "only compare this resource type (string, bool, integer, color, dimen)") + options.addOption("y", "side-by-side", false, "output in columns") + + val optionParser = DefaultParser() + clOptions = optionParser.parse(options, args) + } + + public fun printHelp() { + HelpFormatter().printHelp("resdiff [OPTIONS]... FILES", options) + } + + public fun hasFlag(flag: String) : Boolean = clOptions.hasOption(flag) + + public fun getOptionValue(flag: String) : String = clOptions.getOptionValue(flag) + +} \ No newline at end of file diff --git a/src/main/kotlin/net/headlezz/resdiff/ResDiff.kt b/src/main/kotlin/net/headlezz/resdiff/ResDiff.kt new file mode 100644 index 0000000..e666cec --- /dev/null +++ b/src/main/kotlin/net/headlezz/resdiff/ResDiff.kt @@ -0,0 +1,168 @@ +package net.headlezz.resdiff + +import com.bethecoder.ascii_table.ASCIITable +import org.xml.sax.SAXParseException +import java.io.File +import java.util.* +import kotlin.dom.elements +import kotlin.dom.get +import kotlin.dom.parseXml +import kotlin.reflect.KClass + +public fun main(args: Array) { + // parse arguments + val clo = CLOptions(args) + if (clo.hasFlag("h")) { + clo.printHelp() + System.exit(0) + } + val printTable = clo.hasFlag("y") + val compareBooleanRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("bool") + val compareStringRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("string") + val compareColorRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("color") + val compareIntegerRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("integer") + val compareDimensionRes = !clo.hasFlag("t") || clo.getOptionValue("t").contains("dimen") + + + // assert we have two valid file paths + if (args.size() < 2 || + !File(args.get(args.size() - 2)).exists() || + !File(args.get(args.size() - 1)).exists()) { + println("Missing file arguments.") + clo.printHelp() + System.exit(1) + } + + // get all resources + val resourceList1 = ArrayList() + getFiles(args.get(args.size() - 2)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList1.addAll(resources) } + + val resourceList2 = ArrayList() + getFiles(args.get(args.size() - 1)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList2.addAll(resources) } + + // and compare them! + val differences = ArrayList>() + if (compareStringRes) + differences.addAll(getDifferencesForClass(StringResource::class, resourceList1, resourceList2)) + if (compareBooleanRes) + differences.addAll(getDifferencesForClass(BooleanResource::class, resourceList1, resourceList2)) + if (compareIntegerRes) + differences.addAll(getDifferencesForClass(IntegerResource::class, resourceList1, resourceList2)) + if (compareDimensionRes) + differences.addAll(getDifferencesForClass(DimensionResource::class, resourceList1, resourceList2)) + if (compareColorRes) + differences.addAll(getDifferencesForClass(ColorResource::class, resourceList1, resourceList2)) + + val addCount = differences.count { pair -> pair.first == null && pair.second != null } + val delCount = differences.count { pair -> pair.first != null && pair.second == null } + println("Found ${differences.size()} differences ($addCount added, $delCount removed).") + + if (printTable) + printDiffInTable(differences) + else + printDiffNormal(differences) +} + +fun printDiffNormal(differences: List>) { + differences.forEach { pair -> + val resA = pair.first + val resB = pair.second + val type = (resA?.type ?: resB!!.type).toString().toLowerCase() + val name = resA?.name ?: resB!!.name + println("<$type name=\"$name\">") + if (resA != null) + println("<\t${resA.value}") + if (resA != null && resB != null) + println("---") + if (resB != null) + println(">\t${resB.value}") + } +} + +fun printDiffInTable(differences: List>) { + val header = arrayOf("Type", "Name", "Left", "Right") + val data = differences.map { pair -> + val type = (pair.first?.type ?: pair.second!!.type).toString() + val name = pair.first?.name ?: pair.second!!.name + arrayOf(type, name, pair.first?.value ?: "", pair.second?.value ?: "") + }.toTypedArray() + ASCIITable.getInstance().printTable(header, data) +} + +fun getDifferencesForClass(clazz: KClass, resourceList1: ArrayList, resourceList2: ArrayList): List> { + val className = clazz.simpleName + return getDifferences( + resourceList1.filter { r -> r.javaClass.simpleName == className }, + resourceList2.filter { r -> r.javaClass.simpleName == className }) +} + +fun getDifferences(from: List, to: List): ArrayList> { + val diffs = ArrayList>() + + val pairs = genResourcePairs(from, to) + + pairs.forEach { pair -> + if (!Resource.match(pair.first, pair.second)) + diffs.add(pair) + } + + return diffs +} + +/** + * creates Pairs of resource with matching identity + */ +fun genResourcePairs(from: List, to: List): List> { + val pairs = ArrayList>() + + val tempFrom = ArrayList(from.size()) + tempFrom.addAll(from) + val tempTo = ArrayList(to.size()) + tempTo.addAll(to) + + loop@for (f in tempFrom) { + for (t in tempTo) { + if (f.matchIdent(t)) { + pairs.add(Pair(f, t)) + continue@loop + } + } + pairs.add(Pair(f, null)) + } + pairs.forEach { pair -> + tempFrom.remove(pair.first) + tempTo.remove(pair.second) + } + tempTo.forEach { t -> pairs.add(Pair(null, t)) } + + return pairs +} + +/** + * If location points to a directory, returns all files in this directory (not recursive), else, + * it just returns the file + */ +fun getFiles(location: String): Array { + val file = File(location) + if (file.isDirectory) { + return file.listFiles { file, filter -> true }.filterNot { file -> file.isDirectory }.toTypedArray() + } else + return arrayOf(file) +} + +/** + * returns a list of all resources found in the given file + */ +fun getResourcesFromFile(file: File): ArrayList { + try { + val doc = parseXml(file.absoluteFile) + val resources = doc.get("resources").first() + return resources.elements.map { elem -> Resource.fromElement(elem) }.filterNotNull().toArrayList() + } catch(e: SAXParseException) { + println("${file.name} is not a valid xml file") + return arrayListOf() + } catch(e: NoSuchElementException) { + println("${file.name} is not a valid xml file") + return arrayListOf() + } +} diff --git a/src/main/kotlin/net/headlezz/resdiff/Resource.kt b/src/main/kotlin/net/headlezz/resdiff/Resource.kt new file mode 100644 index 0000000..4b0c80b --- /dev/null +++ b/src/main/kotlin/net/headlezz/resdiff/Resource.kt @@ -0,0 +1,61 @@ +package net.headlezz.resdiff + +import org.w3c.dom.Element + +abstract class Resource( + public val name: String, + public val value: String, + public val type: Resource.Type) { + + enum class Type { + String, Dimension, Boolean, Integer, Color + } + + companion object { + public fun fromElement(elem: Element) : Resource? { + val type= elem.tagName + val name = elem.getAttribute("name") + val value = elem.firstChild.nodeValue + + return when(type) { + "string" -> StringResource(name, value) + "dimen" -> DimensionResource(name, value) + "bool" -> BooleanResource(name, value) + "integer" -> IntegerResource(name, value) + "color" -> ColorResource(name, value) + else -> null + } + } + + public fun match(resource1: Resource?, resource2: Resource?) : Boolean { + if(resource1 == null) + return false + + if(resource2 == null) + return false + + return resource1.match(resource2) + } + + } + + /** + * Check if the given resource matches the this one + */ + public fun match(resource2: Resource) : Boolean { + return matchIdent(resource2) && value == resource2.value + } + + /** + * Check if identity of two resources match (resource name matching) + */ + public fun matchIdent(resource2: Resource) : Boolean { + return this.type == resource2.type && this.name == resource2.name + } +} + +class StringResource(name: String, value: String) : Resource(name, value, Resource.Type.String) +class BooleanResource(name: String, value: String) : Resource(name, value, Resource.Type.Boolean) +class ColorResource(name: String, value: String) : Resource(name, value, Resource.Type.Color) +class IntegerResource(name: String, value: String) : Resource(name, value, Resource.Type.Integer) +class DimensionResource(name: String, value: String) : Resource(name, value, Resource.Type.Dimension) diff --git a/src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt b/src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt new file mode 100644 index 0000000..50d0f55 --- /dev/null +++ b/src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt @@ -0,0 +1,110 @@ +package net.headlezz.resdiff + +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.nio.charset.StandardCharsets +import kotlin.dom.elements +import kotlin.dom.parseXml +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ResourceTest { + + @Test + fun match() { + val resA = StringResource("nameA", "valueA") + val resB = StringResource("nameA", "valueA") + assertTrue(resA.match(resB)) + } + + @Test + fun match_notMatching() { + val resA = StringResource("nameA", "valueA") + val resB = StringResource("nameA", "valueB") + assertFalse(resA.match(resB)) + } + + @Test + fun ident() { + val resA = StringResource("nameA", "valueA") + val resB = StringResource("nameA", "valueA") + assertTrue(resA.matchIdent(resB)) + } + + @Test + fun ident_notMatching() { + val resA = StringResource("nameA", "valueA") + val resB = StringResource("nameB", "valueA") + assertFalse(resA.matchIdent(resB)) + } + + @Test + fun ident_differentResourceTypes() { + val resA = object: Resource("nameA", "valueA", Resource.Type.Boolean){} + val resB = object: Resource("nameA", "valueA", Resource.Type.String){} + assertFalse(resA.matchIdent(resB)) + } + + @Test + fun fromElement_String() { + val res = "a value" + val elem = parseXml(buildInputStream(res)).elements.first() + + val stringResource = Resource.fromElement(elem) + assert(stringResource is StringResource) + assertEquals("a name", stringResource!!.name) + assertEquals("a value", stringResource.value) + } + + @Test + fun fromElement_Boolean() { + val res = "true" + val elem = parseXml(buildInputStream(res)).elements.first() + + val booleanResource = Resource.fromElement(elem) + assert(booleanResource is BooleanResource) + assertEquals("a name", booleanResource!!.name) + assertEquals("true", booleanResource.value) + } + + @Test + fun fromElement_Integer() { + val res = "-123" + val elem = parseXml(buildInputStream(res)).elements.first() + + val intResource = Resource.fromElement(elem) + assert(intResource is IntegerResource) + assertEquals("a name", intResource!!.name) + assertEquals("-123", intResource.value) + } + + @Test + fun fromElement_Color() { + val res = "#000001" + val elem = parseXml(buildInputStream(res)).elements.first() + + val colorResource = Resource.fromElement(elem) + assert(colorResource is ColorResource) + assertEquals("a name", colorResource!!.name) + assertEquals("#000001", colorResource.value) + } + + @Test + fun fromElement_Dimension() { + val res = "12dp" + val elem = parseXml(buildInputStream(res)).elements.first() + + val dimensionResource = Resource.fromElement(elem) + assert(dimensionResource is DimensionResource) + assertEquals("a name", dimensionResource!!.name) + assertEquals("12dp", dimensionResource.value) + } + + + fun buildInputStream(value: String) : InputStream { + return ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)) + } + +} \ No newline at end of file diff --git a/src/test/res/testres.xml b/src/test/res/testres.xml new file mode 100644 index 0000000..3e09ae1 --- /dev/null +++ b/src/test/res/testres.xml @@ -0,0 +1,24 @@ + + + This resource is matching + This resource is not matching + + true + false + + 123 + 122 + + #000000 + #001230 + + 12dp + 1sp + + This misses in the other file + + @string/is_just_a_string + @string/is_just_a_string + + true + \ No newline at end of file diff --git a/src/test/res/testres2.xml b/src/test/res/testres2.xml new file mode 100644 index 0000000..b76f16e --- /dev/null +++ b/src/test/res/testres2.xml @@ -0,0 +1,23 @@ + + + This resource is matching + This resource is not matching!!!! + + true + true + + 123 + 533 + + #000000 + #666666 + + 12dp + 99px + + This misses in the other file + + @string/is_just_a_string + @string/is_just_another_string + + \ No newline at end of file