Configuration

This section describes the configuration system of the JS OSGi runtime and the default configuration format.

Apps

Different configurations are structured in apps. A typical app is structured as follows:

Bundle-Structure
<apps-folder>
\- <app-folder>                   // name of the app
    +- nls                      // i18n directory, see dojo i18n (optional)
    |   +- de                   //  German i18n resource directory
    |   |   \-bundle.js         //  German i18n resource file
    |   \- bundle.js            // root i18n resource file (en)
    |
    +- app.json                 // app configuration(required)
    |
    +- app.js                   // required to trigger loading of the i18n files (optional)
    |
    +- app.css                  // optional css rules, which can overwrite template and theme rules (optional)
    |
    + <anysubdirectory and any other resource>  // you can put into an app folder any resource
                                                // also bundles, templates and themes

The app.json file is the only file required within an app folder and explained in the next section.

The i18n system works the same as for bundles. For basic descriptions, see Module Layer. To trigger loading the i18n files the file app.js is needed. It typically looks like this:

app.js
// AMD
define(["dojo/i18n!./nls/bundle"],{});


// ES6
import "dojo/i18n!./nls/bundle";

The optional file app.css can contain extra CSS rules.

Increase the specificity of the CSS selector by at least one. For example use .ctAppRoot in combination with the template (for example .ctTpl_seasons) or theme class (for example .everlasting). This way the selector has the strength to overwrite existing layout definitions. By using .ctAppRoot you prevent unwanted side effects, because the layout definitions are only effective inside the map.apps app.
app.css
.ctAppRoot.ctTpl_seasons .map_main .search-ui {
    width: 500px;
}

As mentioned earlier, an app folder can also contain templates, themes, and bundles.

Configuration Format (app.json)

The app.json file is structured as follows:

General format
{
  // the name of the app (optional)
  "appName": "traffic",

  // properties section (optional)
  // used to transport metadata
  "properties" : {
    "<propertyname>" : <value>,
  ...
  },

  // load section used to manipulate the loading behavior of an app
  "load": {
    "<propertyname>" : <value>
    ...
  },

  // bundles section used to overwrite or add any property defined by components in the manifest.json file of bundles
  "bundles": {
    "<bundle name>":{
      "<component name>": {
      "<propertyname>" : <value>
      },

      "<factory component name>": [
    {
      "<propertyname>" : <value>
      },
    ...
    ],
    ...
    },
  ...
  }
}

Properties Section

The properties section is optional and used to transport any metadata. A special use case of the properties section is the transport of the app title to the design template.

Properties
{
  ...
  // properties section (optional)
  "properties" : {
    "title" : "${apptitle}"
  },
  ...
}

If an app is provided by the map.apps Manager, the following properties are available, too:

Manager properties
{
  ...
  "properties" : {
    // app id
    id: "sample",
    // title
    title: "Sample",
    // description of the app
    description: "Sample app",
    // editor state
    editorState: "PUBLISHED",
    // user who created the app
    createdBy: "admin",
    // timestamp when the app was created
    createdAt: 1354314261408,
    // user who last modified the app
    modifiedBy: "admin",
    // timestamp when the app was last modified
    modifiedAt: 1354314268706,
    // the uploaded filename of the app template
    templateFile: "poicache.zip",
    // ID of the app template
    templateId: "13B536ACA54D56",
    // preview image url
    thumbnailFile: null
  },
  ...
}

You can access all values of the properties section directly in template.html files using the appProps variable:

Access properties values in template.html
<div ... >
  <div ...>${appProps.title}</div>
</div>

Load Section

The load section is optional but important. It defines which bundles are available in an app and where they are located. Some other properties of the load section are used to define the requirement of i18n loading and the loading of additional CSS files.

Load section sample
{
  ...
  "load": {
    "allowedBundles": ["splashscreen","map", "windowmanager","system","templatelayout","templates","themes","reporttool@>=3.1.1"],
    "require": ["${app}/app"],
    "i18n": ["bundle"],
    "styles": ["${app}:app.css"]
  }
}

Following bundle loading properties exist:

Property Sample Optional Default Description

appRootName

"appRootName": "applicationDomNodeId"

yes

""

ID of the DOM node to which this app should be attached to. Normally you define this ID in the Launcher method and not in the app.json file.

bundleLocations

"bundleLocations": ["bundles", "${app}/bundles"]

yes

["bundles"]

An array of AMD packages which should be searched for bundles.json files. This property is interpreted by the BundleLocator. For more details, see Bundle Locations and Framework Launch.

allowedBundles

"allowedBundles":[
 "splashscreen",
 "map",
 "windowmanager",
 "system",
 "templatelayout",
 "templates",
 "themes",
 "reporttool@>=3.1.1"
 ]

yes

[]

List of bundles that are allowed to be used by this app. If not specified, all available bundles are loaded (if they are not listed in "skipBundles" property). This property is interpreted by the BundleLocator.

A bundle can be bound to a specific version range. This is done by adding ‘@’ followed by a version range identifier to the bundle’s name. Version range identifiers are discussed in the OSGi - Module Layer section. Any bundle not having a version range identifier is assumed to accept the latest available bundle version.

A typical scenario where version ranges are used is when an app should load a bundle of an extension. To ensure that a certain version of this extension is used, a version range identifier, for example @3.1.x, has to be added to the extension bundle.

skipBundles

"skipBundles": ["splashscreen"]

yes

[]

A list of bundles that should not be loaded by this app. This is an alternative to the recommended property "allowedBundles". This property is interpreted by the BundleLocator.

start

"start": [
  "system",
  "splashscreen",
  "map",
  "templatelayout",
  "toolset",
  "toolrules"
]

yes

[]

A list of bundles that should be enabled on startup of this app. This might be useful to load several bundles that are activated later during runtime of the app, for example by using the bundletools bundle.

To support i18n in an app, define the following properties:

Property Sample Optional Default Description

require

"require": ["${app}/app"]

or to make it more clear

"require": ["dijit/form/Button", ...]

yes

[]

This property declars that the app needs to load further JavaScript files. A common use case is to define that the app.js must be loaded by setting "require": "${app}/app".

i18n

"i18n": ["bundle"]

yes

[]

This defines that i18n files exist in the app, which are named "bundle" (/nls/bundle.js).

If an app requires custom CSS files, the following properties have to be declared:

Property Sample Optional Default Description

styles

"styles": ["${app}:app.css"]

or

"styles": [{
  // style definition only required if a special theme is used
  "theme": "night",
  "file": "${app}:app.css"
}]

yes

[]

This property defines that additional CSS file should be loaded. The property is interpreted by the themes bundle. The second declaration style allows to load app specific CSS files dependent on a special theme.

Bundle Locations

To define the locations from which bundles are resolved when the app starts, set the property bundleLocations in the load section of the app.json file.

Use an AMD prefix to identify a bundle location:

// assume that following package is registered at the AMD loader
require({
    packages : [{
        name : "myprefix",
        location : "http://localhost:8080/mybundles"
    }]
});


{
    "load": {
        // define http://localhost:8080/mybundles/bundles.json as source for bundle meta information
        "bundleLocations":["myprefix"]
    }
}


{
    "load": {
        // define http://localhost:8080/mybundles/test/bundles.json as source for bundle meta information
        "bundleLocations":["myprefix/test"]
    }
}


{
    "load": {
        // define http://localhost:8080/mybundles/mybundles.json as source for bundle meta information
        "bundleLocations":["myprefix/mybundles.json"]
    }
}

Use concrete URLs:

{
    "load": {
        // define http://localhost:8080/mybundles/bundles.json as source for bundle meta information
        "bundleLocations":["http://localhost:8080/mybundles"]
    }
}

{
    "load": {
        // define exactly that http://localhost:8080/mybundles/mybundles.json as source for bundle meta information
        "bundleLocations":["http://localhost:8080/mybundles/mybundles.json"]
    }
}

{
    "load": {
        // define an HTML page relative location as bundle location
        "bundleLocations":["./mybundles/mybundles.json"]
    }
}

Use object syntax:

// app.json

{
    "load": {
        "bundleLocations":[{
            // lookup myprefix and load mybundles.json
            "name" : "myprefix/mybundles.json"
        }]
    }
}

{
    "load": {
        "bundleLocations":[{
            // define a name and an explicit location
            "name" : "mybundles",
            "location" : "http://localhost:8080/mybundles"
        }]
    }
}

// The main reason to use the object syntax is to define additional properties which control the loading process from the registry
{
    "load" : {
        "bundleLocations":[{
            // define the location
            "name" : "mybundles",

            // list of bundle names which should not loaded from this registry.
            "excludes" : ["bundlename"],

            // jsonp not allowed for this registry
            "useJSONP" : false,

            // relative or absolute URL which should be requested to prefetch the JavaScript files from this registry
            "prefetch" : "./bundles.js",

            // disable the prefetching of JavaScript files for this registry
            "noprefetch" : true
        }]
    }
}
The first located bundle wins. This means that the order of bundle locations matters. Locations defined earlier have precedence. This can be changed by the explicit definition of bundle exclusions (using the "excludes" property).

Bundles Section

The "bundles" section defines properties for available bundles, whereby each bundle is referenced by its name. Within the "bundles" section, all settings listed in the "properties" section of components can be redefined. When an app is initialized, these property values are used to overwrite the component’s default configuration (as defined in <bundlename>\manifest.json).

Properties whose values are objects have to be redefined as a whole. The values are fully replaced and not merged!

If a component is a factory component, an array style definition is used to declare how many component instances should be created by the factory component. For a sample, see the following AGSStore.

app.json bundles section sample
{
  ...,

  "bundles" : {
    "map" : {                          // name of the bundle
      "MapState" : {                   // name of the component
        "initialExtent":{              // name of the property (if property has complex value, it must fully be redeclared)
          "xmin" : 267804.1562000003,
          "ymin" : 5565553.608100001,
          "xmax" : 544362.0157999999,
          "ymax" : 5832335.593900001,
          "spatialReference": {
            "wkid": 25832
          }
        }
      }
    },
    "agssearch": {
      "AGSStore": [         // sample of factory component, here an array style must be used
      {
        "id": "Helipads",
        "url": "http://...",
        ...
      },
      {
        "id": "LargeAirportPoints",
        "url": "http://...",
        ...
      }
      ]
    }

  ...
  }
}

Referencing i18n values and overwriting i18n keys in bundles

Like in manifest.json files, i18n values can be accessed via ${i18nkey} expressions in the app.json file of the nls/bundle.js like this:

<app>/nls/bundle.js sample
export default {
  root : {
    "mykey" : "Test",
    "map" : {
      "bundleName" : "New Map Bundle Name"
    }
  },
  de: true
}

So ${mykey} can be used directly in the app.json file to introduce the i18n value of the key.

app.json i18n keys
{
  ...,

  "bundles" : {
    "mybundle": {
      "MyComponent":{
        "windowTitle": "${mykey}"
      }
    }
  }
}

To overwrite an i18n value used in a bundle, put the keys that should be overwritten into a "<bundlename>" property, like "map" in the preceding sample. The general structure looks like the following:

<app>/nls/bundle.js - patching bundle i18n keys
export default {
  root : {
    "<bundlename>" : {
      "<original i18nKey in bundle>" : "<new value>"
    },
    ...
  },
  de: true
}
This means, you can overwrite any i18n key used in a bundle. This way you can bring i18n support for additional languages to the application.

If you are using "umlauts" or any other special characters that are not part of the ASCII charset, ensure that the resource files (*.js, *.json, *.html) are using the UTF-8 character set or you use unicode escapes (\uXXXX), like:

ä = \u00E4

ö = \u00F6

ü = \u00FC

…​

You can use http://0xcc.net/jsescape/ to find the correct unicode escape sequence for your character.

Configure application-relative resource paths

Very often bundles need the configuration of external or application-specific resources, like image URLs or links to backend services. The ${app} expression, which occurs in some of the preceeding samples, is used to achieve this goal.

The ${app} expression is evaluated by the ConfigLocator, which changes this placeholder to an AMD package prefix. The general pattern is <configlocation>/<appname>. In most project deployments, it is converted to apps/<appname>. For apps created with the map.apps Manager the replacement looks like this: builderapps/<appname>. The second example demonstrates that this is not a relative URL to a special path, because a builderapps path does not exist anywhere. Instead, the AMD prefixes are registered by the index.html (or dojo-init.js), so that the global AMD method require can be used to build full URLs.

Whether the use of ${app} is sufficient to generate a correct app relative URL depends on the bundle implementation.

If a bundle uses the methods Bundle.getResourceURL, ComponentContext.getResourceURL or resourceURL of ct/_url, use the following style:

app relative resources
{
  ...,

  "bundles" : {
    "mybundle": {
      "MyComponent":{
      // pattern is: "${app}:<path>/<file>"
      "url": "${app}:images/test.png"
    }
  }
}

The : (colon) is the separator between AMD package and the resource, relative to this package. This declaration style allows to point to any resource in any bundle if the namespace of the bundle is used instead of ${app}. for example the prefix ct/bundles/map points to any resource inside the map bundle. This mechanism provides the ability to point to external or server stable URLs, if the prefixes are registered. It also allows the definition of resource bundles (bundles which only contain special resources).

If the direct use of ${app} fails, for example because a bundle does not use the methods listed earlier to build resource URLs, the configuration mechanism provides an additional way: "resource" expressions. A resource expression looks like this:

app.json resource expressions
{
  ...,
  "bundles" : {
    "mybundle": {
      "MyComponent":{
        "url": "resource('${app}:images/test.png')"
      }
    }
  }
}

The resource expressions are evaluated in the context of the bundle, which evaluates the configuration. Internally the Bundle.getResourceURL method is used. Here are some examples:

Sample Sample URL Description

"resource( '${app}:images/test.png')"

http://localhost:8080/js/apps/sample/images/test.png

${app} resolves to apps/sample.

apps is registered as AMD prefix pointing to http://localhost:8080/js/apps.

"resource( 'images/test.png')"

http://localhost:8080/js/bundles/mapcore/map/images/test.png

The resource expression is evaluated in the context of the map bundle and this is located in http://localhost:8080/js/bundles/mapcore/map.

"resource( 'images/test.png')"

http://localhost:8080/js/bundles/base/splashscreen/images/test.png

The resource expression is evaluated in the context of the splashscreen bundle and this is located in http://localhost:8080/js/bundles/base/splashscreen.

This sample demonstrates that — without a module prefix —  the context is important!

"resource( 'templates:images/test.png')"

http://localhost:8080/js/bundles/base/templates/images/test.png

The resource expression uses the AMD package name of the templates bundle (the namespace) and points to this bundle, which is located in http://localhost:8080/js/bundles/base/templates.

Resource expressions help to create correct URLs in most cases.

Background

The configuration system is an adaption of the OSGi Configuration Admin Specification. The internal mechanism works as follows:

config admin

The app.json file is resolved and provided to the system, via a ConfigLocator (see Framework Launch). The content of the app.json file is parsed and converted into a ConfigStore. This store is registered as service ct.bundles.system.config.ConfigurationStore. This configuration store is referenced by the configuration admin service, which is registered as ct.framework.api.ConfigurationAdmin. The configuration admin service is the central component for accessing and changing configuration data. A bundle can register managed services (ct.framework.api.ManagedService) or managed service factories (ct.framework.api.ManagedServiceFactory) which are informed about configuration changes by the configuration admin service. This is performed by the Service Component Runtime which registers such services for any component whose configuration policy is not ignore. The managed services are provided by the Service Component Runtime implementing the modification behavior, for example calling "modified" on the component instance or restarting the component if it cannot handle runtime configuration changes. The Service Component Runtime registers managed service factories for factory components.

During the first start of a component, the Service Component Runtime checks the configuration admin service for configuration data as well.

Programmatic configuration changes

Configuration entries are identified by Persistent Identifiers ("pid") and Factory Persistent Identifiers ("factoryPid").

The current implementation requires the following format for pid and factoryPid:

Type Format Sample Description

pid

<bundleSymbolicName>-<ComponentName>

map-MapWidgetFactory

This is the normal persistent identifier structure used to identify the configuration entries of a special component (which is not a factory component).

factoryPid

<bundleSymbolicName>-<ComponentName>-Factory

agssearch-AGSStore-Factory

The factory persistent identifier marks configuration entries as used for factory component instances. They are additional keys used together with a persistent identifier of the concrete factory component instance.

pid

<bundleSymbolicName>-<ComponentName>-<index>

agssearch-AGSStore-0

This is the structure for persistent identifiers of factory component instances, they are only valid together with a factoryPid.

Changing a component configuration

Component Configuration Updates
// get injected configuration admin service (ct.framework.api.ConfigurationAdmin)
let configAdminService = this.configAdminService;

// need to know a concrete pid
let pid = "map-MapWidgetFactory";

// need to know a concrete bundle identifier (symbolic name)
let bundleIdentifier = "map";

// read configuration entry
let config = configAdminService.getConfiguration(pid, bundleIdentifier);

// properties are a plain object
let configProperties = config.get("properties");

...
// change the configuration
configProperties.newProperty = "test";


// use the update method of the configuration entry to update the configuration data
// note: this triggers a runtime component update
config.update(configProperties);

// deletes the configuration entry
config.remove();

Changing a factory component configuration

If you know an exact pid of a factory component instance, like agssearch-AGSStore-0, you can use the same code as shown preceding. The next examples demonstrate a special method to use in conjunction with factory components.

Create Factory Component Configuration
// get injected configuration admin service (ct.framework.api.ConfigurationAdmin)
let configAdminService = this.configAdminService;

// need to know a concrete factoryPid
let factoryPid= "agssearch-AGSStore-Factory";

// need to know a concrete bundle identifier (symbolic name)
let bundleIdentifier = "agssearch";

// sample configuration properties
let configProperties = { "property" : "test" };

// read configuration entry
let config = configAdminService.createFactoryConfiguration(factoryPid, bundleIdentifier);

// use the update method of the configuration entry to apply the configuration data
// note: this triggers a runtime component update
config.update(configProperties);
List Factory Component Configurations
// get injected configuration admin service (ct.framework.api.ConfigurationAdmin)
let configAdminService = this.configAdminService;

// need to know a concrete factoryPid
let factoryPid= "agssearch-AGSStore-Factory";

// need to know a concrete bundle identifier (symbolic name)
let bundleIdentifier = "agssearch";

// read all factory configuration instance entries
let configs = configAdminService.getFactoryConfigurations(factoryPid, bundleIdentifier);

for (var i=0;i<configs.length;++i){
    var config = configs[i];
    ...
    // for example delete all factory component instances
    config.remove();
}

Change configuration parameters before injecting into a component (ConfigurationPlugin)

In addition to directly changing configuration using the ways explained earlier, there is a way to change configuration properties before they are passed to a ManagedService and without storing/persisting the changes. This way is called ConfigurationPlugin. A ConfigurationPlugin is a service registered by the interface ct.framework.api.ConfigurationPlugin. It has to implement the method modifyConfiguration.

ConfigurationPlugin
modifyConfiguration(targetInfo, configProperties){
    // targetInfo can be used to get information about the target component, which properties are given in "configProperties".
    let managedServiceProperties = targetInfo.managedServiceProperties;
    // the persistent identifier
    let targetPid = targetInfo.pid;
    // the factory persistent identifier
    let targetPid = targetInfo.factoryPid;

    // to change the or extent the configuration, simple change the configProperties
    configProperties.changedProperty = "test";

    // optional, allowed to return fully different properties
    return configProperties;
}

A component configuration of a ConfigurationPlugin might look like this:

ConfigurationPlugin in manifest.json file
{
    "name" : "MyConfigPlugin",
    "provides" : ["ct.framework.api.ConfigurationPlugin"],
    "properties" : {

        // array of pids or factory pids, for which this plugin should be called
        // if the property is not specified or a empty array, the plugin is called for each target service
        "CM-Target-Pids" : ["themes-ThemeModel"],

        // if more than one plugin is configured, the ranking can control the order.
        // lower rankings are called before higher rankings
        // a plugin with high ranking can overwrite the changes of plugin with a lower ranking
        // default value is 0
        "CM-Ranking" : 0
    }]
}

The ConfigurationPlugin mechanism is the recommended way to implement Parametrizables (see parametermanager bundle), because you can change the configuration of a component based on query parameters before the component is started.

Intercepting configuration parameters for configuration versioning (ConfigurationInterceptor)

A component can implement the interface system.config.ConfigurationInterceptor. Such components are informed about the current configuration store and can change the configuration. This can help to migrate a configuration to a new one, for example if bundle names or component names have changed. To make it work it is important that such a ConfigurationInterceptor is started before any component relying on the modified configurations; for example by using an extra bundle with a higher start level or by defining this component as first component in a bundle.

ConfigurationInterceptor
// A ConfigurationInterceptor must implement the method "intercept"

intercept(configStore){

        // read old config
        let oldConfig = configStore.getConfig("oldbundle-ComponentName");

        // delete old config
        configStore.removeConfig("oldbundle-ComponentName");

        // check if a configuration exists
        if (configStore.getConfig("newbundle-ComponentName")){
                // do nothing if configuration is present
            return;
        }

        // write new configuration
        configStore.setConfig("newbundle-ComponentName",{
                bundleIdentifier: "newbundle",
                properties: {
                        ...
                }
        });
}

A component configuration of a ConfigurationInterceptor might look like this:

{
    "name" : "MyConfigInterceptor",
    "provides" : ["system.config.ConfigurationInterceptor"]
}

Persist Configuration Changes

Persist Component Configuration Updates
// get injected configuration admin service (ct.framework.api.ConfigurationAdmin)
let configAdminService = this.configAdminService;

// to trigger the storage of any changed configurations call "save" on the configuration admin serivce
// this works only if a persistence layer exists
configAdminService.save();