From 1e8769b5f79fe00e1b0fe75bf8ce47d288050b59 Mon Sep 17 00:00:00 2001 From: Mikhail Mutcianko Date: Wed, 9 Jul 2014 16:24:36 +0400 Subject: [PATCH] android UI xml bridge initial - add frontend.android module - add xml ids to kotlin extension properties converter - add converter basic functional test --- .idea/modules.xml | 1 + .../frontend.android/frontend.android.iml | 15 ++++ .../resolve/android/AndroidUIXmlParser.kt | 74 ++++++++++++++++++ .../jet/lang/resolve/android/AndroidUtil.kt | 17 +++++ .../lang/resolve/android/AndroidXmlHandler.kt | 40 ++++++++++ .../jet/lang/resolve/android/Context.kt | 76 +++++++++++++++++++ .../jet/lang/resolve/android/KotlinWriter.kt | 71 +++++++++++++++++ compiler/testData/android/layout.kt | 21 +++++ compiler/testData/android/layout.xml | 45 +++++++++++ compiler/tests/compiler-tests.iml | 3 +- .../jetbrains/jet/codegen/AndroidXmlTest.java | 49 ++++++++++++ 11 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 compiler/frontend.android/frontend.android.iml create mode 100644 compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUIXmlParser.kt create mode 100644 compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUtil.kt create mode 100644 compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidXmlHandler.kt create mode 100644 compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/Context.kt create mode 100644 compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/KotlinWriter.kt create mode 100644 compiler/testData/android/layout.kt create mode 100644 compiler/testData/android/layout.xml create mode 100644 compiler/tests/org/jetbrains/jet/codegen/AndroidXmlTest.java diff --git a/.idea/modules.xml b/.idea/modules.xml index 1b08050b935..094a2eef4d5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -18,6 +18,7 @@ + diff --git a/compiler/frontend.android/frontend.android.iml b/compiler/frontend.android/frontend.android.iml new file mode 100644 index 00000000000..95b0d77a8da --- /dev/null +++ b/compiler/frontend.android/frontend.android.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUIXmlParser.kt b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUIXmlParser.kt new file mode 100644 index 00000000000..fe434cf8b6c --- /dev/null +++ b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUIXmlParser.kt @@ -0,0 +1,74 @@ +package org.jetbrains.jet.lang.resolve.android + +import java.nio.file.Path +import org.jetbrains.jet.lang.psi.JetFile +import java.util.ArrayList +import javax.xml.parsers.SAXParserFactory +import java.io.File +import java.io.FileInputStream +import com.intellij.openapi.project.Project +import org.jetbrains.jet.lang.psi.JetPsiFactory + +class AndroidUIXmlParser(val project: Project?, val searchPaths: Collection) { + + val ids: MutableCollection = ArrayList() + val kw = KotlinStringWriter() + val androidImports = arrayListOf("android.app.Activity", + "android.view.View") + + public fun parse(): String { + doParse() + return produceKotlinSignatures().toString() + } + + public fun parseToPsi(): JetFile { + return JetPsiFactory.createFile(project, parse()) + } + + private fun isAndroidUIXml(file: File): Boolean { + return file.extension == "xml" + } + + private fun searchForUIXml(paths: Collection?): Collection { + if (paths == null) return ArrayList(0) + val res = ArrayList() + for (path in paths) { + if (path.isFile() && isAndroidUIXml(path)) { + res.add(path) + } else if (path.isDirectory()) { + res.addAll(searchForUIXml(path.listFiles()?.toArrayList())) + } + } + return res + } + + private fun writeImports() { + for (elem in androidImports) + kw.writeImport(elem) + kw.writeEmptyLine() + } + + private fun doParse() { + val xmlStreams = searchForUIXml(searchPaths).map { FileInputStream(it) } + val factory = SAXParserFactory.newInstance() + factory?.setNamespaceAware(true) + val parser = factory!!.newSAXParser() // TODO: annotate this + val handler = AndroidXmlHandler({ id -> ids.add(AndroidID(id)) }) + writeImports() + for (xmlStream in xmlStreams) { + parser.parse(xmlStream, handler) + } + } + + private fun produceKotlinSignatures(): StringBuffer { + for (id in ids) { + val body = arrayListOf("return findViewById(R.id.${id.toString()})!!") + kw.writeImmutableExtensionProperty(receiver = "Activity", + name = id.toString(), + retType = "View", + getterBody = body ) + } + return kw.output() + } + +} diff --git a/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUtil.kt b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUtil.kt new file mode 100644 index 00000000000..3d7884877fe --- /dev/null +++ b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidUtil.kt @@ -0,0 +1,17 @@ +package org.jetbrains.jet.lang.resolve.android + +trait AndroidResource + +class AndroidID(val rawID: String): AndroidResource { + + override fun equals(other: Any?): Boolean { + return other is AndroidID && this.rawID == other.rawID + } + override fun hashCode(): Int { + return rawID.hashCode() + } + override fun toString(): String { + return rawID + } +} + diff --git a/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidXmlHandler.kt b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidXmlHandler.kt new file mode 100644 index 00000000000..ea5baf6447d --- /dev/null +++ b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/AndroidXmlHandler.kt @@ -0,0 +1,40 @@ +package org.jetbrains.jet.lang.resolve.android + +import org.xml.sax.helpers.DefaultHandler +import org.xml.sax.Attributes +import java.util.HashMap + +class AndroidXmlHandler(val idCallback: (String)-> Unit): DefaultHandler() { + + override fun startDocument() { + super.startDocument() + } + + override fun endDocument() { + super.endDocument() + } + + override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { + val hashMap = attributes.toMap() + val s = hashMap["id"] + val idPrefix = "@+id/" + if (s != null && s.startsWith(idPrefix)) idCallback(s.replace(idPrefix, "")) + } + + override fun endElement(uri: String?, localName: String, qName: String) { + + } + + public fun Attributes.toMap(): HashMap { + val res = HashMap() + for (index in 0..getLength()-1) { + val attrName = getLocalName(index)!! + val attrVal = getValue(index)!! + res[attrName] = attrVal + } + return res + } +} + + + diff --git a/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/Context.kt b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/Context.kt new file mode 100644 index 00000000000..401dd5f81c7 --- /dev/null +++ b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/Context.kt @@ -0,0 +1,76 @@ +package org.jetbrains.jet.lang.resolve.android + +import java.util.ArrayList + + +open class Context(val buffer: StringBuffer = StringBuffer(), var indentDepth: Int = 0) { + open class InvalidIndent(num: Int) : RuntimeException("Indentation level < 0: $num") + + val indentUnit = " " + protected var currentIndent: String = indentUnit.repeat(indentDepth) + val children = ArrayList() + + public fun incIndent() { + indentDepth++ + currentIndent += indentUnit + } + + public fun decIndent() { + indentDepth-- + if (indentDepth < 0) + throw InvalidIndent(indentDepth) + currentIndent = currentIndent.substring(0, currentIndent.length - indentUnit.length) + } + + public open fun write(what: String) { + writeNoIndent(currentIndent) + writeNoIndent(what) + } + + public fun writeNoIndent(what: String) { + buffer.append(what) + } + + public fun writeln(what: String) { + write(what) + newLine() + } + + public fun newLine() { + writeNoIndent("\n") + } + + + public fun trim(num: Int) { + buffer.delete(buffer.length - num, buffer.length) + } + + public fun fork(newBuffer: StringBuffer = StringBuffer(), + newIndentDepth: Int = indentDepth): Context { + val child = Context(newBuffer, newIndentDepth) + children.add(child) + return child + } + + public fun adopt(c: T, inheritIndent: Boolean = true): T { + children.add(c) + if (inheritIndent) c.currentIndent = currentIndent + return c + } + + public fun absorbChildren(noIndent: Boolean = true) { + for (child in children) { + child.absorbChildren() + if (noIndent) + writeNoIndent(child.toString()) + else + write(child.toString()) + } + children.clear() + } + + public override fun toString(): String { + return buffer.toString() + } +} + diff --git a/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/KotlinWriter.kt b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/KotlinWriter.kt new file mode 100644 index 00000000000..deece36773e --- /dev/null +++ b/compiler/frontend.android/src/org/jetbrains/jet/lang/resolve/android/KotlinWriter.kt @@ -0,0 +1,71 @@ +package org.jetbrains.jet.lang.resolve.android + +trait KotlinWriter + +class KotlinStringWriter : KotlinWriter { + + val ctx = Context() + + fun writeFunction(name: String, + args: Collection?, + retType: String, + body: Collection) { + val returnTerm = if (retType == "" || retType == "Unit") "" else ": $retType" + val argStr = if (args != null) args.join(", ") else "" + ctx.writeln("fun $name($argStr)$returnTerm {") + ctx.incIndent() + for (stmt in body) + ctx.writeln(stmt) + ctx.decIndent() + ctx.writeln("}") + } + + fun writeExtensionFunction(receiver: String, + name: String, + args: Collection?, + retType: String, + body: Collection) { + writeFunction("$receiver.$name", args, retType, body) + } + + fun writeImmutableProperty(name: String, + retType: String, + getterBody: Collection) { + ctx.writeln("val $name: $retType") + ctx.incIndent() + ctx.write("get() ") + if (getterBody.size > 1) { + ctx.writeNoIndent("{\n") + ctx.incIndent() + for (stmt in getterBody) { + ctx.writeln(stmt) + } + ctx.decIndent() + ctx.writeln("}") + } else { + ctx.writeNoIndent("=") + ctx.writeNoIndent(getterBody.join("").replace("return", "")) + ctx.newLine() + } + ctx.decIndent() + ctx.newLine() + } + + fun writeImmutableExtensionProperty(receiver: String, + name: String, + retType: String, + getterBody: Collection) { + writeImmutableProperty("$receiver.$name", retType, getterBody) + } + + fun writeImport(what: String) { + ctx.writeln("import $what") + } + + fun writeEmptyLine() { + ctx.newLine() + } + + fun output() = ctx.buffer +} + diff --git a/compiler/testData/android/layout.kt b/compiler/testData/android/layout.kt new file mode 100644 index 00000000000..5c69910f2f3 --- /dev/null +++ b/compiler/testData/android/layout.kt @@ -0,0 +1,21 @@ +import android.app.Activity +import android.view.View + +val Activity.item_detail_container: View + get() = findViewById(R.id.item_detail_container)!! + +val Activity.textView1: View + get() = findViewById(R.id.textView1)!! + +val Activity.password: View + get() = findViewById(R.id.password)!! + +val Activity.textView2: View + get() = findViewById(R.id.textView2)!! + +val Activity.passwordConfirmation: View + get() = findViewById(R.id.passwordConfirmation)!! + +val Activity.login: View + get() = findViewById(R.id.login)!! + diff --git a/compiler/testData/android/layout.xml b/compiler/testData/android/layout.xml new file mode 100644 index 00000000000..364427e595d --- /dev/null +++ b/compiler/testData/android/layout.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + +