android UI xml bridge initial

- add frontend.android module
- add xml ids to kotlin extension properties converter
- add converter basic functional test
This commit is contained in:
Mikhail Mutcianko
2014-07-09 16:24:36 +04:00
committed by Yan Zhulanow
parent c8c1ce521b
commit 1e8769b5f7
11 changed files with 411 additions and 1 deletions
+1
View File
@@ -18,6 +18,7 @@
<module fileurl="file://$PROJECT_DIR$/core/descriptors/descriptors.iml" filepath="$PROJECT_DIR$/core/descriptors/descriptors.iml" group="core" />
<module fileurl="file://$PROJECT_DIR$/eval4j/eval4j.iml" filepath="$PROJECT_DIR$/eval4j/eval4j.iml" group="ide" />
<module fileurl="file://$PROJECT_DIR$/compiler/frontend/frontend.iml" filepath="$PROJECT_DIR$/compiler/frontend/frontend.iml" group="compiler" />
<module fileurl="file://$PROJECT_DIR$/compiler/frontend.android/frontend.android.iml" filepath="$PROJECT_DIR$/compiler/frontend.android/frontend.android.iml" group="compiler" />
<module fileurl="file://$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" filepath="$PROJECT_DIR$/compiler/frontend.java/frontend.java.iml" group="compiler/java" />
<module fileurl="file://$PROJECT_DIR$/generators/generators.iml" filepath="$PROJECT_DIR$/generators/generators.iml" group="infrastructure" />
<module fileurl="file://$PROJECT_DIR$/grammar/grammar.iml" filepath="$PROJECT_DIR$/grammar/grammar.iml" group="compiler" />
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-runtime" level="project" />
<orderEntry type="library" name="intellij-core" level="project" />
<orderEntry type="module" module-name="frontend" />
</component>
</module>
@@ -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<File>) {
val ids: MutableCollection<AndroidID> = 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<File>?): Collection<File> {
if (paths == null) return ArrayList(0)
val res = ArrayList<File>()
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()
}
}
@@ -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
}
}
@@ -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<DefaultHandler>.startDocument()
}
override fun endDocument() {
super<DefaultHandler>.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<String, String> {
val res = HashMap<String, String>()
for (index in 0..getLength()-1) {
val attrName = getLocalName(index)!!
val attrVal = getValue(index)!!
res[attrName] = attrVal
}
return res
}
}
@@ -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<Context>()
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<T : Context>(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()
}
}
@@ -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<String>?,
retType: String,
body: Collection<String>) {
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<String>?,
retType: String,
body: Collection<String>) {
writeFunction("$receiver.$name", args, retType, body)
}
fun writeImmutableProperty(name: String,
retType: String,
getterBody: Collection<String>) {
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<String>) {
writeImmutableProperty("$receiver.$name", retType, getterBody)
}
fun writeImport(what: String) {
ctx.writeln("import $what")
}
fun writeEmptyLine() {
ctx.newLine()
}
fun output() = ctx.buffer
}
+21
View File
@@ -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)!!
+45
View File
@@ -0,0 +1,45 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ItemDetailActivity"
tools:ignore="MergeRootFrame" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Enter your password" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword" >
<requestFocus />
</EditText>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Repeat your password" />
<EditText
android:id="@+id/passwordConfirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword" />
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sign in" />
</FrameLayout>
+2 -1
View File
@@ -21,5 +21,6 @@
<orderEntry type="module" module-name="js.tests" />
<orderEntry type="module" module-name="preloader" />
<orderEntry type="module" module-name="util" />
<orderEntry type="module" module-name="frontend.android" scope="TEST" />
</component>
</module>
</module>
@@ -0,0 +1,49 @@
package org.jetbrains.jet.codegen;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.JetTestCaseBuilder;
import org.jetbrains.jet.lang.resolve.android.AndroidUIXmlParser;
import org.jetbrains.jet.test.TestCaseWithTmpdir;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
public class AndroidXmlTest extends TestCaseWithTmpdir {
@Override
public void setUp() throws Exception {
super.setUp();
}
@NotNull
private static String getTestDataPath() {
return JetTestCaseBuilder.getTestDataPathBase() + "/android";
}
@SuppressWarnings({"ResultOfMethodCallIgnored", "IOResourceOpenedButNotSafelyClosed"})
protected static String loadOrCreate(File file, String data) throws IOException {
try {
return new Scanner(file, "UTF-8" ).useDelimiter("\\A").next();
} catch (IOException e) {
file.createNewFile();
FileWriter fileWriter = new FileWriter(file);
fileWriter.write(data);
fileWriter.close();
fail("Empty expected data, creating from actual");
return data;
}
}
public void testConverterOneFile() throws Exception {
ArrayList<File> paths = new ArrayList<File>();
paths.add(new File(getTestDataPath() + "/layout.xml"));
AndroidUIXmlParser parser = new AndroidUIXmlParser(null, paths);
String actual = parser.parse();
String expected = loadOrCreate(new File(getTestDataPath() + "/layout.kt"), actual);
assertEquals(expected, actual);
}
}