JavaPropFile Gradle Plugin

This plugin is for loading a Map, a Gradle Project or extension object (and objects nested beneath them), with properties from Java properties file; or for expanding given Strings or text files with the objects just listed or with a supplied Map; plus a general Gradle Copy Filter is provided.

IMPORTANT: Users who are upgrading Gradle or upgrading JavaPropFile should read the file upgrade.txt in the doc subdirectory.

Most classes are supported, so that you can write JDK objects, custom objects, array, and collections in addition to String values (using a simply casting syntax like "file.txt(File)"). Several mechanisms provided make it easy use a differentiated property name space for propery file loads (when your use case allows for this). The Copy Filter is very similar to the Gradle built-in operator:

     filter { String line -> output one line }

but eliminating the performance and limitations due to processing a-line-at-a-time.

What's wrong with Gradle's gradle.properties system? You are restricted to a single properties in your home directory and one for each project directory, all with mandated name. If you want to separate your properties into shared properties and personal properties, as is a very good practice, you can't. If you want to set the value of a property based on anything else (like a previously set property, an extension object property value, a system property, or some nested item in your Project), you can't. If you want to set even a basic JDK object like a Boolean or File value, you will have to change your Gradle Groovy code to convert from the String property value. If you want to load or merge new properties from a properties file into an exiting map, perhaps automatically prefixing these properties with a distinguishing prefix, you have to do that manually. If you want to do all of these things at once... settle down for a week of coding.

What's wrong with Gradle's filter { Sring line -> ... }? Nothing if you really want to work on each input file line independently. Otherwise, your app will suffer from a massive performance penalty from executing the closure once for every input file line stead of just once; and you are subject to regular expression and EOL-transformation limitations.

A sample build setup is provided in subdirectory doc. Even if you don't care to run the demo, you would probably benefit by looking at the .properties files in there, if not the build.gradle file. If you want to run the example and are pulling this project from Git, then cd to the doc subdirectory and run ../gradlew (..\gradlew on Windows). You do not need to have Gradle installed to run the demonstration (if you pull the entire project from Git). WARNING: If you pull from Git trunk, you will be working with pre-production code. The sample may be in the process of being updated. If you experience problems running the sample, work with a Git tag instead of trunk, or open a Github Issue to get help.

Features

Usage

Pull plugin from Internet.

Couldn't be easier. This will pull the plugin from jCenter:

        buildscript {
            repositories { jcenter() }  // Before plugin v. 1.0.0 use mavenCentral()
            dependencies {
                classpath 'com.admc:gradle-javaPropFile-plugin:latest.integration'
                // For strictly repeatable builds, replace 'latest.integration'
                // with the latest literal GA version label.
            }
        }
        apply plugin: 'com.admc.javaPropFile'
        // Following loads 'app.properties' then 'local.properties' files
        // from project directory if they exist there.
        propFileLoader.traditionalPropertiesInit()
        // See
        // http://admc.com/projectdocs/gradle-javaPropFile-plugin/readme.html
        // for examples of specifying your own file names and settings,
        // including usage of typeCasting.

        // Create a new Map by loading a properties file:
        someMap = propFileLoader.load(file('mail.properties'), [:])
        // The "[:]" parameter causes a map to be returned (as opposed to
        // adding to the project's properties and returning nothing).

        // To use the ContentAsStringFilter, you don't need to 'apply' anything.
        import com.admc.gradle.ContentAsStringFilter
        task anyCopyTask(type: Copy) {
            from 'src/main/resources'
            into 'build/tmp/x'
            filter(ContentAsStringFilter, closure: { it.toLowerCase() })
        }
        // See
        // http://admc.com/projectdocs/gradle-javaPropFile-plugin/readme.html
        // for some other very useful examples using ContentAsStringFilter.

Use plugin jar file locally.

Just use your browser to go to the JavaPropFile directory at jCenter. http://jcenter.bintray.com/com/admc/gradle-javaPropFile-plugin/. Click into the version that you want. Right-click and download the only file in that directory that ends in "-<VERSION_LABEL>.jar" (i.e. the only jar file which is neither for JavaDoc nor source code).

You can save the plugin jar with your project, thereby automatically sharing it with other project developers (assuming you use some SCM system). Or you can store it in a local directory, perhaps with other Gradle plugin jars. The procedure is the same either way:

        buildscript { dependencies {
            classpath fileTree(
                dir: 'directory/containing/the/plugin/jar/file',
                include: 'gradle-javaPropFile-plugin-*.jar'
            )
        } }
        apply plugin: 'com.admc.javaPropFile'
        // Following loads 'app.properties' then 'local.properties' files
        // from project directory if they exist there.
        propFileLoader.traditionalPropertiesInit()
        // https://github.com/unsaved/gradle-javaPropFile-plugin/tree/master/doc
        // for examples of specifying your own file names and settings,
        // including usage of typeCasting.

        // Create a new Map by loading a properties file:
        someMap = propFileLoader.load(file('mail.properties'), [:])
        // The final "[:]" parameter causes a map to be returned (as opposed to
        // adding to the project's properties and returning nothing).

        // To use the ContentAsStringFilter, you don't need to 'apply' anything.
        import com.admc.gradle.ContentAsStringFilter
        task anyCopyTask(type: Copy) {
            from 'src/main/resources'
            into 'build/tmp/x'
            filter(ContentAsStringFilter, closure: { it.toLowerCase() })
        }
        // See
        // https://github.com/unsaved/gradle-javaPropFile-plugin/tree/master/doc
        // for some other very useful examples using ContentAsStringFilter.

Details

Definitions

IMPORTANT! Key/Value Delimiter

Java properties files allow for the key to be terminated by equal sign, colon character, or a white space character. (Our keys contain a property and may contain other typing and parent-object-identifying information). Since white-space is optional after the property keys, and since property file rules even allow for the '=' to follow white space, we use white space in this position to toggle DotDeref behavior.

Compare:

        x.a=eks
        # No whitespace immediately after key x.a so property name is x.a.

vs.

        x.a =eks
        # Whitespace immediately after key x.a so this means property a of
        # object x.

(MNEMONIC: Groovy is distinctive for allowing white space to delimit between method name and parameters. We use white space as delimiter to trigger Groovy-style dereferencing.) The presence of absence of white space after the key enables or disables DotDeref both for the key and for ${references} in the value. Another example to show this:

        x=Value includes a reference to ${property.with.name.containg.dots}

vs.

        x =Value includes a reference to ${subObject1.subObject2.propertyName}

Reference Syntax

References are not allowed on the left-hand side of property file records. They may be used on the value (right-hand) side of property file records, and in Strings and Files that are expand()ed. These are the variants:

        ${propName}          # Simple property 'propName' of the target
                             # object (which defaults to the Gradle Project).
        ${sys|propName}      # For Java system property 'propName', using
                             # default systemPropPrefix of 'sys|'.
        ${extObj$propName}   # Property 'propName' of extension object 'extObj'
        ${obj1.obj2.propName}  # If DotDeref operator is not active,
                               # then this is just property
                               # 'obj1.obj2.propName' of the target object
                               # like the first case above.
                               # If DotDeref operator is active,
                               # then this is property 'projName' of property
                               # obj2 of property obj1 or the target object.
                               # In the second case, it is an error to
                               # reference a non-defined property.
        # In all cases above (except nested properties, as noted), references
        # to properties that are missing are handled according to the default
        # or specified unsatisfiedRefBehavior.
        $~{!propName}         # For all variants above, if the reference
        $~{!sys|propName}     # property name is immediately preceded by !,
        etc.                 # then the unsatisfiedRefBehavior is thereby
                             # overridden so that if the referenced property
                             # is not set, JavaPropFile wil throw.
        , etc.   # Just like previous case, but if the property is
                             # not set, the  expression will be replace
                             # with nothing at all (an empty string).
        ${.propName}, etc.   # Just like previous case, but if the property is
                             # not set, the ${....} expression will be left
                             # exactly as it is.
        # The !, -, . prefixes, which override unsatisfiedRefBehavior, are
        # known as 'behaviorRefPrefixes'.

Sequence of Assignments and References

In all cases, sequence is consistent and understandable. According to Java's property file rules, it is useless to assign to a single property twice in a single properties file. We therefore prohibit this confusing and misleading scenario. In the great majority of cases, sequence makes no difference. You can assign as many properties as you wish to, and reference as many properties as you wish to, and as long as the following two situations do not apply, the sequence doesn't matter. The ${reference} value of property 'x' will be precisely what 'x' is set to in the same file (or, if it is not set in this file, then what it was before the file was loaded). If 'x' is referenced 100 times like ${x}, it will have the same value every time, regardless of whether it is referenced before or after 'x' is assigned.

  1. ExceptionIf property 'x' has a value before loading a property file that assigns 'x', then sequence matters, because ${x} will resolve to the original value before the assignment but to the new value after the assignment.
  2. ExceptionYou can't assign to an object before the object is present. The sequence must be like this: t1(Thread)=one t1.name=two

Escaping

You escape with backslash \, whether escaping is required by Java properties file format, java.util.regex.Pattern format, or JavaPropFile. As is very well documented at http://docs.oracle.com/javase/6/docs/api/java/util/Properties.html#load(java.io.Reader) you must \ to escape all white space characters (including line breaks), ':', and '=' inside of property keys. As a consequence of the java.util.Properties \-escaping rules, if you want to totally avoid expansion-processing of a particular variable reference like ${this}, you must double-\-escape it like this: \\${this}.

        colon\:and\ space_inside_property_key=hello

But to tell JavaPropFile to escape characters (list of examples follows), you must double escape with \\, because that is how you tell any Java properties file to pass | to the application (and in this context, JavaPropFile is an application). Examples:

        # A literal | in a regex must be escaped, and you must use \\ to get
        # \ to JavaPropFile:
        mockBean$intList(Integer[\\|]ArrayList)=91|72|101
        # The ' ' here does not need to be escaped in a regex, but does need
        # to be escaped in a properties file key or the key would end at the
        # space:
        alpha(Integer[\ ]ArrayList)=94 3 12
        expressionVar=Value will contain literal \${dollar sign and curlies}
        enableX\\(Boolean\\)=With typeCasting on you need this for property  \
                             name "enableX(Boolean)".
        # With no space after the key, no need to escape dots in keys and refs.
        owner.id=x5klh.umagumma
        # But if you want to use DotDeref for only part of
        # the definition, you will need to escape:
        owner\\.id=x5klh.${plugBean.user.name}

Property names may begin with the characters ! - . but if you ${reference} them, you must escape that character with \\, like:

        nestingVar=${\\!nested!var}}  # References property '!nested!var'
        # Note that you only escape these characters in references and only
        # when they are the first character.  The 2nd ! above should and may
        # not be escaped.

What characters must be escaped just for JavaPropFile? In .properties name (left) sides:

            \\$  Unless $ used to specify an extension object
            \\(  Unless ( used to start a typecast
            \\)  Unless ) used to end a typecast

Inside of ${...} (which are only allowed in .properties value (right) sides and in expand() Strings or text files):

            \\$  Unless $ used to specify an extension object
            \\}  Otherwise it would end the ${...}

Precedence works intuitively, not freakishly like Ant properties. The value of a property will be the last value that was assigned to it. You can prohibit attempts to overwrite by throwing or silently ignoring.

Gradle provides no way to unset/remove any Project property, therefore JavaPropFile has no capability to remove a property.

Provided Public Methods:

void propFileLoader.load(File propertiesFile)

Loads a properties file and writes Gradle Project properties with those values.

void propFileLoader.load(File propertiesFile, String keyAssignPrefix)

Exact same as previous, except that each property written to the Project is prefixed with the supplied String. If you ran "propFileLoader.load(file('a.properties'), 'pref')" and you have a line in a.properties like:

                key=val

then JavaPropFile would end up doing

                gradleProject.setProperty('prefkey', 'val')

Map propFileLoader.load(File propertiesFile, Map aMap)

Loads a properties file and writes map properties with those values. You must specify a Map to be populated, so if you want to load a new Map, just specify value [:] as the map. The given aMap reference will be returned.

void propFileLoader.load(File propertiesFile, String keyAssignPrefix, Map aMap)

Exact same as previous, except that each property written to the Map is prefixed with the supplied String. See description for the load(File, String) method above for details about how the prefix value is applied.

void propFileLoader.loadIntoExensionObject(File, String defaultExtObjName)

The String is an extension object name. This is the default object to apply property values to (or with as-dereference-operator, this is where the dereferencing begins).

void propFileLoader.traditionalPropertiesInit()

Loads app.properties (if it is present), prohibiting use of undefined ${...} references; then loads local.properties (if it is present), allowing use of undefined references. Overwriting is allowed. (It will use whatever settings you have made previously regarding typeCasting, system property assignment, and system property expansion. Only the last of these is enabled by default). File build.properties is actually a more traditional file name than app.properties or local.properties but unfortunately the name build.properties does not distinguish whether it is intended for shared or private/local usage, so I have standardized on the distinctive names app.properties and local.properties

Map<String, Map<String, Object>> propFileLoader.getDeferredExtensionProps()

Most users will not use this. It is for checking the state of deferred extension object properties. Returns a map of extension object names to map of deferred property definitions for that extension object.

static Class JavaPropFile.resolveClass(String className)

This probably won't be called in the context of using JavaPropFile, but some developers may want to copy this, as it successfully uses the context class loader and Groovy's default package rules to resolve class names.

void propFileLoader.executeDeferrals()

Most users will not use this. It is invoked automatically by a Gradle callback to execute deferred extension property settings when the target extension object comes online.

String propFileLoader.expand(...)

The following methods all expand references in the specified String or File (1st parameter) and return the resulting String. propFileLoader Settings like propFileLoader.unsatisfiedRefBehavior effect how expansions are performed. A peculiarity is that since Behavior.NO_SET makes no sense when only doing expansion, in this method a setting of Behavior.NO_SET causes expansion exactly like Behavior.LITERAL. If a method variant with File parameter and a final String param, the final String parameter is the encoding type that will be used when reading the input text file. If a boolean is specified, that enables DotDeref, which is disabled by default. If a Map is specified, then that mapping will be used by default for expansion mappings instead of the Gradle Project properties. See the Expand methods section below for more details.

        String propFileLoader.expand(File, Map<String, Object>)
        String propFileLoader.expand(File, boolean)
        String propFileLoader.expand(File)
        String propFileLoader.expand(File, Map<String, Object>, boolean)
        String propFileLoader.expand(File, Map<String, Object>, String)
        String propFileLoader.expand(File, boolean dotDeref, String)
        String propFileLoader.expand(File, String)
        String propFileLoader.expand(
                File, Map<String, Object> , boolean, String)
        String propFileLoader.expand(String, Map<String, Object> sourceMap)
        String propFileLoader.expand(String, boolean)
        String propFileLoader.expand(String)
        String propFileLoader.expand(String, Map<String, Object>, boolean)

Configurations:

After you "apply plugin 'com.admc.javaPropFile'", you can set the following properties on extension object propFileLoader.

boolean propFileLoader.unsatisfiedRefBehavior

What to do when ${x} is used in a property value, but 'x' is not defined. Defaults to com.admc.gradle.THROW, which will cause the property file load to fast-fail. You can change the behavior to any of the following. (My syntax here assumes that you imported the class com.admc.gradle.JavaPropFile).

                JavaPropFile.LITERAL  Leave literal ${x}
                JavaPropFile.EMPTY    Replace ${x} with the empty string
                JavaPropFile.NO_SET   Don't set the property at all
                JavaPropFile.THROW    Here only for completeness.  See above.

unsatisfiedRef behavior does not apply to references to missing extension object references like ${x$y} (on the value side of assignments or in expand() Strings/files). This situation will always throw. The only reason for this exception is the amount of coding and complexity needed for a capability with unknown user demand. The unsatisfiedRefBehavior setting can be overridden for individual references by using property name prefixes (., -, N/A, ! prefixes corresponding to the listed behaviors).

boolean propFileLoader.overwriteThrow

Property file loading will throw and abort immediately if a property assignment is attempted to a property that already has a value. (Empty string values and nulls are still values). Attempts to assign the same value that a property already has is always allowed. If this value is true, the value of propFileLoader.overwrite doesn't matter. Defaults to false

boolean propFileLoader.overwrite

If set to true, and .overwriteThrow is false (its default), then you can change existing property values. Defaults to true. If false, and .overwriteThrow is false, then attempts to change existing property values are silently ignored.

String propFileLoader.systemPropPrefix

If this is non-null, then you can both reference system properties in property definition values, and can assign system properties. An example of each with systemPropPrefix set to sys|:

                projectOwner=Mr. ${sys|user.name}
                sys|java.io.tmpdir=/usr/local/tmp

Set to null to prevent referencing and assignment of system properties. Defaults to sys|.

boolean propFileLoader.typeCasting

If set to true, then whenever you set a property with name beginning or ending with a (ParenthesizedString), the parentheses and contents will be stripped off and an instance of the specified class name will be instantiated. Use just () to assign a null. Details about this follow.

boolean propFileLoader.defer

If false then assignments to missing extension objects will cause the property file load to fail. If true, then assignments for missing extensions will be deferred until the target extension objects become available. Defaults to true.

Type Casting

Type casting is in effect if you set: propFileLoader.typeCasting = true

Property settings with specified name that does not start or end with parentheses will behave without any type casting, exactly as if typeCasting were off.

Put the name of the desired Java class in parentheses immediately before or after the property name, with no intervening spaces. An instance of that class will be instantiated using the specified property value as String parameter to either the static valueOf(String) method of the class, or a String-parameter constructor.

Groovy's rules for package defaults apply.

Some examples:

    output(File)=/path/to/file.txt
    (Long)xferTime.max=31.25
    compileJava.options.debug(Boolean) = false
    monitor(com.admc.net.NetworMonitor)=Custom Network Monitor
    mavenRepository.dest.url(URL)=file:/tmp/fakeMvn

To assign a null, do the same thing, but give no value at all for class name nor property value:

    envTarget()=

Arrays and Collections

Immediately after the element typeCasting class, add [ + splitting-pattern + ] + optional-CollectionImplementationClass. The Splitting pattern is a java.util.regex.Pattern String, and you must, of course, follow Java properties file escaping rules. If no collection implementation class is specifies, an Array will be instantiated. Examples:

        mockBean$intList(Integer[\\|]ArrayList)=91|72|101
        # Note the extra ugly backslashes needed to satisfy both Java
        # properties escaping and java.util.regex.Pattern escaping for '|'.
        alpha(Integer[\ ]ArrayList)=94 3 12
        alpha(String[,]HashSet)=one,two,three

Type Validation

If you have JavaPropFile configured to allow property overwriting (by default it is allowed), you are still never allowed to directly change a property value from one non-null type to another non-null type. If you really want to, you can get around htis constraint by assigning null and then assign to the new value.

ContentAsStringFilter

Set attribute closure to a closure that transforms the entire input file text into output file text. As noted above, the advantages of ContentAsStringFilter are, it invokes the supplied closure only once for each input file, and it has no limitations on regex support or EOL-handling. See doc/build.gradle for useful examples with Copy task configuration, functional copy {...} calls, and filtering a just subset of a predefined file collection.

Expand methods

File doc/build.gradle has an example of expanding property references in application files using a ContentAsStringFilter and an .expand() call.

When using the expand method nested in some closures... definitely when calling expand inside of a ContentAsStringFilter... reference failures (throws) will just indicate failure whatever executes the closure. In the case of a reference failure in a ContentAsStringFilter, you will get a Gradle error message including "Cause: Could not copy file...". Just re-run the Gradle command with the addition of the -s switch and look through the stack traces for the precise failure cause.

JavaPropFile is distinctive from other templating/expansions systems in that it can be used with the other systems without breaking them. I very frequently want to perform build-time substitutions on files that will later be run-time substituted. This is a major pain with other systems out there, requiring extra staging directories, escaping characters, non-standard delimiters, loss of validation, etc. This use case is handled intuitively by JavaPropFile by three easy steps:

  1. Use a sensible naming convention to make it clear to developers whether each reference is intended for use by JavaPropFile or the other tool. (If loading properties from a file, you can use the keyAssignPrefix load parameter to facilitate this).
  2. Set...
    propFileLoader.unsatisfiedRefBehavior = JavaPropFile.Behavior.LITERAL
    
  3. For each reference that you want to require JavaPropFile to expand, bad prefix the name with !, like... Some text in the template file requires ${!name} to be set.

Instead of duplicating an example here, see the configuration of task processResources in this production build file: https://github.com/unsaved/jcreole/raw/master/build.gradle