Configuration
Development time configuration
Development targets
At development time, you can define properties in the top level directory of the project that get expanded at build time.
The default "target" is called devel
and it's read from a file called devel.properties
, if present.
This is better explained with a simple example. Edit your src/dist/deploy/99_sysmon.xml
file from the tutorial and add a realm
attribute like this:
<sysmon logger="Q2" realm="@sysmonrealm@">
<attr name="sleepTime" type="java.lang.Long">3600000</attr>
<attr name="detailRequired" type="java.lang.Boolean">true</attr>
</sysmon>
If you call gradle installApp
and then cat build/install/tutorial/deploy/99_sysmon.xml
you'll see exactly the same attribute, @sysmonrealm@
doesn't get expanded. Now edit a file called devel.properties
in the top level directory with the following content:
sysmonrealm=sysmon-devel
If you call gradle installApp
once again and you cat build/install/tutorial/deploy/99_sysmon.xml
you'll see that this time the realm attribute has been replaced and the file looks like this:
<sysmon logger="Q2" realm="sysmon-devel">
<attr name="sleepTime" type="java.lang.Long">3600000</attr>
<attr name="detailRequired" type="java.lang.Boolean">true</attr>
</sysmon>
devel
is the default build-time target.
Now write a file called prod.properties
like this:
sysmonrealm=sysmon-prod
If you call gradle -Ptarget=prod installApp
you'll notice that the placeholder @sysmonrealm@
is replaced this time with the content sysmon-prod
instead of sysmon-devel
.
This uses the standard Ant's Replace task.
Building systems for various environments like development, QA, or Docker images often benefits from development-time configuration. However, as we'll explore in the next section, this isn't the only method available for configuring jPOS.
Runtime configuration
QBeans offer flexible runtime configuration options. The primary method is "Push" configuration, often referred to as Inversion of Control. However, the system is also equipped to handle "Pull" configuration, along with combinations of both Push and Pull approaches.
Configurable
If a QBean implements the very simple org.jpos.util.Configurable
interface, a configuration object gets pushed at initialization time.
The Configuratble
interface looks like this:
public interface Configurable {
/**
* @param cfg Configuration object
* @throws ConfigurationException
*/
void setConfiguration(Configuration cfg)
throws ConfigurationException;
}
The Configuration
object has methods to get different configuration properties and some handy conversion methods to get Integers (e.g.: int getInt(String propertyName)
, longs, doubles, booleans, with or without defaults, and also arrays.
Imagine a configuration like this:
<myqbean name='myqbean' class='....'>
<property name="host" value="192.168.1.1" />
<property name="port" value="8000" />
</myqbean>
if your QBean implements Configurable, you could use code like this:
public class MyQBean implements Configurable {
String host;
int port;
public void setConfiguration (Configuration cfg) {
host = cfg.get("host", "localhost");
port = cfg.getInt("port", 8000);
}
}
We've mentioned push/pull configuration, as an alternative to the previous code, you can use something like this:
public class MyQBean implements Configurable {
Configuration cfg;
public void setConfiguration (Configuration cfg) {
this.cfg = cfg;
}
}
then, whenever you need to access properties hold in the Configuration object, you just call the get
method in your Configuration
reference. This would be a push/pull approach, the 'Configuration' object gets pushed to the QBean, which in turn, pulls properties as needed.
Worth noting, if your configuration has multiple instances of a given property, you can use one of the getAll
alternatives and get an array of objects, e.g.:
<myqbean name='myqbean' class='....'>
<property name="host" value="192.168.1.1" />
<property name="host" value="192.168.1.2" />
<property name="host" value="192.168.1.3" />
<property name="host" value="192.168.1.4" />
</myqbean>
If you call cfg.getAll("host")
you'll get an array of four Strings, and same goes for the getInts
method.
Configuring properties in the XML file is OK for small projects, but as the system grows, you usually want to have an external file.
If instead of using property name/value combinations, you use the the file attribute, the configuration can be pulled from a .properties
file (that we tend to name as .cfg
) or YAML files, i.e:
<myqbean name='myqbean' class='....'>
<property file="cfg/hosts.cfg" />
</myqbean>
The cfg/hosts.cfg
file would look like this:
host=192.168.1.1
port=8000
Wait until the end of this section to learn about a powerful configuration tool, the Environments that complement very well the XML configuration by taking properties from a per-environment YAML file.
@Config annotation
Implementing Configuration
is quite simple, and is still the preferred method for large QBeans, specially sing they can throw ConfigurationException
to orderly halt the life-cycle of a QBean and provides clear indication in the jPOS logs of any possible configuration problem. But in addition to that, Q2 supports the @Config
annotation, your QBean can be implemented like this:
public class MyQBean {
@Config("host") String host;
@Config("port") int port;
}
Environments
Imagine you have a YAML file called default.yml
located in the cfg
directory with the following content:
acquiring:
host: "192.168.1.1"
port: 8000
Them you can change the XML descriptor to look like this:
<myqbean name='myqbean' class='....'>
<property name="host" value="${acquiring.host}" />
<property name="port" value="${acquiring.port}" />
</myqbean>
When you start Q2
, the default environment is cfg/default.yml
but if you use the -E
(or --environment
) startup switch, you can change the current environment. If you have a cfg/prod.yml
with content like this:
acquiring:
host: "10.10.0.1"
port: 8000
and you call bin/q2 -Eprod
then the acquirer.host
and acquirer.port
will get expanded to the values configured in cfg/prod.yml
.
Worth noting:
- You can use the
-E
switch multiple times in order to read some defaults, e.g.:bin/q2 -Edefault -Eprod
. - You can concatenate several projects in the same line, e.g.:
<property name="hostport" value="${acquiring.host}:${acquiring.port}" />
. - You can define defaults by using a colon, e.g.:
${acquiring.port:8000}
.
The values provided in the YAML files can be overriden using either Java System Properties or environment variables.
If you alter the bin/q2
script to include a parameter like -Dacquiring.port=8888
, this specified value will be given priority.
Lastly, environment variables can also be utilized by following a straightforward naming convention:
- Replace any dots in the property name with underscores (
_
). - Convert the entire property name to uppercase.
From the earlier example, if you set the environment variables ACQUIRING_HOST
and ACQUIRING_PORT
, these will take precedence over the configurations defined in the YAML or Java properties. This feature is particularly beneficial in container deployment scenarios.
Environment providers
Feel free to bypass this section for now, but remember to revisit it later. It will be extremely useful when you're getting your jPOS system ready for PCI compliance and ensuring its security.
You've learned that properties can be set via YAML files, Java properties, or environment variables. You also know you can merge multiple YAML files (using the -E
switch repeatedly with Q2), and that Q2 makes these properties accessible to QBean implementations.
However, when handling sensitive data like database credentials or API keys, it's not advisable to directly place such information in a YAML file in plain text, as shown below:
db:
password: mySuperSecurePassword
A common practice is to use a secret management solution, often provided by your infrastructure, whether it's on-premises or cloud-based. These solutions typically expose secrets as runtime environment variables. For example, you could store the above password in a secret management console under the name DB_PASSWORD
, and the system would then access it "securely".
But this approach has its drawbacks:
- In a system with multiple sensitive environment variables, all Java process components can access them. In a multi-tenant system, this means every component of a process might have access to every tenant's credentials.
- Any operator with access to a running container can easily reveal all environment variables, including sensitive ones, with a simple
env
command. - Operators with access to the secrets management console can view all stored secrets, often in plain text.
Being aware of these potential security risks is vital for safeguarding the integrity and confidentiality of sensitive data in your system. To this end, jPOS offers environment providers
designed with a defense in depth mindset, further strengthening your system's security infrastructure.
The EnvironmentProvider
interface is very simple and looks like this:
package org.jpos.core;
public interface EnvironmentProvider {
String prefix();
String get (String config);
}
The environent providers are configured at runtime using Java ServiceLoader.
If you take a look at jpos/src/main/resources/META-INF/services/org.jpos.core.EnvironmentProvider
you'll see it looks like this:
org.jpos.core.FileEnvironmentProvider
org.jpos.core.ObfEnvironmentProvider
This defines just two providers used mostly as an example. Let's take a look at the FileEnvironentProvider implementation:
package org.jpos.core;
public class FileEnvironmentProvider implements EnvironmentProvider {
@Override
public String prefix() {
return "file::";
}
@Override
public String get(String config) {
Path path = Paths.get(config);
try {
return String.join(System.lineSeparator(), Files.readAllLines(path));
} catch (IOException e) {
return config;
}
}
}
~
In our YAML configuration example above, instead of:
db:
password: mySuperSecurePassword
we use a configuration like this:
db:
password: file::/etc/my/vault/securepassword
This setup reads the password from the file /etc/my/vault/securepassword.
By now, you probably grasp the concept. For example, these configurations can be read from a database using jPOS-EE's sysconfig module environment provider. Your YAML configuration (or system property, or environment variable) would then look like this:
db:
password: sys::dbpass
Why sys::? Because in the jPOS-EE sysconfig module, the SysConfigEnvironmentProvider class is implemented as follows:
public class SysConfigEnvironmentProvider implements EnvironmentProvider {
public String prefix() {
return "sys::";
}
...
...
}
The ObfEnvironmentProvider aligns well with our defense in depth mentality. It's an obfuscator, and while obfuscation is often criticized as mere 'security through obscurity', it can add an extra thin layer of security in combination with other providers. This can be enough to deter a casual intruder. The Obf provider includes a CLI command. For instance, if you execute `bin/q2 --cli`` and run obf mySuperSecurePassword, you'll see outputs like this:
q2> obf mySuperSecurePassword
obf::aji0cwAAABXjIDkTc/RwOziQL3vsNCqxg49sa2hM9lz4+smcKvQlaZXPOv8=
Interesting enough, every time you call it, you'd get a different obscured version, with different lengths, of the same cleartext. e.g.:
q2> obf mySuperSecurePassword
obf::Y7TM5wAAABVxF+RJfN1dMZ1Nxa4FUTf30oniLdXR3MxdORsYrXKwig==
q2> obf mySuperSecurePassword
obf::YieCuwAAABWJvrmilue2GkcKdFuutUcZKE6MhNGhzyhgoH161vBc8PU3
q2> obf mySuperSecurePassword
obf::CJbEGQAAABUWd+s5X2vEngsSFAh8qqwKUIkhO34=
Each call generates a unique, variably lengthed obfuscated version of the same plaintext. You can use any of these in your configuration (or as an environment variable):
db:
password: obf::YieCuwAAABWJvrmilue2GkcKdFuutUcZKE6MhNGhzyhgoH161vBc8PU3
If you obfuscate sys::dbpass instead of 'mySuperSecurePassword', you'll get an obfuscated string (e.g., obf::dWMt0gAAAAtVQs6HxtKfQKIWptnG0ihFWQWGdGGYKvBBPn4=
) which, when de-obfuscated, resolves against the dbpass
id in the sysconfig database table.
This infrastructure allows for the implementation of an HsmEnvironmentProvider that decrypts sensitive data from an HSM or devices like a YubiKey.
obf::GRF38QAAAE4pmxd//aX5/t+mV5GPEsgkAvEBCYoRtMrljRBcWhPcTub99wC7jwF/LmwmIMZ4VpSxS1xW87mk9XtMgDCe+YwfwV/f0+pWrMzqq+J11Ajpo5vW0KIyx0kVoco2c81Ade0eWbicdKtF3g==
Here’s a revised version of the XmlConfigurable
section with improved clarity, grammar, and flow:
XmlConfigurable
The Configurable
interface allows your QBean to receive a Configuration
object, providing access to all the properties configured inside the XMLDescriptor
. However, for more complex XML-based DSLs—such as the one used in the TransactionManager
with its participants, groups, etc., or in QMUX
with its in/out queues—a QBean can choose to receive an XML element at startup by implementing the XmlConfigurable
interface.
package org.jpos.core;
public interface XmlConfigurable {
/**
* @param xml Configuration element
* @throws ConfigurationException on error
*/
void setConfiguration(Element xml)
throws ConfigurationException;
}
Within your setConfiguration(Element xml)
implementation, you can define any complex configuration you need. We frequently use this approach when integrating jPOS with scripting languages such as BeanShell, JavaScript, Jython, or JRuby. For example, you can easily write a QBean like this:
<script>
log.info("This is a QBean");
</script>
In this case, the BeanShell QBean implements XmlConfigurable
, allowing it to receive the XML element and extract the source code. Keep in mind that XML is quite flexible, and you can use <![CDATA[...]]>
blocks to include complex logic that may contain reserved XML characters, such as <>&
, etc.