Código del Plugin
ReplacePropertiesResourcesPlugin.groovy
package com.blogspot.nombre_temp.gradle.replace.properties.resources
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* This Gradle plugin replaces the values in ".properties" files inside the project's resources with values from files with the same name inside a different
* name after the "processResources" Gradle task, which is used during the build process for projects with code to be executed in the JVM (like Java and Groovy).
*
* For files other than ".properties" they will also be copied and filtered using using the ReplaceTokens feature from Ant.
* Any text between "@" symbols (tokens) will be replaced with a value from a system property with the same key as the surrounded text.
*/
class ReplacePropertiesResourcesPlugin implements Plugin {
@Override
public void apply(Project project) {
project.afterEvaluate { p ->
def processResourcesTask = p.tasks.findByName('processResources')
if (processResourcesTask == null) {
println "processResources task not found. Make sure to apply the plugin that has it before (for example 'java' or 'groovy')."
} else {
processResourcesTask.inputs.properties System.properties
def environment = System.properties.env
if (environment != null) {
def configEnvironmentFolder = p.properties.configEnvironmentFolder ? p.properties.configEnvironmentFolder : 'config'
def environmentFolder = p.file("$configEnvironmentFolder/$environment")
if (!p.file(configEnvironmentFolder).exists()) {
throw new InvalidUserDataException("Configuration environment folder not found: $configEnvironmentFolder")
}
if (!environmentFolder.exists()) {
throw new InvalidUserDataException("Environment folder not found: $configEnvironmentFolder/$environment")
}
// Executed only if the configuration files or the system properties changed from previous execution
processResourcesTask.inputs.dir p.file("$configEnvironmentFolder/$environment")
processResourcesTask.doLast {
println "***********************************************************"
println "Using environment: $environment"
println "***********************************************************"
// Copy all resources files, except ".properties"
p.fileTree(dir: "$configEnvironmentFolder/$environment" , exclude: '**/*.properties').each { file ->
def fileRelativePath = environmentFolder.toURI().relativize( file.toURI() ).path
p.sourceSets.each { source ->
// Gets the corresponding file in the resources build folder
def ouputFile = p.file("$source.output.resourcesDir/$fileRelativePath")
if (ouputFile.exists()) {
p.copy {
into ouputFile.parent
from(file) {
filter(ReplaceTokens, tokens: System.properties)
}
}
}
}
}
p.fileTree(dir: "$configEnvironmentFolder/$environment" , include: '**/*.properties').each { file ->
def fileRelativePath = environmentFolder.toURI().relativize( file.toURI() ).path
p.sourceSets.each { source ->
// Gets the corresponding file in the resources build folder
def ouputFile = p.file("$source.output.resourcesDir/$fileRelativePath")
if (ouputFile.exists()) {
def environmentProperties = new Properties()
file.withInputStream {
environmentProperties.load(it);
}
// Overwrites the values in the file with the ones given from command line arguments -Dkey
System.properties.each { key, value ->
if (environmentProperties.containsKey(key)) {
environmentProperties.put(key, value)
}
}
// Replaces all values on "ouputFile" with the ones contained in "environmentProperties"
environmentProperties.each { propKey, propValue ->
p.ant.propertyfile(file: ouputFile) {
entry(key: propKey, type: 'string', operation: '=', value: propValue)
}
}
}
}
}
}
}
}
}
}
}
Línea 15: Se implementa la interfaz
Plugin, la cual se requiere para el desarrollo de nuevos
plugins.
Línea 17: Implementación del método "
apply", el cual se ejecuta al aplicar el
plugin a un proyecto, recibiendo precisamente como parámetro el proyecto en que se aplica.
Línea 19: Mediante un
Closure se indica el código a ejecutar luego de que el
script de construcción del
proyecto ha sido evaluado y se han aplicado las configuraciones iniciales. El código de este
plugin se deja dentro de este bloque para asegurar que al momento de buscar la tarea "
processResources" el
plugin que la adicione ("
java", "
groovy", o cualquier otra) ya se encuentre aplicado y la tarea haga parte del proyecto.
Línea 25: Adición de las propiedades del sistema a las entradas (
inputs), para ejecutar la tarea "
processResources" sólo cuando se cambien estos datos (adicional a los archivos de recursos) y así conservar la
Construcción Incremental.
Línea 26: Se asigna el ambiente a usar indicado como parámetro (propiedad del sistema).
Línea 28: Si no se indicó un ambiente, no se continua con la ejecución de este
plugin, usando entonces los valores de las propiedades que se encuentran directamente en la carpeta de recursos.
Línea 29: Con esta línea se permite cambiar el nombre de la carpeta en la cual se encontrarán las sub-carpetas de cada ambiente agregando la
propiedad extra "
configEnvironmentFolder", bien sea dentro del archivo "
build.gradle" o en "
gradle.properties" del proyecto en que se aplique el
plugin.
Líneas 32-37: Se valida que las carpetas de configuración y de ambientes existan. Esta es una mejora con respecto al
script del
post anterior, ya que no se está dependiendo de una lista.
Línea 40: Adición de la carpeta del ambiente usado a las entradas (
inputs), para ejecutar la tarea "
processResources" sólo cuando se cambie su estructura o el contenido de sus archivos, adicional a las entradas previamente agregadas.
Línea 42: Se usa el bloque "
doLast" de la tarea "
processResources" para que el código sea ejecutado únicamente cuando se ejecute esta tarea (no en la fase de configuración) y después de que se copien los archivos de recursos (por defecto en "
src/main/resources") a la carpeta de construcción (por defecto en "
build/resources/main").
Línea 48: Iteración de los archivos que
no son "
.properties" ubicados en la carpeta del ambiente de despliegue a usar.
Línea 49: Se obtiene la ruta relativa del archivo iterado con respecto al proyecto. Esto con el fin de poder determinar si por ejemplo un archivo "
src/main/resources/logback.xml" tiene su contra-parte en la carpeta del ambiente, por ejemplo "
config/prod_1/logback.xml". No se valida sólo con el nombre del archivo para permitir tener sub-carpetas.
Línea 51: Iteración de las carpetas de código fuente (
SourceSets) y recursos del proyecto. Por defecto se tienen "
main" y "
test" pero
pueden añadirse más según se necesite en el proyecto.
Líneas 53-54: La instrucción "
source.output.resourcesDir" obtiene la carpeta de salida (en la que se copian al compilar y construir el proyecto) de los recursos del
SourceSet iterado. Por ejemplo, al iterar "
main", la carpeta de salida de los recursos sería "
build/resources/main". Luego se adiciona la ruta del archivo de propiedades iterado desde la línea 48 y se verifica si este existe la carpeta de salida.
Líneas 55-61: Se copia el archivo desde su carpeta origen (ambiente seleccionado) hacia la carpeta de salida de los recursos, reemplazando los
tokens que dicho archivo pueda tener. Si el archivo no tiene
tokens, se copia el archivo sin modificaciones.
Línea 65: Iteración de los archivos "
.properties" que se encuentran en la carpeta del ambiente de despliegue a usar.
Línea 66: Se obtiene la ruta relativa del archivo iterado con respecto al proyecto.
Línea 68: Iteración de las carpetas de código fuente (
SourceSets) y recursos del proyecto.
Líneas 74-76: Se cargan las propiedades que se encuentran en el archivo del ambiente usado.
Líneas 79-83: Las propiedades anteriormente cargadas se sobrescriben con las propiedades del sistema que tengan la misma llave.
Líneas 86-90: En aquel
post anterior se estaba usando el filtro
ReplaceTokens de
Ant para reemplazar los
tokens de los archivos de recursos procesados. En este
plugin se usa en cambio se usa la tarea "
PropertyFile" (también de Ant) para tomar las propiedades del archivo que fue copiado en la carpeta de salida y reemplazar sus valores con los indicados en el archivo correspondiente en la carpeta del ambiente o en las propiedades del sistema. De esta manera el archivo de propiedades original no se modifica y no requiere
tokens en su contenido, permitiendo tener los valores del ambiente por defecto allí sin afectar la construcción para otros ambientes.
En general este código es muy similar al del
post anterior con un par de mejoras.
Nota: Este
plugin no tiene una clase para definir una tarea propia, ya que lo que se hace en este caso es modificar la tarea ya existente "
processResources". Para más información sobre el desarrollo de tareas dentro de un
plugin de Gradle puede consultarse la
documentación oficial.
Pruebas Locales del Plugin
Antes de publicar el
plugin, este puede aplicarse y probarse en un proyecto Gradle localmente. Para ello puede usarse la tarea "
install" del
plugin "
maven" con lo cual se instala el JAR del proyecto en el repositorio local de Maven.
gradlew.bat install
:compileJava UP-TO-DATE
:compileGroovy
:processResources
:classes
:jar
:groovydoc
:publishPluginGroovyDocsJar
:publishPluginJar
:javadoc UP-TO-DATE
:publishPluginJavaDocsJar
:install
BUILD SUCCESSFUL
Una vez hecho esto puede incluirse en un proyecto Gradle. En este caso se usará una copia del proyecto que usaba el
script original:
https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin-demo
Para usar el
plugin desde el repositorio local se debe cambiar el archivo "
build.gradle" de la siguiente manera:
buildscript {
repositories {
mavenLocal()
maven {
url 'https://plugins.gradle.org/m2/'
}
}
dependencies {
classpath 'net.researchgate:gradle-release:2.3.4'
classpath 'com.blogspot.nombre-temp.replace.properties.resources:0.0.1-SNAPSHOT'
}
}
plugins {
id 'java'
id 'application'
id 'net.researchgate.release' version '2.3.4'
id 'com.blogspot.nombre-temp.replace.properties.resources' version '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
compileJava.options.encoding = 'UTF-8'
mainClassName = 'com.blogspot.nombre_temp.jetty.jersey.multi.project.example.ExampleStarter'
jar {
manifest {
attributes 'Implementation-Title': 'Jetty and Jersey Multi Environment Example', 'Implementation-Version': version
attributes 'Main-Class': mainClassName
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.9'
}
repositories {
jcenter()
}
dependencies {
compile "org.eclipse.jetty:jetty-server:$jettyVersion"
compile "org.eclipse.jetty:jetty-servlet:$jettyVersion"
compile "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
compile "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"
compile "commons-configuration:commons-configuration:1.10"
}
Las diferencias principales, con respecto a la
versión original, están en las líneas 3, 10 y 17, en las cuales se adiciona el repositorio de Maven local como fuente de
plugins, se agrega la dependencia al
plugin que se está probando y se aplica.
También puede verse que esta versión del archivo es mucho más corta y fácil de leer al no tener que modificar la tarea "
processResources", porque ello ya es realizado en el nuevo
plugin.
Para probar rápidamente el funcionamiento del plugin puede ejecutarse simplemente la tarea "
processResources" y los archivos de recursos procesados estarán en la carpeta "
build/resources".
gradlew.bat processResources
:processResources
***********************************************************
Using environment: local
***********************************************************
BUILD SUCCESSFUL
build/resources/main/application.properties
app.instance.name=Jetty-Server
app.instance.number=1
gradlew.bat processResources -Denv=prod2
:processResources
***********************************************************
Using environment: prod2
***********************************************************
BUILD SUCCESSFUL
build/resources/main/application.properties
app.instance.name=Jetty-Server
app.instance.number=2
gradlew.bat processResources -Denv=prod2 -Dapp.instance.number=5
:processResources
***********************************************************
Using environment: prod2
***********************************************************
BUILD SUCCESSFUL
build/resources/main/application.properties
app.instance.name=Jetty-Server
app.instance.number=5
Publicación del Plugin
Para publicar el
plugin en el portal oficial, luego de tener configurado "
Publishing Plugin", se debe crear una cuenta y seguir las instrucciones en
https://plugins.gradle.org/docs/submit para obtener un "API key" y "API secret".
Dado que se tienen integrados
plugins "
Publishing Plugin" y "
Gradle Release", simplemente al ejecutar la tarea de Gradle "
release", se versiona el proyecto en el repositorio de GitHub y se carga el
plugin compilado al portal de
plugins. Este
plugin puede encontrarse en:
https://plugins.gradle.org/plugin/com.blogspot.nombre-temp.replace.properties.resources
Nota: Al momento de escribir este post el
plugin "
Publishing Plugin" tiene un
error ya reportado, en el que al usar un archivo "
gradle.properties" en el proyecto no se toman los valores de "
$USER_HOME/.gradle/gradle.properties". Para solucionar esto, sin incluir la llave y contraseña de Gradle en el archivo del proyecto, se puede ejecutar la tarea de Gradle con dichos valores:
gradlew.bat release -Dgradle.publish.key=xxxxxxx -Dgradle.publish.secret=yyyyyyy
Aplicando el Plugin
Una vez se tiene el
plugin publicado en el portal de Gradle es posible modificar el archivo "
build.gradle" del proyecto con el que se hizo la prueba local, para que use la versión publicada:
plugins {
id 'java'
id 'application'
id 'net.researchgate.release' version '2.3.4'
id 'com.blogspot.nombre-temp.replace.properties.resources' version '0.0.4'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
compileJava.options.encoding = 'UTF-8'
mainClassName = 'com.blogspot.nombre_temp.jetty.jersey.multi.project.example.ExampleStarter'
jar {
manifest {
attributes 'Implementation-Title': 'Jetty and Jersey Multi Environment Example', 'Implementation-Version': version
attributes 'Main-Class': mainClassName
}
}
task wrapper(type: Wrapper) {
gradleVersion = '2.9'
}
repositories {
jcenter()
}
dependencies {
compile "org.eclipse.jetty:jetty-server:$jettyVersion"
compile "org.eclipse.jetty:jetty-servlet:$jettyVersion"
compile "org.glassfish.jersey.core:jersey-server:$jerseyVersion"
compile "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"
compile "commons-configuration:commons-configuration:1.10"
}
Las pruebas sobre el funcionamiento del plugin pueden ser las mismas que se hicieron localmente. El proyecto usando este plugin publicado puede descargarse en:
https://github.com/guillermo-varela/gradle-replace-properties-resources-plugin-demo
Nota: En este caso el
plugin desarrollado aparece con versión "
0.0.4" debido a que se realizaron un par de mejoras a la primera versión publicada, pero el portal de Gradle no permite subir cambios sobre la misma versión del
plugin sino que esta debe incrementarse.
Ejecutando la Aplicación desde un IDE
Como se comentó al inicio, este
plugin permite que al ejecutar el proyecto desde un IDE (sin ejecutar las tareas de Gradle) puedan usarse los valores de un ambiente local de desarrollo, lo cual facilita la ejecución y depuración al no tener que configurar desde el IDE la ejecución de Gradle y la depuración remota.
Figura 1 - Aplicación ejecutada desde Eclipse sin ejecutar Gradle
Referencias
https://docs.gradle.org/current/userguide/custom_plugins.html
https://docs.gradle.org/current/userguide/custom_tasks.html
https://plugins.gradle.org/docs/publish-plugin
https://docs.gradle.org/current/userguide/build_lifecycle.html
https://docs.gradle.org/current/userguide/java_plugin.html
https://docs.gradle.org/current/javadoc/org/gradle/language/jvm/tasks/ProcessResources.html
Más sobre Gradle
http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html