Refactor Android plugin

This commit is contained in:
Yan Zhulanow
2014-12-30 19:18:13 +03:00
parent 4f9aba2cdc
commit 346dbe3f7c
23 changed files with 311 additions and 292 deletions
@@ -83,7 +83,7 @@ public class AndroidCommandLineProcessor : CommandLineProcessor {
public class CliAndroidDeclarationsProvider(private val project: Project) : ExternalDeclarationsProvider {
override fun getExternalDeclarations(moduleInfo: ModuleInfo?): Collection<JetFile> {
val parser = ServiceManager.getService<AndroidUIXmlProcessor>(project, javaClass<AndroidUIXmlProcessor>())
return emptyOrSingletonList(parser?.parseToPsi(project))
return emptyOrSingletonList(parser.parseToPsi())
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.jet.lang.resolve.android
import com.intellij.openapi.util.Key
public object AndroidConst {
val ANDROID_USER_PACKAGE: Key<String> = Key.create<String>("ANDROID_USER_PACKAGE")
val SYNTHETIC_FILENAME: String = "ANDROIDXML.kt"
val ANDROID_NAMESPACE: String = "android"
val ID_ATTRIBUTE_NO_NAMESPACE: String = "id"
val ID_ATTRIBUTE: String = "$ANDROID_NAMESPACE:$ID_ATTRIBUTE_NO_NAMESPACE"
val CLASS_ATTRIBUTE_NO_NAMESPACE: String = "class"
val ID_DECLARATION_PREFIX = "@+id/"
val ID_USAGE_PREFIX = "@id/"
}
class WrongIdFormat(id: String) : Exception("Id \"$id\" has wrong format")
public fun nameToIdDeclaration(name: String): String = AndroidConst.ID_DECLARATION_PREFIX + name
public fun idToName(id: String): String {
return if (isResourceIdDeclaration(id)) id.replace(AndroidConst.ID_DECLARATION_PREFIX, "")
else if (isResourceIdUsage(id)) id.replace(AndroidConst.ID_USAGE_PREFIX, "")
else throw WrongIdFormat(id)
}
public fun isResourceIdDeclaration(str: String?): Boolean = str?.startsWith(AndroidConst.ID_DECLARATION_PREFIX) ?: false
public fun isResourceIdUsage(str: String?): Boolean = str?.startsWith(AndroidConst.ID_USAGE_PREFIX) ?: false
public fun isResourceDeclarationOrUsage(id: String?): Boolean = isResourceIdDeclaration(id) || isResourceIdUsage(id)
@@ -19,32 +19,47 @@ package org.jetbrains.jet.lang.resolve.android
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiElement
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
import com.intellij.openapi.vfs.VirtualFile
public abstract class AndroidResourceManager(protected val project: Project, protected val searchPath: String?) {
private val idDeclarationPrefix = "@+id/"
private val idUsagePrefix = "@id/"
public val androidNamespace: String = "android"
public val idAttributeNoNamespace: String = "id"
public val idAttribute: String = androidNamespace + ":" + idAttributeNoNamespace
public val classAttributeNoNamespace: String = "class"
public val classAttribute: String = androidNamespace + ":" + classAttributeNoNamespace
public abstract class AndroidResourceManager(val project: Project) {
abstract fun getLayoutXmlFiles(): Collection<PsiFile>
public abstract fun idToXmlAttribute(id: String): PsiElement?
public fun nameToIdDeclaration(name: String): String = idDeclarationPrefix + name
public fun nameToIdUsage(name: String): String = idUsagePrefix + name
public fun idToName(id: String?): String {
return if (isResourceIdDeclaration(id)) id!!.replace(idDeclarationPrefix, "")
else if (isResourceIdUsage(id)) id!!.replace(idUsagePrefix, "")
else throw WrongIdFormat(id)
public abstract val androidModuleInfo: AndroidModuleInfo?
public open fun idToXmlAttribute(id: String): PsiElement? = null
open fun getLayoutXmlFiles(): List<PsiFile> {
val info = androidModuleInfo
if (info == null) return listOf()
val psiManager = PsiManager.getInstance(project)
val fileManager = VirtualFileManager.getInstance()
fun VirtualFile.getAllChildren(): List<VirtualFile> {
val allChildren = arrayListOf<VirtualFile>()
val currentChildren = getChildren() ?: array()
for (child in currentChildren) {
if (child.isDirectory()) {
allChildren.addAll(child.getAllChildren())
}
else {
allChildren.add(child)
}
}
return allChildren
}
val resDirectory = fileManager.findFileByUrl("file://" + info.mainResDirectory)
val allChildren = resDirectory?.getAllChildren() ?: listOf()
return allChildren
.filter { it.getParent().getName().startsWith("layout") && it.getName().toLowerCase().endsWith(".xml") }
.map { psiManager.findFile(it) }
.filterNotNull()
.sortBy { it.getName() }
}
public fun isResourceIdDeclaration(str: String?): Boolean = str?.startsWith(idDeclarationPrefix) ?: false
public fun isResourceIdUsage(str: String?): Boolean = str?.startsWith(idUsagePrefix) ?: false
public fun isResourceDeclarationOrUsage(id: String?): Boolean = isResourceIdDeclaration(id) || isResourceIdUsage(id)
abstract fun readManifest(): AndroidManifest
inner class NoUIXMLsFound : Exception("No android UI xmls found in $searchPath")
inner class WrongIdFormat(id: String?) : Exception("Id \"$id\" has wrong format")
inner class NoLayoutXmlFound : Exception("No android UI xmls found in " + androidModuleInfo?.mainResDirectory)
}
@@ -1,45 +0,0 @@
/*
* Copyright 2010-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.jet.lang.resolve.android
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
import java.util.ArrayList
import com.intellij.openapi.vfs.VirtualFile
abstract class AndroidResourceManagerBase(project: Project, searchPath: String?) : AndroidResourceManager(project, searchPath) {
override fun getLayoutXmlFiles(): Collection<PsiFile> {
val fileManager = VirtualFileManager.getInstance()
val watchDir = fileManager.findFileByUrl("file://" + searchPath)
val psiManager = PsiManager.getInstance(project)
val files = watchDir?.getChildren()?.toArrayList()?.map { psiManager.findFile(it) }?.mapNotNull { it } ?: listOf()
return files.sortBy({it.getName()})
}
protected fun virtualFileToPsi(vf: VirtualFile): PsiFile? {
val psiManager = PsiManager.getInstance(project)
return psiManager.findFile(vf)
}
override fun idToXmlAttribute(id: String): PsiElement? {
return null
}
}
@@ -46,124 +46,89 @@ import com.intellij.psi.impl.PsiModificationTrackerImpl
import java.util.Queue
import com.intellij.psi.PsiFile
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.jet.lang.resolve.android.AndroidConst.*
import org.jetbrains.jet.analyzer.ModuleInfo
import com.intellij.openapi.module.Module
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.CachedValueProvider.Result
import java.util.concurrent.atomic.AtomicInteger
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.openapi.util.ModificationTracker
public abstract class AndroidUIXmlProcessor(protected val project: Project) {
public class NoAndroidManifestFound : Exception("No android manifest file found in project root")
private enum class CacheAction { HIT; MISS }
private val androidImports = listOf(
"android.app.Activity",
"android.view.View",
"android.widget.*")
private val androidImports = arrayListOf("android.app.Activity",
"android.view.View",
"android.widget.*")
//TODO
private val cachedSources = cachedValue {
Result.create(parse(), ProjectRootModificationTracker.getInstance(project))
}
protected abstract val searchPath: String?
protected abstract val androidAppPackage: String
private val cachedJetFiles = cachedValue {
val psiManager = PsiManager.getInstance(project)
val applicationPackage = resourceManager.androidModuleInfo?.applicationPackage
private val fileCache = HashMap<PsiFile, String>()
var lastCachedPsi: JetFile? = null
private set
protected val fileModificationTime: HashMap<PsiFile, Long> = HashMap()
val jetFiles = cachedSources.getValue().mapIndexed { (index, text) ->
val virtualFile = LightVirtualFile(AndroidConst.SYNTHETIC_FILENAME + index + ".kt", text)
val jetFile = psiManager.findFile(virtualFile) as JetFile
if (applicationPackage != null) {
jetFile.putUserData(AndroidConst.ANDROID_USER_PACKAGE, applicationPackage)
}
jetFile
}
protected val filesToProcess: Queue<PsiFile> = ConcurrentLinkedQueue()
Result.create(jetFiles, cachedSources)
}
public abstract val resourceManager: AndroidResourceManager
protected val LOG: Logger = Logger.getInstance(this.javaClass)
protected val LOG: Logger = Logger.getInstance(javaClass)
public fun parseToString(): String? {
val cacheState = doParse()
if (cacheState == null) return null
return renderString()
public fun parse(): List<String> {
return resourceManager.getLayoutXmlFiles().map { file ->
val widgets = parseSingleFile(file)
if (widgets.isNotEmpty()) {
val layoutPackage = file.genSyntheticPackageName()
val stringWriter = KotlinStringWriter()
stringWriter.writePackage(layoutPackage)
stringWriter.writeAndroidImports()
widgets.forEach { stringWriter.writeSyntheticActivityProperty(it) }
val contents = stringWriter.toStringBuffer().toString()
contents
} else null
}.filterNotNull()
}
public fun parseToPsi(): List<JetFile>? = cachedJetFiles.getValue()
public fun parseToPsi(project: Project): JetFile? {
val cacheState = doParse()
if (cacheState == null) return null
return if (cacheState == CacheAction.MISS || lastCachedPsi == null) {
try {
val vf = LightVirtualFile(SYNTHETIC_FILENAME, renderString())
val psiFile = PsiManager.getInstance(project).findFile(vf) as JetFile
psiFile.putUserData(ANDROID_USER_PACKAGE, androidAppPackage)
lastCachedPsi = psiFile
psiFile
}
catch (e: Exception) {
invalidateCaches()
null
}
}
else lastCachedPsi
protected abstract fun parseSingleFile(file: PsiFile): Collection<AndroidWidget>
private fun KotlinStringWriter.writeAndroidImports() {
androidImports.forEach { writeImport(it) }
writeEmptyLine()
}
private fun writeImports(kw: KotlinStringWriter): KotlinWriter {
kw.writePackage(androidAppPackage)
for (elem in androidImports)
kw.writeImport(elem)
kw.writeEmptyLine()
return kw
private fun PsiFile.genSyntheticPackageName(): String {
return AndroidConst.SYNTHETIC_PACKAGE + getName().substringBefore('.')
}
private fun parseSingleFileWithCache(file: PsiFile): Pair<String, CacheAction> {
val lastRecorded = fileModificationTime[file] ?: -1
if (file.getModificationStamp() > lastRecorded)
return Pair(parseSingleFile(file), CacheAction.MISS)
else
return Pair(fileCache[file]!!, CacheAction.HIT)
private fun KotlinStringWriter.writeSyntheticActivityProperty(widget: AndroidWidget) {
val body = arrayListOf("return findViewById(0) as ${widget.className}")
writeImmutableExtensionProperty(receiver = "Activity",
name = widget.id,
retType = widget.className,
getterBody = body)
}
private fun parseSingleFile(file: PsiFile): String {
val res = parseSingleFileImpl(file)
fileModificationTime[file] = file.getModificationStamp()
fileCache[file] = res
return res
private fun <T> cachedValue(result: () -> CachedValueProvider.Result<T>): CachedValue<T> {
return CachedValuesManager.getManager(project).createCachedValue(result, false)
}
protected abstract fun parseSingleFileImpl(file: PsiFile): String
private fun doParse(): CacheAction? {
if (searchPath == null || searchPath == "") return null
populateQueue()
var overallCacheMiss = false
var file = filesToProcess.poll()
while (file != null) {
val res = parseSingleFileWithCache(file!!)
overallCacheMiss = overallCacheMiss or (res.second == CacheAction.MISS)
file = filesToProcess.poll()
}
return if (overallCacheMiss) CacheAction.MISS else CacheAction.HIT
}
private fun renderString(): String {
val buffer = writeImports(KotlinStringWriter()).output()
for (buf in fileCache.entrySet().sortBy({it.key.getName()}))
buffer.append(buf.value)
return buffer.toString()
}
private fun invalidateCaches() {
fileCache.clear()
fileModificationTime.clear()
lastCachedPsi = null
}
protected fun populateQueue() {
filesToProcess.addAll(resourceManager.getLayoutXmlFiles())
}
protected fun produceKotlinProperties(kw: KotlinStringWriter, ids: Collection<AndroidWidget>): StringBuffer {
for (id in ids) {
val body = arrayListOf("return findViewById(0) as ${id.className}")
kw.writeImmutableExtensionProperty(receiver = "Activity",
name = id.id,
retType = id.className,
getterBody = body)
}
return kw.output()
}
}
@@ -26,25 +26,7 @@ import com.intellij.psi.PsiClass
import org.jetbrains.jet.utils.emptyOrSingletonList
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Computable
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
}
}
public class AndroidWidget(val id: String, val className: String) : AndroidResource
public class AndroidManifest(public val _package: String) : AndroidResource
import com.intellij.openapi.module.ModuleServiceManager
fun isAndroidSyntheticFile(f: PsiFile?): Boolean {
return f?.getUserData(org.jetbrains.jet.lang.resolve.android.AndroidConst.ANDROID_USER_PACKAGE) != null
@@ -55,15 +37,3 @@ public fun isAndroidSyntheticElement(element: PsiElement?): Boolean {
element?.getContainingFile()
}))
}
public fun isRClassField(element: PsiElement): Boolean {
return if (element is PsiField) {
val outerClass = element.getParent()?.getParent()
if (outerClass !is PsiClass) return false
val processor = ServiceManager.getService<AndroidUIXmlProcessor>(element.getProject(), javaClass<AndroidUIXmlProcessor>())
val packageName = processor?.resourceManager?.readManifest()?._package
if ((outerClass : PsiClass).getQualifiedName()?.startsWith(packageName ?: "") ?: false)
true else false
}
else false
}
@@ -20,7 +20,10 @@ import org.xml.sax.helpers.DefaultHandler
import org.xml.sax.Attributes
import java.util.HashMap
class AndroidXmlHandler(private val resourceManager: AndroidResourceManager, private val elementCallback: (String, String) -> Unit) : DefaultHandler() {
class AndroidXmlHandler(
private val resourceManager: AndroidResourceManager,
private val elementCallback: (String, String) -> Unit
) : DefaultHandler() {
override fun startDocument() {
super<DefaultHandler>.startDocument()
@@ -32,9 +35,9 @@ class AndroidXmlHandler(private val resourceManager: AndroidResourceManager, pri
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
val attributesMap = attributes.toMap()
val idAttr = attributesMap[resourceManager.idAttributeNoNamespace]
val classNameAttr = attributesMap[resourceManager.classAttributeNoNamespace] ?: localName
if (resourceManager.isResourceDeclarationOrUsage(idAttr)) elementCallback(resourceManager.idToName(idAttr), classNameAttr)
val idAttr = attributesMap[AndroidConst.ID_ATTRIBUTE_NO_NAMESPACE]
val classNameAttr = attributesMap[AndroidConst.CLASS_ATTRIBUTE_NO_NAMESPACE] ?: localName
if (isResourceDeclarationOrUsage(idAttr)) elementCallback(idToName(idAttr), classNameAttr)
}
override fun endElement(uri: String?, localName: String, qName: String) {
@@ -23,35 +23,46 @@ import org.xml.sax.helpers.DefaultHandler
import org.xml.sax.Attributes
import javax.xml.parsers.SAXParser
import javax.xml.parsers.SAXParserFactory
import kotlin.properties.Delegates
public class CliAndroidResourceManager(project: Project, searchPath: String?, private val manifestPath: String?) : org.jetbrains.jet.lang.resolve.android.AndroidResourceManagerBase(project, searchPath) {
public class CliAndroidResourceManager(
project: Project,
private val manifestPath: String,
private val mainResDirectory: String
) : AndroidResourceManager(project) {
override val androidModuleInfo by Delegates.lazy {
AndroidModuleInfo(getApplicationPackage(manifestPath), mainResDirectory)
}
val saxParser: SAXParser = initSAX()
protected fun initSAX(): SAXParser {
val saxFactory = SAXParserFactory.newInstance()
saxFactory?.setNamespaceAware(true)
return saxFactory!!.newSAXParser()
saxFactory.setNamespaceAware(true)
return saxFactory.newSAXParser()
}
override fun readManifest(): org.jetbrains.jet.lang.resolve.android.AndroidManifest {
private fun getApplicationPackage(manifestPath: String): String {
try {
val manifestXml = File(manifestPath!!)
var _package: String = ""
val manifestXml = File(manifestPath)
var applicationPackage: String = ""
try {
saxParser.parse(FileInputStream(manifestXml), object : DefaultHandler() {
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
if (localName == "manifest")
_package = attributes.toMap()["package"] ?: ""
applicationPackage = attributes.toMap()["package"] ?: ""
}
})
}
catch (e: Exception) {
throw e
}
return org.jetbrains.jet.lang.resolve.android.AndroidManifest(_package)
return applicationPackage
}
catch (e: Exception) {
throw org.jetbrains.jet.lang.resolve.android.AndroidUIXmlProcessor.NoAndroidManifestFound()
throw AndroidUIXmlProcessor.NoAndroidManifestFound()
}
}
}
@@ -20,17 +20,21 @@ import java.util.ArrayList
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import java.io.ByteArrayInputStream
import kotlin.properties.Delegates
public class CliAndroidUIXmlProcessor(project: Project, override val searchPath: String?, val manifestPath: String?) : org.jetbrains.jet.lang.resolve.android.AndroidUIXmlProcessor(project) {
public class CliAndroidUIXmlProcessor(
project: Project,
private val manifestPath: String,
private val mainResDirectory: String
) : AndroidUIXmlProcessor(project) {
override var androidAppPackage: String = ""
get() = resourceManager.readManifest()._package
override val resourceManager: CliAndroidResourceManager by Delegates.lazy {
CliAndroidResourceManager(project, manifestPath, mainResDirectory)
}
override val resourceManager = org.jetbrains.jet.lang.resolve.android.CliAndroidResourceManager(project, searchPath, manifestPath)
override fun parseSingleFileImpl(file: PsiFile): String {
override fun parseSingleFile(file: PsiFile): String {
val ids: MutableCollection<AndroidWidget> = ArrayList()
val handler = org.jetbrains.jet.lang.resolve.android.AndroidXmlHandler(resourceManager, { id, wClass -> ids.add(AndroidWidget(id, wClass)) })
val handler = AndroidXmlHandler(resourceManager, { id, clazz -> ids.add(AndroidWidget(id, clazz)) })
try {
val inputStream = ByteArrayInputStream(file.getVirtualFile().contentsToByteArray())
resourceManager.saxParser.parse(inputStream, handler)
@@ -29,12 +29,12 @@ class KotlinStringWriter : KotlinWriter {
fun writeFunction(name: String,
args: Collection<String>?,
retType: String,
stmts: Collection<String>) {
statements: Collection<String>) {
val returnTerm = if (retType == "" || retType == "Unit") "" else ": $retType"
val argStr = if (args != null) args.join(", ") else ""
body.writeln("fun $name($argStr)$returnTerm {")
body.incIndent()
for (stmt in stmts)
for (stmt in statements)
body.writeln(stmt)
body.decIndent()
body.writeln("}")
@@ -14,11 +14,10 @@
* limitations under the License.
*/
package org.jetbrains.jet.lang.resolve.android;
package org.jetbrains.jet.lang.resolve.android
import com.intellij.openapi.util.Key;
public data class AndroidModuleInfo(val applicationPackage: String, val mainResDirectory: String?)
public class AndroidConst {
public static final Key<String> ANDROID_USER_PACKAGE = Key.create("ANDROID_USER_PACKAGE");
public static final String SYNTHETIC_FILENAME = "ANDROIDXML.kt";
}
trait AndroidResource
public class AndroidWidget(val id: String, val className: String) : AndroidResource