Creating Custom Configurations

October 8, 2024

Introduction

As Jahia has evolved over the years, many different configuration systems were introduced in the system, but in this document, we will focus on the recommended practices to introduce and manage configuration for your custom modules.

 

image1.png

What are configurations?

Configurations are basically values that are passed to a Java class to change their behavior. Configurations may be deployed using default values and then be modified - even in real-time and over a cluster of Jahia servers - to fit varying needs. The value may range from simple values such as integers, booleans, or strings all the way to more complex structures such as lists or JSON strings.

About (legacy) Jahia.properties configurations

It is not recommended to use jahia.properties files; these were the previous custom configuration standard in Jahia. They have been replaced by OSGi configuration files, providing more modularity and more features. OSGI configuration files are the recommended way to manage configurations.

OSGi configurations

As Jahia modules are also OSGi bundles, they can leverage the OSGi configuration administration service to provide configuration for the classes packaged in the bundle. Here’s an example of OSGi configuration properties for a file with a name such as org.acme.example.config.cfg :

key=customChoiceList1
values.test1=Test 1
values.test2=Test 2
values.test3=Test 3    

Property lists of key/value pairs will be passed to an OSGi service once it is activated. Here’s an example of how this is passed to a service:

@Component(name = "my-component", immediate = true, configurationPid = "org.acme.example.config")
public class MyComponent {

    @Activate
    public void activate(ComponentContext context) {
        Dictionary<String, Object> properties = context.getProperties();
        Enumeration<String> keys = properties.keys();
        while (keys.hasMoreElements()) {
            String key = keys.nextElement();
            System.out.println(key + " = " + properties.get(key));
        }
    }
}    

In this example, OSGi declarative service annotations are used to declare an OSGi service. On the activate method, an object is passed that will contain the properties as well as other component context information coming from the declarative services implementation as well as the configuration administration service.

You might wonder how the configuration admin knows which configuration must be sent to a service. This is done through the usage of a PID (Persistent IDentity) that is specified in the componentId parameter of the @Component annotation. In this example, it means that it will look for a configuration with the org.acme.example.config PID. You can find a complete example in our OSGi module samples here.

The configuration admin service is responsible for storing configurations in files, and in the case of Jahia’s Karaf OSGi engine, this will happen in the karaf/etc directory. Any .cfg or .yaml/.yml files deployed in that directory will be registered in the configuration admin service and will update any service if the configurations are deployed or modified. You can find much more information about the possibilities of the Karaf configuration on its website.

It is also possible to structure the configuration by using Java interfaces and annotations to map the properties to the annotation interfaces, as in the following example:

@Component(configurationPid = "org.foo.modules.samples.config.metatype.example", immediate = true)
@Designate(ocd = MetatypeConfigExample.Config.class)
public class MetatypeConfigExample {

    private static final Logger logger = LoggerFactory.getLogger(MetatypeConfigExample.class);

    @ObjectClassDefinition(name = "Metatype configuration", description = "An example of configuration with metatype")
    public @interface Config {
        @AttributeDefinition(name = "Simple key", defaultValue = "nokey", description = "Simple key value")
        String key() default "nokey";

        // Dropdown
        @AttributeDefinition(name = "Dropdown", options = { @Option(label = "Option 1", value = "option1"),
                @Option(label = "Option 2", value = "option 2"), @Option(label = "Option 3", value = "option3") })
        String dropdown() default "option1";

        @AttributeDefinition(name = "Values.test1", defaultValue = "none", description="Key with . character in it")
        String values_test1() default "none";
    }

    @Activate
    public void activate(Config config) {
        logger.info("key={}", config.key());
        logger.info("dropdown={}", config.dropdown());
        logger.info("values.test1={}", config.values_test1());
    }
}

    

As you can see, this is quite powerful, even offering the possibility of providing options that can represent dropdowns. The Felix Web Console OSGi configuration UI (available in the Jahia Tools) will then be able to use this metatype (metadata) information to customize the UI when modifying configuration. It also takes care of all the type conversions automatically. Here is a screenshot of what the UI looks like:

 

An example configuration for such a structure can look like this:

key=customChoiceList1
dropdown=option2
values.test1=Test 1    

If interested, you can find a complete example here.

Creating, Editing, and Deleting Configurations

There are many different ways of creating/editing and deleting configurations. We first give an overview of what is supported and if it is available in the Jahia Cloud offering.

  In server deployed config files Jahia Tools Config Admin UI Karaf Command SSH Shell Jahia GraphQL API JSP Taglib Config Admin OSGi Service (Java) Provisioning API
Officially supported by Jahia Yes Yes No Yes Yes Yes Yes
Available on Jahia cloud No Yes No Yes Yes Yes Yes

 

The table below gives a good overview of the different possibilities and their strengths and weaknesses. We, of course, only include the options supported by Jahia.

Actions / features In server deployed config files Jahia Tools Config Admin UI Jahia GraphQL API JSP Taglib Config Admin OSGi Service (Java) Provisioning API
Read value Yes Yes Yes Yes Yes No
Edit existing value Yes Yes Yes No Yes Yes
Create new entry Yes No Yes No Yes Yes
Create new file/config Yes No Yes No Yes Yes
Available without root access Yes* Yes Yes** Yes Yes No

*Provided you have file-system write permissions

**You will need some GraphQL-specific permissions at a minimum

In server-deployed configuration files

Possibly the simplest way to create, edit, and delete configurations is to manage them at the filesystem level. This is done in the karaf/etc directory. Files within the directory have the following naming conventions:

configurationPid.cfg or configurationPid.yaml

Or in the case of factories:

factoryPid-factoryIdentifier.cfg or factoryPid-factoryIdentifer.yaml

Creating or editing such files in that directory will immediately trigger configuration admin changes and push the changes to the related OSGi services. This is especially useful during development, where quick changes must be tested frequently.

Any changes done through any other means listed below will also update the deployed files so everything is always kept in sync.

In the OSGi console config administration UI

The OSGi console web UI to administer configurations is provided by the Apache Felix Web Console project and generates a UI based on existing configuration files to allow modification. It has some limitations as it is not able to create new configurations and cannot create new configuration entries, but it can be very useful to quickly view and perform small modifications that will be applied in real time on a server. The UI is accessible through the UI:

http://localhost:8080/tools/osgi/console/configMgr

Using the GraphQL API

Jahia’s GraphQL API also provides a way to manipulate configurations through its configuration node, and more documentation can be found by directly accessing the GraphQL Playground at the following URL:

http://localhost:8080/jahia/developerTools/graphql-playground

For more information about Jahia’s GraphQL API, please check the related documentation.

In the following example, we use a GraphQL mutation to the Jahia admin API to add a configuration with PID org.jahia.modules.kibana_dashboards_provider with specific value pairs that we identify using GraphQL aliases (url, user, password,... in this example)

mutation {
 admin {
  jahia {
   configuration(
    pid: "org.jahia.modules.kibana_dashboards_provider"
   ) {
    url:value(name:"kibana_dashboards_provider.kibanaURL",value: "http://kibana:5601" ),
    user:value(name:"kibana_dashboards_provider.kibanaUser",value: "elastic"),
    password:value(name:"kibana_dashboards_provider.kibanaPassword",value: "thepassword" ),
    enable:value(name:"kibana_dashboards_provider.KibanaProxy.enable",value: "true" ),
    cloud:value(name:"kibana_dashboards_provider.KibanaProxy.cloud",value: "false" )
   }
  }    

Using the provisioning API

Jahia’s provisioning API is a great way to initialize a Jahia installation, for example, in cases of continuous integration and continuous delivery (CI/CD) deployments. A docker image can be used as a base, and then modules and configurations can be deployed to deliver a final custom installation. You can find more information about the provisioning API in the related documentation, but in this section, we will focus on the parts that are specific to the OSGi configuration.

Installing a configuration

Installing a configuration using the provisioning API can be done remotely by using a request that will upload a local file to the remote server using a request such as the one provided in the example below:

curl -X POST 'http://localhost:8080/modules/api/provisioning' \
-u 'root:root1234' \
--form 'file=@"<local-folder-of-file>/org.foo.modules.example.conf.cfg"' \
--form 'script="[{\"installConfiguration\":\"org.foo.modules.example.conf.cfg\"}]"'    

As you can see, the file's name is present twice because it is possible to rename the configuration if needed (but we don’t do it in this example).

It is also possible to install a configuration by providing a link to a file already available on the Jahia server filesystem.

Example:

- installConfiguration: "file:/tmp/org.jahia.services.usermanager.ldap-config-rqa5.cfg"
    

Editing a configuration

You can create or edit configurations by using editConfiguration with the configuration PID. If the PID is a factory PID, you must specify the factoryIdentifier value or use the <factory-pid>-<factoryIdentifier> syntax. For example:

- editConfiguration: "org.jahia.modules.jexperience.settings"
  configIdentifier: "global"
  properties: 
    jexperience.jCustomerURL: "https://jcustomer:9443"
    jexperience.jCustomerUsername: "karaf"
    jexperience.jCustomerPassword: "karaf"
    jexperience.jCustomerTrustAllCertificates: "true"
    jexperience.jCustomerUsePublicAddressesForAdmin: "false"
    jexperience.jCustomerKey: "670c26d1cc413346c3b2fd9ce65dab41"    

You can also put the content of the properties file in the script (the formatting will be maintained for config creation only):

- editConfiguration: "org.jahia.modules.test"
  configIdentifier: "id1"
  content: |
    # LDAP configuration
    user.uid.search.name:dc=jahia,dc=com
    group.search.name:dc=jahia,dc=com
    url=ldap://rqa5.jahia.com:389/    

Using the Configuration Admin Java OSGi service

The last (and most powerful) way to manipulate configuration is to directly access the Configuration Admin OSGi service through its Java interface. We won’t go into all the details of its interface, but here is a quick example of how to create a configuration using Java code:

ConfigurationAdmin configAdmin = // obtain a reference to the Config Admin Service, for example using a @Reference 
Configuration config = configAdmin.createFactoryConfiguration("org.jahia.modules.test");
Dictionary<String, Object> properties = new Hashtable<>();
properties.put("user.uid.search.name", "dc=jahia,dc=com");
config.update(properties);    

Factory configurations

Until now, we have always presented configurations as a direct one-to-one relationship with the classes that will receive them from the Configuration Administration service. However, OSGi allows for another type of configuration: factories.

Let’s have a look at various use cases for factory configurations:

  1. Providing multiple configurations for connectors
  2. LDAP
  3. Database
  4. Providing different configurations for different sites
  5. jExperience
  6. Providing different configurations for a single code base (generic)

Compared to a singleton instance of services, factories make it possible to instantiate a service multiple times and provide it with different configurations.

 

For example, a service factory could provide an LDAP connector, and each configuration to a different LDAP server could be managed as a configuration of that same LDAP connector service implementation. This is actually how the LDAP connector module is implemented for Jahia.

It is possible to use Declarative Services annotations to create factory implementations, such as in the following example:

@Component(service = {SiteConfig.class}, configurationPid = "org.foo.modules.samples.config.factory.site", configurationPolicy = ConfigurationPolicy.REQUIRE)
@Designate(ocd = SiteConfigImpl.Config.class, factory=true)
public class SiteConfigImpl implements SiteConfig {

    private Config config;

    @ObjectClassDefinition(name = "Site configuration", description = "A configuration for a site")
    public @interface Config {

        @AttributeDefinition(name = "Site key", defaultValue = "siteKey", description = "The identifier for the site")
        String key() default "siteKey";

        // Dropdown
        @AttributeDefinition(name = "Site type", options = { @Option(label = "Landing page", value = "landing"),
                @Option(label = "Marketing", value = "marketing"), @Option(label = "Support", value = "support") })
        String type() default "marketing";
    }

    @Activate
    public void activate(final Config config) {
        this.config = config;
    }

    @Override
    public String getKey() {
        return config.key();
    }

    @Override
    public String getType() {
        return config.type();
    }
}
    

In this example (that also uses meta-type annotations), it is a basic component that will be instantiated multiple times if multiple configurations are detected with the proper naming convention. You might have noticed that we set up a configuration policy. The default policy (optional) means that the system may create instances of the component even if no configuration is provided, which can be a bit surprising or unexpected. By changing the policy to REQUIRE, we ensure that the component will only be created if a configuration has been added. This ensures better control of the lifecycle of the component instances.

Configuration files for factory configurations follow a different naming convention:

factoryPID-factoryIdentifier.cfg/.yaml
    

The factoryPID is the persistent identifier for the factory, usually something like org.acme.modules.sample.connector and the factoryIdentifier will be an identifier for the specific instance of the factory. Because a dash (-) character separates the PID and the identifier, you should never use dashes in PIDs or factory identifiers to avoid potential deployment problems. Examples of factory identifiers might include : default, global, server1. It is up to the implementation of the factory to use the identifiers as it sees fit. The configuration will be delivered to a factory implementation through the activation method, exactly in the same way as for regular configurations, except that two properties will be automatically added:

  • component.name : the name of the component
  • component.factory : the factory identifier

This way, the factory implementation will be able to retrieve the factory identifier and use it to perform custom configuration handling.

Site-local configurations

A good usage of a factory configuration is to build site-local configurations. For example, jExperience uses this method to provide default and site-specific configurations. Here, the convention is to use the siteKey as a factory identifier and then implement the server-side logic to map the factory configuration to the proper site. This is by no means the only way to do this, and for example, configurations could be made to match siteKeys using regular expressions or any other means. But in general, factory configurations will be useful to provide configurations that must differ from site to site.

Example

Reusing the example component from the factory example, we can then create new configuration files named:

org.foo.modules.samples.config.factory.site-site1.cfg
org.foo.modules.samples.config.factory.site-site2.cfg
org.foo.modules.samples.config.factory.site-site3.cfg
    

With for example the following content:

key=mySite
type=landing    

The key will be the actual site identifier, the factoryIdentifier used in the file name is not used to identify the site, to differentiate the configuration. Usually, it’s a good idea to have them both equal to the key to make things easier to find, but in this example, we wanted to explain that they don’t have to be the same as the factoryIdentifier is not accessible to the Java implementation.

Now, what remains is using the configurations to actually do something with them, so here is an example of a configuration consumer:

@Component
public class SiteConfigConsumer {

    private static final Logger logger = LoggerFactory.getLogger(SiteConfigConsumer.class);

    private List<SiteConfig> siteConfigs = new ArrayList<>();

    // The GREEDY policy makes sure that ALL the instances of the configuration are instantiated, otherwise only one would be created.
    // Making it DYNAMIC also avoids the component to activate/deactivate for each config binding.
    @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
    public void addSiteConfig(SiteConfig siteConfig) {
        this.siteConfigs.add(siteConfig);
        logger.info("Adding site key={} type={}", siteConfig.getKey(), siteConfig.getType());
    }

    public void removeSiteConfig(SiteConfig siteConfig) {
        this.siteConfigs.remove(siteConfig);
        logger.info("Removing site key={}", siteConfig.getKey());
    }

}    

As the code comments explain, the @Reference configuration is important because it will force all configurations to be properly instantiated. Without these settings, only one configuration would be instantiated, which is not what we (usually) would want. You can find a complete working example here.

Embedding module configuration

Sometimes, you want to be able to deploy OSGI configuration files when you deploy modules. You can add your configuration files to your module source folder under the /src/main/resources/META-INF/configurations directory.

The following example comes from the OSGi-modules-samples module.

This automatically deploys the configuration files in the /digital-factory-data/karaf/etc folder. The OSGI framework automatically registers the configuration during runtime.

Note that, by default, a configuration file is overridden each time the module is deployed. This is useful if the configuration file needs to evolve in future module versions. If you want to avoid the override of the configuration file during the module upgrade, you need to start the first line of your configuration file with the # default configuration marker.

Here is a simple example of such a configuration file:

# default configuration
testValue=123
    

Here is the deployment mechanism: if a module contains configuration files, then they are deployed if:

  • No existing configuration already exists in digital-factory-data/etc/deploy
  • Or if the configuration file does NOT start with the line # default configuration

If the file is not a default configuration, the following line will be added to the beginning of the configuration file on deployment:

# Do not edit - Configuration file provided by module, any change will be lost
    

Use cases for different (default, non-default) configuration files

Default configuration:

  • Provide initial/partial configuration
  • Provide example
  • Module upgrade without resetting config (provide default config)

Non-default, non-user editable configuration:

  • Configuration of external OSGi services (GraphQL servlet, …)
  • Security and permissions configs (CSRF, API permissions, …)
  • Provide commented-out sample files (inactive sample)

Properties (.cfg) versus Yaml file formats

Jahia supports two formats for configuration files (by default, OSGi only supports the properties file format):

  • Property files that use extension .cfg or .config
  • Yaml files that use the extension .yaml or .yml

Usually, if possible, properties files should be used as they are an OSGi standard and simpler to use. But in some cases, when complex structures are needed, Yaml files can make things easier to manage.

Properties vs. Yaml

Here is a table comparing properties files with Yaml files.

  Properties (.cfg) Yaml (.yaml, .yml)
Default Encoding ISO-8859-1 UTF-8
Structured keys No Yes
Standard configuration format Yes No
Complex case readability No Yes
Available in UI/APIs Yes No
Recommended for simple cases Yes No

 

Here is an example of a typical Properties (.cfg) configuration file:

# default configuration - won't be overriden
# Legacy configuration - only used if org.jahia.bundles.api.security / security.legacyMode is set to true

// All API calls allowed on the configured node types inside /sites/*
permission.siteResources.nodeType=jnt:folder,jnt:page,jnt:portlet,jnt:navMenuText
permission.siteResources.pathPattern=/sites/.*

// All API calls allowed on jnt:category
permission.category.nodeType=jnt:category

// treeRootItem json view allowed for virtualsite
permission.treeRootItem.api=view.json.treeRootItem
permission.treeRootItem.nodeType=jnt:virtualsite
permission.treeRootItem.pathPattern=/sites/.*

// No permission check by default for html views
permission.htmlviews.api=view.html
permission.htmlviews.priority=99998

// Require write permission by default, if no other rule matches
permission.global.access=restricted
permission.global.priority=99999

// Graphql
permission.gql.api=graphql
permission.gql.pathPattern=/
permission.gql.requiredPermission=jcr:read_default
permission.gql.priority=99997    

Yaml files, when loaded, are “flattened,” meaning that their properties are internally transformed from something like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: my-app:latest
          ports:
            - containerPort: 80
          env:
            - name: ENV
              value: prod
          resources:
            limits:
              cpu: "1"
              memory: "1Gi"
            requests:
              cpu: "0.5"
              memory: "512Mi"
        - name: sidecar
          image: my-sidecar:latest
          env:
            - name: ENV
              value: prod
      initContainers:
        - name: init-my-db
          image: busybox
          command: ['sh', '-c', 'until nslookup my-db; do echo waiting for my-db; sleep 2; done;']
        - name: init-my-config
          image: busybox
          command: ['sh', '-c', 'until nslookup my-config; do echo waiting for my-config; sleep 2; done;']



    

To something like this:

   apiVersion = apps/v1
   felix.fileinstall.filename = file:/var/jahia/karaf/etc/org.jahia.modules.test.complex.yml
   kind = Deployment
   metadata.labels.app = my-app2
   metadata.name = my-app
   service.pid = org.jahia.modules.test.complex
   spec.replicas = 3
   spec.selector.matchLabels.app = my-app
   spec.template.metadata.labels.app = my-app
   spec.template.spec.containers[0].env[0].name = ENV
   spec.template.spec.containers[0].env[0].value = prod
   spec.template.spec.containers[0].image = my-app:latest
   spec.template.spec.containers[0].name = my-app
   spec.template.spec.containers[0].ports[0].containerPort = 80
   spec.template.spec.containers[0].resources.limits.cpu = 1
   spec.template.spec.containers[0].resources.limits.memory = 1Gi
   spec.template.spec.containers[0].resources.requests.cpu = 0.5
   spec.template.spec.containers[0].resources.requests.memory = 512Mi
   spec.template.spec.containers[1].env[0].name = ENV
   spec.template.spec.containers[1].env[0].value = prod
   spec.template.spec.containers[1].image = my-sidecar:latest
   spec.template.spec.containers[1].name = sidecar
   spec.template.spec.initContainers[0].command[0] = sh
   spec.template.spec.initContainers[0].command[1] = -c
   spec.template.spec.initContainers[0].command[2] = until nslookup my-db; do echo waiting for my-db; sleep 2; done;
   spec.template.spec.initContainers[0].image = busybox
   spec.template.spec.initContainers[0].name = init-my-db
   spec.template.spec.initContainers[1].command[0] = sh
   spec.template.spec.initContainers[1].command[1] = -c
   spec.template.spec.initContainers[1].command[2] = until nslookup my-config; do echo waiting for my-config; sleep 2; done;
   spec.template.spec.initContainers[1].image = busybox
   spec.template.spec.initContainers[1].name = init-my-config
   spec.template.spec.volumes[0].configMap.name = my-config
   spec.template.spec.volumes[0].name = config-volume
   spec.template.spec.volumes[1].name = data-volume
   spec.template.spec.volumes[1].persistentVolumeClaim.claimName = my-pv-claim
    

And, if edited in the OSGi Configuration Administration interface, it will be presented like this:

 

Any changes to a Yaml configuration will then be re-transformed back into the Yaml syntax when updating the configuration file.

Using configuration in views

It is possible to access OSGi configuration in JSPs using JSP functions. In this section, we give some examples of what is possible. This may also be used to surface configurations to single-page applications (SPAs) that would be initialized using a view or a template.

Retrieving a complete configuration as a map of properties from its config PID:

<c:set var="configValues" value="${functions:getConfigValues('org.jahia.modules.test')}"/>

<p>configValues.configKey1=${configValues.configKey1}</p>
<p>configValues.configKey2=${configValues.configKey2}</p>    

Get a specific configuration entry from a specific configuration PID:

<c:set var="configKey1Value" value="${functions:getConfigValue('org.jahia.modules.test', 'configKey1')}"/>
<p>configKey1=${configKey1Value}</p>    

Retrieve a factory configuration using a factory PID and an default identifier:

<c:set var="defaultFactoryConfigs" value="${functions:getConfigFactoryValues('org.jahia.modules.test.factory', 'default')}"/>
<p>defaultFactoryConfigs.configKey1=${defaultFactoryConfigs.configKey1}</p>
<p>defaultFactoryConfigs.configKey2=${defaultFactoryConfigs.configKey2}</p>    

Retrieving all the configuration PIDs:

<c:set var="configPIDs" value="${functions:getConfigPids()}"/>

<h1>All Config PIDs</h1>
<p>allConfigPIDs=${configPIDs}</p>    

Retrieving all the identifiers for a given factory PID:

<c:set var="testModuleFactoryIdentifiers" value="${functions:getConfigFactoryIdentifiers('org.jahia.modules.test.factory')}"/>

<h1>Test Module Factory Config Identifiers</h1>
<p>testModuleFactoryIdentifiers=${testModuleFactoryIdentifiers}</p>    

Container-compatible (Docker) configuration

To have configurations that will work well in container environments such as Docker, this usually implies:

  • Being able to access environment variables to pass configuration values
  • Be compatible with multiple instances and clustering

Accessing environment variables from OSGi configuration

It is possible in configuration files to access environment variables using the following syntax in a configuration file:

property=${env:ENV_VARIABLE}    

You can combine this, of course, with the default value system provided by Karaf:

property=${env:ENV_VARIABLE:-default_value}

Here’s an example:

userHomeDirectory=${env:HOME:-/tmp}    

The above example will try to read the user running the server home directory from the HOME environment variable, and if it is not set, it will use /tmp as a default value.

Note: This method of accessing properties could also be very useful to access passwords that would be passed as environment variables, preventing the need to store them unsafely in configuration files.

Clustered configurations

Clustering support is built into the Jahia OSGi configuration implementation. Any configuration changes done through anyway to modify configurations will be automatically propagated through the cluster to all the Jahia nodes. In general, when performing modifications to a cluster configuration, it is a good idea to wait for propagation to be completed to all the nodes before making further changes. Checking the server logs should be the simplest way to do that, or checking the values using the Karaf Web Console OSGi Configuration on the different nodes.

JCR vs. OSGi Config

In some cases, it might be interesting to store configuration inside of the JCR instead of in the OSGi configuration admin service. The following table summarizes the differences and the cases where it might be interesting to use one or the other.

This document will not detail storing configuration inside the JCR in detail as it is no different from handling regular content. The only recommendation is that usually, it makes sense to store JCR configuration either at the /settings level of the JCR if it is system-wide or underneath a site node (/sites/SITEKEY) if it is site-specific. Content definitions can be provided in CND files for configuration so that you can enforce the structure and use the content editor to edit configurations.

In general, OSGi configurations should be preferred, and JCR configurations should only be used when really needed, for example, when letting end-users directly edit the configuration. Using both in the same module, nothing is preventing that.

 

  Lives with source code Hot reload Environment variables access Cluster replication APIs UI Editing
OSGI Yes Yes Yes Yes Yes Yes (Tools)
JCR node Yes Yes No Yes Yes Yes (Content Editor)

Resources