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:
<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 the 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 is the only file required within an app folder and explained in the next section.
The i18n system is the same as within bundles. For basic descriptions, see Module Layer. To trigger the load of the i18n files the app.js is needed, which typically looks like this:
// AMD
define(["dojo/i18n!./nls/bundle"],{});
// ES6
import "dojo/i18n!./nls/bundle";
The optional app.css can contain extra css rules.
In most cases it is needed to increase specificity of css selector by at least one. You can use ".ctAppRoot" in combination with the template (for example .ctTpl_seasons) or theme class (for example .everlasting). Like this, the selector has the strength to overwrite existing layout definitions comming from map.apps. By using .ctAppRoot you also prevent unwanted side effects as the layout definitions are only effective inside the map.apps app. |
.ctAppRoot.ctTpl_seasons .map_main .omnisearch {
width: 500px;
}
As mentioned earlier, an app folder can also contain templates, themes and bundles.
Configuration Format (app.json)
The app.json is structured as follows:
{
// the name of the application (optional)
"appName": "traffic",
// properties section (optional)
// used to transport metadata
"properties" : {
"<propertyname>" : <value>,
...
},
// load section used to manipulate the loading behavior of an application
"load": {
"<propertyname>" : <value>
...
},
// bundles section used to overwrite or add any property defined by components in the manifest.json 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 application title to the design template.
{
...
// properties section (optional)
"properties" : {
"title" : "${apptitle}"
},
...
}
If an application is provided by the map.apps Manager, the following properties are available, too:
{
...
"properties" : {
// app id
id: "sample",
// title
title: "Sample",
// description of the app
description: "Sample application",
// editor state
editorState: "PUBLISHED",
// user who created the application
createdBy: "admin",
// timestamp when the application was created
createdAt: 1354314261408,
// user who last modified the application
modifiedBy: "admin",
// timestamp when the application was last modified
modifiedAt: 1354314268706,
// the uploaded filename of the application template
templateFile: "poicache.zip",
// id of the application 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:
<div ... >
<div ...>${appProps.title}</div>
</div>
Load Section
The load section is optional, but very important, because it defines which bundles are available in an application 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": {
"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 |
---|---|---|---|---|
|
|
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. |
|
|
yes |
|
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. |
|
|
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. |
|
|
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. |
|
|
yes |
|
A list of bundles that should be enabled on startup of this app. This might be useful if you want to load several bundles that are activated later during runtime of the app (for example by using the bundletools bundle). |
If an application needs i18n support, you have to define the following properties:
Property | Sample | Optional | Default | Description |
---|---|---|---|---|
|
or to make it more clear
|
yes |
|
This property declars that the app needs to load further JavaScript files.
A common use case is to define that the |
|
|
yes |
|
This defines that i18n files exist in the app, which are named "bundle" ( |
If an application requires custom css files, the following properties have to be declared:
Property | Sample | Optional | Default | Description |
---|---|---|---|---|
|
or
|
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
In the load section the property bundleLocations defines from which locations bundles are resolved during startup. In the following the supported syntax is defined.
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 an 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, which means that the order of bundle locations matter. Earlier locations have precedence. This can be changed by explicit definition of exclusions for bundles (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 (written 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".
{
...,
"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 ff the nls/bundle.js looks like this:
module.exports = {
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.
{
...,
"bundles" : {
"mybundle": {
"MyComponent":{
"windowTitle": "${mykey}"
}
}
}
}
If you want to overwrite an i18n value used in a bundle, you have to put the keys that should be overwritten into a <bundlename> property, like "map" in the preceding sample. The general structure looks like the following:
module.exports = {
root : {
"<bundlename>" : {
"<original i18nKey in bundle>" : "<new value>"
},
...
},
de: true
}
This means, you can overwrite any i18n key used in a bundle. On this way, you can also add completely new languages to the application. |
If you are using 'umlauts' or any other special characters that are not part of the ASCII charset, you have to 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 … The use of http://0xcc.net/jsescape/ is recommended 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 URLs to images or to backend services. The ${app} statement, which occurs in some of the preceeding samples, is used to achieve this goal.
The ${app} statement 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 sample 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 application 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:
{
...,
"bundles" : {
"mybundle": {
"MyComponent":{
// pattern is: "${app}:<path>/<file>"
"url": "${app}:images/test.png"
}
}
}
The ':' sign 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 preceding to build resource URLs, the configuration mechanism provides an additional way: "resource" expressions. A resource expression looks like:
{
...,
"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. Some samples:
Sample | Possible URL | Description |
---|---|---|
|
|
apps is registered as AMD prefix pointing to http://localhost:8080/js/apps |
|
|
The resource expression is evaluated in the context of the map bundle and this is located in http://localhost:8080/js/bundles/mapcore/map |
|
|
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 shows that - without a module prefix - the context is important! |
|
|
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 Config Admin Specification. The internal mechanism works as follows:
The app.json is resolved and provided to the system, via a ConfigLocator (see Framework Launch). The contents of the app.json are 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 SCR implementing the modification behavior, for example calling "modified" on the component instance or restarting the component if it can not handle runtime configuration changes. The SCR registers managed service factories for factory components.
During the first start of a component, the SCR checks the configuration admin service for configuration data as well. |
Programmatic configuration changes
Configuration Entries are identified by Persistent Identifiers ("pid") an Factory Persistent Identifiers ("factoryPid").
The current implementation requires the following format for pid and factoryPid:
Type | Format | Sample | Description |
---|---|---|---|
|
|
|
This is the normal persistent identifier structure used to identify the configuration entries of a special component (which is not a factory component). |
|
|
|
The factory persistent identifier marks configuration entries as used for factory component instances. They are additional keys used together with an persistent identifier of the concrete factory component instance. |
|
|
|
This is the structure for persistent identifiers of factory component instances, they are only valid together with an factoryPid. |
Changing a component configuration
// 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 an factory component instance, like agssearch-AGSStore-0, you can use the same code as shown preceding. The next samples show special method for use together with factory components.
// 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)
Besides the direct configuration changes via the ways explained preceding, exists a way to change configuration properties before they are given 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 and has to implement the method modifyConfiguration.
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:
{
"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 an higher start level or by defining this component as first component in a bundle.
// 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 config exists
if (configStore.getConfig("newbundle-ComponentName")){
// do nothing if config is present
return;
}
// write new configuration
configStore.setConfig("newbundle-ComponentName",{
bundleIdentifier: "newbundle",
properties: {
...
}
});
}
A component configuration of a ConfigurationInterceptor might look like:
{
"name" : "MyConfigInterceptor",
"provides" : ["system.config.ConfigurationInterceptor"]
}
Persist Configuration Changes
// 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
// note this works only if a persistence layer exists
configAdminService.save();