An application property generator framework that validates and generates yourapplication.properties
file on runtime.
- Setup
- Gradle Plugin configurations
- Plugin Tasks
- Runtime Property Validation
- Natively Supported Property Types
- Writing Your Custom Property Types
- Working With Multiple Environments
- Blank Values For Properties (Rela xing Mandatory Values Condition)
- Further Test Support With The Environment Factory
- Demo Project (w/ a CICD Pipeline)
Using theplugins
&dependencies
blocks, you can set up Propactive as follows:
plugins {
id("io.github.propactive") version"2.1.0"
}
dependencies {
implementation("io.github.propactive:propactive-jvm:2.1.0")
}
/** Placed at the root of the main source directory. (i.e. src/main/kotlin/ApplicationProperties.kt)*/
@Environment
objectApplicationProperties {
@Property(["HelloWorld"])
constvalproperty="propactive.property.key"
}
Running the Propactive task./gradlew generateApplicationProperties
will generate the following properties file:
propactive.property.key=HelloWorld
The file will be namedapplication.properties
and it will be located within thedist
directory of your set build
destination. If you want to learnhow to configure the location of your properties object,
how to set a custom application properties filename,orhow to work with multiple environments when using Propactive,
then have a look at the rest this guide.
Proactive provides a plugin extension that allows you to specify the destination of the created application properties file, set the location of the implementation class, and/or specify which environments you want to generate application properties files for by default.
Here is an example that generates the files to a directory calledproperties
within your build folder, locates the implementation
class of the application properties object atio.github.propactive.demo.Properties
,and will only generate theprod
environment
application properties within a file namedapplication.properties
when the taskgenerateApplicationProperties
is executed:
propactive {
environments="prod"
implementationClass="io.github.propactive.demo.Properties"
destination=layout.buildDirectory.dir("properties").get().asFile.absolutePath
filenameOverride="application.properties"
}
The default values for thepropactive
extension are as follows:
propactive {
environments="*"
implementationClass="ApplicationProperties"
destination=layout.buildDirectory.dir("resources/main").get().asFile.absolutePath
autoGenerateApplicationProperties=true
filenameOverride=null
classCompileDependency=null
}
Since Propactive is a runtime property generator that relies on loading a properties class at runtime, we need to ensure that
the properties class is compiled before thegenerateApplicationProperties
task is executed. By default, the plugin will set
theclassCompileDependency
option tocompileJava
or/andcompileKotlin
if you are using Kotlin.
However, you can set theclassCompileDependency
option to something else if you want to optimise your build time, or if
you are compiling your classes from a different source set. For example, if you areusing Scala,
you can set theclassCompileDependency
option tocompileScala
:
propactive {
classCompileDependency="compileScala"
}
By default, the plugin will generate the properties file when theclasses
task is executed. If you want to disable this
behaviour, you can set theautoGenerateApplicationProperties
option tofalse
:
propactive {
autoGenerateApplicationProperties=false
}
Propactive provides 2 tasks that you can use to generate and validate your application properties files:
Propactive tasks
----------------
generateApplicationProperties - Generates application properties file for each given environment.
Optional configurations:
-Penvironments
Description: Comma separated list of environments to generate the properties for.
Example: "test,stage,prod"
Default: "*" (All provided environments)
-PimplementationClass
Description: Sets the location of your properties object.
Example: "com.package.path.to.your.ApplicationProperties"
Default: "ApplicationProperties" (At the root of your project, without a package path.)
-Pdestination
Description: Sets the location of your generated properties file within the build directory.
Example: layout.buildDirectory.dir( "properties" ).get().asFile.absolutePath
Default: layout.buildDirectory.dir( "resources/main" ).get().asFile.absolutePath (In the main resources directory)
-PfilenameOverride
Description: Allows overriding given filename for when you're generating properties for a single environment.
Example: "dev-application.properties"
Note: This can only be used when generating application properties for a singular environment.
validateApplicationProperties - Validates the application properties without generating any files.
Optional configurations:
-PimplementationClass
Description: Sets the location of your properties object.
Example: "com.package.path.to.your.ApplicationProperties"
Default: "ApplicationProperties" (At the root of your project, without a package path.)
One of the key features Propactive has is the ability to validate given property values on runtime in a modular manner.
Let's consider the following scenario, You have an environment dependant URL values for a property calledapp.web.server.url
:
- prod:
https:// prodland
- test:
http:// nonprodland
- dev:
http://127.0.0.1/
Therefore, you will end up creating 3application.properties
files:
#prod-application.properties
app.web.server.url=https:// prodland
#test-application.properties
app.web.server.url=http:// nonprodland
#dev-application.properties
app.web.server.url=http://127.0.0.1/
Usually, this is fine, but as you scale, you have many environments, and dozens of application properties that have different
values for each environment. Therefore, this becomes a mundane process and error-prone. Not only you will need to define a constant for
app.web.server.url
to test your property values, and perhaps another constant to reference it on your application side,
you will also need to parse each file if you want to test that the URL value is of valid format, if such precision is required.
With Propactive, this could simply be written like so:
@Environment(["prod/test/dev: *-application.properties"])
objectProperties {
@Property(
value=[
"prod: https:// prodland",
"test: http:// nonprodland",
"dev: http://127.0.0.1/",
],
type=URL::class
)
constvalappWebServerUrlPropertyKey="app.web.server.url"
}
Now locally, orwithin your CI/CD,you can generate the required application properties
file by running the following command: (omit-Penvironments
option to generate the files for all environments)
#TIP: This can be added as part of your deployment or build process as required
./gradlew generateApplicationProperties -Penvironments=prod
This will generate a file namedprod-application.properties
with the following entries:
app.web.server.url=https:// prodland
On top of that, it will validate the key value set by type (e.g.URL
), if it's an invalid type, it will
fail with a verbose error. For example, the error message below is produced by having a malformed protocol keyword: (e.g. "htps" instead of "https" )
Property named: "propactive.demo.url.key" within environment named: "prod" was expected to be of type: "URL", but value was: "htps:// prodland"
You can have a look below for thelist of natively supported property typesor learn how to write your custom property typesthat you can use for runtime validation.
Propactive comes with a set of natively supported property types that you can use for validating your property values on runtime. Below is a reference for each type and the specification followed:
- BASE64:type as defined byRFC 4648
- BOOLEAN:type as defined by your JVM.
- DECIMAL:type as defined byIEEE 754
- INTEGER:type is a 32-bit signed integer, as defined by your JVM.
- JSON:type as defined byRFC 8259
- STRING:type represents character strings, as defined by your JVM.
- URI:type as defined byRFC 3986
- URL:type as defined byRFC 2396
- UUID:type as defined byRFC 4122
- CLASS:type defined as a syntactically valid format as perJLS 3.8.
- PORT:type as defined byRFC 6335
If you believe we missed a common property type, feel free to let us know by opening anissueor make a PR, and we will be happy to merge. Otherwise, please see the next section to learnhow to write your custom property types.
Writing your custom property types is quite straightforward, you just need to implement thepropactive.type.Type
interface,
override thevalidate
type, returntrue
(or the constantio.github.propactive.type.Type.VALID
) when validation pass orfalse
(or the constantio.github.propactive.type.Type.INVALID
)
when the validation fails, then you can use the type within your@Property
annotation as usual.
Here is aPORT_NUMBER
type that you can use to validate if a port number is within a valid range: (i.e.0 till 65535
)
importio.github.propactive.type.Type
objectPORT_NUMBER: Type {
overridefunvalidate(value:Any)=value
.runCatching { toString().toInt() }
.getOrDefault(-1)
.let{ number->numberin(1..65535) }
}
importio.github.propactive.environment.Environment
importio.github.propactive.property.Property
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties",
])
objectApplicationProperties {
@Property(
value=[
"prod: 433",
"stage/test: 80",
"dev: 8080",
],
type=PORT_NUMBER::class
)
constvalappWebServerPortPropertyKey="app.web.server.port"
}
Running./gradlew generateApplicationProperties
will generate the relevant application properties files, and the
typed port number validation will occur at runtime. You cansee this code running within our demo project.
Working with multiple environments' means you will need a way to distinguish between different environment filenames and
different environment values. Proactive provides you the option to define multiple environments perApplicationProperties
object and allows you to cascade multiple key entries against a single value.
Below is an example with 4 environments wherestage
andtest
share the same values, butprod
anddev
have
separate entries. Note that the@Environment
annotation supports a special wildcard expansion key (*
) that is evaluated
to the environment name. (i.e. in the following examplestage
andtest
entries will generate 2 files named
stage-application.properties
andtest-application.properties
)
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties"
])
objectApplicationProperties {
@Property(
value=[
"prod: https:// prodland",
"stage/test: http:// nonprodland",
"dev: http://127.0.0.1/",
],
type=URL::class
)
constvalappWebServerUrlPropertyKey="app.web.server.url"
}
You can also map a property value with multiple environment keys. Above example shows that thestage
andtest
entries
will share the same"app.web.server.url"
value. (i.e.http:// nonprodland
)
By default, properties key are expected to have a value assigned to it, and will error out if not. (i.e. an environment key cannot
have a blank value) This condition can be relaxed by setting themandatory
option to false:
@Environment
objectApplicationProperties {
@Property(mandatory=false)
constvalproperty="propactive.property.key"
}
This will generate a YAML file withpropactive.property.key=
and assign no value (blank) to it.
Sometimes you might want to do more granular testing on the application property keys and values. For that,
we provide theEnvironmentFactory
object for creating an Environment model that you can use for extracting
any property name or value to uphold any assertions. Here is an example:
//The properties object
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties",
])
objectProperties {
@Property(
value=[
"prod: 3000",
"stage/test: 10000",
"dev: 30000",
],
type=INTEGER::class
)
constvaltimoutInMsPropertyKey="propactive.demo.timout-in-ms.key"
}
//The test class that's making use of the EnvironmentFactory object:
classPropertiesTest{
@Test
funshouldHaveTimeoutLargerThan250ms() {
findAllMatchingPropertiesFor(timoutInMsPropertyKey)
.forEach {
assertTrue(
it.value.toInt()>250,
"Expected:$timoutInMsPropertyKeyfor environment:${it.environment}to have a value larger than 250ms but was:${it.value}"
)
}
}
privatefunfindAllMatchingPropertiesFor(propertyKey:String):List<PropertyModel>=EnvironmentFactory
.create(Properties::class)
.mapNotNull { env->env.properties.firstOrNull { it.name==propertyKey } }
}
You cansee this code running within our demo project.
To make the usecase of the Proactive framework clear, we provide an example project that makes use of above-mentioned features and is integrated with its own CI/CD pipeline. You will see how the application properties are validated and generated per environment. To top it up, a docker image is created/ran for each environment on deployment with a job summary outputted for each environment properties.
The project can be found here:propactive/proactive-demo