diff --git a/.idea/modules.xml b/.idea/modules.xml index 0c5456a6960e..38d7655e9578 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -942,6 +942,7 @@ + diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index bcdbb2d13d51..6abb38864e90 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -208,6 +208,7 @@ + diff --git a/tools/devLauncher/src/ModuleResourceFileFinder.kt b/tools/devLauncher/src/ModuleResourceFileFinder.kt new file mode 100644 index 000000000000..3c8cabee2eb2 --- /dev/null +++ b/tools/devLauncher/src/ModuleResourceFileFinder.kt @@ -0,0 +1,102 @@ +package com.intellij.tools.devLauncher + +import java.nio.file.Path +import javax.xml.stream.XMLInputFactory +import javax.xml.stream.XMLStreamConstants +import kotlin.io.path.* + +/** + * Provides a way to locate a resource file under source roots by its relative path in an IntelliJ project. + */ +class ModuleResourceFileFinder(private val projectDir: Path) { + private val moduleFiles: Map + + init { + val modulesXmlFile = projectDir.resolve(".idea/modules.xml") + require(modulesXmlFile.exists()) { ".idea/modules.xml not found in $projectDir"} + modulesXmlFile.inputStream().buffered().use { input -> + val moduleFilesMap = LinkedHashMap() + val reader = XMLInputFactory.newDefaultFactory().createXMLStreamReader(input) + while (reader.hasNext()) { + val event = reader.next() + if (event == XMLStreamConstants.START_ELEMENT && reader.localName == "module") { + val attributeName = reader.getAttributeLocalName(0) + require(attributeName == "fileurl") { "Unexpected first attribute in 'module' tag: $attributeName"} + val imlUrl = reader.getAttributeValue(0) + val prefix = "file://${'$'}PROJECT_DIR${'$'}/" + require(imlUrl.startsWith(prefix)) { "Unexpected format of URL: $imlUrl"} + val imlPath = projectDir.resolve(imlUrl.removePrefix(prefix)) + val fileName = imlPath.name + val suffix = ".iml" + require(fileName.endsWith(suffix)) { "Unexpected file extension in file path $imlPath" } + val moduleName = fileName.removeSuffix(suffix) + moduleFilesMap[moduleName] = imlPath + } + } + moduleFiles = moduleFilesMap + } + } + + fun findResourceFile(moduleName: String, relativePath: String): Path? { + for ((prefix, rootPath) in loadRootsWithPrefixes(moduleName)) { + val relativePathWithoutPrefix = when { + prefix == null -> relativePath + relativePath.startsWith("$prefix/") -> relativePath.removePrefix("$prefix/") + else -> continue + } + val file = rootPath.resolve(relativePathWithoutPrefix) + if (file.exists()) { + return file + } + } + return null + } + + private fun loadRootsWithPrefixes(moduleName: String): List> { + val imlPath = moduleFiles[moduleName] ?: error("Cannot find module '$moduleName' in project '$projectDir'") + require(imlPath.exists()) { "Module file $imlPath doesn't exist" } + val moduleDir = imlPath.parent + val rootsWithPrefixes = ArrayList>() + imlPath.inputStream().buffered().use { input -> + val reader = XMLInputFactory.newDefaultFactory().createXMLStreamReader(input) + while (reader.hasNext()) { + val event = reader.next() + if (event == XMLStreamConstants.START_ELEMENT && reader.localName == "sourceFolder") { + require(reader.attributeCount > 1) { "At least two attributes expected in 'sourceFolder' tag in $imlPath, but ${reader.attributeCount} found" } + val attributeName = reader.getAttributeLocalName(0) + require(attributeName == "url") { "Unexpected first attribute in 'sourceFolder' tag in $imlPath: $attributeName" } + val rootUrl = reader.getAttributeValue(0) + val prefix = "file://" + require(rootUrl.startsWith(prefix)) { "Unexpected format of URL: $rootUrl" } + val rootPath = Path(rootUrl.removePrefix(prefix).replace("${'$'}MODULE_DIR${'$'}", moduleDir.pathString)) + + val typeAttributeName = reader.getAttributeLocalName(1) + val typeAttributeValue = reader.getAttributeValue(1) + val directoryPrefix = when { + typeAttributeName == "isTestSource" && typeAttributeValue == "false" -> { + if (reader.attributeCount > 2 && reader.getAttributeLocalName(2) == "packagePrefix") { + reader.getAttributeValue(2).replace('.', '/').takeIf { it.isNotEmpty() } + } + else { + null + } + } + typeAttributeName == "type" && typeAttributeValue == "java-resource" -> { + if (reader.attributeCount > 2 && reader.getAttributeLocalName(2) == "relativeOutputPath") { + reader.getAttributeValue(2).removeSuffix("/").takeIf { it.isNotEmpty() } + } + else { + null + } + } + else -> { + continue + } + } + rootsWithPrefixes.add(directoryPrefix to rootPath) + } + } + } + return rootsWithPrefixes + } +} \ No newline at end of file diff --git a/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml b/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml new file mode 100644 index 000000000000..9cc2e470b043 --- /dev/null +++ b/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml new file mode 100644 index 000000000000..48cfda831550 --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/additional-resources/c.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/additional-resources/c.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml new file mode 100644 index 000000000000..768559d7e3ef --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/resources/a.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/resources/a.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml new file mode 100644 index 000000000000..b9de36531b8b --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/src/b/b.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/src/b/b.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/testSrc/c.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/testSrc/c.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/resources/a.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/resources/a.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/src/b/b.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/src/b/b.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml new file mode 100644 index 000000000000..a574e901b2f2 --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt b/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt new file mode 100644 index 000000000000..2bc342d0b7ca --- /dev/null +++ b/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt @@ -0,0 +1,39 @@ +package com.intellij.tools.devLauncher + +import com.intellij.openapi.application.ex.PathManagerEx +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.relativeTo + +class ModuleResourceFileFinderTest { + private lateinit var projectDir: Path + private lateinit var finder: ModuleResourceFileFinder + + @BeforeEach + fun setUp() { + projectDir = Path(PathManagerEx.getCommunityHomePath()).resolve("tools/devLauncher/tests/testData/moduleResourceFinderProject") + finder = ModuleResourceFileFinder(projectDir) + } + + @Test + fun `simple roots`() { + assertPath("simple/resources/a.txt", finder.findResourceFile("simple", "a.txt")) + assertPath("simple/src/b/b.txt", finder.findResourceFile("simple", "b/b.txt")) + assertPath(null, finder.findResourceFile("simple", "c.txt")) + } + + @Test + fun `roots with prefixes`() { + assertPath("withPrefix/resources/a.txt", finder.findResourceFile("withPrefix", "prefix1/a.txt")) + assertPath("withPrefix/src/b/b.txt", finder.findResourceFile("withPrefix", "prefix2/b/b.txt")) + assertPath("additional-resources/c.txt", finder.findResourceFile("withPrefix", "prefix3/c.txt")) + } + + private fun assertPath(expectedPath: String?, file: Path?) { + assertEquals(expectedPath, file?.relativeTo(projectDir)?.invariantSeparatorsPathString) + } +} \ No newline at end of file