Documentation

7 Developing instructions

7.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 issue trackers. The issue tracking for Oskari has been centralized to the oskari-documentation repository as we have different repositories for the server and frontend for example and it's not always easy to know which repository the issue relates to when submitting one.

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-frontend and/or oskariorg/oskari-server repositories, and check out the source code from your forks to your local computer when participating in the development for the Oskari-repositories. If you are not participating in developing Oskari itself, you can just use the oskariorg/sample-application and oskariorg/sample-server-extension template repositories to start customizing your own Oskari-based application.

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 and CSS 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 download package with Tomcat 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) 17
  • Maven (3.6.3)

You will need to install those tools in the manner relevant to your development platform, principally Linux, Mac OS X or Windows with WSL.

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 17, 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 it is configured to use, in reponse to the command line command

mvn --version

JavaScript and HTML development tools

The Oskari frontend consists of JavaScript code, CSS with resources, as well as internationalization and localization 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 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 - Tomcat

The Oskari server template application builds into a WAR file executed inside a servlet container.

A good servlet container for development and production is Tomcat 10.1, but the application can just as well be run on other servers like Jetty as well.

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 lib folder under {tomcat.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.

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 with Flyway migrations 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.2 Git process

This document describes the source management process used by the Oskari project. As internal development at NLS uses a ​rather ​well ​documented ​branching ​model called ​Git Flow, this document emphasizes interfacing with external developers/teams instead of reiterating ​Git Flow documentation readily available on the Web.

7.2.1 Oskari Git Flow overview

gitflow.svg

Read Atlassian's awesome Git Flow documentation for more information.

7.2.2 Branches & Repos

master

  • The latest stable release.
  • Tagged according to Oskari's versioning scheme.
  • Commits merged from hotfix and release branches.

develop

  • The work-in-progress next release.
  • Commits merged from pull requests.

release/x.y.z

  • A release branch is opened as a code freeze some time before the next release based on the current develop-branch.
  • Release branch should only include bugfixes to the features that will be in the next release.
  • Any new features should be merged to develop-branch for a release after the next one.
  • Once the release testing has been completed the branch will be merged into develop and master.

hotfix/x.y.z

  • When urgent bugfixes are required for the latest release a hotfix-branch is opened based on the current master-branch.
  • Once the fixes are completed the branch will be merged into develop and master.

{any-other-branches}

  • Sometimes opened for developing larger change sets that might require multiple pull requests of development before they can be merged to develop without breaking things unintentionally.
  • Once the feature is completed the branch will be merged into develop
  • Most functionality development should be done in forks, not the official repositories.
  • See how to contribute for Git instructions

External branches

The branching model utilized by an external team is generally irrelevant to the process of handling external contributions. It is, however, assumed for the purposes of this document that there exists a continuous sequence of commits from the latest merge from Oskari branches on GitHub to the commit(s) presented for consideration for inclusion in Oskari proper. This is required in order to be able to rebase such commits on the develop branch.

7.3 Developing Oskari

This section contains information on how to develop Oskari.

In this context developing refers to creating new functionalities or configuring the existing ones. Developing can also mean configuring the back-end (server) of Oskari.

  • The configurations of the front-end include creating and editing the functionalities, which is basically creation or modification of bundles. The configuring of the front-end requires proficiency in JavaScript. A sample bundle is provided for the developers to use as a basis for developing or configuring new bundles.
  • Making configurations to the server (for example, creating a custom Oskari server extension) requires knowledge of Maven, JDK 8 and Jetty. For server configuring, too, the developers can use a template of an Oskari server webapp.

This section begins with the general guidelines and best practices, then moves into more detailed instructions of creating bundles, configuring them, adding 3rd party JavaScript libraries and finally creating custom server extensions.

Before you begin to develop new functionalities, see if it is already done: the unofficial Oskari bundles created by the Oskari community.

When you're ready to start developing Oskari:

  1. Git clone the sources:
  1. Create a branch for your changes
  2. Submit a pull request (usually targetting the develop branch)

7.3.1 Editors

  • VS Code is the Oskari team's editor of choice for JavaScript development
  • IntelliJ IDEA has been found useful IDE for Oskari Java development.
  • If you're developing on a Windows machine, we recommend using WSL or cmder

7.4 Development Guidelines

7.4.1 How to contribute to Oskari project

You can submit pull requests through GitHub as usual. Target the develop branch for next release and only use other branches if you are submitting an urgent fix for an upcoming release.

New contributors are usually required to fill in the CLA contributor license agreement. Trivial changes are allowed even without the CLA.

TODO: add information about CLA here.

7.4.2 General

  • There's a difference for developing generic Oskari functionality and application specific functionality.
  • Oskari repositories should not contain application specific functionalities (the community-repository can contain application specific code as examples).
  • In general smaller pull requests will be reviewed and merged faster as they usually are easier to review and test than large ones.
  • In most cases you want to use develop branch as baseline. Only use master as base if you need something urgently fixed (included in a hotfix for latest version).
  • If you are uncertain, ask for help. You can reach other Oskari developers at Gitter and Oskari-users mailing list

7.4.3 Code

  • Always remember to write/update documentation: API docs, MigrationGuide, ReleaseNotes, Changelog
  • Use english and descriptive names for variables/methods etc.
  • Format your code and use spaces instead of tabs
  • Don't make long or overly complex methods - keep it simple
  • Try to create generic functionalities that can be used by others. The application specific UI can be separated in most cases from the generic functionality.
  • Try to keep functions self-contained with clear input and output and no side-effects when possible.
  • Use existing features like PropertyUtil in oskari-server or the localization support from Oskari in the frontend instead of reinventing the wheel.
  • Try to use existing libraries when creating new features. For each new framework added to the client side code the more end-users need to download to get the application.

7.4.4 Commits

  • Configure GIT line endings setting: see GitHub's guide on line endings
  • Never commit on master - always work with the latest develop version
  • Keep commits small and use descriptive comments
  • This means you don't dump your entire feature from svn into single massive git commit.

7.4.5 Pull requests

  • Keep pull requests small/having a single feature
  • See GitHub's guide on how to write the perfect pull request
  • Be very careful when making changes to existing sources (maven modules or frontend bundles) since it's easy to break another part of an app calling the changed function.
  • Create separate pull request for changes to existing source with documentation what the change enables you to do.
  • Entirely new features/functionalities should be created as new Maven modules on oskari-server and bundles on oskari-frontend. Oskari-server uses layered naming for modules:
    • service-[functionality] as a library for the generic functionality
    • service-[functionality]-[plugin name] as a plugin part to service-[functionality] with non-generic functionality
    • control-[functionality] as a wrapper for action routes/http-layer where you parse params and format a JSON response for the result of the operation.

7.4.6 Changing the API

Be very careful when changing the API. Changing the API means that others will need to change their code as well. This is a especially problematic on the RPC API. For frontend this means request/event/conf/state/services

  • Try to think of your new addition as a library, especially the API. Keep it clean, simple and as self-contained as possible.
  • Oskari requests should have mandatory parameters as the first parameters and any optional parameters should be gathered in to an options object with describing names. - Any data in conf, state, requests or events should be serializable to JSON -> Don't send functions etc. If you need to send functions, use services instead.
  • If you have developed a new feature or changed existing one please document your work: API docs / Generated API.

For server:

  • action_route parameters and response
  • properties
  • any external dependencies

Any changes to API need to be documented always!

7.4.7 Frontend

Requirements for new bundles to the core

  • API documentation (event, requests, service, config, state)
  • Implementation for the bundle API
  • Usage instructions documentation of any external dependencies (server routes for example) for the implementation of the bundle
  • Implementation needs a responsible party. If there's a bug or something is broken the responsible party is the first contact point. If there's no action taken and no-one else wants to take responsibility the bundle implementation will be migrated to community-repository.
  • Avoid global variables
  • Avoid id and name attributes on DOM elements. It's very easy to use conflicting names/ids for elements between different functionalities and in the case of conflict the behavior in browser is propably not what you expect. If you have an input with name="name" in two elements the browser will remove the attribute from the other. This will result in errors even if you use very specific selector to read/assign the value for that field. Use data-attributes or classes instead to avoid this.

WRONG:

jQuery('<input type="text" name="name" />');
jQuery('<input type="text" id="searchfield" />');
jQuery('#searchfield').val();

RIGHT:

jQuery('<input type="text" data-name="name" />');
jQuery('<div class="search-mainpanel"><input type="text" class="searchfield" /></div>');
jQuery('.search-mainpanel .searchfield').val();

Bundles

Bundles are independent components:

  • A bundle should not hard code references to other bundles or modules
  • A bundle should not poke other bundles' internal structures

Bundles should not have hard coded references to any backend etc. outside source

  • Any such references should be given to the bundle via configuration
  • Use jQuery with jQuery(), not $()
  • Attach event handling functions to DOM with JavaScript assignments rather than HTML markup:

WRONG:

$('<button onclick="myglobal.myinstance.someMethod()"></button>');

RIGHT:

var btn = jQuery('button.myButton')
btn.bind('click', function() {
    myinstance.someMethod();
});

7.4.8 User interface

  • Use template variables for defining DOM elements in class and build UI for the bundle by cloning them
  • Retain handles (a variable or class member field) to UI elements and use them when modifying UI
  • Use CSS selectors and traversal to access DOM snippet substructure under the current functionality. Don't alter the UI created by another functionality.
  • Prefix your custom CSS definitions with your <bundle-identifier>
  • Avoid post processing of library generated DOM (f.ex ExtJS dom) with jQuery
  • Avoid visible DOM rendering. Hide element before editing and when editing is finished make element again visible. Hide element adding class oskari-hidden and make it visible again by removing oskari-hidden class.

7.4.9 Documentation

You should comment your Oskari classes in a format recognized by the API generator tool YUIDoc:

/*
 * Returns a 'foobared' string.
 *
 * @method fooBar
 * @param {String} arg
 * @return {String} returns the argument prefixed with 'foo' and postfixed with 'bar'
 */
function fooBar(arg) {
    return 'foo ' + arg + ' bar';
}

7.4.10 Server

  • JUnit tests for any action routes that test parameter/user combinations
  • documentation of external dependencies and possible configurations/properties
  • TO-DO: more requirements probably

7.5 How to create a bundle

If you haven't done it already, copy the oskari-frontend repo here.

Now decide a <bundle-identifier> which is unique and describes the functionality the bundle offers e.g. search (already implemented so prefix it with something like mysearch).

Create a folder with the name of your <bundle-identifier> under /bundles/framework/. If you require styling/images, create a folder under /bundles/framework/<bundle-identifier>/resources/css, too. The /framework/ directory isn't enforced and you can replace it with something fitting your bundle compilation. The framework directory refers to the namespace of the same name and it includes (almost) all code written by the Oskari core team.

You can create your own namespace (and folders) for your own bundles. If you plan to contribute the bundle to oskari-frontend, place the bundle either to framework, mapping or admin depending on what the functionality is/does.

Create a index.js file under bundles/framework/<bundle-identifier>/. You can use the sample file as a template (explained below).

7.5.1 The Sample bundle

A file named index.js under /bundles/<mynamespace>/<bundle-identifier>/ folder should contain this sort of content. It defines that the bundles implementation file instance.js is located under /bundles/mynamespace/<bundle-identifier>/ and localization data under that in resources/locale/<lang>.js files. At the end it installs the bundle to the Oskari framework so it can be started by the Oskari loader.

Change the <bundle-identifier> and <mynamespace> to the identifiers of your choice before actually using this sample template.

import { MyBundleInstance } from './instance';

// Register a factory function for the bundle id '<bundle-identifier>'
Oskari.bundle('<bundle-identifier>', () => new MyBundleInstance());

7.5.2 Editing the sample bundle

  • Change all the <bundle-identifier>s
  • Add the implementation for MyBundleInstance class in instance.js file (or filename to match the class)
  • Change the bundle instance name (MyBundleInstance). Usually its the bundle name postfixed with "Instance".
  • You can use import to any css/scss files on the index.js OR in the instance.js file
  • Add any localization files under resources/locale.

Create a instance.js file under /bundles/framework/<bundle-identifier>/. The instance.js is a file that is referenced as import on index.js . You can use the SampleInfoBundleInstance.js file from https://github.com/oskariorg/sample-application/blob/2.0.0/bundles/sample-info/SampleInfoBundleInstance.js as a template.

The instance.js file is usually responsible for creating all the classes a bundle might need during its lifetime, registering possible request handlers and registering the bundle to the sandbox to be able to listen to events.

By extending DefaultExtension you can forget the nitty gritty details and focus on writing the application logic instead. All the functions can be overridden should you need to do something differently (in the example below, we override the getName function as it returns the name from config by default). Refer to the API documentation to see all the functions of DefaultExtension.

Add your bundle to the applications main.js (change the path and <bundle-identifier> to match where your index.js is located):

import oskari-bundle!oskari-frontend/bundles/framework/<bundle-identifier>;

Start adding your code in instance.js. If you have lots of code it is encouraged to add multiple .js files beside the instance.js (under the bundles implementation folder structure). These files can define other Oskari classes that the instance.js creates and operates.

See also 7.1.6 How to use a bundle.

7.6 How to add third party JavaScript libraries

Usually using npm install --save to add a library and import it as ES-module is enough and the recommended approach. However sometimes you might need a more old school approach that is described below:

  • Add library files under [oskari-frontend or your repository]/libraries/<yourLibrary>/

  • If your library is just for your own bundle

    • reference library files in your bundle.js
  • If your library is for several bundles

    • create a new bundle.js under [oskari-frontend or your repository]/packages/libraries/<yourLibrary>/
    • include this new "library bundle" in the startup sequence of your bundle

Example of library bundle.js:

(function() {
    /**
     * @class Oskari.libraries.bundle.geostats.GeostatsBundle
     */
    Oskari.clazz.define("Oskari.libraries.bundle.geostats.GeostatsBundle", function() {
    }, {
        "create" : function() {
            return this;
        },
        "update" : function(manager, bundle, bi, info) {},
        "start" : function() {},
        "stop" : function() {}
    }, {
        "protocol" : ["Oskari.bundle.Bundle","Oskari.bundle.BundleInstance"],
        "source" : {
            "scripts" : [{
                "type" : "text/javascript",
                "src" : "../../../../libraries/geostats/geostats.min.js"
            },
            {
                "type" : "text/javascript",
                "src" : "../../../../libraries/geostats/jenks.util.js"
            }]
        }
    });

    // Install this bundle by instantating the Bundle Class
    Oskari.bundle_manager.installBundleClass("geostats", "Oskari.libraries.bundle.geostats.GeostatsBundle");
})();

7.7 How to create a custom Oskari Server Extension

This document describes how to use maven artifacts provided in Oskari Maven repository to build customized Oskari-server.

7.7.1 Requirements

Template maven project

Generate a copy for an Oskari based server webapp from our template.

Start modifying the content

  • Edit the pom.xmls to change the groupId/artifactId and oskari.version.
  • Edit the pom.xmls to add/change the included dependencies
  • The app-resources contain configurations what functionalities, users, map layers and other content to initialize for the Oskari-based server.
  • Edit the geoportal.jsp under webapp-map to modify the base HTML.
  • Create your own action routes like MyAction under server-extension.

The Oskari dependencies are downloaded from oskari.org Maven repository:

<repositories>
    <repository>
        <id>oskari_org</id>
        <name>Oskari.org release repository</name>
        <url>https://oskari.org/nexus/content/repositories/releases/</url>
    </repository>
    <repository>
        <id>oskari_org_snapshot</id>
        <name>Oskari.org snapshot repository</name>
        <url>https://oskari.org/nexus/content/repositories/snapshots/</url>
    </repository>
</repositories>

Build

This will build your webapp and include your code on top of the Oskari-server.

cd sample-server-extension
mvn clean install

Deploy to servers

Copy the oskari-map.war file from under sample-server-extension/webapp-map/target/ to your servers deploy folder {JETTY_HOME}/webapps replacing the old one.

Note! You will need to adjust oskari-ext.properties accordingly. For example if you only add one view, the id of that view should be the default view in oskari-ext.properties OR you can remove the default view configuration so it will use the view type "DEFAULT" on database to detect the default view.

Examples

7.7.2 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']).

Starting with Oskari 3.0 all tool panels and publisher tools provided by Oskari are written in es / react. Using jQuery for creating custom publisher tools (even though old jquery-implementations might still work) is highly discouraged.

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:

  1. UIChangeEvent is sent to notify other functionalities to shut themselves down to avoid conflicts for screen space.
  2. StateHandler.SetStateRequest is used to set the application state (based on the embedded map that is being edited).
  3. mapmodule is queried for plugins with plugin.isShouldStopForPublisher() returning true (this defaults to plugin.hasUI() returning true). All of these plugins are shutdown/stopped to clean the map state for publisher functionality.
  4. 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
  5. Creates the panels and calls tool.init(data) for all tools
  6. Any panels that have tools where tool.isDisplayed(data) returns true are shown to the end user

When the user exits the publisher:

  1. On save: calls tool.getValues() to gather a payload for saving the embedded map based on end user selections to database
  2. calls tool.stop() - this is where the tool should clean up anything that it has started when the publisher functionality was running
  3. 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/AbstractPublisherTool 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.
  • 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 AbstractPublisherTool 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.
  • getComponent() This is used by tools that have extra options to render. 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
  • setEnabled(boolean) Most tools should just use the version from AbstractPublisherTool. Override only when necessary. Even then it's probably safest to add a call to super() in your overridden function and do your custom thing only afterwards. Starts/stop the functionality on the map (like map plugin). Should send Publisher2.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.
  • isEnabled() should return true if the tool is selected
  • getValues() should return how the user selections for the tool should affect the appsetup JSON on the embedded map. This should match the logic that init() uses for detecting if the tool should be enabled when editing an embedded map.
  • 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).
  • isDisplayed() 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
  • isDisabled() 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 legends selection is shown but is disabled if layers on the map don't have legends available.

Tool, component and handler

When a tool has more options than just enabling or disabling a plugin on map we need to provide a component for rendering extra options in publisher as well as a handler to maintain the component's state and possibly handle more complex logic. Here's an example how one would go forth to add a new react tool to map tools panel.

The tool

First we would need to create the actual tool class. To keep things simple this will just control an existing plugin (index map) on the map and have some bogus extra options. The tool class needs to implement the Oskari.mapframework.publisher.Tool - protocol to be discoverable by the publisher. Also the code of the tool class needs to be imported somehow. For tools that handle mapmodule's plugins there is the file bundles/mapping/mapmodule/publisher/tools.js which imports / exports all those tools.

import { AbstractPublisherTool } from '../../../../framework/publisher2/tools/AbstractPublisherTool';
import { IndexMapToolComponent } from './IndexMapToolComponent';
import { IndexMapToolHandler } from './IndexMapToolHandler';

class IndexMapTool2 extends AbstractPublisherTool {
    constructor (...args) {
        super(...args);
        // index of the tool within the panel
        this.index = 30;
        // id of the panel this tool belongs to
        this.group = 'tools';
        this.handler = new IndexMapToolHandler(this);
    }

    init (data) {
        super.init(data);
        const config = this.state?.pluginConfig || {};
        // restore config for saved map
        this.handler.init(config);
    }

    getTool () {
        return {
            id: 'Oskari.mapframework.bundle.mapmodule.plugin.IndexMapPlugin',
            title: Oskari.getMsg('MapModule', 'publisherTools.IndexMapPlugin.toolLabel') + ' 2',
            config: this.state.pluginConfig || {}
        };
    }

    getComponent () {
        // returns the component to render extra options in publisher and the statehandler for this tool
        return {
            handler: this.handler,
            component: IndexMapToolComponent
        };
    }

    getValues () {
        if (!this.isEnabled()) {
            return null;
        }

        const pluginConfig = this.getPlugin().getConfig() || {};
        // handler's state contains the extra options stacked on top of AbstractPublisherTool's defaults
        const state = this.handler.getState();
        for (const key in state) {
            if (state.hasOwnProperty(key)) {
                pluginConfig[key] = state[key];
            }
        }

        return {
            configuration: {
                mapfull: {
                    conf: {
                        plugins: [{ id: this.getTool().id, config: pluginConfig }]
                    }
                }
            }
        };
    }
}

// Attach protocol to make this discoverable by Oskari publisher
Oskari.clazz.defineES('Oskari.publisher.IndexMapTool2',
    IndexMapTool2,
    {
        protocol: ['Oskari.mapframework.publisher.Tool']
    }
);

export { IndexMapTool2 };
The component

The component is basically any react-component containing the extraoptions (if any) shown when the tool is enabled. In this case it's just a checkbox that is either on or off.

import React from 'react';
import { Message, Checkbox } from 'oskari-ui';

export const IndexMapToolComponent = ({ state, controller }) => {
    const { myExtraChoiceForIndexMap } = state;

    return <>
        <Checkbox checked={myExtraChoiceForIndexMap} onChange={evt => controller.myExtraChoiceForIndexMapChanged(evt.target.checked)}>
            <Message bundleKey={'MapModule'} messageKey={'publisherTools.IndexMapPlugin.myExtraChoice'}/>
        </Checkbox>
    </>;
};
The handler

The handler handles the tool's and component's state and could be used to handle more complex logic as well. Often the state can be used as-is when saving and restoring the published map.

import { StateHandler, controllerMixin } from 'oskari-ui/util';

class UIHandler extends StateHandler {
    constructor (tool) {
        super();
        this.tool = tool;
        // set the extra choice off by default
        this.setState({
            myExtraChoiceForIndexMap: false
        });
    };

    init (pluginConfig) {
        // this will restore whatever the state of the extra choice was for the published map
        this.updateState({
            ...pluginConfig
        });
    }

    myExtraChoiceForIndexMapChanged (checked) {
        const newState = {
            myExtraChoiceForIndexMap: checked
        };

        this.updateState(newState);
    }
}

const wrapped = controllerMixin(UIHandler, [
    'myExtraChoiceForIndexMapChanged'
]);

export { wrapped as IndexMapToolHandler };

7.8 Adding your own action routes

Oskari backend maps XHR requests to specific handler classes based on a http parameter action_route. Adding an custom route to Oskari backend goes like this:

  1. Create a new class implementing fi.nls.oskari.control.ActionHandler
  2. Add your code handling the request to handleAction(ActionParameters params) method
  3. Annotate the class with @OskariActionRoute("myRoute")
  4. Compile with control-base module on the classpath (for example as a maven dependency):
<dependency>
    <groupId>org.oskari.service</groupId>
    <artifactId>control-base</artifactId>
</dependency>
  1. Done - deploy the app and call your action with the oskari ajaxUrl + action_route=myRoute

Hello world Sample of ActionHandler

import fi.nls.oskari.annotation.OskariActionRoute;
import fi.nls.oskari.control.ActionException;
import fi.nls.oskari.control.ActionHandler;
import fi.nls.oskari.control.ActionParameters;
import fi.nls.oskari.util.ResponseHelper;

/**
 * Responds with a hello message
 */
@OskariActionRoute("HelloUser")
public class HelloWorldHandler extends ActionHandler {
    public void handleAction(ActionParameters params) throws ActionException {
        ResponseHelper.writeResponse(params, "Hello " + params.getUser().getFirstname());
    }
}

7.9 Adding your own view modifiers for GetAppSetup action route

If the Oskari application view loaded from database needs to be modified based on request parameters, it can be done by implementing a fi.nls.oskari.control.view.modifier.param.ParamHandler. This can be used to modify startupsequence and bundle configurations. For example http parameter coords modifies the mapfull configuration so that map location is changed based on the parameter value.

  1. Create a new class implementing fi.nls.oskari.control.view.modifier.param.ParamHandler
  2. Add your code handling the parameter to handleParam(final ModifierParams params) method
  3. Annotate the class with @OskariViewModifier("myParam")
  4. Compile with control-base on the classpath (for example as a maven dependency)
  5. Done - deploy the app and get your modified view by calling the oskari ajaxUrl + action_route=GetAppSetup&myParam=myValue

Hello world Sample of ParamHandler

Modifies config for "myBundle" adding the property "echoing" with value of an http parameter name "echo" if it's present.

import fi.nls.oskari.annotation.OskariViewModifier;
import fi.nls.oskari.util.JSONHelper;
import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;
import org.json.JSONObject;

@OskariViewModifier("echo")
public class HelloParamHandler extends ParamHandler {

    public boolean handleParam(final ModifierParams params) throws ModifierException {
        final JSONObject config = getBundleConfig(params.getConfig(), "myBundle");
        JSONHelper.putValue(config, "echoing", params.getParamValue());
        return false;
    }
}

ParamHandlers are run in priority order where priority 1 is run before priority 10. By default the priority for all ParamHandlers is 500 and you can affect this by overriding the getPriority() function to return a lower or higher priority.

If a bundles config needs to be modified after loading from database, it can be done by implementing a fi.nls.oskari.control.view.modifier.bundle.BundleHandler. This is used for example for the mapfull bundle with more than few modifications when a view is loaded:

  • database only lists layer ids in the config, but the modifier replaces the id arrays with a layer object.
  • The base ajax url is replaced with one coming from the ModifierParams so it can be switched based on the users locale.
  • If the modifierParams.isLocationModified() returns true, Oskari.mapframework.bundle.mapmodule.plugin.GeoLocationPlugin is removed from the plugins.
  1. Create a new class implementing fi.nls.oskari.control.view.modifier.bundle.BundleHandler
  2. Add your code for modifying to modifyBundle(final ModifierParams params) method
  3. Annotate the class with @OskariViewModifier("mybundleid")
  4. Compile with control-base on the classpath (for example as a maven dependency)
  5. Done - deploy the app and get your modified view by calling the oskari ajaxUrl + action_route=GetAppSetup. If a bundle with id "mybundleid" was present, your modifier is being called to do its magic.

Hello world Sample of BundleHandler

Modifies config for "myBundle" adding the property "hello" with value "world" and if the user is not logged in also modifies state by adding property "hello" with value "stranger"

import fi.nls.oskari.annotation.OskariViewModifier;
import fi.nls.oskari.log.Logger;
import fi.nls.oskari.util.JSONHelper;
import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;
import org.json.JSONObject;

@OskariViewModifier("myBundle")
public class HelloWorldHandler extends BundleHandler {

    /**
     * Adds a property "hello" to bundles config with the value "world".
     * Returning false since we didn't modify map location
     */
    public boolean modifyBundle(final ModifierParams params) throws ModifierException {
        final JSONObject bundleConfig = getBundleConfig(params.getConfig());
        JSONHelper.putValue(bundleConfig, "hello", "world");
        if(params.getUser().isGuest()) {
            final JSONObject bundleState = getBundleState(params.getConfig());
            JSONHelper.putValue(bundleState, "hello", "stranger");
        }
        return false;
    }
}

If a parameter needs to be preprocessed/sanitized before calling the actual ParamHandler you can add a preprocessor. Whenever a ParamHandler is registered to Oskari a property is checked if any preprocessors should be added as well:

view.modifier.param.{param name}.prepocessors={fully qualified classname for class implementing ParamHandler}

The value of the property can have one or more preprocessors classes defined as a comma-separated list. The referenced class should be a subclass of ParamHandler. Preprocessors are run before the actual ParamHandler and in the order they are defined in the comma-separated list. Note that preprocessors are NOT annotated with @OskariViewModifier (otherwise they would be added as normal paramhandlers).

Example for preprocessor ParamHandler

If param value contains "something offensive", changes the value to "something less offensive". The actual ParamHandler will then receive the preprocessed value("something less offensive") for the parameter.

import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;

public class HelloParamHandler extends ParamHandler {

    public boolean handleParam(final ModifierParams params) throws ModifierException {
        if(params.getParamValue().indexOf("something offensive") != -1) {
            params.setParamValue("something less offensive");
            return true;
        }
        return false;
    }
}

The return values are not used for anything at the moment but a good practice is to return true if something was modified and false if the preprocessor let the parameter pass as is.

7.10 Adding your own view modifiers for GetAppSetup action route

If the Oskari application view loaded from database needs to be modified based on request parameters, it can be done by implementing a fi.nls.oskari.control.view.modifier.param.ParamHandler. This can be used to modify startupsequence and bundle configurations. For example http parameter coords modifies the mapfull configuration so that map location is changed based on the parameter value.

  1. Create a new class implementing fi.nls.oskari.control.view.modifier.param.ParamHandler
  2. Add your code handling the parameter to handleParam(final ModifierParams params) method
  3. Annotate the class with @OskariViewModifier("myParam")
  4. Compile with control-base on the classpath (for example as a maven dependency)
  5. Done - deploy the app and get your modified view by calling the oskari ajaxUrl + action_route=GetAppSetup&myParam=myValue

Hello world Sample of ParamHandler

Modifies config for "myBundle" adding the property "echoing" with value of an http parameter name "echo" if it's present.

import fi.nls.oskari.annotation.OskariViewModifier;
import fi.nls.oskari.util.JSONHelper;
import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;
import org.json.JSONObject;

@OskariViewModifier("echo")
public class HelloParamHandler extends ParamHandler {

    public boolean handleParam(final ModifierParams params) throws ModifierException {
        final JSONObject config = getBundleConfig(params.getConfig(), "myBundle");
        JSONHelper.putValue(config, "echoing", params.getParamValue());
        return false;
    }
}

ParamHandlers are run in priority order where priority 1 is run before priority 10. By default the priority for all ParamHandlers is 500 and you can affect this by overriding the getPriority() function to return a lower or higher priority.

If a bundles config needs to be modified after loading from database, it can be done by implementing a fi.nls.oskari.control.view.modifier.bundle.BundleHandler. This is used for example for the mapfull bundle with more than few modifications when a view is loaded:

  • database only lists layer ids in the config, but the modifier replaces the id arrays with a layer object.
  • The base ajax url is replaced with one coming from the ModifierParams so it can be switched based on the users locale.
  • If the modifierParams.isLocationModified() returns true, Oskari.mapframework.bundle.mapmodule.plugin.GeoLocationPlugin is removed from the plugins.
  1. Create a new class implementing fi.nls.oskari.control.view.modifier.bundle.BundleHandler
  2. Add your code for modifying to modifyBundle(final ModifierParams params) method
  3. Annotate the class with @OskariViewModifier("mybundleid")
  4. Compile with control-base on the classpath (for example as a maven dependency)
  5. Done - deploy the app and get your modified view by calling the oskari ajaxUrl + action_route=GetAppSetup. If a bundle with id "mybundleid" was present, your modifier is being called to do its magic.

Hello world Sample of BundleHandler

Modifies config for "myBundle" adding the property "hello" with value "world" and if the user is not logged in also modifies state by adding property "hello" with value "stranger"

import fi.nls.oskari.annotation.OskariViewModifier;
import fi.nls.oskari.log.Logger;
import fi.nls.oskari.util.JSONHelper;
import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;
import org.json.JSONObject;

@OskariViewModifier("myBundle")
public class HelloWorldHandler extends BundleHandler {

    /**
     * Adds a property "hello" to bundles config with the value "world".
     * Returning false since we didn't modify map location
     */
    public boolean modifyBundle(final ModifierParams params) throws ModifierException {
        final JSONObject bundleConfig = getBundleConfig(params.getConfig());
        JSONHelper.putValue(bundleConfig, "hello", "world");
        if(params.getUser().isGuest()) {
            final JSONObject bundleState = getBundleState(params.getConfig());
            JSONHelper.putValue(bundleState, "hello", "stranger");
        }
        return false;
    }
}

If a parameter needs to be preprocessed/sanitized before calling the actual ParamHandler you can add a preprocessor. Whenever a ParamHandler is registered to Oskari a property is checked if any preprocessors should be added as well:

view.modifier.param.{param name}.prepocessors={fully qualified classname for class implementing ParamHandler}

The value of the property can have one or more preprocessors classes defined as a comma-separated list. The referenced class should be a subclass of ParamHandler. Preprocessors are run before the actual ParamHandler and in the order they are defined in the comma-separated list. Note that preprocessors are NOT annotated with @OskariViewModifier (otherwise they would be added as normal paramhandlers).

Example for preprocessor ParamHandler

If param value contains "something offensive", changes the value to "something less offensive". The actual ParamHandler will then receive the preprocessed value("something less offensive") for the parameter.

import fi.nls.oskari.view.modifier.ModifierException;
import fi.nls.oskari.view.modifier.ModifierParams;

public class HelloParamHandler extends ParamHandler {

    public boolean handleParam(final ModifierParams params) throws ModifierException {
        if(params.getParamValue().indexOf("something offensive") != -1) {
            params.setParamValue("something less offensive");
            return true;
        }
        return false;
    }
}

The return values are not used for anything at the moment but a good practice is to return true if something was modified and false if the preprocessor let the parameter pass as is.

7.11 Creating releases

Assumes:

git remote add origin https://github.com/someuser/oskari-frontend.git
git remote add upstream https://github.com/oskariorg/oskari-frontend.git

You can check your repository names and URLs with git remote -v.

7.11.1 creating a branch for version `x.y.z`

git checkout develop
git pull upstream develop
git checkout -b release/x.y.z

Bump version on the release branch at this point (see below for instructions) and push it to GitHub:

git push upstream release/x.y.z

Get the version commit to develop

git checkout develop
git merge --no-ff release/x.y.z

Bump next development version on the develop branch (see below for instructions).

Merging pull requests to release branch

git checkout release/x.y.z
git pull https://github.com/someuser/oskari-frontend.git some-bugfix-branch
## git cherry-pick from develop etc
git push upstream release/x.y.z

Merging release to `master` branch

Ensure you have the latest codes for the release and master branches, then merge the release to master and tag it with the version.

git checkout release/x.y.z
git pull upstream release/x.y.z
git checkout master
git pull upstream master
git merge --no-ff release/x.y.z
# Tagging and pushing to remote
git tag -a x.y.z -m "Release x.y.z"
git push upstream master
git push upstream --tags

Merging changes back to `develop` branch from `master` branch

git checkout develop
git pull upstream develop
git merge --no-ff master
## possible merging of conflicts
git push upstream develop

Cleanup

## remove local branch
git branch -D release/x.y.z
## remove remote branch
git push upstream :release/x.y.z

7.11.2 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 upstream master
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/someuser/oskari-frontend.git hotfix/my-urgent-fix
## git cherry-pick from develop etc
git push upstream hotfix/x.y.z

Merging changes back to master

git pull upstream hotfix/x.y.z
git checkout master
git pull upstream master
git merge --no-ff hotfix/x.y.z
# Tagging and pushing to remote
git tag -a x.y.z -m "Hotfix x.y.z"
git push upstream master
git push upstream --tags

Merging changes back to develop

git checkout develop
git pull upstream develop
git merge --no-ff hotfix/x.y.z
## possible merging of conflicts
git push upstream develop

Cleanup

## remove local branch
git branch -D hotfix/x.y.z
## remove remote branch
git push upstream :hotfix/x.y.z

7.12 Review

Reviewing code is an invaluable process that is used to ensure that code meets an acceptable level of quality prior to being included in an Oskari release.

Much of the following has been adopted from the GeoServer project as the rules they have are very pragmatic.

7.12.1 Informal Code review

For Oskari developers there is no hard policy relating to code review such that every line of code needs to be reviewed, over time we have adopted the following practice:

  • if the change is to a core module and the developer does not have core commit access it requires a pull request and formal review
  • if the change is non-trivial and to a core module, it should be be handed as a pull-request
  • if the change is local to a module the developer maintains it usually does not require a review
  • community modules are a playground for experimentation - collaborate with the module maintainer as appropriate

In general a code review is always welcome and if there is a question as to whether a change should be reviewed or not, always err on the side of getting it reviewed.

7.12.2 Pull Request Review Guide

This guide provides guidance to help pull requests be reviewed in a consistent fashion (and help ensure we do not forget anything).

Before you start:

  • Double check the Guidelines policy capturing requirements for submitted code.
  • Be clear when reviewing between directives (iso speak MUST,FIX,SHALL) and suggestions or ideas for improvements (iso speak SHOULD, MAY, IDEA).
  • As a reviewer we must balance between not having enough time for thorough review, and the desire to provide quick feedback.

7.12.3 Initial Checks

The following checks can be performed quickly and represent common mistakes or oversites. These quick checks can be performed by anyone as an initial inspection.

**Note:** Providing an initial check, while better than no follow up at all, may seem petty and discouraging to the contributor. It is a good idea to state that this is only quick feedback - while either more time or an appropriate expert is found.
  • Contribution agreement on file. Check if the contribution requires a contribution agreement, and if it matches the requisites, verify that a contribution agreement has been provided to Oskari, if not, demand for one.
  • Presence of tests. Given a large code base, that pretty much nobody really can hope to master fully, the large number of external contributors, as well as focused changes to the code base, and the fast evolution of the code base, tests are really the only line of defense against accidental breakage of the contributed functionality. That is why we always demand to have at least one test, it’s not a “punishment”, but a basic guarantee that the fix or new functionality being contributed will still be there, and working, a bare few months later. So always check there is at least one test, one that would break in a evident way if there ever was a regression in the contributed code.
  • Presence of documentation. For functional, user or protocol facing changes, check that some basic documentation has been contributed. Almost nobody will know functionality is there if we don’t have documentation on how it works. As a plus, documentation will help you understand quicker what is getting contributed, both from a high level view, and as a guide though the changes being made.
  • Presence of a proposal, if required. For any large change our process demands a formal proposal to be made and discussed in the community before a pull request gets made. If the pull request is made anyways, point them to the process and warn that the community might eventually request changes in the approach
  • No commented out code sections. The version control is able to tell differences between existing and past versions of a certain file, thus, the commit should not contain commented out code sections.
  • Javadocs and comments. Public classes and methods should have a minimum of javadoc (a simple sentence explaning what the method or class does will be sufficient), difficult parts of the code should have some comments explaining what the code does (how it does it, is evident by the code). Comments should be professional (no personal opinions or jokes), current to the code (no need to explain what was there before, unless there is a high risk of it coming back), with no reference to the comment author (in case we need to know that information, a git blame will do)
  • Code formatting. The project uses the standard code formatting from IntelliJ IDEA. It’s basically the official Java coding conventions (spaces instead of tabs) with “long” lines.
  • Reformats and other changes obfuscating the actual patch. We recommend contributors to limit changes to a minimum and avoid reformatting the code, even if the existing code is not following the existing coding conventions. Reformats can be put in separate commits if necessary.

7.12.4 Extensive Review

The key point of a review is to make sure that Oskari remains stable (does not regress in behaviour) and that the codebase remains flexible and maintainable into the future.

**Note:** This review often requires some experience with the code being modified, and will require more time and focus than the initial check described in the previous section.
  • Backwards compatibility. The change being proposed should not hamper backwards compatibility, any changes to the especially to the frontend API needs to be documented in the Changelog and any major server changes need to be mentioned in the MigrationGuide.
  • Performance. The change should not introduce evident performance regressions. This is not to say that every pull request must be load tested, but some attention should be paid during the review to changes that might be damaging in those respects, looking for CPU hungry code or heavy memory allocation.
  • Leaks. A java server side application like Oskari should be able to run for months uninterrupted, thus particular attention should be paid to resource control, in particular resources that ought to to closed (files, connections, pools in general), significant allocation of classes with finalizers.
  • Thread safety. Oskari-server is, like all Java server side application, serving one request per thread. In this respect thread safety is of paramount importance. Be on the lookout for lazy initialization, stateful classes shared among threads, thread locals that fail to be properly cleaned at the end of the request, and static fields and data structures in general.
  • Good usage and fit with the existing code and architecture. The code is easier to understand and maintain when it follows common pattern across the code base, and when there is little or no code duplication. Check the pull request for conformance with the existing code, and proper usage of existing facilities.
  • Proper module usage. There is often a strong temptation to put new functionality in core as opposed to a new community module. If this is the case, verify the functionality is indeed core worthy, that is, relevant for many users, properly documented, has core developers interested in maintaining it long term, and heavily tested.
  • IP checks. When there is evidence that some of the code is coming from a different code base, check the contributor actually has the rights to donate it to Oskari, and that the original licence is compatible (or that the author owns the code, and can thus relicense it under the MIT/EUPL terms).
  • Current Java version and library usage. Check the new code uses the current version of Java (e.g. foreach, try with resources, generics, lambdas), and current library facilities (JUnit, Spring) instead of using outdated structures, rolling its own replacements or adding new dependencies. Attention should be paid to patterns that while elegant, might incur in significant overhead in performance sensitive areas of the code (e.g., arrays vs collection, inheritance and overridden methods, and other forms of abstraction above the “bare metal”).
  • Malicious code. While unlikely, a pull request might contain malicious code to create, by design or accident, openings in the security of Oskari that an external attacker might use. Attention should be paid to input checks, XML expansion attacks, reflection through serialization (which can be used to generate a remote execution attack).

7.13 Core commit access/reviewer status

The second allows a developer to make commits to the core modules of Oskari. Being granted this stage of access takes time, and is obtained only after the developer has gained the trust of the other core committers.

The process of obtaining core commit access is far less mechanical than the one to obtain community commit access. It is based solely on trust. To obtain core commit access a developer must first obtain the trust of the other core committers.

The way this is typically done is through continuous code review of patches. After a developer has submitted enough patches that have been met with a positive response, and those patches require little modifications, the developer can be nominated for core commit access.

There is no magic number of patches that make the criteria, it is based mostly on the nature of the patches, how in depth the they are, etc... Basically it boils down to the developer being able to show that they understand the code base well enough to not seriously break anything.

The reviewer status is tracked with GitHub teams:

7.14 Maplayer definitions

Oskari supports map layer types listed below. Support for more layer types can be added to the system by registering custom layer models and builders (see Extending with custom type). Maplayers are represented as JSON and parsed into Oskaris internal models by createMapLayer function in Oskari.mapframework.service.MapLayerService. The parsed layers can then be added to the system by calling addLayer, removed from the system by calling removeLayer or updated after it has been added with updateLayer. The MapLayerService provides functions to find layers added to the system etc.

When layers are added to the map the added layer domain object can be requested with sandbox.findAllSelectedMapLayers or you can check if a layer is already added to the map with sandbox.isLayerAlreadySelected(<layerId>).

7.14.1 Supported layer types

Abstract Layer

Abstract Layer implements common functionality for all layers. All layers should extend the Abstract Layer and not use the Abstract Layer directly. Layers extending Oskari.mapframework.domain.AbstractLayer needs to define this._layerType to function properly. Abstract Layer constructor parameters params and options can be passed and replaces default values (since version 1.8).

WMS Layer

WMS layer are used to display data from OGC Web Mapping Services. It consists of image tiles forming up the map. The domain class can be found here. The plugin displaying the layer is under mapmodule/plugins.

Base Layer and Group Layer

Baselayer is a a group of WMS layers that has a common parent/metadata layer with concrete WMS layers with wmsurls as sublayers. Baselayers are always added to the bottom of the layer stack on map. Grouplayer is basically the same thing as baselayer but it is treated as a normal layer when added to the stack of layers on map. Both use the same domain/mapplugin classes than WMS-layer.

WFS Layer

WFS layer is based on features described with GML as opposed to WMS image tiles. Oskari implementation of WFS layers uses serverside functionality to render image tiles of the WFS data and display them as a WMS layer. This is done so features can be rendered to the map quickly. Especially older browser (and older machines) cannot draw substantial amounts of features on the client side without a significant lag.

WFS layers can be queried and data for the features displayed in the view port can be shown on a data table. WFS features can also be highlighted by clicking them on map or by selecting it in the data table. This functionality is implemented in bundles mapwfs2 (display) and featuredata (data table).

WMTS Layer

WMTS layer allows viewing of tiles from a service that implements the OGC WMTS specification version 1.0.0.

The mapwmts bundle handles any wmts related operations from parsing layer JSON to domain object to displaying it on screen.

User generated data layers

There are currently myplaceslayer and userlayer layer types for user generated data. There are plans on merging these two to a single type that is even more flexible.

ArcGIS Layer

Layer type arcgis93layer is for ArcGis rest service. Rendering and GFI is supported. Use Map Layer Administration module to add ArcGis layers.

GFI is not supported for ArcGis Rest group layer

data format explained

7.14.2 Extending with custom type

After creating Oskari.mapframework.service.MapLayerService you can register custom layer model builders for the custom layer type. This must be done before any layers of the custom type is being parsed by MapLayerService.createMapLayer function.

The builder is registered like this:

var mapLayerService = sandbox.getService('Oskari.mapframework.service.MapLayerService'),
    layerModelBuilder = Oskari.clazz.create('<builder class definition>');

mapLayerService.registerLayerModel('<custom layer type>','<custom layer domain definition>');
mapLayerService.registerLayerModelBuilder('<custom layer type>',layerModelBuilder);

Concrete example how mapfull bundle adds support for wmts:

var layerModelBuilder = Oskari.clazz.create('Oskari.mapframework.wmts.service.WmtsLayerModelBuilder');

mapLayerService.registerLayerModel('wmtslayer','Oskari.mapframework.wmts.domain.WmtsLayer');
mapLayerService.registerLayerModelBuilder('wmtslayer', layerModelBuilder);

See Oskari.mapframework.service.MapLayerServiceModelBuilder for API documentation.

Simplest case of custom domain layer object must define:

this._layerType = "CustomLayerType";

Sample WfsLayer extending Abstract Layer

/**
 * MapLayer of type WFS
 *
 * @class Oskari.mapframework.domain.WfsLayer
 */
Oskari.clazz.define('Oskari.mapframework.domain.WfsLayer',
/**
 * @method create called automatically on construction
 * @static
 */
function() {
    /* Layer Type */
    this._layerType = "WFS";
}, {
   /* Layer type specific functions */
}, {
    "extend": ["Oskari.mapframework.domain.AbstractLayer"]
});

Layer admin UI

When registering layertypes, you can also give a list of admin fields that should be shown for admins when adding/modifying that type of layer.