7 Operating instructions
The section contains instructions for usage of various functionalities of Oskari.
7.1 Usage instructions
This documentation section contains general usage instructions on various parts of Oskari.
7.1.1 How to use development tools
GitHub, the collaboration platform
The Oskari source code is stored in several separate Git repositories hosted under the oskariorg organization on GitHub.
The source code projects on GitHub also facilitate collaboration through hosting the project's external bug / ticket trackers.
Source code contributions to the project should be sent in as GitHub Pull Requests to the relevant repository's "develop" branch, according to the Git Flow branch policies.
Pull Requests and the merging thereof are amply documented in the GitHub documentation.
You will need to install git
to actively participate to the development
process.
Collaboration activities
Register a personal account at GitHub.
Fork the oskariorg/oskari and oskariorg/oskari-server repositories, and check out the source code from your forks to your local computer.
Read Atlassian's Git Flow documentation.
Create a new feature branch with git checkout -b feature/[name]
and publish/push it into your own GitHub
repository as a feature branch.
Please make sure to configure your Git tooling so that any new files, or changes to old files, use only UNIX End-Of-Line markers, and never DOS/Windows End-Of-Line markers.
Oskari source code
The github.com/oskariorg/oskari-frontend
repository contains the frontend JavaScript, CSS and HTML source
code. It assumes being served from webserver path /Oskari with a capital O so you can checkout the repository under the name Oskari
or configure the webserver to do this. The Jetty bundle is configured properly for this.
The github.com/oskariorg/oskari-server
contains the Oskari serverside code.
Licensing
The Oskari source code is dual licensed with EUPL and the MIT licenses, which are made available in the source code repositories.
Not all of the source code files contain a license header.
Software development tools
Java development tools
A large part of the Oskari software development process consists of adding new features to the backend server, using the Java development environment.
The principal tools necessary for Java development are:
- Java Development Kit (JDK) 8
- Maven 3.5.0
You will need to install those tools in the manner relevant to your development platform, principally Linux, Mac OS X or Windows with Cygwin.
For Java files, the project uses indentation of 4 spaces, and the so-called Sun-Style with the exception of maximum line length of 120 characters.
Java development tools, validation
A properly installed Java Development Kit should report version 8, or some update version of it, in response to the command line command
java -version
Maven, on the other hand, should report both its own version, as well as the Java version, in reponse to the command line command
mvn --version
JavaScript and HTML development tools
The Oskari frontend consists of JavaScript code, HTML, as well as internationalization and localisation data files.
The frontend JavaScript and CSS files are stored in the Git version control repository in structured, and easy to manage hierarchies. These are later compiled into small deployment packages using Node.js tooling.
Therefore, a developer must install a platform-appropriate Node.js execution and runtime environment, along with the NPM package manager.
Databases
The primary Oskari server components store their data on PostgreSQL 9.x+ databases, and perform some of the spatial analysis of the data on the database layer, using the PostGIS extension.
Some of the backend functionality cache data on a Redis server.
Again, these components should be installed locally, in a platform-appropriate manner. On Linux,
using apt-get
or yum
, on Mac OS X using Homebrew, and on Windows, installer EXE or MSIs.
The PostGIS extension must be separately enabled on each database it is invoked on. Please follow the relevant documentation.
To validate a successful installation of these tools, use either the psql
command line tool, or a
GUI tool, if you prefer, to connect to the PostgreSQL server.
To validate a successful installation of PostGIS, use your connection to the PostgreSQL database to execute the PostGIS version query
SELECT PostGIS_full_version();
Your Redis installation can be checked with the command line command
redis-cli info
Application server - Jetty
The Oskari server builds into a WAR file executed inside a servlet runner.
A good servlet runner for development and production is Jetty 8 / 9, but the application can just as well be run on a properly configured Tomcat instance.
The oskari-server/webapp-map
Maven project doesn't currently have the org.eclipse.jetty:jetty- maven-plugin
configured for running the application in an embedded container, but that should be
trivial to do for an active developer, who doesn't want to keep manually moving WAR files around to
test things.
The default application configuration settings reside in the oskari.properties
configuration file in the
WAR classes
-directory. These can be overridden with oskari-ext.properties file in the servers classpath (for example resources folder under {jetty.base}).
The servlet in the WAR will attempt to connect to a pre-existing PostgreSQL database using the
settings specified in the oskari(-ext).properties
file.
Configuring and building the service
Database creation and configuration
The PostgreSQL tools createuser
and createdb
should be used to create a new database user
oskari
with a password. These settings should also be transferred to your oskari-ext.properties
or
tomcat-6.0.29/conf/server.xml
files.
After creating the database and starting the server the application creates the database schema and populates it with example content.
The documentation for the process of upgrading the database is also worth going over.
Building the backend
The development build for the backend is compiled and run using Maven, as described in the development documentation.
7.1.2 How to use localization
Localization files are referenced in bundle definitions (bundle.js
- locales array). Each languages localization is in its own file with structure like this:
Oskari.registerLocalization({
// localization language
"lang" : "fi",
// Bundle name or some other identifier which is used when retrieving this localization data
"key" : "{{MyBundlesLocalizationKey}}",
"value" : {
// Actual localization data in custom structure, bundle using this data is responsible for interpreting the structure
"title" : "Bundle title",
"steps" : {
"step1" : {
"tab" : "Step 1",
"title" : "Step 1 title",
"content" : "{name}'s layer was created on {created, date}"
},
"step2" : {
"tab" : "Step 2",
"title" : "Step 2 title",
"content" : "Remove {count, plural, one {# map layer} other {# map layers}}"
},
"step3" : {
"tab" : "Step 3",
"title" : "Step 3 title",
"content" : "No interpolation here"
}
}
}
});
The message strings are in ICU message syntax. Value names to be included into the string are in curly braces. Type (number, date or time) of the value can be given after the comma (default is string). The number/date/time will be formatted according to the locale rules (see steps.step1.content above). If the language has different forms for singular/plural, all forms must be defined (see steps.step2.content above). Note: some languages have multiple plural forms. It also possible to define different messages depending on value.
The language that will be returned is the one set with Oskari.setLang
method (usually during before application start):
Oskari.setLang('en');
Formatting
To get a localized string in the current language:
Oskari.getMsg('<MyBundlesLocalizationKey>', '<path.to.message>', {key1: value1, key2: value2});
Where keys are the ones used inside curly braces in the ICU format. From the example above:
Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step1.content', {name: 'Oscar', created: new Date()});
Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step2.content', {count: 4});
Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step3.content');
To avoid typing the bundles's localization key in every localization call, you can bind the first argument and create an instance variable for it:
Oskari.clazz.define(
'Oskari.my.bundle.clazz',
function () {
this.loc = Oskari.getMsg.bind(null, '<MyBundlesLocalizationKey>');
}, {
someMethod: function() {
this.loc('steps.step1.content', {name: 'Oscar', created: new Date()});
}
},
...
Deprecated old way
In some older bundles the whole localization data tree is retrieved on runtime with a call:
var localization = Oskari.getLocalization('<MyBundlesLocalizationKey>');
this.showMessage(localization.steps.step1.tab);
This way does not support interpolation, pluralization or number/time formatting and therefore should not be used in new bundles.
7.1.3 How to use Oskari filter
JSON filter object for filtering features by features' property keys and values.
Use cases
- Define filter for optional styles
- Define filter for WFS layer
Oskari filter definition:
{
"property": {
"key": "type", // feature's property name
// Operators
"value": "road" || 10000 || true // equal comparison
"in": ["tarmac","sand", "gravel"], // for multiple possible values (OR)
"notIn": ["sand", "gravel"], // for multiple possible NOT values (OR)
"like": "tarm*", // for a pattern tarm*
"notLike": "*tarm*", // for a NOT pattern *tarm*
// Operators for numerical comparison
"greaterThan": 60, // strictly greater than
"atLeast": 60, // greater than or equal to
"lessThan": 80, // strictly less than
"atMost": 80, // less than or equal to
// Case sensivity for value, in, notIn, like and notLike operators
"caseSensitive": false
},
"AND": [...], // to add multiple conditions with AND operator
"OR": [...] // to add multiple conditions with OR operator
}
Property filter should have key and one operator. Key defines which feature's property and operator which comparison operator are used for filttering. For range filters two operators can be used in same property filter. If additional definitions are given then filter doesn't work as intended.
Filtered WFS layer
WFS layer filter is stored in attributes field. If layer has filter then GetFeature requests are made with bbox and specified property filter.
WFS layer filter examples
To have WFS layer that contains only features which have regionCode property value '091'. CQL: regionCode = '091'
{
"filter": {
"property": {
"key": "regionCode",
"value": "091"
}
}
}
To have WFS layer that contains only features which have population between 10000 and 200000 (begin and end values are excluded). CQL: population > 10000 and population < 200000
{
"filter": {
"property": {
"key": "population",
"greaterThan": 10000,
"lessThan": 200000
}
}
}
To have WFS layer that contains only features which regionCode '091' and serviceType is 'school', 'pre-school' or 'high-school'. CQL: regionCode = '091' and (serviceType = 'school' or serviceType = 'pre-school' or serviceType = 'high-school')
{
"filter": {
"AND": [{
"key": "regionCode",
"value": "091"
}, {
"key": "serviceType",
"in": ["school", "pre-school", "high-school"]
}]
}
}
Oskari style
Using filter with optional styles see: Oskari style
7.1.4 How to use Oskari style
JSON style object for styling layers. VisualizationForm component also supports getting and setting style values in Oskari style format.
Use cases
- Define styling for vector layers handled by WfsVectorLayerPlugin
- Define styling for feature layers
- VisualizationForm
Oskari style definition
All of the fields and objects defined here are optional in the Oskari style JSON. Anything can be omitted from the example below.
{
"fill": { // fill styles
"color": "#FAEBD7", // fill color
"area": {
"pattern": -1 // fill style
}
},
"stroke": { // stroke styles
"color": "#000000", // stroke color
"width": 1, // stroke width
"lineDash": "solid", // line dash, supported: dash, dashdot, dot, longdash, longdashdot and solid
"lineCap": "round", // line cap, supported: butt, round and square
"lineJoin": "round" // line corner, supported: bevel, round and miter
"area": {
"color": "#000000", // area stroke color
"width": 1, // area stroke width
"lineDash": "dot", // area line dash
"lineJoin": "round" // area line corner, supported: bevel, round and miter
}
},
"text": { // text style
"fill": { // text fill style
"color": "#000000" // fill color
},
"stroke": { // text stroke style
"color": "#ffffff", // stroke color
"width": 1 // stroke width
},
"font": "bold 12px Arial", // font
"textAlign": "top", // text align
"offsetX": 12, // text offset x
"offsetY": 12, // text offset y
"labelText": "example", // label text
"labelProperty": "propertyName" // read label from feature property
},
"image": { // image style
"shape": 5, // 0-6 for default markers. svg or external icon path
"size": 3, // Oskari icon size.
"sizePx": 20, // Exact icon px size. Used if 'size' not defined.
"offsetX": 0, // image offset x
"offsetY": 0, // image offset y
"opacity": 0.7, // image opacity
"radius": 2, // image radius
"fill": {
"color": "#ff00ff" // image fill color
}
},
"inherit": false, // For hover. Set true if you wan't to extend original feature style.
"effect": "auto normal" // Requires inherit: true. Lightens or darkens original fill color. Values [darken, lighten, auto] and [minor, normal, major].
}
Vector layer styling
Vector layer styling is stored in options field. Feature style overrides default style definitions for all features. Optional styles are used for specific features only which overrids default and feature style definitions. One named style has only one feature style and may have more than one optional styles.
{
"styles": {
"Custom MVT style": {
"featureStyle": {...},
"optionalStyles": [{...}]
}
}
...
}
WFS layer styling example
WFS layer options field:
{
"styles":{
"Red lines": {
"featureStyle": {
"stroke": {
"color": "#ff0000"
}
}
}
}
}
Hover style
Hover options describes how to visualize layer's features on hover and what kind of tooltip should be shown. Note that features isn't hovered while drawing is active (DrawTools).
{
featureStyle: {
inherit: true,
effect: 'darken',
// Oskari style definitions
fill,
stroke,
image,
text
},
// Tooltips content as an array. Each object creates a row to the tooltip.
content: [
// "key" is a label and will be rendered as is.
{ key: 'Feature Data' },
// "valueProperty" and "keyProperty" are fetched from the feature's properties.
{ key: 'Feature ID', valueProperty: 'id' },
{ keyProperty: 'name', valueProperty: 'value' }
]
}
Optional styles
The styling definitions for optional style is same as above. Features that uses optional style are filtered with Oskari filter definition by features' property keys and values. To filter features by different property keys you can use AND or OR operators.
You can pass the optional styles array with the key optionalStyles
when you pass the base style with key featureStyle
in for example AddFeaturesToMapRequest.
Optional style examples
To define style which is affected to featuers which regionCode is '091'.
[{
"property": {
"key": "regionCode",
"value": "091"
},
"fill": {
"color": "#ff0000"
}
}]
Filtering features with different property keys use AND and OR operators. To define style which is affected to features which type is 'city' or 'town' and population is greater than 10000.
[{
"AND": [{
"key": "type",
"in": ["city", "town"]
}, {
"key": "population",
"greaterThan": 10000
}],
"image": {
"shape": 5,
"size": 4,
"fill": {
"color": "#3333ff"
}
},
"text": {
"labelProperty": "name"
}
}]
To define style which is affected to features which surface property is 'tarmac' (case insensitive) or type is '*road*' or speedLimit is between 50 and 80 (begin and end values are included).
[{
"OR": [{
"key": "surface",
"value": "tarmac",
"caseSensitive": false
}, {
"key": "type",
"like": "road"
}, {
"key": "speedLimit",
"atLeast": 60,
"atMost": 100
}],
"stroke": {
"color": "#ff0000",
"width": 3
}
}]
7.1.5 How to use publisher tools
Bundles in Oskari can provide a tool object(/Oskari clazz) that the publisher
bundle/functionality discovers at runtime. This allows bundles to extend the publisher user-interface by adding new options for the user to see when that particular bundle is part of an application.
The tools are discovered by querying Oskari for classes with protocol Oskari.mapframework.publisher.Tool
(See example tool class below with 'protocol': ['Oskari.mapframework.publisher.Tool']
).
Oskari.clazz.define('Oskari.mybundle.publisher.MyPluginTool', function () {},
{
// in which group this tools should be in. Every group gets its own panel in publisher UI
group: 'data',
// bundleName is used only by this tool
bundleName: 'mybundle',
// definition of what plugin is controlled by this tool
// and what is its starting configuration when the tool is enabled
getTool: function () {
return {
id: 'Oskari.mybundle.MyPlugin',
// title is the UI label for the tool
title: Oskari.getMsg('mybundle', 'tool.label'),
config: this.state.pluginConfig || {}
};
},
// process the embedded map "data"/app setup and detect if this tool was enabled on it
init: function (data) {
if (!data || !data.configuration[this.bundleName]) {
return;
}
const conf = data.configuration[this.bundleName].conf || {};
this.storePluginConf(conf);
this.setEnabled(true);
},
// return values to be saved based on if this tool was enabled or not
getValues: function () {
if (!this.isEnabled()) {
return null;
}
return {
[this.bundleName]: {
whatever: 'config'
}
}
}
}, {
'extend': ['Oskari.mapframework.publisher.tool.AbstractPluginTool'],
'protocol': ['Oskari.mapframework.publisher.Tool']
});
With ES class syntax:
const AbstractPublisherTool = Oskari.clazz.get('Oskari.publisher.AbstractPublisherTool');
const BUNDLE_ID = 'mybundle';
class MyPluginTool extends AbstractPublisherTool {
getTool () {
return {
id: 'Oskari.mybundle.MyPlugin',
// title is the UI label for the tool
title: Oskari.getMsg(BUNDLE_ID, 'tool.label'),
config: this.state.pluginConfig || {}
};
}
// process the embedded map "data"/app setup and detect if this tool was enabled on it
init (data) {
if (!data || !data.configuration[BUNDLE_ID]) {
return;
}
const conf = data.configuration[BUNDLE_ID].conf || {};
this.storePluginConf(conf);
this.setEnabled(true);
}
// return values to be saved based on if this tool was enabled or not
getValues () {
if (!this.isEnabled()) {
return null;
}
return {
[BUNDLE_ID]: {
whatever: 'config'
}
}
}
}
// Attach protocol to make this discoverable by Oskari publisher
Oskari.clazz.defineES('Oskari.publisher.MyPluginTool',
MyPluginTool,
{
'protocol': ['Oskari.mapframework.publisher.Tool']
}
);
When the user starts the publisher:
UIChangeEvent
is sent to notify other functioanalities to shut themselves down to avoid conflicts for screen space.StateHandler.SetStateRequest
is used to set the application state (based on the embedded map that is being edited).mapmodule
is queried for plugins withplugin.isShouldStopForPublisher()
returning true (this defaults toplugin.hasUI()
returning true). All of these plugins are shutdown/stopped to clean the map state for publisher functionality.- Gathers/discovers all tool classes that are available on the application, creates an instance of them and groups the tools based on the group value of the tools
- Creates the panels based on tool groups and calls
tool.init(data)
for all tools - Any panels that have tools where
tool.isDisplayed(data)
returns true are shown to the end user
When the user exits the publisher:
- On save: calls
tool.getValues()
to gather a payload for saving the embedded map based on end user selections to database - calls
tool.stop()
- this is where the tool should clean up anything that it has started when the publisher functionality was running - Starts all plugins that were stopped on the step 3 of startup to restore normal geoportal functionality.
Publisher tool API
Many of these are handled by the publisher/tools/AbstractPluginTool
base class. Take a look at it before overriding the functions:
init(dataObject)
receives the data for the embedded map and should use that data to detect if the tool is enabled when a user edits an embedded map.isDisplayed(dataObject)
should return true if a selection for the tool should be shown to user. The function receives the publisher data as parameter and can use the data to detect if it should be shown. Most of the tools return true, but some examples of cases for this are:- the feature data table isn't shown if there are no layers that have feature data
- LogoPlugin isn't displayed as a selection to user as it's always included. However a tool is provided for it to allow user to select the placement for the logo on the embedded map.
- statistical data tools return false if there are no statistical data on the map
setEnabled(boolean)
Try using the version from base class and use _setEnableImpl() instead when possible. Starts/stops the functionality on the map (like map plugin). Should sendPublisher2.ToolEnabledChangedEvent
to allow the plugin placement mode to be activated when a tool is selected when the mode is active. Called when the user selects/deselects the tool on the publisher._setEnabledImpl(boolean)
setEnabled is implemented onAbstractPluginTool
base class. This is an extension hook for custom handling for enabling/disabling the tool.isEnabled()
should return true if the tool is selectedisDisabled()
related to isDisplayed(). This allows the selection to be shown even if user can't select it. A tooltip can be included to show the user why the tool is disabled. An example is that map legeneds selection is shown but is disabled if layers on the map don't have legend available.stop()
called when the user exits publisher functionality. The tool should clean up after itself on this function (for example shutting down things added to the map while on publisher).getValues()
should return how the user selections for the tool should affect the appsetup JSON on the embedded map. This should match the logic thatinit()
uses for detecting if the tool should be enabled when editing an embedded map.getTool()
This should return an object with keys:- id: the value should match a plugin class name that this tool controls. If this doesn't control a plugin it can be any unique value, but then you need to override some of the functions in base class
- title: This should return a label that is shown to the user as a selection what this tool does (jQuery-based tools assume this is a key reference to get the label from localization)
- config: This should return the plugin config that the tool.init() received when the tool is started
- hasNoPlugin: If the tool is NOT handling a plugin that is referenced in the id, you can return true here to have
AbstractPluginTool
skip starting and stopping the plugin. - disabledReason: Oskari.getMsg('mybundle', 'tool.toolDisabledReason') This can be used to return a string to show the user as a tooltip when the tool is disabled for letting the user know why it can't be selected. (Works only for React-based tools currently)
For older jQuery-based tools:
getExtraOptions()
should return a jQuery-object that allows users to do additional selections for the tool. For React-based tools:getComponent()
This is used by new tools that create the tool "extra options" using React. Returns an object that can have keys (if keys are missing the tool doesn't have extra options):- component: React-component that is rendered to show the extra options for this tool
- handler: Reference to a
UIhandler
for handling the extra options
7.1.6 How to use a bundle
A bundle is a component in an Oskari application. A bundle is a selection of Oskari classes which form a component that offers additional functionality for an application. A bundle can offer multiple implementations for a functionality which can then be divided into smaller packages for different application setups. Packages can be used to offer a multiple views for the same functionality for example search functionality as a small on-map textfield or a window-like UI (see Tile/Flyout) for the same functionality. For a short introduction see create your own bundle.
Directory structure
See here for info about structure and conventions.
Implementation
Bundles implementation files should be located under the /bundles
folder. If the bundle has a BundleInstance (ie. something that is started/instantiated when the bundle is played) it is usually defined in a file called instance.js
, but this is not enforced and any file referenced in bundle definition (bundle.js
) can be used. The bundle doesn't need to have an instance and can be just used to import dependency files that can be instantiated elsewhere. Usually you want to implement a BundleInstance since you can think of it as a starting point for your functionality which is triggered by just adding your bundle in an applications startup sequence.
A Bundle instance is an Oskari class which implements Oskari.bundle.BundleInstance
protocol. A Bundle instance is created as a result from a Bundle definitions (see above) create method. Bundle instance state and lifecycle is managed by Bundle Manager.
Bundle lifecycle methods
start
- called by application to start any functionality that this bundle instance might haveupdate
- called by Bundle Manager when Bundle Manager state is changed (to inform any changes in current 'bundlage')stop
- called by application to stop any functionality this bundle instance has Bundle instance is injected with a mediator object on startup with provides the bundle with its bundleid:
instance.mediator = {
bundleId : bundleId
}
Definition
The bundle definition (or package) should be located in bundle.js
file under the /packages
folder. The bundle package definition should not implement any actual functionality. It should only declare the JavaScript, CSS and localization resources (== files) and metadata if any. If the bundle package can be instantiated the package's create
method should create the bundle's instance. The bundle doesn't need to have an instance and can be used to import dependency files that can be instantiated elsewhere. In that case the create method should return the bundle class itself (return this;
).
Bundle should install itself to Oskari framework by calling installBundleClass
at the end of bundle.js
Oskari.bundle_manager.installBundleClass("{{bundle-identifier}}", "Oskari.{{mynamespace}}.{{bundle-identifier}}.MyBundle");
Adding new bundle to view
In order to get bundle up and running in your map application, the bundle needs to be added to the database. There are two tables where it shoud be added:
- portti_bundle
- portti_view_bundle_seq
portti_bundle
includes definitions of all available bundles. Definition of the new bundle should be added here to be able to use it in a view. It is recommended to use flyway-scripts
when making changes to database. Documentation can be found here and here.
Below is an example of an flyway-script (which is actually SQL
) adding new bundle to portti_bundle
table (Replace
--insert to portti_bundle table
-- Add login bundle to portti_bundle table
INSERT
INTO portti_bundle
(
name,
startup
)
VALUES
(
'login',
'{
"bundlename":"login",
"metadata": {
"Import-Bundle": {
"{{bundle-identifier}}": {
"bundlePath":"/Oskari/packages/bundle/"
}
}
}
}'
);
When bundle is added to portti_bundle
table, it can be added to portti_view_bundle_seq
table to be used in a view. Below is an example of an flyway-script adding new bundle to portti_view_bundle_seq
table.
-- Add login bundle to default view
INSERT
INTO portti_view_bundle_seq
(
view_id,
bundle_id,
seqno,
config,
state,
startup,
bundleinstance
)
VALUES (
(SELECT id FROM portti_view WHERE application='servlet' AND type='DEFAULT'),
(SELECT id FROM portti_bundle WHERE name='login'),
(SELECT max(seqno)+1 FROM portti_view_bundle_seq WHERE view_id=(SELECT id FROM portti_view WHERE application='servlet' AND type='DEFAULT')),
(SELECT config FROM portti_bundle WHERE name='login'),
(SELECT state FROM portti_bundle WHERE name='login'),
(SELECT startup FROM portti_bundle WHERE name='login'),
'login'
);
After these steps, and when bundle is defined correctly in front-end code, the bundle should be loaded when starting your map application. Great way to check if the bundle is loaded at start is to look at startupSequence in GetAppSetup in developer console.
Resources
Any additional CSS definitions or images the bundle needs are located under the bundle implementation resources
folder. Any image links should be relative paths.
7.1.7 How to use a bundle manager/loader
- loads Bundle Definitions
- manages bundle state and lifecycle
- loads Bundle JavaScript sources and CSS resources
- instantiates Bundle Instances
- manages Bundle Instance lifecycle
The code for Oskari class system/bundle manager/Oskari loader can be found in Oskari/src
. The minified version of this is available in Oskari/bundles/bundle.js
and a new version of this core-functionality can be built by running npm run core
command in Oskari/tools
folder.
Oskari loader
Oskari loader is part of Oskari core functionality. The loader takes in a JSON-array describing bundles that need to be loaded, started and configured:
var bundleSequence = [];
var applicationConfig = {};
var loader = Oskari.loader(bundleSequence, applicationConfig);
loader.processSequence(function() {
// all done
});
The loader determines any JavaScript that must be loaded for a bundle, loads the scripts and resources that aren't already loaded and creates an instance of that bundle. The bundle/startup sequence for the loader needs JSON-objects like this:
{
"bundlename" : "openlayers-default-theme",
"metadata" : {
"Import-Bundle" : {
"openlayers-single-full" : {
"bundlePath" : "/{{path to}}/packages/openlayers/bundle/"
},
"openlayers-default-theme" : {
"bundlePath" : "/{{path to}}/packages/openlayers/bundle/"
}
}
}
}
Where:
- bundlename is the name of the bundle that we want to start
- metadata["Import-Bundle"] describes and bundles that need to be loaded and basepaths for them. Atleast the bundle that is referenced in bundlename should be present here, but it can also reference other bundles. The other bundles aren't started, but the scripts and resources they provide are loaded to the browser (think libraries).
- bundleinstancename is an optional key that can be used to reference a specific bundle configuration. This is useful if you want to start the same bundle multiple times with different configurations.
Processing the bundle sequence block
The loader goes through each key in Import-Bundle object
It tries to load a file named bundle.js from path constructed from [bundlePath]/[bundleid]/bundle.js
. Where bundlePath above would be /<path to>/packages/openlayers/bundle/
, bundleid would be openlayers-single-full
and openlayers-default-theme
. These are loaded one after each other, the loading order of any Import-Bundle
reference is not guaranteed.
When the bundle.js is loaded, it's expected to call a global function and provide a bundleid and an Oskari class name that describes the scripts and resources for such bundle:
Oskari.bundle_manager.installBundleClass("[bundle id]", "[class name like Oskari.framework.MyBundle]");
This registers the bundle so Oskari is aware of it and can start it. You can check that a bundle has been registered by calling Oskari.bundle('[bundle id]');
. It should return an object with the descriptor class instance and metadata from the descriptor.
Note! If Oskari is already aware of the bundles to be loaded (installBundleClass has been called on them) the loader assumes the bundle to be loaded and moves to step 3 to start the bundle.
Script/resource loading
Any bundle referenced in Import-Bundle
should have called the installBundleClass-function to register itself with the bundle id. The loader tries to create an descriptor instances for the referenced bundles using Oskari.clazz.create([class name])
where the class name is referenced in the installBundleClass call. The created class lists all the scripts, resources, locale files that are part of the bundle. This information is used to load the files implementing the bundle to the browser. Once again at this point the files are expected to work with global context (mainly referencing Oskari.clazz.define()).
The bundle descriptor system is something that could be improved in the future. The bundle definition could be described as JSON that is interpreted on the loader instead of using a class with the bundle register through a global Oskari.
Starting the bundle
After all the imports have been handled the loader tries to find a registered bundle for the bundle id that is marked with bundlename
. If the installBundleClass call has been made, the loader fetches the bundle descriptor instance of the referenced bundle. The loader then calls a create()
function on the descriptor class. The create call should return an instance of the main access point of that bundle.
Bundle instance configuration
The loader tries to find a block named after the bundle on the configuration given in the start. If bundleinstancename is given, the configuration is located using it. If the block is present, anything under the block will be made available as properties in the bundle instances main access point. Usually the configuration has keys named conf and state, but it could have also others or nothing at all. Conf is used to communicate settings that are not changed during runtime like the map projection or zoom levels. State is used for settings that can change on runtime like the current location or zoom level that the map is in.
After the configuration has been injected the loader hands over the execution to the bundle instance by calling its start()
function. The start() function works as the main entrypoint and the bundle handles what happens on/after that call.
Repeat
After this the loader moves to the next block in startup sequence and after the whole sequence has been processed it calls the optional "done"-callback that was given to the initial processSequence() call.
Manually starting bundles
You can also manually start bundles by calling:
Oskari.app.playBundle({
"bundlename" : "coordinatetool",
"metadata" : {
"Import-Bundle": {
"coordinatetool": {
"bundlePath": "/Oskari/packages/framework/bundle/"
}
}
}
});
This loads the bundle implementation to the browser as it would have been in the startup sequence and starts it.
If you need to give the bundle a config and know when it has been started you can give the config as second parameter and a callback function as third:
Oskari.app.playBundle({
"bundlename" : "coordinatetool",
"metadata" : {
"Import-Bundle": {
"coordinatetool": {
"bundlePath": "/Oskari/packages/framework/bundle/"
}
}
}
}, {
conf : {
isReverseGeocode : true,
reverseGeocodingIds : "WHAT3WORDS_CHANNEL"
}
},
function() {
console.log('Bundle started');
});
You can also give the callback function as the second parameter to playBundle() function.
7.1.8 How to use Oskari classes
Most code in Oskari is defined as classes. Defining a class is a simple call to Oskari.clazz.define()
method:
Oskari.clazz.define('Oskari.mynamespace.MyClass',
function(pState) {
this._state = this.states[pState || 'open'];
}, {
/**
* @property states
*/
states : {
'open' : 1,
'close' : 2
},
getState : function() {
return this._state;
}
},
{
// a list of protocols this class implements
"protocol" : []
});
The define method takes parameters that it uses to produce a class prototype:
/**
* Defines a class implementation in Oskari framework
*
* @method define
* @param {String} name fully qualified class name
* @param {Function} contructor the method that will be called on create that should initialize any class properties.
* It can also have parameters that can be passed when creating an instance of the class
* @param {Object} implementation JavaScript object defining the methods and 'static' properties of the class
* @param {Object} metadata (optional) class metadata which can for example declare which protocols/interfaces the class implements
*/
Once a class has been defined, it can be created by calling Oskari.clazz.create()
method and it's methods can be called:
var openState = Oskari.clazz.create('Oskari.mynamespace.MyClass'),
closeState = Oskari.clazz.create('Oskari.mynamespace.MyClass', 'close');
alert('MyClass open state is ' + openState.getState());
alert('MyClass close state is ' + closeState.getState());
The create method returns an instance of the class which you specify as parameter:
/**
* Creates an instance of Oskari framework class
*
* @method create
* @param {String} name fully qualified class name
* @param {Object} params (optional 0-n) parameters that are passed to the constructor. All the parameters after the class name will be passed.
* @return {Object} instantiated class prototype
*/
Protocol
Classes can declare implementing a protocol (none to many) or classes can also be used to define a protocol. A protocol can be thought of as an interface declaration and contract for a set of functions that a class must provide/implement. For example for a class to register to Oskari sandbox it needs to implement the Oskari.mapframework.module.Module
protocol which defines that the class must have for example a getName()
and onEvent()
methods. This way we can depend that any registered component can handle operations that will be expected from registered components.
Oskari.clazz.define('Oskari.mynamespace.MyProtocol', function() {}, {
"myProtocolMethod": function(arg) {}
});
Defining a protocol class is not really used for anything other than explaining what methods an implementation should offer. Oskari class system provides functions to query classes that implement a protocol:
var classesImplementingProtocol = Oskari.clazz.protocol("Oskari.mynamespace.MyProtocol");
Category
Oskari Clazz system supports splitting Class implementations in multiple compilation units (files) by a Category concept. This means you can add functions to an existing class to "extend" it. When loading asynchronously a stub class is defined if class definition is not yet loaded.
Oskari.clazz.category('Oskari.mynamespace.MyClass', 'my-set-of-methods', {
"myOtherMethod" : function() {
return "one";
},
"myAnotherMethod" : function() {
return "two";
}
});
When the category()
method call has completed, your class will have the new methods declared in the category call and they can be called as any original method in the class prototype:
var openState = Oskari.clazz.create('Oskari.mynamespace.MyClass');
openState.myOtherMethod();
The category method takes parameters that it uses to extend a class prototype:
/**
* Appends a class implementation in Oskari framework
*
* @method category
* @param {String} name fully qualified class name
* @param {String} categoryName identifier for the set of methods.
* @param {Object} implementation JavaScript object defining new methods and 'static' properties for the class
*/
7.1.9 How to use user authentication
Users in Oskari are described with a few attributes listed below:
The datasource for users can be configured to read and manage users using JSON, SAML etc, but default to the core database for Oskari. The Java-interface for managing users is fi.nls.oskari.service.UserService
under the service-base Maven module with fi.nls.oskari.user.DatabaseUserService
under service-users Maven module as the reference implementation.
Permissions for resources are mapped using roles and user-specific content uses the UUID to link the content with the user.
TODO: description/use cases for UserService implementations.
User registration
Oskari includes a simple user registration functionality that needs to be activated by configuration (see details below). This results in a "register" link being shown under the login form on the default geoportal-page and also enables the registration backend to accept requests.
The registration pages can be customized like any other JSP-file in Oskari and the emails sent by Oskari can also be customized.
Registration form
The first step of registration is just asking for the users email. This will always respond by saying an email has been sent to the address. The only error it will show if the email is not valid or if the server can detect that the mail was not sent.
The mail the user receives has two options:
- if the email is already registered to an existing user - a message with password reminder link is sent.
- if the email is new the user receives a message with a link that can be used to access a form for user details.
Completing the user profile
Once the user clicks on the email link the token included in the link is validated. If the link token has expired or is unknown the first step of registration is shown with a message about expired link. The user can start the registration from the beginning.
On most cases the user is presented with a form to complete the registration:
As usernames are used to link the user password to user details it needs to be unique. This is checked while the user is filling the form. The password strength requirements are based on the server configuration. They can be customized by configuration.
After the user has completed the profile the user can log in to the geoportal.
Password reset
Users can request a link for setting a new password for the account:
For logged in users the email is pre-filled. For guest users the mail the user receives has two options:
- if the email is new the user receives a message saying that there is no account for the email and a link to the registration page.
- if the email is already registered to an existing user - a link to set the password is sent:
Modify user details
Currently only the name can be changed as changing the email would require a mail to be sent to check the email and usernames are problematic since they are used to map passwords to users.
Configuration options
User registration for Oskari can be enabled by modifying oskari-ext.properties
:
allow.registration=true
oskari.email.sender=
oskari.email.host=
The password requirement can be configured with:
# min length for user password
user.passwd.length=8
# Require lower and UPPER chars
user.passwd.case=true
# Number of days that registration/passwd recover links are valid
oskari.email.link.expirytime=2
If you are running an oskari-server-extension
you need to also add the dependency for the code:
fi.nls.oskari.service
oskari-control-users
${oskari.version}
The functionality is mostly contained under oskari-server/control-users with JSPs that can be overridden
in oskari-server-extensions
using the same filename as the original. There are also localizations for the user registration that can be overridden with locale/messages-ext.properties files in the server classpath (like jetty/resources/locale/messages-ext.properties
).
To customize email-templates configure oskari-ext.properties
(add files in classpath for example under jetty/resources/templates
):
# defaults
# on registration init
oskari.email.registration.tpl=/templates/registration_email.html
# on registration init if there's already a user account with the email
oskari.email.exists.tpl=/templates/registration_email_exists.html
# on "forgot my password"
oskari.email.passwordrecovery.tpl=/templates/user_passwordreset_email.html
# on "forgot my password" when there's no user account associated with the email
oskari.email.passwordrecovery.noaccount.tpl=/templates/user_passwordreset_email_new_user.html
# you can specify localized versions by adding the language code at the end of the property key
oskari.email.registration.tpl.fi=/templates/registration_email_finnish_version.html
The default templates are stored in control-users/src/main/resources/fi/nls/oskari/control/users/service
.
The templates receive variables for:
- URL to continue the process (
link_to_continue
) - number of days before the token expires (
days_to_expire
)
Tokens with expiration dates are tracked in the oskari_users_pending
database table.
If an email has an existing token it will be replaced with a new one with
new expiration date so one email can only have one token at a time. This means
that for example registration links will automatically expire if the user uses
the password reminder. Emails and usernames are checked in case-insensitive fashion.
Note! User registration has been tested/implemented only for the case where users are in the Oskari database. Not for SAML logins etc.
User removal
User content (myplaces, saved views, embedded maps, userlayers, indicators) is removed from the database with the user. The content removal is done programmatically by searching for instances of UserContentServices with @Oskari annotation. You can search the oskari-server codebase for examples of this if you need to add additional cleanup for user removal.
Note! Removing a user from the database directly will not remove all content related to the user!
Known issues
If you have problems having the mail sent first try to send a mail fron the server with telnet:
telnet [mail server] [port]
MAIL FROM: noreply@mysite.org
RCPT TO: someuser@somedomain.com
From terminal it might look like this:
[user@server ~]$ telnet [mail server] 25
Trying [mail server]...
Connected to [mail server].
Escape character is '^]'.
220 [mail server] ESMTP Postfix (Ubuntu)
MAIL FROM: noreply@mysite.org
250 2.1.0 Ok
RCPT TO: someuser@somedomain.com
454 4.7.1 : Relay access denied
In this case the mail server doesn't allow mails to be sent to the recipient and there's nothing you can do but ask the hosting party to loosen the restrictions. The good news is that the connection to the server was successful so there's no firewall blocking the connection.
If everything is good to go with the mail server and you still have problems sending mails you should check the Oskari logs for more information about the issue.
On one instance adding a javax.mail implementation manually was required, but it should be included in the Jetty-bundle. If this is the case you can add the jar-file to jetty/lib/ext folder (http://mvnrepository.com/artifact/com.sun.mail/javax.mail/1.5.4).
7.2 Other instructions
This operating instructions section contains this and that.
7.2.1 How to contribute to Oskari project
Code acquired from external sources will be subject to adaptation (if deemed necessary), normal feature testing and bugfixes before it can be merged into the develop branch.
7.3 Creating releases
For creating a branch for version x.y.z
git checkout develop
git pull
git checkout -b release/x.y.z
## TODO: Bump version at this point on the *release* branch (see below for instructions)
## Get the version commit to develop
git checkout develop
git merge --no-ff release/x.y.z
## TODO: Bump next development version on the *develop* branch (see below for instructions)
## Checkout to the release branch
git checkout release/x.y.z
Merging pull requests to release
git pull https://github.com/zakarfin/oskari-frontend.git some-bugfix
## git cherry-pick from develop etc
git push origin release/x.y.z
Merging changes back to master
git pull
git checkout master
git pull
git merge --no-ff release/x.y.z
git tag -a x.y.z -m "Release x.y.z"
git push origin master
git push origin --tags
Merging changes back to develop
git checkout develop
git pull
git merge --no-ff release/x.y.z
## possible merging of conflicts
git push origin develop
Cleanup
## remove local branch
git branch -D release/x.y.z
## remove remote branch
git push origin :release/x.y.z
7.3.1 Versioning the code
- Releases should move the minor version 1.0.0 -> 1.1.0
- Hotfixes should move the patch version so 1.0.0 -> 1.0.1
Server
## Checkout to branch that should have the version updated
git checkout {branch}
## Run the maven versions plugin to update version
mvn -N versions:set -DnewVersion=x.y.z
## Commit the changes to Git
git add .
git commit -m 'Bump version'
git push
Develop branch version should always be the next version + "-SNAPSHOT". For an example if the version in master is "1.0.0", develop should be "1.1.0-SNAPSHOT".
Frontend
## checkout to branch that should have the version updated
git checkout {branch}
## Edit the version number on package.json
nano package.json
## Commit the changes to Git
git add .
git commit -m 'Bump version'
git push
Creating hotfixes
Much like creating releases except hotfixes are based on the master version (releases are based on develop).
For creating a branch for version x.y.z
git checkout master
git pull
git checkout -b hotfix/x.y.z
Merging pull requests to hotfix
## TODO: Bump version at this point (see below for instructions)
git pull https://github.com/zakarfin/oskari-frontend.git hotfix/my-urgent-fix
## git cherry-pick from develop etc
git push origin hotfix/x.y.z
Merging changes back to master
git pull
git checkout master
git pull
git merge --no-ff hotfix/x.y.z
git tag -a x.y.z -m "Hotfix x.y.z"
git push origin master
git push origin --tags
Merging changes back to develop
git checkout develop
git pull
git merge --no-ff hotfix/x.y.z
## possible merging of conflicts
git push origin develop
Cleanup
## remove local branch
git branch -D hotfix/x.y.z
## remove remote branch
git push origin :hotfix/x.y.z