From 3bb1b95aeb36970f1a43594362769eb767d79bfb Mon Sep 17 00:00:00 2001 From: Vishal Kumar Date: Mon, 11 Nov 2024 14:16:44 -0800 Subject: [PATCH] Prepared resourcemanager to be distributed as gradle plugin. --- resourcemanager/.gitignore | 1 + resourcemanager/build.gradle.kts | 42 +++ .../src/main/kotlin/ResourceManagerPlugin.kt | 24 ++ .../ResourceManagerGenerator.kt | 125 ++++++++ .../file/generation/ClassFileGenerator.kt | 285 ++++++++++++++++++ .../resourcemanager/file/parser/XmlParser.kt | 104 +++++++ .../resourcemanager/manager/ModuleManager.kt | 167 ++++++++++ .../resourcemanager/model/ModuleDetails.kt | 18 ++ .../randos/resourcemanager/model/Resource.kt | 14 + .../resourcemanager/model/ResourceType.kt | 9 + .../resourcemanager/model/ValueResource.kt | 13 + .../model/ValueResourceType.kt | 17 ++ .../utils/ExtensionFunction.kt | 11 + settings.gradle.kts | 1 + 14 files changed, 831 insertions(+) create mode 100644 resourcemanager/.gitignore create mode 100644 resourcemanager/build.gradle.kts create mode 100644 resourcemanager/src/main/kotlin/ResourceManagerPlugin.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/ResourceManagerGenerator.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/generation/ClassFileGenerator.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/parser/XmlParser.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/manager/ModuleManager.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ModuleDetails.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/Resource.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ResourceType.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResource.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResourceType.kt create mode 100644 resourcemanager/src/main/kotlin/dev/randos/resourcemanager/utils/ExtensionFunction.kt diff --git a/resourcemanager/.gitignore b/resourcemanager/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/resourcemanager/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/resourcemanager/build.gradle.kts b/resourcemanager/build.gradle.kts new file mode 100644 index 0000000..d7a0551 --- /dev/null +++ b/resourcemanager/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + `kotlin-dsl` + java + id("com.gradle.plugin-publish") version "1.3.0" +} + +dependencies { + implementation(gradleApi()) + implementation(localGroovy()) +} + +group = "dev.randos" +version = "0.0.1" + +gradlePlugin { + website.set("https://github.com/vsnappy1/ResourceManager") + vcsUrl.set("https://github.com/vsnappy1/ResourceManager") + plugins { + create("resourcemanager") { + id = "dev.randos.resourcemanager" + implementationClass = "ResourceManagerPlugin" + displayName = "Resource Manager" + description = "ResourceManager is an Android plugin that simplifies accessing Android resources (strings, colors, drawables, etc.) in both Android and non-Android components (e.g., ViewModel) using generated code." + tags.set(listOf("android", "androidResources", "codeGeneration")) + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +kotlin { + jvmToolchain(8) +} + +publishing { + repositories { + mavenLocal() + } +} \ No newline at end of file diff --git a/resourcemanager/src/main/kotlin/ResourceManagerPlugin.kt b/resourcemanager/src/main/kotlin/ResourceManagerPlugin.kt new file mode 100644 index 0000000..ba96e24 --- /dev/null +++ b/resourcemanager/src/main/kotlin/ResourceManagerPlugin.kt @@ -0,0 +1,24 @@ +import dev.randos.resourcemanager.ResourceManagerGenerator +import org.gradle.api.Plugin +import org.gradle.api.Project +import java.io.File + +class ResourceManagerPlugin : Plugin { + override fun apply(project: Project) { + val generatedFile = File(project.projectDir, "build/generated/resourcemanager/main/ResourceManager.kt") + val resourceManager = ResourceManagerGenerator(project.projectDir, generatedFile) + + val generateResourceManagerTask = project.tasks.register("generateResourceManager") { + inputs.files(resourceManager.getFilesUnderObservation()) + outputs.files(generatedFile) + doLast { + resourceManager.generate() + } + } + + project.tasks.matching { it.name.startsWith("compile") } + .configureEach { + dependsOn(generateResourceManagerTask) + } + } +} \ No newline at end of file diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/ResourceManagerGenerator.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/ResourceManagerGenerator.kt new file mode 100644 index 0000000..48aedac --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/ResourceManagerGenerator.kt @@ -0,0 +1,125 @@ +package dev.randos.resourcemanager + +import java.io.File + +import dev.randos.resourcemanager.model.Resource +import dev.randos.resourcemanager.model.ResourceType +import dev.randos.resourcemanager.model.ModuleDetails +import dev.randos.resourcemanager.manager.ModuleManager +import dev.randos.resourcemanager.file.generation.ClassFileGenerator + +internal class ResourceManagerGenerator( + private val moduleFile: File, + private val generatedFile: File +) { + + fun generate() { + + val moduleManager = ModuleManager(moduleFile) + val resources = getResources(moduleFile, moduleManager.getModuleDependencies()) + + try { + // Create the new file with a dependency on all source files + generatedFile.parentFile.mkdirs() + + // Write the generated class content to the file + generatedFile.bufferedWriter().use { out -> + + val classFile = ClassFileGenerator.generateClassFile( + namespace = moduleManager.getNamespace() + ?: throw IllegalStateException("Namespace could not be found in either build.gradle, build.gradle.kts or AndroidManifest.xml. Please ensure the module is properly configured."), + files = resources + ) + + out.write(classFile) + out.close() + } + } catch (e: Exception) { + println("Error: ${e.localizedMessage}") + e.printStackTrace() + } + } + + fun getFilesUnderObservation(): List { + val moduleManager = ModuleManager(moduleFile) + val resources = getResources(moduleFile, moduleManager.getModuleDependencies()) + return resources.getFilesUnderObservation() + listOf(moduleManager.getBuildGradleFile()) // Also observe the module build.gradle file. + } + + /** + * Scans the directory structure starting from the given file path to locate default resource file + * within an Android project's res/values directories. + * + * @return A list of [Resource]. + */ + private fun getResources( + moduleFile: File, + moduleDependencies: List + ): List { + val list = mutableListOf() + + var resFile = File(moduleFile, "src/main/res") + + // Locate the "values" directory within the "res" directory. + list.add( + Resource( + type = ResourceType.VALUES, + moduleDetails = ModuleDetails(resDirectory = File(resFile, "values")) + ) + ) + + // Locate the "drawable" directory within the "res" directory. + list.add( + Resource( + type = ResourceType.DRAWABLES, + moduleDetails = ModuleDetails(resDirectory = File(resFile, "drawable")) + ) + ) + + /* + Also add resources for project dependencies. (i.e. implementation(project(":my_library"))) + */ + val projectFile = moduleFile.parentFile + moduleDependencies.forEach { module -> + val dependencyModuleFile = File(projectFile, module) + resFile = File(dependencyModuleFile, "src/main/res") + + ModuleManager(dependencyModuleFile).getNamespace()?.let { namespace -> + // Locate the "values" directory within the "res" directory. + list.add( + Resource( + type = ResourceType.VALUES, + moduleDetails = ModuleDetails(module, namespace, File(resFile, "values")) + ) + ) + + // Locate the "drawable" directory within the "res" directory. + list.add( + Resource( + type = ResourceType.DRAWABLES, + moduleDetails = ModuleDetails(module, namespace, File(resFile, "drawable")) + ) + ) + } + } + return list + } + + /** + * Generates a list of all files within the directories specified by each `Resource` object. + * + * @receiver List of `Resource` objects, each containing a directory path. + * @return A list of all files under each directory in the `Resource` list. + */ + private fun List.getFilesUnderObservation(): List { + val files = mutableListOf() + this.map { it.moduleDetails.resDirectory }.forEach { directory -> + directory.walkTopDown().forEach { file -> + if (file.isFile) { + files.add(file) + } + } + } + return files + } +} diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/generation/ClassFileGenerator.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/generation/ClassFileGenerator.kt new file mode 100644 index 0000000..dfd3b20 --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/generation/ClassFileGenerator.kt @@ -0,0 +1,285 @@ +package dev.randos.resourcemanager.file.generation + +import dev.randos.resourcemanager.file.parser.XmlParser +import dev.randos.resourcemanager.model.ModuleDetails +import dev.randos.resourcemanager.model.Resource +import dev.randos.resourcemanager.model.ResourceType +import dev.randos.resourcemanager.model.ValueResource +import dev.randos.resourcemanager.model.ValueResourceType +import dev.randos.resourcemanager.utils.toCamelCase +import java.io.File + +internal class ClassFileGenerator { + + companion object { + fun generateClassFile( + namespace: String, + files: List + ): String { + return StringBuilder().apply { + appendLine("package $namespace\n") + appendLine("import android.app.Application") + appendLine("import android.graphics.drawable.Drawable") + appendLine("import android.content.res.Resources.Theme") + appendLine("import ${namespace}.R\n") + appendLine("object ResourceManager {\n") + appendLine("\tprivate var _application: Application? = null") + appendLine("\tprivate val application: Application") + appendLine("\t\tget() = _application ?: throw IllegalStateException(\"ResourceManager is not initialized. Please call ResourceManager.initialize(this) in your Application class.\")\n") + appendLine("\t@JvmStatic") + appendLine("\tfun initialize(application: Application) {") + appendLine("\t\t_application = application") + appendLine("\t}\n") + + val resourceMap = files.groupBy { it.type } + + resourceMap.forEach { (resourceType, value) -> + appendLine("\t// ----- ${resourceType::class.simpleName} -----") + when (resourceType) { + ResourceType.VALUES -> { + generateObjectForValueResources(value) + } + + ResourceType.DRAWABLES -> { + generateObjectForDrawableResources(value) + } + } + } + appendLine("}") + }.toString() + } + + private fun StringBuilder.generateObjectForDrawableResources(resources: List) { + appendLine("\tobject Drawables {") + resources.forEach { resource -> + resource.moduleDetails.resDirectory.listFiles()?.forEach { file -> + appendDrawableResource( + name = file.nameWithoutExtension, + defaultIndentation = "\t\t", + moduleDetails = resource.moduleDetails + ) + } + } + appendLine("\t}") + } + + private fun StringBuilder.generateObjectForValueResources(resources: List) { + val map = mutableMapOf>>() + resources.forEach { resource -> + resource.moduleDetails.resDirectory.listFiles().getXmlFiles().forEach { file -> + val xmlResources = XmlParser.parseXML(file) + xmlResources.forEach { + val key = it.type::class.simpleName.toString() + if (!map.containsKey(key)) { + map[key] = mutableListOf() + } + map[key]?.add(Pair(resource.moduleDetails, it)) + } + } + } + + map.forEach { + val resourceObject = generateObject( + "${it.key}s", + it.value + ) + appendLine(resourceObject) + } + } + + private fun generateObject( + name: String, + pairs: List> + ): String { + val defaultIndentation = "\t\t" + return StringBuilder().apply { + appendLine("\tobject $name {") + pairs.sortedBy { it.second.name }.forEach { (moduleDetails, resource) -> + when (resource.type) { + ValueResourceType.Array, ValueResourceType.StringArray -> appendStringArrayResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Boolean -> appendBooleanResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Color -> appendColorResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Dimension -> appendDimensionResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Fraction -> appendFractionResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.IntArray -> appendIntArrayResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Integer -> appendIntegerResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + ValueResourceType.Plural -> appendPluralResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + + is ValueResourceType.String -> appendStringResource( + resource = resource, + defaultIndentation = defaultIndentation, + moduleDetails = moduleDetails + ) + } + } + appendLine("\t}") + }.toString() + } + + private fun StringBuilder.appendDrawableResource( + name: String, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + val moduleNameString = getModuleNameString(moduleDetails) + appendLine("${defaultIndentation}@JvmOverloads @JvmStatic fun ${name.toCamelCase()}${moduleNameString}(theme: Theme = application.theme) : Drawable = application.resources.getDrawable(${namespaceString}R.drawable.${name}, theme)") + } + + private fun StringBuilder.appendDimensionResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : Float = application.resources.getDimension(${namespaceString}R.dimen.${resource.name})") + } + + private fun StringBuilder.appendColorResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmOverloads @JvmStatic fun ${getMethodName(resource, moduleDetails)}(theme: Theme = application.theme) : Int = application.resources.getColor(${namespaceString}R.color.${resource.name}, theme)") + } + + private fun StringBuilder.appendIntegerResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : Int = application.resources.getInteger(${namespaceString}R.integer.${resource.name})") + } + + private fun StringBuilder.appendBooleanResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : Boolean = application.resources.getBoolean(${namespaceString}R.bool.${resource.name})") + } + + private fun StringBuilder.appendFractionResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}(base: Int = 0, pbase: Int = 0) : Float = application.resources.getFraction(${namespaceString}R.fraction.${resource.name}, base, pbase)") + } + + private fun StringBuilder.appendStringResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + if (resource.type is ValueResourceType.String && resource.type.isParameterized) { + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}(vararg args: Any = emptyArray()) : String = if (args.isEmpty()) application.resources.getString(${namespaceString}R.string.${resource.name}) else application.resources.getString(${namespaceString}R.string.${resource.name}, *args)") + } else { + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : String = application.resources.getString(${namespaceString}R.string.${resource.name})") + } + } + + private fun StringBuilder.appendPluralResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}(quantity: Int, vararg args: Any = emptyArray()) : String = application.resources.getQuantityString(${namespaceString}R.plurals.${resource.name}, quantity, args)") + } + + private fun StringBuilder.appendStringArrayResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : kotlin.Array = application.resources.getStringArray(${namespaceString}R.array.${resource.name})") + } + + private fun StringBuilder.appendIntArrayResource( + resource: ValueResource, + defaultIndentation: String, + moduleDetails: ModuleDetails + ) { + val namespaceString = getNamespace(moduleDetails) + appendLine("${defaultIndentation}@JvmStatic fun ${getMethodName(resource, moduleDetails)}() : IntArray = application.resources.getIntArray(${namespaceString}R.array.${resource.name})") + } + + /** + * Generates a method name based on the provided resource name and module details. + * Combines the resource name in camel case with the module name if it exists. + * + * @return A string representing the generated method name in camel case format. + */ + private fun getMethodName(resource: ValueResource, moduleDetails: ModuleDetails): String { + val moduleNameString = getModuleNameString(moduleDetails) + return "${resource.name.toCamelCase()}${moduleNameString}" + } + + /** + * Retrieves a formatted module name string based on the provided module details. + * If the module name is not empty, it is appended in camel case format prefixed with an underscore. + * + * @return The formatted module name string, or an empty string if no module name is provided. + */ + private fun getModuleNameString(moduleDetails: ModuleDetails) = + if (moduleDetails.moduleName.isNotEmpty()) "_${moduleDetails.moduleName.toCamelCase()}" else "" + + /** + * Retrieves the namespace from the module details, if specified, followed by a period. + * + * @return The namespace with a trailing period, or an empty string if no namespace is specified. + */ + private fun getNamespace(moduleDetails: ModuleDetails) = + if (moduleDetails.namespace.isNotEmpty()) "${moduleDetails.namespace}." else "" + } +} + +private fun Array?.getXmlFiles(): List { + return this?.filter { it.extension == "xml" } ?: emptyList() +} diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/parser/XmlParser.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/parser/XmlParser.kt new file mode 100644 index 0000000..5caf286 --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/file/parser/XmlParser.kt @@ -0,0 +1,104 @@ +package dev.randos.resourcemanager.file.parser + +import dev.randos.resourcemanager.model.ValueResource +import dev.randos.resourcemanager.model.ValueResourceType +import org.w3c.dom.Element +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory + +internal class XmlParser { + + companion object { + + private const val STRING_TAG = "string" + private const val COLOR_TAG = "color" + private const val BOOLEAN_TAG = "bool" + private const val INTEGER_TAG = "integer" + private const val DIMENSION_TAG = "dimen" + private const val FRACTION_TAG = "fraction" + private const val ARRAY_TAG = "array" + private const val STRING_ARRAY_TAG = "string-array" + private const val INTEGER_ARRAY_TAG = "integer-array" + private const val PLURAL_TAG = "plurals" + + /** + * Parses an XML file and its variants to create a list of resources. + * + * @param file The XML file to parse. + * @return A list of com.randos.resourcemanager.model.Resource objects containing the parsed data. + */ + fun parseXML(file: File): List { + val resources = mutableListOf() + + // Create a DocumentBuilder + val factory = DocumentBuilderFactory.newInstance() + val builder = factory.newDocumentBuilder() + + // Parse the XML file + val document = builder.parse(file) + document.documentElement.normalize() + + // Get the root element + val root = document.documentElement + + // Get all the elements under + val resourceItems = root.childNodes + + for (i in 0 until resourceItems.length) { + val node = resourceItems.item(i) + if (node is Element) { + // Handle different types of resources + handleElement(node, resources) + } + } + return resources + } + + private fun handleElement( + node: Element, + resources: MutableList + ) { + val tagName = node.tagName + val attributeName = node.getAttribute("name") + + when (tagName) { + STRING_TAG -> { + val isParameterized = node.textContent.contains(Regex("%s|%d|%f|%x|%o|%c|%e")) + resources.add(ValueResource(attributeName, ValueResourceType.String(isParameterized))) + } + + COLOR_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Color)) + } + + BOOLEAN_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Boolean)) + } + + INTEGER_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Integer)) + } + + DIMENSION_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Dimension)) + } + + FRACTION_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Fraction)) + } + + STRING_ARRAY_TAG, ARRAY_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.StringArray)) + } + + INTEGER_ARRAY_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.IntArray)) + } + + PLURAL_TAG -> { + resources.add(ValueResource(attributeName, ValueResourceType.Plural)) + } + } + } + } +} diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/manager/ModuleManager.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/manager/ModuleManager.kt new file mode 100644 index 0000000..89b15dd --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/manager/ModuleManager.kt @@ -0,0 +1,167 @@ +package dev.randos.resourcemanager.manager + +import org.w3c.dom.Document +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Manages module-related operations, including retrieving namespaces and module dependencies + * for an Android project module. + * + * @property moduleFile The root directory of the module. + */ +class ModuleManager(private val moduleFile: File) { + + + /** + * Retrieves the namespace of the module by checking both the build.gradle file and the AndroidManifest.xml. + * It prioritizes the namespace specified in the build.gradle file. + * + * @return The module's namespace if found, or `null` if not specified. + */ + fun getNamespace(): String? { + return getNamespaceFromBuildGradle() ?: getNamespaceFromManifest() + } + + /** + * Retrieves a list of dependencies for this module from the build.gradle file, + * specifically those declared like `implementation project(':module-name')` or `implementation(project(":module-name"))`. + * + * @return A list of module dependencies as strings, or an empty list if no dependencies are found. + */ + fun getModuleDependencies(): List { + val gradleFile = getBuildGradleFile() + if (!gradleFile.exists()) { + return emptyList() + } + + val dependencies = mutableListOf() + + // Define regex patterns for the two styles of module dependencies + val dependencyPatterns = listOf( + Regex("\\s*implementation\\s*\\(\\s*project\\s*\\(\\s*['\"]\\s*:\\s*(.+)\\s*['\"]\\s*\\)\\s*\\)"), // Matches: implementation(project(":module-name")) + Regex("\\s*implementation\\s* \\s*project\\s*\\(\\s*['\"]\\s*:\\s*(.+)\\s*['\"]\\s*\\)"), // Matches: implementation project(':module-name') + ) + + var isBlockComment = false + val lines = gradleFile.readLines().filter { it.isNotBlank() } + + for (line in lines) { + // Handle block comments + if (line.trim().startsWith("/*")) { + isBlockComment = true + } + if (isBlockComment) { + if (line.contains("*/")) { + isBlockComment = false + } + continue + } + + // Ignore single-line comments + if (line.trim().startsWith("//")) continue + + // Check each regex pattern to find module dependencies. + dependencyPatterns.forEach { pattern -> + val mathResults = pattern.find(line) + mathResults?.run { + dependencies.add(mathResults.groupValues[1]) + } + } + } + return dependencies + } + + /** + * Finds and returns the build.gradle file in the module directory, supporting both `.gradle` and `.gradle.kts` extensions. + * + * @return The build.gradle file if it exists, or a reference to a non-existent file if neither variant is present. + */ + fun getBuildGradleFile(): File { + // Find the appropriate build.gradle file (either .gradle or .gradle.kts) + var gradleFile = File(moduleFile, "build.gradle") + if (!gradleFile.exists()) { + gradleFile = File(moduleFile, "build.gradle.kts") + } + return gradleFile + } + + /** + * Extracts the namespace from the build.gradle file. + * + * @return The namespace as specified in the build.gradle file, or `null` if not found. + */ + private fun getNamespaceFromBuildGradle(): String? { + // Find the appropriate build.gradle file (either .gradle or .gradle.kts) + var gradleFile = File(moduleFile, "build.gradle") + if (!gradleFile.exists()) { + gradleFile = File(moduleFile, "build.gradle.kts") + } + if (!gradleFile.exists()) { + println("Error: Failed to find build.gradle/build.gradle.kts at path: ${gradleFile.absolutePath}") + return null + } + + // Define regex pattern for the style of namespace + val namespacePattern = + Regex("\\s*namespace\\s*=\\s*[\"'](.+)[\"']") // Matches: namespace = "com.example.app" + + var isBlockComment = false + val lines = gradleFile.readLines().filter { it.isNotBlank() } + + for (line in lines) { + // Handle block comments + if (line.trim().startsWith("/*")) { + isBlockComment = true + } + if (isBlockComment) { + if (line.contains("*/")) { + isBlockComment = false + } + continue + } + + // Ignore single-line comments + if (line.trim().startsWith("//")) continue + + // Check each regex pattern to find module dependencies. + val mathResults = namespacePattern.find(line) + mathResults?.run { + return mathResults.groupValues[1] + } + } + + println("Error: Failed to find namespace in gradle file at path: ${gradleFile.absolutePath}") + return null + } + + /** + * Retrieves the namespace from the AndroidManifest.xml. + * + * @return The package name as specified in AndroidManifest.xml, or `null` if the attribute is not found. + */ + private fun getNamespaceFromManifest(): String? { + val manifestFile = File(moduleFile, "src/main/AndroidManifest.xml") + if (!manifestFile.exists()) { + println("Error: Failed to find AndroidManifest.xml at path: ${manifestFile.absolutePath}") + return null + } + + try { + val document: Document = + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(manifestFile) + document.documentElement.normalize() + + // Find the 'package' attribute in the tag + val packageName = document.documentElement.getAttribute("package") + if (packageName.isNotEmpty()) { + return packageName + } + } catch (e: Exception) { + e.printStackTrace() + } + + println("Error: Failed to find attribute `package` in AndroidManifest.xml at path: ${manifestFile.absolutePath}") + return null + } +} \ No newline at end of file diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ModuleDetails.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ModuleDetails.kt new file mode 100644 index 0000000..1f7d9ee --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ModuleDetails.kt @@ -0,0 +1,18 @@ +package dev.randos.resourcemanager.model + +import java.io.File + +/** + * A data class representing the details of a module in an Android project. + * + * @property moduleName The name of the module, Default value is an empty string if not provided. + * @property namespace The namespace associated with the module, usually matching the module's package name. + * Default value is an empty string if not provided. + * @property resDirectory The directory where the module's resources are stored, usually in the module's + * `src/main/res` path. + */ +internal data class ModuleDetails( + val moduleName: String = "", + val namespace: String = "", + val resDirectory: File +) diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/Resource.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/Resource.kt new file mode 100644 index 0000000..d75637f --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/Resource.kt @@ -0,0 +1,14 @@ +package dev.randos.resourcemanager.model + +/** + * Represents a resource in the application, including its type and file path. + * + * @property type refers to type of Resource. + * @property moduleDetails refers to module details. + * @see [ResourceType] + * @see [ModuleDetails] + */ +internal data class Resource( + val type: ResourceType, + val moduleDetails: ModuleDetails +) \ No newline at end of file diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ResourceType.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ResourceType.kt new file mode 100644 index 0000000..82b9320 --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ResourceType.kt @@ -0,0 +1,9 @@ +package dev.randos.resourcemanager.model + +/** + * Sealed class representing different types of resources in an android application. + */ +internal sealed class ResourceType{ + object VALUES: ResourceType() + object DRAWABLES: ResourceType() +} diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResource.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResource.kt new file mode 100644 index 0000000..3b1e70a --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResource.kt @@ -0,0 +1,13 @@ +package dev.randos.resourcemanager.model + +/** + * Represents a value resource in the application. + * + * @property name The name of the resource (i.e. is_premium in case of false) + * @property type The type of the resource + * @see [ValueResourceType] + */ +internal data class ValueResource( + val name: String, + val type: ValueResourceType, +) diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResourceType.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResourceType.kt new file mode 100644 index 0000000..c01b829 --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/model/ValueResourceType.kt @@ -0,0 +1,17 @@ +package dev.randos.resourcemanager.model + +/** + * Sealed class representing various types of value resources in an android application. + */ +internal sealed class ValueResourceType{ + class String(val isParameterized: kotlin.Boolean = false) : ValueResourceType() + object Color: ValueResourceType() + object Boolean: ValueResourceType() + object Integer: ValueResourceType() + object Dimension: ValueResourceType() + object Fraction: ValueResourceType() + object Array: ValueResourceType() + object StringArray: ValueResourceType() + object IntArray: ValueResourceType() + object Plural: ValueResourceType() +} diff --git a/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/utils/ExtensionFunction.kt b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/utils/ExtensionFunction.kt new file mode 100644 index 0000000..d7eb27f --- /dev/null +++ b/resourcemanager/src/main/kotlin/dev/randos/resourcemanager/utils/ExtensionFunction.kt @@ -0,0 +1,11 @@ +package dev.randos.resourcemanager.utils + +// Function to convert snake_case to camelCase +internal fun String.toCamelCase(): String { + return split('_') // Split by underscores + .mapIndexed { index, word -> + if (index == 0) word.lowercase() // First word stays lowercase + else word.replaceFirstChar { it.uppercase() } // Capitalize the first letter of subsequent words + } + .joinToString("") // Join the words back together without spaces +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b900363..7b8d5d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,4 @@ rootProject.name = "ResourceProvider" include(":app") include(":resourcemanager-compiler") include(":resourcemanager-runtime") +include(":resourcemanager")