All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.gradle_plugins.plugins.ide.idea.IdeaSettingsExtension.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.plugins.ide.idea

import name.remal.*
import name.remal.gradle_plugins.dsl.Extension
import name.remal.gradle_plugins.dsl.extensions.atTheEndOfAfterEvaluationAllProjectsOrNow
import name.remal.gradle_plugins.dsl.extensions.getOrNull
import name.remal.gradle_plugins.dsl.extensions.isPluginApplied
import name.remal.gradle_plugins.dsl.utils.XML_PRETTY_OUTPUTTER
import name.remal.gradle_plugins.dsl.utils.getGradleLogger
import name.remal.gradle_plugins.dsl.utils.getPluginIdForLogging
import name.remal.gradle_plugins.plugins.ci.CommonCIPlugin
import org.gradle.api.Action
import org.gradle.api.Project
import org.jdom2.Document
import org.jdom2.Element
import org.jdom2.input.SAXBuilder
import java.io.File

const val WORKSPACE_IDEA_DIR_RELATIVE_PATH = "workspace.xml"

@Extension
class IDEASettingsExtension(private val project: Project) : IDEASettings {

    companion object {
        private val logger = getGradleLogger(IDEASettingsExtension::class.java)
        private fun newSAXBuilder() = SAXBuilder().apply {
            setNoValidatingXMLReaderFactory()
            setNoOpEntityResolver()
        }
    }

    private val allprojectsDirs: Set by lazy {
        project.rootProject.allprojects.mapTo(HashSet(), Project::getProjectDir)
            .also { logger.debug("allprojectsDirs = {}", it) }
    }

    @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
    private fun hasCorrespondingIDEAModule(configFile: File, projectDir: File): Boolean {
        if (!configFile.isFile) {
            logger.debug("{} doesn't exist", configFile)
            return false
        }

        try {
            logger.debug("Processing {}", configFile)
            val projectSubstitutions = globalSubstitutions + mapOf("\$PROJECT_DIR\$" to projectDir.invariantSeparatorsPath)
            sequenceOf(newSAXBuilder().build(configFile).rootElement)
                .filter { IDEA_PROJECT_VERSION == it.getAttributeValue("version") }
                .flatMap { it.getChildren("component").asSequence() }
                .filter { it.getAttributeValue("name") == "ProjectModuleManager" }
                .flatMap { it.getChildren("modules").asSequence() }
                .flatMap { it.getChildren("module").asSequence() }
                .mapNotNull { it.getAttributeValue("filepath")?.replace(projectSubstitutions)?.let(project::file) }
                .filter(File::isFile)
                .forEach forEachModule@{ moduleConfigFile ->
                    try {
                        logger.debug("Processing {}", moduleConfigFile)
                        val moduleSubstitutions = projectSubstitutions + mapOf(
                            "\$MODULE_IML_DIR\$" to moduleConfigFile.parentFile.invariantSeparatorsPath,
                            "\$MODULE_DIR\$" to moduleConfigFile.parentFile.invariantSeparatorsPath
                        )
                        val moduleProjectDir = newSAXBuilder().build(moduleConfigFile).rootElement.getAttributeValue("external.linked.project.path")?.replace(moduleSubstitutions)?.let(project::file)
                            ?: moduleConfigFile.parentFile.nullIf { name != ".idea" }?.parentFile?.let(project::file)
                            ?: return@forEachModule
                        logger.debug("{}: moduleProjectDir = {}", moduleConfigFile, moduleProjectDir)
                        if (moduleProjectDir in allprojectsDirs) {
                            return true
                        }

                    } catch (e: Exception) {
                        logger.warn(e)
                        return@forEachModule
                    }
                }

        } catch (e: Exception) {
            logger.warn(e)
        }

        return false
    }

    private val ideaDirs: List by lazy {
        buildList {
            project.projectDir.forSelfAndEachParent forEachDir@{ projectDir ->
                val ideaDir = File(projectDir, ".idea")
                if (hasCorrespondingIDEAModule(File(ideaDir, "modules.xml"), projectDir)) {
                    add(ideaDir)
                }
            }
        }
            .also { logger.debug("ideaDirs = {}", it) }
    }

    private val iprFiles: List by lazy {
        buildList {
            project.projectDir.forSelfAndEachParent forEachDir@{ projectDir ->
                projectDir.listFiles { file -> file.extension == "ipr" }?.forEach { iprFile ->
                    if (hasCorrespondingIDEAModule(iprFile, projectDir)) {
                        add(iprFile)
                    }
                }
            }
        }
            .also { logger.debug("iprFiles = {}", it) }
    }

    private val iwsFiles: List by lazy {
        iprFiles.map { it.resolveSibling(it.nameWithoutExtension + ".iws") }
            .filter(File::isFile)
            .also { logger.debug("iwsFiles = {}", it) }
    }

    private data class ComponentLocation(
        val componentName: String,
        val ideaDirRelativePath: String?
    )

    private val configurers = concurrentMapOf>>()

    init {
        project.atTheEndOfAfterEvaluationAllProjectsOrNow(Int.MAX_VALUE) atTheEndOfAfterEvaluation@{ project ->
            if (project.isPluginApplied(CommonCIPlugin::class.java)) {
                if (logger.isDebugEnabled) {
                    logger.debug("IDEA extended configuration has been skipped, as {} plugin is applied", getPluginIdForLogging(CommonCIPlugin::class.java))
                }
                return@atTheEndOfAfterEvaluation
            }

            project.allprojects { proj ->
                val ideaSettings = proj.getOrNull(IDEASettings::class.java) as? IdeaSettingsDelegateToRoot
                val configurers = ideaSettings?.configurers
                configurers?.forEach {
                    configureIDEAComponent(it.componentName, it.ideaDirRelativePath, it.action)
                }
            }

            val thread = Thread {
                logger.debug("Configurers count: {}", configurers.size)
                configurers.forEach { (componentName, ideaDirRelativePath), actions ->

                    val canBeCreated = ideaDirRelativePath != null && !ideaDirRelativePath.endsWith("/")

                    ideaDirs.forEach forEachIdeaDir@{ ideaDir ->
                        try {
                            var isFound = false
                            logger.debug("Scanning {}", ideaDir)
                            ideaDir.walk()
                                .filter { it.extension == "xml" && it.isFile }
                                .onEach { logger.debug("Processing {}", it) }
                                .forEach forEachFile@{ file ->
                                    val document = newSAXBuilder().build(file)
                                    val documentStrOrig = document.asString()
                                    sequenceOf(document.rootElement)
                                        .filter(Element::isIdeaProjectElement)
                                        .flatMap { it.getChildren("component").asSequence() }
                                        .plus(
                                            sequenceOf(document.rootElement).filter { it.name == "component" }
                                        )
                                        .filter { it.getAttributeValue("name") == componentName }
                                        .toList().asSequence()
                                        .onEach { isFound = true }
                                        .onEach { logger.debug("Processing component {}: {}", file, componentName) }
                                        .forEach forEachElement@{ componentElement ->
                                            actions.forEach {
                                                it.execute(componentElement)
                                                if (componentElement.parent == null) return@forEachElement
                                            }
                                        }

                                    if (document.asString() != documentStrOrig) {
                                        logger.debug("Writing {}", file)
                                        file.outputStream().use { XML_PRETTY_OUTPUTTER.output(document, it) }
                                    }
                                }

                            if (!isFound && canBeCreated && ideaDirRelativePath != null) {
                                val ideaDirFile = ideaDir.resolve(ideaDirRelativePath.trim('/'))
                                logger.debug("Creating component {}: {}", ideaDirFile, componentName)
                                val document: Document = if (ideaDirFile.exists()) {
                                    newSAXBuilder().build(ideaDirFile)
                                } else {
                                    Document()
                                }

                                if (!document.hasRootElement()) {
                                    document.rootElement = Element("project").setAttribute("version", IDEA_PROJECT_VERSION)
                                } else if (!document.rootElement.isIdeaProjectElement()) {
                                    return@forEachIdeaDir
                                }

                                document.rootElement.addContent(
                                    Element("component").setAttribute("name", componentName).also { componentElement ->
                                        actions.forEach { it.execute(componentElement) }
                                    }
                                )

                                logger.debug("Writing {}", ideaDirFile)
                                ideaDirFile.createParentDirectories().outputStream().use { XML_PRETTY_OUTPUTTER.output(document, it) }
                            }

                        } catch (e: Exception) {
                            logger.warn(e)
                            return@forEachIdeaDir
                        }
                    }

                    (if (ideaDirRelativePath == WORKSPACE_IDEA_DIR_RELATIVE_PATH) iwsFiles else iprFiles).forEach forEachIdeaFile@{ ideaFile ->
                        try {
                            logger.debug("Processing {}", ideaFile)
                            val document = newSAXBuilder().build(ideaFile)
                            val documentStrOrig = document.asString()
                            var isFound = false
                            sequenceOf(document.rootElement)
                                .filter(Element::isIdeaProjectElement)
                                .flatMap { it.getChildren("component").asSequence() }
                                .filter { it.getAttributeValue("name") == componentName }
                                .toList().asSequence()
                                .onEach { isFound = true }
                                .onEach { logger.debug("Processing component {}: {}", ideaFile, componentName) }
                                .forEach forEachElement@{ componentElement ->
                                    actions.forEach {
                                        it.execute(componentElement)
                                        if (componentElement.parent == null) return@forEachElement
                                    }
                                }

                            if (!isFound && canBeCreated) {
                                logger.debug("Creating component {}: {}", ideaFile, componentName)
                                document.rootElement.addContent(
                                    Element("component").setAttribute("name", componentName).also { componentElement ->
                                        actions.forEach { it.execute(componentElement) }
                                    }
                                )
                            }

                            if (document.asString() != documentStrOrig) {
                                logger.debug("Writing {}", ideaFile)
                                ideaFile.outputStream().use { XML_PRETTY_OUTPUTTER.output(document, it) }
                            }

                        } catch (e: Exception) {
                            logger.warn(e)
                            return@forEachIdeaFile
                        }
                    }

                }
            }
            thread.start()
            project.gradle.buildFinished {
                if (thread.isAlive) {
                    thread.join(10_000)
                }
            }
        }
    }

    override fun configureIDEAComponent(componentName: String, ideaDirRelativePath: String?, action: Action) {
        if (ideaDirRelativePath != null) {
            logger.debug("Adding configurer for {} (.idea/{})", componentName, ideaDirRelativePath)
        } else {
            logger.debug("Adding configurer for {}", componentName)
        }
        val locationConfigurers = configurers.computeIfAbsent(ComponentLocation(componentName, ideaDirRelativePath), { mutableListOf>().asSynchronized() })
        locationConfigurers.add(action)
    }

}


private val globalSubstitutions = mapOf(
    "\$USER_HOME\$" to USER_HOME_DIR.invariantSeparatorsPath,
    "\$MAVEN_REPOSITORY\$" to File(USER_HOME_DIR, ".m2/repository").invariantSeparatorsPath
)

private const val IDEA_PROJECT_VERSION: String = "4"
private fun Element.isIdeaProjectElement() = name == "project" && getAttributeValue("version") == IDEA_PROJECT_VERSION




© 2015 - 2024 Weber Informatics LLC | Privacy Policy