initial commit

This commit is contained in:
danijoo
2015-09-24 00:16:00 +02:00
commit a9f6b02adc
10 changed files with 462 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
build
.idea
gradlew
gradlew.bat
*.iml
.gradle
gradle

37
build.gradle Normal file
View File

@@ -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'
}

Binary file not shown.

2
settings.gradle Normal file
View File

@@ -0,0 +1,2 @@
rootProject.name = 'resdiff'

View File

@@ -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<String>) {
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)
}

View File

@@ -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<String>) {
// 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<Resource>()
getFiles(args.get(args.size() - 2)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList1.addAll(resources) }
val resourceList2 = ArrayList<Resource>()
getFiles(args.get(args.size() - 1)).map { file -> getResourcesFromFile(file) }.forEach { resources -> resourceList2.addAll(resources) }
// and compare them!
val differences = ArrayList<Pair<Resource?, Resource?>>()
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<Pair<Resource?, Resource?>>) {
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<Pair<Resource?, Resource?>>) {
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<out Resource>, resourceList1: ArrayList<Resource>, resourceList2: ArrayList<Resource>): List<Pair<Resource?, Resource?>> {
val className = clazz.simpleName
return getDifferences(
resourceList1.filter { r -> r.javaClass.simpleName == className },
resourceList2.filter { r -> r.javaClass.simpleName == className })
}
fun getDifferences(from: List<Resource>, to: List<Resource>): ArrayList<Pair<Resource?, Resource?>> {
val diffs = ArrayList<Pair<Resource?, Resource?>>()
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<Resource>, to: List<Resource>): List<Pair<Resource?, Resource?>> {
val pairs = ArrayList<Pair<Resource?, Resource?>>()
val tempFrom = ArrayList<Resource>(from.size())
tempFrom.addAll(from)
val tempTo = ArrayList<Resource>(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<File> {
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<Resource> {
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()
}
}

View File

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

View File

@@ -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 = "<string name=\"a name\">a value</string>"
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 = "<bool name=\"a name\">true</bool>"
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 = "<integer name=\"a name\">-123</integer>"
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 = "<color name=\"a name\">#000001</color>"
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 = "<dimen name=\"a name\">12dp</dimen>"
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))
}
}

24
src/test/res/testres.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="en">
<string name="matching_res">This resource is matching</string>
<string name="not_matching_res">This resource is not matching</string>
<bool name="matching_bool">true</bool>
<bool name="not_matching_bool">false</bool>
<integer name="matching_int">123</integer>
<integer name="not_matching_int">122</integer>
<color name="matching_color">#000000</color>
<color name="not_matching_color">#001230</color>
<dimen name="matching_dimen">12dp</dimen>
<dimen name="not_matching_dimen">1sp</dimen>
<string name="missing_string_res">This misses in the other file</string>
<string name="a_reference">@string/is_just_a_string</string>
<string name="a_reference_not_matching">@string/is_just_a_string</string>
<bool name="a_missing_boolean">true</bool>
</resources>

23
src/test/res/testres2.xml Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="en">
<string name="matching_res">This resource is matching</string>
<string name="not_matching_res">This resource is not matching!!!!</string>
<bool name="matching_bool">true</bool>
<bool name="not_matching_bool">true</bool>
<integer name="matching_int">123</integer>
<integer name="not_matching_int">533</integer>
<color name="matching_color">#000000</color>
<color name="not_matching_color">#666666</color>
<dimen name="matching_dimen">12dp</dimen>
<dimen name="not_matching_dimen">99px</dimen>
<string name="missing_string_res_no2">This misses in the other file</string>
<string name="a_reference">@string/is_just_a_string</string>
<string name="a_reference_not_matching">@string/is_just_another_string</string>
</resources>