blob: 894dd1b15dfe5d037ddf0b88581b8d2c957c3d37 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2i.classid
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.util.PsiTreeUtil
import org.eclipse.scout.sdk.core.log.SdkLog
import org.eclipse.scout.sdk.core.log.SdkLog.onTrace
import org.eclipse.scout.sdk.core.s.IScoutRuntimeTypes
import org.eclipse.scout.sdk.core.s.dto.AbstractDtoGenerator
import org.eclipse.scout.sdk.s2i.environment.TransactionManager
import org.eclipse.scout.sdk.s2i.findAllTypesAnnotatedWith
import java.util.Collections.newSetFromMap
import java.util.concurrent.ConcurrentHashMap
class ClassIdCacheImplementor(val project: Project) : PsiTreeChangeAdapter(), ClassIdCache {
private val m_classIdCache = ConcurrentHashMap<String /* class id */, MutableSet<String /* fqn */>>()
private val m_fileCache = ConcurrentHashMap<String /* file path */, MutableMap<String /* fqn */, String /* classid */>>()
private val m_stopTypes: Array<Class<out PsiElement>> = arrayOf(PsiClass::class.java, PsiModifierList::class.java, PsiTypeElement::class.java, PsiTypeParameter::class.java)
@Volatile
private var m_cacheReady = false
init {
Disposer.register(project, this) // ensure it is disposed when the project closes
}
override fun findAllClassIds(scope: SearchScope, indicator: ProgressIndicator?) =
project.findAllTypesAnnotatedWith(IScoutRuntimeTypes.ClassId, scope, indicator)
.filter { it.isValid }
.mapNotNull { ClassIdAnnotation.of(it) }
.filter { it.hasValue() }
override fun typesWithClassId(classId: String): Set<String> = synchronized(m_classIdCache) {
// synchronized so that asking for the cache waits until it is initially built
return classIdCache()[classId] ?: emptySet()
}
override fun dispose() {
PsiManager.getInstance(project).removePsiTreeChangeListener(this)
fileCache().clear()
classIdCache().clear()
}
override fun isCacheReady() = m_cacheReady
override fun setup() = synchronized(m_classIdCache) {
if (isCacheReady()) {
return
}
try {
TransactionManager.repeatUntilPassesWithIndex(project, false) {
trySetupCache()
PsiManager.getInstance(project).addPsiTreeChangeListener(this) // initial cache is ready, register listener to keep it up to date
m_cacheReady = true
}
duplicates().forEach { SdkLog.debug("Duplicate @ClassId value '{}' found for types {}.", it.key, it.value) }
} catch (e: ProcessCanceledException) {
SdkLog.debug("@ClassId value cache creation canceled. Retry on next use.", onTrace(e))
} catch (t: Exception) {
SdkLog.warning("Error building @ClassId value cache.", t)
}
}
override fun duplicates(): Map<String, Set<String>> = classIdCache()
.filter { it.value.size > 1 }
override fun duplicates(absoluteFilePath: String): Map<String, Set<String>> {
val classIdCache = classIdCache()
val classIdsInFile = fileCache()[absoluteFilePath]
?.map { it.value }
?.toSet() ?: return emptyMap()
return classIdCache
.filter { classIdsInFile.contains(it.key) }
.filter { it.value.size > 1 }
}
internal fun trySetupCache() {
SdkLog.debug("Starting to build @ClassId value cache.")
val start = System.currentTimeMillis()
val fileCache = fileCache()
val allClassIds = findAllClassIds(GlobalSearchScope.projectScope(project))
.filter { !ignoreClassId(it) }
.toList() // collect to list so that it is executed in the read action (terminal operation)
allClassIds.forEach {
val fqn = it.ownerFqn() ?: return@forEach
val classIdValue = it.value() ?: return@forEach
val filePath = it.psiClass.containingFile.virtualFile.path
updateOrAddType(fqn, classIdValue, null)
fileCache.computeIfAbsent(filePath) { ConcurrentHashMap() }[fqn] = classIdValue
}
SdkLog.debug("Finished building initial @ClassId value cache in {}ms. {} distinct @ClassId values found in {} files.", System.currentTimeMillis() - start, classIdCache().size, fileCache.size)
}
internal fun ignoreClassId(classId: ClassIdAnnotation) =
classId.value()?.endsWith(AbstractDtoGenerator.FORMDATA_CLASSID_SUFFIX) ?: true
override fun childrenChanged(event: PsiTreeChangeEvent) {
val file = event.file ?: return
val path = file.virtualFile.path
val mappingsInFile = ConcurrentHashMap<String /* fqn */, String /* classid value */>()
file.accept(object : JavaRecursiveElementWalkingVisitor() {
override fun visitLiteralExpression(expression: PsiLiteralExpression) {
if (expression.parent !is PsiNameValuePair) {
return
}
val declaringAnnotation = PsiTreeUtil.getParentOfType(expression, PsiAnnotation::class.java, false, *m_stopTypes) ?: return
val classId = ClassIdAnnotation.of(declaringAnnotation) ?: return
if (ignoreClassId(classId)) {
return
}
val value = classId.value() ?: return
val qualifiedName = classId.ownerFqn() ?: return
mappingsInFile[qualifiedName] = value
}
})
updateCache(path, mappingsInFile)
}
internal fun updateCache(file: String, mappingsInFile: MutableMap<String /* fqn */, String /* classid value */>) {
val fileCache = fileCache()
try {
val oldMappings = fileCache[file]
if (oldMappings == null) {
// new file
mappingsInFile.forEach { updateOrAddType(it.key, it.value, null) }
return
}
val newOrChangedMappings = HashMap(mappingsInFile)
newOrChangedMappings.entries.removeAll(oldMappings.entries) // remove all entries that did not change (the same for key and value)
newOrChangedMappings.forEach { updateOrAddType(it.key, it.value, oldMappings[it.key]) }
val removedMappings = HashMap(oldMappings)
mappingsInFile.keys.forEach { removedMappings.remove(it) }
removedMappings.forEach { removeType(it.key, it.value) }
} finally {
// ensure file cache is update to date
if (mappingsInFile.isEmpty()) {
fileCache.remove(file)
} else {
fileCache[file] = mappingsInFile
}
}
}
internal fun fileCache() = m_fileCache
internal fun classIdCache() = m_classIdCache
internal fun updateOrAddType(fqn: String, newClassId: String, oldClassId: String?) {
classIdCache().computeIfAbsent(newClassId) { newSetFromMap(ConcurrentHashMap(1)) }.add(fqn)
if (oldClassId != null) {
removeType(fqn, oldClassId)
}
}
internal fun removeType(fqn: String, classId: String) {
val cache = classIdCache()
val usedClassIds = cache[classId] ?: return
usedClassIds.remove(fqn)
if (usedClassIds.isEmpty()) {
cache.remove(classId)
}
}
}