An application property generator framework that validates and generates your application.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 (Relaxing Mandatory Values Condition)
- Further Test Support With The Environment Factory
- Demo Project (w/ a CICD Pipeline)
Using the plugins
& 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
object ApplicationProperties {
@Property(["HelloWorld"])
const val property = "propactive.property.key"
}
Running the Propactive task ./gradlew generateApplicationProperties
will generate the following properties file:
propactive.property.key=HelloWorld
The file will be named application.properties
and it will be located within the dist
directory of your set build
destination. If you want to learn how to configure the location of your properties object,
how to set a custom application properties filename, or how 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 called properties
within your build folder, locates the implementation
class of the application properties object at io.github.propactive.demo.Properties
, and will only generate the prod
environment
application properties within a file named application.properties
when the task generateApplicationProperties
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 the propactive
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 the generateApplicationProperties
task is executed. By default, the plugin will set
the classCompileDependency
option to compileJava
or/and compileKotlin
if you are using Kotlin.
However, you can set the classCompileDependency
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 are using Scala,
you can set the classCompileDependency
option to compileScala
:
propactive {
classCompileDependency = "compileScala"
}
By default, the plugin will generate the properties file when the classes
task is executed. If you want to disable this
behaviour, you can set the autoGenerateApplicationProperties
option to false
:
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 called app.web.server.url
:
- prod:
https://www.prodland.com
- test:
http://www.nonprodland.com
- dev:
http://127.0.0.1/
Therefore, you will end up creating 3 application.properties
files:
# prod-application.properties
app.web.server.url=https://www.prodland.com
# test-application.properties
app.web.server.url=http://www.nonprodland.com
# 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"])
object Properties {
@Property(
value = [
"prod: https://www.prodland.com",
"test: http://www.nonprodland.com",
"dev: http://127.0.0.1/",
],
type = URL::class
)
const val appWebServerUrlPropertyKey = "app.web.server.url"
}
Now locally, or within 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 named prod-application.properties
with the following entries:
app.web.server.url=https://www.prodland.com
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://www.prodland.com"
You can have a look below for the list of natively supported property types or learn how to write your custom property types that 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 by RFC 4648
- BOOLEAN: type as defined by your JVM.
- DECIMAL: type as defined by IEEE 754
- INTEGER: type is a 32-bit signed integer, as defined by your JVM.
- JSON: type as defined by RFC 8259
- STRING: type represents character strings, as defined by your JVM.
- URI: type as defined by RFC 3986
- URL: type as defined by RFC 2396
- UUID: type as defined by RFC 4122
- CLASS: type defined as a syntactically valid format as per JLS 3.8.
- PORT: type as defined by RFC 6335
If you believe we missed a common property type, feel free to let us know by opening an issue or make a PR, and we will be happy to merge. Otherwise, please see the next section to learn how to write your custom property types.
Writing your custom property types is quite straightforward, you just need to implement the propactive.type.Type
interface,
override the validate
type, return true
(or the constant io.github.propactive.type.Type.VALID
) when validation pass or false
(or the constant io.github.propactive.type.Type.INVALID
)
when the validation fails, then you can use the type within your @Property
annotation as usual.
Here is a PORT_NUMBER
type that you can use to validate if a port number is within a valid range: (i.e. 0 till 65535
)
import io.github.propactive.type.Type
object PORT_NUMBER : Type {
override fun validate(value: Any) = value
.runCatching { toString().toInt() }
.getOrDefault(-1)
.let { number -> number in (1..65535) }
}
import io.github.propactive.environment.Environment
import io.github.propactive.property.Property
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties",
])
object ApplicationProperties {
@Property(
value = [
"prod: 433",
"stage/test: 80",
"dev: 8080",
],
type = PORT_NUMBER::class
)
const val appWebServerPortPropertyKey = "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 can see 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 per ApplicationProperties
object and allows you to cascade multiple key entries against a single value.
Below is an example with 4 environments where stage
and test
share the same values, but prod
and dev
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 example stage
and test
entries will generate 2 files named
stage-application.properties
and test-application.properties
)
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties"
])
object ApplicationProperties {
@Property(
value = [
"prod: https://www.prodland.com",
"stage/test: http://www.nonprodland.com",
"dev: http://127.0.0.1/",
],
type = URL::class
)
const val appWebServerUrlPropertyKey = "app.web.server.url"
}
You can also map a property value with multiple environment keys. Above example shows that the stage
and test
entries
will share the same "app.web.server.url"
value. (i.e. http://www.nonprodland.com
)
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 the mandatory
option to false:
@Environment
object ApplicationProperties {
@Property(mandatory = false)
const val property = "propactive.property.key"
}
This will generate a YAML file with propactive.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 the EnvironmentFactory
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",
])
object Properties {
@Property(
value = [
"prod: 3000",
"stage/test: 10000",
"dev: 30000",
],
type = INTEGER::class
)
const val timoutInMsPropertyKey = "propactive.demo.timout-in-ms.key"
}
// The test class that's making use of the EnvironmentFactory object:
class PropertiesTest {
@Test
fun shouldHaveTimeoutLargerThan250ms() {
findAllMatchingPropertiesFor(timoutInMsPropertyKey)
.forEach {
assertTrue(
it.value.toInt() > 250,
"Expected: $timoutInMsPropertyKey for environment: ${it.environment} to have a value larger than 250ms but was: ${it.value}"
)
}
}
private fun findAllMatchingPropertiesFor(propertyKey: String): List<PropertyModel> = EnvironmentFactory
.create(Properties::class)
.mapNotNull { env -> env.properties.firstOrNull { it.name == propertyKey } }
}
You can see 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