Skip to content

Commit ae2d567

Browse files
authored
Introduce utbot-spring-analyzer module (#1818)
1 parent 18fcbdd commit ae2d567

13 files changed

+427
-0
lines changed

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,5 @@ if (goIde.split(",").contains(ideType)) {
6969
include("utbot-cli-go")
7070
include("utbot-intellij-go")
7171
}
72+
73+
include("utbot-spring-analyzer")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
id("org.springframework.boot") version "2.7.8"
3+
id("io.spring.dependency-management") version "1.1.0"
4+
id("java")
5+
}
6+
7+
java {
8+
sourceCompatibility = JavaVersion.VERSION_11
9+
targetCompatibility = JavaVersion.VERSION_17
10+
}
11+
12+
dependencies {
13+
implementation("org.springframework.boot:spring-boot-starter")
14+
implementation("org.springframework.boot:spring-boot-starter-web")
15+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package application
2+
3+
import analyzers.SpringApplicationAnalyzer
4+
import utils.PathsUtils
5+
6+
/**
7+
* To run this app, arguments must be passed in the following way:
8+
* args[0] - classpath of current project
9+
* args[1] - fully qualified name of configuration class
10+
* args[2] - `.properties` file paths, separated via `;`, empty string if no files exist
11+
* args[3] - `.xml` configuration file paths
12+
*
13+
* Several items in one arg are separated via `;`.
14+
* If there are no files, empty string should be passed.
15+
*/
16+
fun main(args: Array<String>) {
17+
18+
/* FOR EXAMPLE
19+
val arg0 = "/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/build/classes/java/main"
20+
val arg1 = "com.dmdev.spring.config.ApplicationConfiguration"
21+
val arg2 = "/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/src/main/resources/application.properties;/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/src/main/resources/application-web.properties"
22+
val arg3 = "/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/src/main/resources/application.xml;/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/src/main/resources/application2.xml"
23+
*/
24+
25+
val springApplicationAnalyzer = SpringApplicationAnalyzer(
26+
applicationPath = args[0],
27+
configurationClassFqn = args[1],
28+
propertyFilesPaths = args[2].split(";").filter { it != PathsUtils.EMPTY_PATH },
29+
xmlConfigurationPaths = args[3].split(";").filter { it != PathsUtils.EMPTY_PATH },
30+
)
31+
32+
springApplicationAnalyzer.analyze()
33+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package analyzers
2+
3+
import application.utils.FakeFileManager
4+
import application.configurators.PropertiesConfigurator
5+
import application.configurators.XmlFilesConfigurator
6+
import config.TestApplicationConfiguration
7+
import org.springframework.boot.builder.SpringApplicationBuilder
8+
import org.springframework.context.ApplicationContextException
9+
import utils.ConfigurationManager
10+
import java.net.URL
11+
import java.net.URLClassLoader
12+
import java.nio.file.Path
13+
14+
15+
class SpringApplicationAnalyzer(
16+
private val applicationPath: String,
17+
private val configurationClassFqn: String,
18+
private val propertyFilesPaths: List<String>,
19+
private val xmlConfigurationPaths: List<String>,
20+
) {
21+
22+
private val applicationUrl: URL
23+
get() = Path.of(applicationPath).toUri().toURL()
24+
25+
fun analyze() {
26+
val fakeFileManager = FakeFileManager(propertyFilesPaths + xmlConfigurationPaths)
27+
fakeFileManager.createFakeFiles()
28+
29+
val classLoader: ClassLoader = URLClassLoader(arrayOf(applicationUrl))
30+
val userConfigurationClass = classLoader.loadClass(configurationClassFqn)
31+
32+
val configurationManager = ConfigurationManager(classLoader, userConfigurationClass)
33+
val propertiesConfigurator = PropertiesConfigurator(propertyFilesPaths, configurationManager)
34+
val xmlFilesConfigurator = XmlFilesConfigurator(xmlConfigurationPaths, configurationManager)
35+
36+
propertiesConfigurator.configure()
37+
xmlFilesConfigurator.configure()
38+
39+
val app = SpringApplicationBuilder(SpringApplicationAnalyzer::class.java)
40+
app.sources(TestApplicationConfiguration::class.java, userConfigurationClass)
41+
for (prop in propertiesConfigurator.readProperties()) {
42+
app.properties(prop)
43+
}
44+
45+
try {
46+
app.build()
47+
app.run()
48+
} catch (e: ApplicationContextException) {
49+
println("Bean analysis finished successfully")
50+
}finally {
51+
fakeFileManager.deleteFakeFiles()
52+
}
53+
}
54+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package analyzers
2+
3+
import org.w3c.dom.Document
4+
import org.w3c.dom.Element
5+
import javax.xml.parsers.DocumentBuilderFactory
6+
import javax.xml.transform.TransformerFactory
7+
import javax.xml.transform.dom.DOMSource
8+
import javax.xml.transform.stream.StreamResult
9+
10+
class XmlConfigurationAnalyzer(private val userXmlFilePath: String, private val fakeXmlFilePath: String) {
11+
12+
fun fillFakeApplicationXml() {
13+
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
14+
val doc = builder.parse(userXmlFilePath)
15+
16+
// Property placeholders may contain file names relative to user project,
17+
// they will not be found in ours. We import all properties using another approach.
18+
deletePropertyPlaceholders(doc)
19+
writeXmlFile(doc)
20+
}
21+
22+
private fun deletePropertyPlaceholders(doc: Document) {
23+
val elements = doc.getElementsByTagName("context:property-placeholder")
24+
val elementsCount = elements.length
25+
26+
// Xml file may contain several property placeholders:
27+
// see https://stackoverflow.com/questions/26618400/how-to-use-multiple-property-placeholder-in-a-spring-xml-file
28+
for (i in 0 until elementsCount) {
29+
val element = elements.item(i) as Element
30+
element.parentNode.removeChild(element)
31+
}
32+
33+
doc.normalize()
34+
}
35+
36+
private fun writeXmlFile(doc: Document) {
37+
val tFormer = TransformerFactory.newInstance().newTransformer()
38+
val source = DOMSource(doc)
39+
val destination = StreamResult(fakeXmlFilePath)
40+
41+
tFormer.transform(source, destination)
42+
}
43+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package config
2+
3+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
4+
import org.springframework.context.annotation.Bean
5+
import org.springframework.context.annotation.Configuration
6+
import post_processors.UtBotBeanFactoryPostProcessor
7+
8+
@Configuration
9+
open class TestApplicationConfiguration {
10+
11+
@Bean
12+
open fun utBotBeanFactoryPostProcessor(): BeanFactoryPostProcessor {
13+
return UtBotBeanFactoryPostProcessor()
14+
}
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package application.configurators
2+
3+
import utils.ConfigurationManager
4+
import utils.PathsUtils
5+
import java.io.BufferedReader
6+
import java.io.FileReader
7+
import kotlin.io.path.Path
8+
9+
class PropertiesConfigurator(
10+
private val propertiesFilesPaths: List<String>,
11+
private val configurationManager: ConfigurationManager
12+
) {
13+
14+
fun configure() {
15+
configurationManager.clearPropertySourceAnnotation()
16+
17+
propertiesFilesPaths
18+
.map { Path(it).fileName }
19+
.forEach { fileName -> configurationManager.patchPropertySourceAnnotation(fileName) }
20+
}
21+
22+
fun readProperties(): ArrayList<String> {
23+
val props = ArrayList<String>()
24+
25+
for (propertiesFilePath in propertiesFilesPaths) {
26+
if (propertiesFilePath == PathsUtils.EMPTY_PATH) continue
27+
28+
val reader = BufferedReader(FileReader(propertiesFilePath))
29+
var line = reader.readLine()
30+
while (line != null) {
31+
props.add(line)
32+
line = reader.readLine()
33+
}
34+
35+
reader.close()
36+
}
37+
38+
return props
39+
}
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package application.configurators
2+
3+
import analyzers.XmlConfigurationAnalyzer
4+
import application.utils.FakeFileManager
5+
import utils.ConfigurationManager
6+
import utils.PathsUtils
7+
import kotlin.io.path.Path
8+
9+
class XmlFilesConfigurator(
10+
private val userXmlFilePaths: List<String>,
11+
private val configurationManager: ConfigurationManager,
12+
) {
13+
14+
fun configure() {
15+
configurationManager.clearImportResourceAnnotation()
16+
17+
for (userXmlFilePath in userXmlFilePaths) {
18+
if(userXmlFilePath == PathsUtils.EMPTY_PATH)continue
19+
20+
val xmlConfigurationAnalyzer =
21+
XmlConfigurationAnalyzer(userXmlFilePath, PathsUtils.createFakeFilePath(userXmlFilePath))
22+
23+
xmlConfigurationAnalyzer.fillFakeApplicationXml()
24+
configurationManager.patchImportResourceAnnotation(Path(userXmlFilePath).fileName)
25+
}
26+
}
27+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package post_processors
2+
3+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition
4+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor
5+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
6+
import org.springframework.beans.factory.support.BeanDefinitionRegistry
7+
import org.springframework.core.PriorityOrdered
8+
9+
import java.io.File
10+
import java.io.FileWriter
11+
import java.util.Arrays
12+
13+
class UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered {
14+
15+
/**
16+
* Sets the priority of post processor to highest to avoid side effects from others.
17+
*/
18+
override fun getOrder(): Int = PriorityOrdered.HIGHEST_PRECEDENCE
19+
20+
override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
21+
println("Started post-processing bean factory in UtBot")
22+
23+
val beanClassNames = findBeanClassNames(beanFactory)
24+
//TODO: will be replaced with more appropriate IPC approach.
25+
writeToFile(beanClassNames)
26+
27+
// After desired post-processing is completed we destroy bean definitions
28+
// to avoid further possible actions with beans that may be unsafe.
29+
destroyBeanDefinitions(beanFactory)
30+
31+
println("Finished post-processing bean factory in UtBot")
32+
}
33+
34+
private fun findBeanClassNames(beanFactory: ConfigurableListableBeanFactory): ArrayList<String> {
35+
val beanClassNames = ArrayList<String>()
36+
for (beanDefinitionName in beanFactory.beanDefinitionNames) {
37+
val beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName)
38+
39+
if (beanDefinition is AnnotatedBeanDefinition) {
40+
val factoryMethodMetadata = beanDefinition.factoryMethodMetadata
41+
if (factoryMethodMetadata != null) {
42+
beanClassNames.add(factoryMethodMetadata.returnTypeName)
43+
}
44+
} else {
45+
var className = beanDefinition.beanClassName
46+
if (className == null) {
47+
className = beanFactory.getBean(beanDefinitionName).javaClass.name
48+
}
49+
className?.let { beanClassNames.add(it) }
50+
}
51+
}
52+
53+
return beanClassNames
54+
}
55+
56+
private fun destroyBeanDefinitions(beanFactory: ConfigurableListableBeanFactory) {
57+
for (beanDefinitionName in beanFactory.beanDefinitionNames) {
58+
val beanRegistry = beanFactory as BeanDefinitionRegistry
59+
beanRegistry.removeBeanDefinition(beanDefinitionName)
60+
}
61+
}
62+
63+
private fun writeToFile(beanClassNames: ArrayList<String>) {
64+
try {
65+
val springBeansFile = File("SpringBeans.txt")
66+
val fileWriter = FileWriter(springBeansFile)
67+
68+
val distinctClassNames = beanClassNames.stream()
69+
.distinct()
70+
.toArray()
71+
Arrays.sort(distinctClassNames)
72+
73+
for (beanClassName in distinctClassNames) {
74+
fileWriter.append(beanClassName.toString())
75+
fileWriter.append("\n")
76+
}
77+
78+
fileWriter.flush()
79+
fileWriter.close()
80+
81+
println("Storing bean information completed successfully")
82+
} catch (e: Throwable) {
83+
println("Storing bean information failed with exception $e")
84+
}
85+
}
86+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package utils
2+
3+
import org.springframework.context.annotation.ImportResource
4+
import org.springframework.context.annotation.PropertySource
5+
import java.lang.reflect.InvocationHandler
6+
import java.nio.file.Path
7+
import java.util.Arrays
8+
import kotlin.reflect.KClass
9+
10+
class ConfigurationManager(private val classLoader: ClassLoader, private val userConfigurationClass: Class<*>) {
11+
12+
fun clearPropertySourceAnnotation() = patchAnnotation(PropertySource::class, null)
13+
14+
fun clearImportResourceAnnotation() = patchAnnotation(ImportResource::class, null)
15+
16+
fun patchPropertySourceAnnotation(userPropertiesFileName: Path) =
17+
patchAnnotation(PropertySource::class, String.format("classpath:%s", "fake_$userPropertiesFileName"))
18+
19+
fun patchImportResourceAnnotation(userXmlFilePath: Path) =
20+
patchAnnotation(ImportResource::class, String.format("classpath:%s", "fake_$userXmlFilePath"))
21+
22+
private fun patchAnnotation(annotationClass: KClass<*>, newValue: String?) {
23+
val proxyClass = classLoader.loadClass("java.lang.reflect.Proxy")
24+
val hField = proxyClass.getDeclaredField("h")
25+
hField.isAccessible = true
26+
27+
val propertySourceAnnotation = Arrays.stream(
28+
userConfigurationClass.annotations
29+
)
30+
.filter { el: Annotation -> el.annotationClass == annotationClass }
31+
.findFirst()
32+
33+
if (propertySourceAnnotation.isPresent) {
34+
val annotationInvocationHandler = hField[propertySourceAnnotation.get()] as InvocationHandler
35+
36+
val annotationInvocationHandlerClass =
37+
classLoader.loadClass("sun.reflect.annotation.AnnotationInvocationHandler")
38+
val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues")
39+
memberValuesField.isAccessible = true
40+
41+
val memberValues = memberValuesField[annotationInvocationHandler] as MutableMap<String, Any>
42+
addNewValue(memberValues, newValue)
43+
}
44+
}
45+
46+
private fun addNewValue(memberValues: MutableMap<String, Any>, newValue: String?){
47+
if(newValue == null){
48+
memberValues["value"] = Array(0){""}
49+
}
50+
else {
51+
val list: MutableList<String> = (memberValues["value"] as Array<String>).toMutableList()
52+
list.add(newValue)
53+
memberValues["value"] = list.toTypedArray()
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)