8 Developing instructions
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:
- Download sources here
- Extract the files from the downloaded archive.
8.1 Development Guidelines
8.1.1 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 Rocket.chat, Gitter and Oskari-userlist
8.1.2 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 for 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.
8.1.3 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.
8.1.4 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 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.
8.1.5 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!
8.1.6 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('');
jQuery('');
jQuery('#searchfield').val();
RIGHT:
jQuery('');
jQuery('');
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:
$('');
RIGHT:
var btn = jQuery('button.myButton')
btn.bind('click', function() {
myinstance.someMethod();
});
8.1.7 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 removingoskari-hidden
class.
8.1.8 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';
}
8.1.9 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
8.2 How to create a bundle
If you haven't done it already, download sources here. Then extract the files from the downloaded archive.
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 /packages/framework/
and /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. It is encouraged to create your own namespace (and directories) for your own bundles.
Create a bundle.js
file under /packages/framework/<bundle-identifier>/
. You can use the following sample as a template.
8.2.1 The Sample bundle
A file named bundle.js
under /packages/<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.
/**
* Definition for bundle. See source for details.
*
* @class Oskari...MyBundle
*/
Oskari.clazz.define("Oskari.{{mynamespace}}.{{bundle-identifier}}.MyBundle",
/**
* Called automatically on construction. At this stage bundle sources
have been
* loaded, if bundle is loaded dynamically.
*
* @contructor
* @static
*/
function() {
}, {
/*
* called when a bundle instance will be created
*
* @method create
*/
"create" : function() {
return Oskari.clazz.create("Oskari.{{mynamespace}}.{{bundle-identifier}}.MyBundleInstance");
},
/**
* Called by Bundle Manager to provide state information to
*
* @method update
* bundle
*/
"update" : function(manager, bundle, bi, info) {
}
},
/**
* metadata
*/
{
"protocol" : ["Oskari.bundle.Bundle"],
"source" : {
"scripts" : [{
"type" : "text/javascript",
"src" : "../../../bundles/{{mynamespace}}/{{bundle-identifier}}/instance.js"
}],
"locales" : [{
"lang" : "fi",
"type" : "text/javascript",
"src" : "../../../bundles/{{mynamespace}}/{{bundle-identifier}}/resources/locale/fi.js"
}, {
"lang" : "sv",
"type" : "text/javascript",
"src" : "../../../bundles/{{mynamespace}}/{{bundle-identifier}}/resources/locale/sv.js"
}, {
"lang" : "en",
"type" : "text/javascript",
"src" : "../../../bundles/{{mynamespace}}/{{bundle-identifier}}/resources/locale/en.js"
}]
}
});
// Install this bundle by instantating the Bundle Class
Oskari.bundle_manager.installBundleClass("{{bundle-identifier}}", "Oskari.{{mynamespace}}.{{bundle-identifier}}.MyBundle");
8.2.2 Editing the sample bundle
- Change all the
<bundle-identifier>
s - Change the bundle name (MyBundle) to something more describing.
- Change the bundle instance name (MyBundleInstance). Usually its the bundle name postfixed with "Instance".
- List all the implementation files and css files under
scripts
. Note that CSS files need to have"type" : "text/css"
- List all the localization files under
locales
or you can remove the locales property if your bundle does not include localization.
Create a instance.js
file under /bundles/framework/<bundle-identifier>/
. instance.js
is a file you referenced in bundle.js
scripts. You can use the instance.js
file from oskari-frontend/bundles/sample/myfirstbundle/
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
.
Next,
- Change all the
<bundle-identifier>
s - Change the bundle instance name (MyBundleInstance). Use the same one you used before.
Add your bundle to the applications startup sequence like you did in previous steps (change the <bundle-identifier>
):
{
"bundlename" : "{{bundle-identifier}}",
"metadata" : {
"Import-Bundle" : {
"{{bundle-identifier}}" : {
"bundlePath" : "../../../packages/framework/"
}
}
}
}
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. You'll need to add references to these additional files in bundle.js
scripts array.
See also 7.1.6 How to use a bundle.
8.3 How to configure a bundle
When Bundles are created Oskari Loader sets configuration properties on them if available. Configurations are matched using the <bundle-identifier>
and any properties defined are set as properties on the instances. Like the example below, mapfull
instance will have conf
and state
properties available when it fires up:
{
"mapfull": {
"state": {
"selectedLayers": [{
"id": "base_35"
}],
"zoom": 1,
"north": "6874042",
"east": "517620"
},
"conf": {
"globalMapAjaxUrl": "/ajax?",
"plugins": [
{
"id": "Oskari.mapframework.bundle.mapmodule.plugin.LayersPlugin"
},
{
"id": "Oskari.mapframework.mapmodule.WmsLayerPlugin"
},
{
"id": "Oskari.mapframework.mapmodule.ControlsPlugin"
},
{
"id": "Oskari.mapframework.bundle.mapmodule.plugin.ScaleBarPlugin"
},
{
"id": "Oskari.mapframework.bundle.mapmodule.plugin.Portti2Zoombar"
}
],
"layers": [
"...layers as JSON objects..."
],
"imageLocation": "/Oskari/resources"
}
}
}
The conf
and state
properties are used throughout Oskari bundles to setup the application. Their contents can be anything that the bundle requires. The conf property should be used to relay information about the runtime environment and state is used to set the bundles initial values for things that are likely to change at runtime.
8.4 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
- reference library files in your
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
- create a new
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");
})();
8.5 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.
8.5.1 Requirements
- JDK 8
- Maven 3+ (developed using 3.5.0)
- Jetty bundle installed
Template maven project
Generate a copy for an Oskari based server webapp from our template.
Start modifying the content
- Edit the
pom.xml
s to change the groupId/artifactId and oskari.version. - Edit the
pom.xml
s 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:
oskari_org
Oskari.org release repository
https://oskari.org/nexus/content/repositories/releases/
oskari_org_snapshot
Oskari.org snapshot repository
https://oskari.org/nexus/content/repositories/snapshots/
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
- National Geoportal of Finland: https://github.com/nls-oskari/kartta.paikkatietoikkuna.fi
- Arctic-SDI geoportal: https://github.com/arctic-sdi/oskari-server-extensions