initial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
build
|
||||
.idea
|
||||
gradlew
|
||||
gradlew.bat
|
||||
*.iml
|
||||
.gradle
|
||||
gradle
|
||||
37
build.gradle
Normal file
37
build.gradle
Normal 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'
|
||||
}
|
||||
BIN
libs/btc-ascii-table-1.0.jar
Normal file
BIN
libs/btc-ascii-table-1.0.jar
Normal file
Binary file not shown.
2
settings.gradle
Normal file
2
settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
rootProject.name = 'resdiff'
|
||||
|
||||
30
src/main/kotlin/net/headlezz/resdiff/CLOptions.kt
Normal file
30
src/main/kotlin/net/headlezz/resdiff/CLOptions.kt
Normal 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)
|
||||
|
||||
}
|
||||
168
src/main/kotlin/net/headlezz/resdiff/ResDiff.kt
Normal file
168
src/main/kotlin/net/headlezz/resdiff/ResDiff.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
61
src/main/kotlin/net/headlezz/resdiff/Resource.kt
Normal file
61
src/main/kotlin/net/headlezz/resdiff/Resource.kt
Normal 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)
|
||||
110
src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt
Normal file
110
src/test/kotlin/net/headlezz/resdiff/ResourceTest.kt
Normal 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
24
src/test/res/testres.xml
Normal 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
23
src/test/res/testres2.xml
Normal 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>
|
||||
Reference in New Issue
Block a user