Skip to main content

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>
note

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.

tip

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
tip

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

TL;DR

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
tip

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.

note

This infrastructure allows for the implementation of an HsmEnvironmentProvider that decrypts sensitive data from an HSM or devices like a YubiKey.

Quiz
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.