diff --git a/settings.gradle.kts b/settings.gradle.kts index 8337e46250..c80a375ffc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,3 +69,5 @@ if (goIde.split(",").contains(ideType)) { include("utbot-cli-go") include("utbot-intellij-go") } + +include("utbot-spring-analyzer") diff --git a/utbot-spring-analyzer/build.gradle.kts b/utbot-spring-analyzer/build.gradle.kts new file mode 100644 index 0000000000..ee9048e4a8 --- /dev/null +++ b/utbot-spring-analyzer/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("org.springframework.boot") version "2.7.8" + id("io.spring.dependency-management") version "1.1.0" + id("java") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/ApplicationRunner.kt b/utbot-spring-analyzer/src/main/kotlin/ApplicationRunner.kt new file mode 100644 index 0000000000..bf25d6b979 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/ApplicationRunner.kt @@ -0,0 +1,33 @@ +package application + +import analyzers.SpringApplicationAnalyzer +import utils.PathsUtils + +/** + * To run this app, arguments must be passed in the following way: + * args[0] - classpath of current project + * args[1] - fully qualified name of configuration class + * args[2] - `.properties` file paths, separated via `;`, empty string if no files exist + * args[3] - `.xml` configuration file paths + * + * Several items in one arg are separated via `;`. + * If there are no files, empty string should be passed. + */ +fun main(args: Array) { + + /* FOR EXAMPLE + val arg0 = "/Users/kirillshishin/IdeaProjects/spring-starter-lesson-28/build/classes/java/main" + val arg1 = "com.dmdev.spring.config.ApplicationConfiguration" + 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" + 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" + */ + + val springApplicationAnalyzer = SpringApplicationAnalyzer( + applicationPath = args[0], + configurationClassFqn = args[1], + propertyFilesPaths = args[2].split(";").filter { it != PathsUtils.EMPTY_PATH }, + xmlConfigurationPaths = args[3].split(";").filter { it != PathsUtils.EMPTY_PATH }, + ) + + springApplicationAnalyzer.analyze() +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/analyzers/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/analyzers/SpringApplicationAnalyzer.kt new file mode 100644 index 0000000000..6277a24116 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/analyzers/SpringApplicationAnalyzer.kt @@ -0,0 +1,54 @@ +package analyzers + +import application.utils.FakeFileManager +import application.configurators.PropertiesConfigurator +import application.configurators.XmlFilesConfigurator +import config.TestApplicationConfiguration +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.context.ApplicationContextException +import utils.ConfigurationManager +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Path + + +class SpringApplicationAnalyzer( + private val applicationPath: String, + private val configurationClassFqn: String, + private val propertyFilesPaths: List, + private val xmlConfigurationPaths: List, +) { + + private val applicationUrl: URL + get() = Path.of(applicationPath).toUri().toURL() + + fun analyze() { + val fakeFileManager = FakeFileManager(propertyFilesPaths + xmlConfigurationPaths) + fakeFileManager.createFakeFiles() + + val classLoader: ClassLoader = URLClassLoader(arrayOf(applicationUrl)) + val userConfigurationClass = classLoader.loadClass(configurationClassFqn) + + val configurationManager = ConfigurationManager(classLoader, userConfigurationClass) + val propertiesConfigurator = PropertiesConfigurator(propertyFilesPaths, configurationManager) + val xmlFilesConfigurator = XmlFilesConfigurator(xmlConfigurationPaths, configurationManager) + + propertiesConfigurator.configure() + xmlFilesConfigurator.configure() + + val app = SpringApplicationBuilder(SpringApplicationAnalyzer::class.java) + app.sources(TestApplicationConfiguration::class.java, userConfigurationClass) + for (prop in propertiesConfigurator.readProperties()) { + app.properties(prop) + } + + try { + app.build() + app.run() + } catch (e: ApplicationContextException) { + println("Bean analysis finished successfully") + }finally { + fakeFileManager.deleteFakeFiles() + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/analyzers/XmlConfigurationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/analyzers/XmlConfigurationAnalyzer.kt new file mode 100644 index 0000000000..6b2eb425dd --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/analyzers/XmlConfigurationAnalyzer.kt @@ -0,0 +1,43 @@ +package analyzers + +import org.w3c.dom.Document +import org.w3c.dom.Element +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class XmlConfigurationAnalyzer(private val userXmlFilePath: String, private val fakeXmlFilePath: String) { + + fun fillFakeApplicationXml() { + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = builder.parse(userXmlFilePath) + + // Property placeholders may contain file names relative to user project, + // they will not be found in ours. We import all properties using another approach. + deletePropertyPlaceholders(doc) + writeXmlFile(doc) + } + + private fun deletePropertyPlaceholders(doc: Document) { + val elements = doc.getElementsByTagName("context:property-placeholder") + val elementsCount = elements.length + + // Xml file may contain several property placeholders: + // see https://stackoverflow.com/questions/26618400/how-to-use-multiple-property-placeholder-in-a-spring-xml-file + for (i in 0 until elementsCount) { + val element = elements.item(i) as Element + element.parentNode.removeChild(element) + } + + doc.normalize() + } + + private fun writeXmlFile(doc: Document) { + val tFormer = TransformerFactory.newInstance().newTransformer() + val source = DOMSource(doc) + val destination = StreamResult(fakeXmlFilePath) + + tFormer.transform(source, destination) + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/config/TestApplicationConfiguration.kt b/utbot-spring-analyzer/src/main/kotlin/config/TestApplicationConfiguration.kt new file mode 100644 index 0000000000..45fafe6955 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/config/TestApplicationConfiguration.kt @@ -0,0 +1,15 @@ +package config + +import org.springframework.beans.factory.config.BeanFactoryPostProcessor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import post_processors.UtBotBeanFactoryPostProcessor + +@Configuration +open class TestApplicationConfiguration { + + @Bean + open fun utBotBeanFactoryPostProcessor(): BeanFactoryPostProcessor { + return UtBotBeanFactoryPostProcessor() + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/configurators/PropertiesConfigurator.kt b/utbot-spring-analyzer/src/main/kotlin/configurators/PropertiesConfigurator.kt new file mode 100644 index 0000000000..f2abe3eef1 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/configurators/PropertiesConfigurator.kt @@ -0,0 +1,40 @@ +package application.configurators + +import utils.ConfigurationManager +import utils.PathsUtils +import java.io.BufferedReader +import java.io.FileReader +import kotlin.io.path.Path + +class PropertiesConfigurator( + private val propertiesFilesPaths: List, + private val configurationManager: ConfigurationManager +) { + + fun configure() { + configurationManager.clearPropertySourceAnnotation() + + propertiesFilesPaths + .map { Path(it).fileName } + .forEach { fileName -> configurationManager.patchPropertySourceAnnotation(fileName) } + } + + fun readProperties(): ArrayList { + val props = ArrayList() + + for (propertiesFilePath in propertiesFilesPaths) { + if (propertiesFilePath == PathsUtils.EMPTY_PATH) continue + + val reader = BufferedReader(FileReader(propertiesFilePath)) + var line = reader.readLine() + while (line != null) { + props.add(line) + line = reader.readLine() + } + + reader.close() + } + + return props + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/configurators/XmlFilesConfigurator.kt b/utbot-spring-analyzer/src/main/kotlin/configurators/XmlFilesConfigurator.kt new file mode 100644 index 0000000000..677b1aee51 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/configurators/XmlFilesConfigurator.kt @@ -0,0 +1,27 @@ +package application.configurators + +import analyzers.XmlConfigurationAnalyzer +import application.utils.FakeFileManager +import utils.ConfigurationManager +import utils.PathsUtils +import kotlin.io.path.Path + +class XmlFilesConfigurator( + private val userXmlFilePaths: List, + private val configurationManager: ConfigurationManager, +) { + + fun configure() { + configurationManager.clearImportResourceAnnotation() + + for (userXmlFilePath in userXmlFilePaths) { + if(userXmlFilePath == PathsUtils.EMPTY_PATH)continue + + val xmlConfigurationAnalyzer = + XmlConfigurationAnalyzer(userXmlFilePath, PathsUtils.createFakeFilePath(userXmlFilePath)) + + xmlConfigurationAnalyzer.fillFakeApplicationXml() + configurationManager.patchImportResourceAnnotation(Path(userXmlFilePath).fileName) + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/post_processors/UtBotBeanFactoryPostProcessor.kt b/utbot-spring-analyzer/src/main/kotlin/post_processors/UtBotBeanFactoryPostProcessor.kt new file mode 100644 index 0000000000..c0c15023e3 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/post_processors/UtBotBeanFactoryPostProcessor.kt @@ -0,0 +1,86 @@ +package post_processors + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition +import org.springframework.beans.factory.config.BeanFactoryPostProcessor +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.core.PriorityOrdered + +import java.io.File +import java.io.FileWriter +import java.util.Arrays + +class UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered { + + /** + * Sets the priority of post processor to highest to avoid side effects from others. + */ + override fun getOrder(): Int = PriorityOrdered.HIGHEST_PRECEDENCE + + override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) { + println("Started post-processing bean factory in UtBot") + + val beanClassNames = findBeanClassNames(beanFactory) + //TODO: will be replaced with more appropriate IPC approach. + writeToFile(beanClassNames) + + // After desired post-processing is completed we destroy bean definitions + // to avoid further possible actions with beans that may be unsafe. + destroyBeanDefinitions(beanFactory) + + println("Finished post-processing bean factory in UtBot") + } + + private fun findBeanClassNames(beanFactory: ConfigurableListableBeanFactory): ArrayList { + val beanClassNames = ArrayList() + for (beanDefinitionName in beanFactory.beanDefinitionNames) { + val beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName) + + if (beanDefinition is AnnotatedBeanDefinition) { + val factoryMethodMetadata = beanDefinition.factoryMethodMetadata + if (factoryMethodMetadata != null) { + beanClassNames.add(factoryMethodMetadata.returnTypeName) + } + } else { + var className = beanDefinition.beanClassName + if (className == null) { + className = beanFactory.getBean(beanDefinitionName).javaClass.name + } + className?.let { beanClassNames.add(it) } + } + } + + return beanClassNames + } + + private fun destroyBeanDefinitions(beanFactory: ConfigurableListableBeanFactory) { + for (beanDefinitionName in beanFactory.beanDefinitionNames) { + val beanRegistry = beanFactory as BeanDefinitionRegistry + beanRegistry.removeBeanDefinition(beanDefinitionName) + } + } + + private fun writeToFile(beanClassNames: ArrayList) { + try { + val springBeansFile = File("SpringBeans.txt") + val fileWriter = FileWriter(springBeansFile) + + val distinctClassNames = beanClassNames.stream() + .distinct() + .toArray() + Arrays.sort(distinctClassNames) + + for (beanClassName in distinctClassNames) { + fileWriter.append(beanClassName.toString()) + fileWriter.append("\n") + } + + fileWriter.flush() + fileWriter.close() + + println("Storing bean information completed successfully") + } catch (e: Throwable) { + println("Storing bean information failed with exception $e") + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/utils/ConfigurationManager.kt b/utbot-spring-analyzer/src/main/kotlin/utils/ConfigurationManager.kt new file mode 100644 index 0000000000..daff28f9b3 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/utils/ConfigurationManager.kt @@ -0,0 +1,56 @@ +package utils + +import org.springframework.context.annotation.ImportResource +import org.springframework.context.annotation.PropertySource +import java.lang.reflect.InvocationHandler +import java.nio.file.Path +import java.util.Arrays +import kotlin.reflect.KClass + +class ConfigurationManager(private val classLoader: ClassLoader, private val userConfigurationClass: Class<*>) { + + fun clearPropertySourceAnnotation() = patchAnnotation(PropertySource::class, null) + + fun clearImportResourceAnnotation() = patchAnnotation(ImportResource::class, null) + + fun patchPropertySourceAnnotation(userPropertiesFileName: Path) = + patchAnnotation(PropertySource::class, String.format("classpath:%s", "fake_$userPropertiesFileName")) + + fun patchImportResourceAnnotation(userXmlFilePath: Path) = + patchAnnotation(ImportResource::class, String.format("classpath:%s", "fake_$userXmlFilePath")) + + private fun patchAnnotation(annotationClass: KClass<*>, newValue: String?) { + val proxyClass = classLoader.loadClass("java.lang.reflect.Proxy") + val hField = proxyClass.getDeclaredField("h") + hField.isAccessible = true + + val propertySourceAnnotation = Arrays.stream( + userConfigurationClass.annotations + ) + .filter { el: Annotation -> el.annotationClass == annotationClass } + .findFirst() + + if (propertySourceAnnotation.isPresent) { + val annotationInvocationHandler = hField[propertySourceAnnotation.get()] as InvocationHandler + + val annotationInvocationHandlerClass = + classLoader.loadClass("sun.reflect.annotation.AnnotationInvocationHandler") + val memberValuesField = annotationInvocationHandlerClass.getDeclaredField("memberValues") + memberValuesField.isAccessible = true + + val memberValues = memberValuesField[annotationInvocationHandler] as MutableMap + addNewValue(memberValues, newValue) + } + } + + private fun addNewValue(memberValues: MutableMap, newValue: String?){ + if(newValue == null){ + memberValues["value"] = Array(0){""} + } + else { + val list: MutableList = (memberValues["value"] as Array).toMutableList() + list.add(newValue) + memberValues["value"] = list.toTypedArray() + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/utils/FakeFileManager.kt b/utbot-spring-analyzer/src/main/kotlin/utils/FakeFileManager.kt new file mode 100644 index 0000000000..835262a072 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/utils/FakeFileManager.kt @@ -0,0 +1,34 @@ +package application.utils + +import utils.PathsUtils +import java.io.File +import java.io.IOException + +class FakeFileManager(private val fakeFilesList: List) { + + fun createFakeFiles() { + for (fileName in fakeFilesList) { + val fakeXmlFileAbsolutePath = PathsUtils.createFakeFilePath(fileName) + + try { + File(fakeXmlFileAbsolutePath).createNewFile() + } catch (e: IOException) { + println("Fake xml file creation failed with exception $e") + } + + } + } + + fun deleteFakeFiles() { + for (fileName in fakeFilesList) { + val fakeXmlFileAbsolutePath = PathsUtils.createFakeFilePath(fileName) + + try { + File(fakeXmlFileAbsolutePath).delete() + } catch (e: IOException) { + println("Fake xml file deletion failed with exception $e") + } + + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/utils/PathsUtils.kt b/utbot-spring-analyzer/src/main/kotlin/utils/PathsUtils.kt new file mode 100644 index 0000000000..e2b3b88830 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/utils/PathsUtils.kt @@ -0,0 +1,21 @@ +package utils + +import kotlin.io.path.Path + +object PathsUtils { + const val EMPTY_PATH = "" + + fun createFakeFilePath(fileName: String): String = + Path(buildResourcesPath, "fake_${Path(fileName).fileName}").toString() + + //TODO: it is better to do it without marker files + private val buildResourcesPath: String + get() { + val resourcesMarker = + this.javaClass.classLoader.getResource("resources_marker.txt") + ?: error("Resources marker file is not found") + + return Path(resourcesMarker.path).parent.toString() + } + +} \ No newline at end of file diff --git "a/utbot-spring-analyzer/src/main/resources/resour\321\201es_marker.txt" "b/utbot-spring-analyzer/src/main/resources/resour\321\201es_marker.txt" new file mode 100644 index 0000000000..4c7bb149cb --- /dev/null +++ "b/utbot-spring-analyzer/src/main/resources/resour\321\201es_marker.txt" @@ -0,0 +1 @@ +DO NOT DELETE THIS FILE!!! \ No newline at end of file