wiki:AnotherPluginDevelopmentGuide

Version 1 (modified by dgf, 13 years ago) (diff)

draft

+++ this guide relies on the upstream revise-plugin-js branch version 4.0.12-SNAPSHOT +++

A DeepaMehta plugin is an OSGi bundle. The user can install a plugin by means of the OSGi console.

DeepaMehta provides a framework for the plugin developer which has:

  • Access to the DeepaMehta Core service.
  • OSGi handling.
  • Migration management.

What a plugin can do

  • Provide new topic types or extending extisting ones.
  • Provide and migrate topic instances and associations.
  • Provide application logic.
    • React upon server events.
    • Provide REST resources.
  • Extending the browser client.
    • React upon UI events.
    • Add page and field renderer.
    • Add another map renderer.
    • Extend the REST client.

Creating a plugin project

To start from scratch use the Maven example archetype

TBD create an archetype that supports a mvn archetype ... call

How it looks like

The structure within your plugin project directory is determined by a combination of Maven and DeepaMehta conventions. All possible plugin constituents are optional (besides the POM). Obviously an useful plugin will provide some (or all) constituents. Which one these are depends on the nature of your plugin. Every snippet on this page can be found in the example reference project [dm4-example https://github.com/dgf/dm4-example].

dm4-example/ An example plugin project directory.
. pom.xml The POM of your plugin project (mandatory).
. README.md A project description.
. src/
. . main/
. . . java/ Server-side Java code goes into src/main/java.
. . . . de/ de/deepamehta/plugins/example is the root package of your Java classes.
. . . . . deepamehta/ The package name is under your choice.
. . . . . . plugins/
. . . . . . . example/
. . . . . . . . migrations/ Migrations go into this directory and have a fixed name.
. . . . . . . . . Migration2.java This programmatic migration 2 is called after the declarative 1.
. . . . . . . . provider/ REST specific provider that supports the conversion between a Java type and a stream.
. . . . . . . . . ExampleProvider.java
. . . . . . . . ExamplePlugin.java The plugin's "main" class.
. . resources/ Additional (non-Java) files.
. . . plugin.properties Plugin configuration properties.
. . . migrations/ Declarative migrations are stored here.
. . . . migration1.json The first migration of this plugin project.
. . . web/ Content is web-accessible.
. . . . images/ Images and other resources can be stored in folders
. . . . . bucket.png of your choice, all of them are accessible via HTTP.
. . . . script/ Client-side JavaScipt goes here.
. . . . . plugin.js The JavaScipt? plugin "main" file.
. . . . style/ Another custom folder to separate the styles.
. . . . . screen.css

Setting up the development environment

If you want to see the plugin in action, you need to build and deploy it. You can use a released binary DeepaMehta distribution and deploy the plugin over the OSGi shell like any other plugin.

Building the plugin

Inside the plugin project directory:

$ mvn package

This builds the jar file, an OSGi bundle. The jar is placed in the directory target (next to src).

dm4-plugin-example/ Plugin project directory.
src/
target/ Contains artifacts build by Maven.
. dm4-plugin-example-0.0.1-SNAPSHOT.jar The jar ready for deployment in OSGi console.

Deploying the plugin

TBD describe

Hotdeploy the plugin bundle

In an ongoing development it make sense to use the hotdeploy setup, that automatically monitors all configured bundle archives. To start using it, create a directory where your development should take place and build DeepaMehta from source. Then adapt the 'felix.fileinstall.dir' configuration of the DeepaMehta global POM run profile to your needs:

<!-- ... -->
    <profiles>
        <profile>
            <id>run</id>
                <felix.fileinstall.dir>
                    <![CDATA[
                        ${project.basedir}/modules/dm4-core/target,
                        <!-- add your plugin target location here, like the following -->
                        ${project.basedir}/../dm4-example/target
                    ]]>
                </felix.fileinstall.dir>
<!-- ... -->

Start DeepaMehta with the Maven run profile and go on with your development

$ mvn pax:run

pom.xml

A lot of configuration is done already by the parent POM. However, you must supply some settings individual to your plugin project. These comprise of:

  • Human-readable Project name.
  • Project identification in the Maven space (group ID, artifact ID, and version number).
  • Instructions for the OSGi bundle packager.

Plugin POM example:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <name>Example Plugin</name>          <!-- Human-readable project name.               -->
    <groupId>de.deepamehta</groupId>     <!-- Identify your project in the Maven space.  -->
    <artifactId>dm4-example</artifactId> <!-- Choose a reasonable group ID, artifact ID, -->
    <version>0.0.1-SNAPSHOT</version>    <!-- and version number.                        -->
    <packaging>bundle</packaging>        <!-- The packaging type must be "bundle".       -->

    <parent>
        <groupId>de.deepamehta</groupId>           <!-- Relates to the parent POM.       -->
        <artifactId>deepamehta-parent</artifactId> <!-- Copy this declaration as is.     -->
        <version>2</version>
    </parent>

    <dependencies>                                   <!-- Most DeepaMehta plugin projects           -->
        <dependency>                                 <!-- depend on the DeepaMehta core module.     -->
            <groupId>de.deepamehta</groupId>         <!-- Copy this declaration as is.              -->
            <artifactId>deepamehta-core</artifactId> <!-- Just update the version number, if a      -->
            <version>4.0.12-SNAPSHOT</version>       <!-- newer version of DeepaMehta is available. -->
        </dependency>                                <!-- If your plugin has no Java code at all,   -->
    </dependencies>                                  <!-- the dependencies element is not needed.   -->

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <configuration>
                    <instructions>
                        <Bundle-Activator>   <!-- fully qualified name of your plugin "main" class. -->
                            de.deepamehta.plugins.example.ExamplePlugin
                        </Bundle-Activator>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Migrations

There are two ways to create and migrate the model of your plugin. An initially setup can be described in declarative style. To update exiting data or change the model in the lifetime of your plugin project use the programmatic way and implement a Java migration. An order of execution is determined with the increasing file name number suffix. To configure the required migration number use the requiredPluginMigrationNr property, see plugin.properties.

Declarative JSON migrations

A declarative migration uses the REST DataFormat and can create types, topics and associations. Just write the declarations in the corresponding property of your migration.

Declarative plugin migration example:

{
    topic_types: [ # -------------------------------- create types
        {
            value: "Example Name",
            uri: "dm4.example.name",
            data_type_uri: "dm4.core.text",
            index_mode_uris: ["dm4.core.fulltext"],
        }
    ],
    assoc_types: [ # -------------------- create association types
        {
            value: "Example Association",
            uri: "dm4.example.association",
            data_type_uri: "dm4.core.text"
        }
    ],
    topics: [ # --------------------------- create topic instances
        {
            value: "An example",
            type_uri: "dm4.example.name",
            uri: "dm4.example.topic.name"
        }
    ],
    associations: [ # --------------------------- associate topics
        {
            type_uri: "dm4.example.association",
            role_1: {
                topic_uri: "de.deepamehta.dm4-example",
                role_type_uri: "dm4.core.default"
            },
            role_2: {
                topic_uri: "dm4.example.topic.name",
                role_type_uri: "dm4.core.default"
            }
        }
    ]
}

Programmatic Java migrations

Real migrations of existing data can be written in Java, you have fully access to the DeepaMehtaService?.

Programmatic plugin migration example:

package de.deepamehta.plugins.example.migrations;

import de.deepamehta.core.model.TopicModel;
import de.deepamehta.core.service.Migration;

/**
 * update search icon back to the good old bucket
 */
public class Migration2 extends Migration {

    @Override
    public void run() {
        // get web client icon configuration of the search topic
        TopicModel iconConfig = dms.getTopicType("dm4.webclient.search", null)//
                .getViewConfig().getConfigTopic("dm4.webclient.view_config")//
                .getCompositeValue().getTopic("dm4.webclient.icon");

        // update icon value
        iconConfig.setSimpleValue("/de.deepamehta.dm4-example/images/bucket.png");
        dms.updateTopic(iconConfig, null);
    }

}

Properties

DeepaMehta tries to find out most of the plugin behaviors by convention, nevertheless some of configurations have to be done and these are written down in the plugin.properties file.

plugin.properties example:

# Models used by the plugin.
importModels = de.deepamehta.webclient

# Name of the plugin’s Java main package
pluginPackage= de.deepamehta.plugins.example

# Name of the service interface that the plugin exports and provides.
providedServiceInterface = de.deepamehta.plugins.example.ExampleService

# The number of the migration the plugin requires to run.
requiredPluginMigrationNr = 4

JavaScript

The DeepaMehta web client is written in JavaScript itself and provides a UI framework.

In the global name space you can access the following:

  • dm4c the entire DeepaMehta 4 web client core
  • dm4c.restc connected REST client
  • dm4c.render DeepaMehta-specific rendering functions
  • js generic JavaScript utilities (DeepaMehta independent)
  • $ jQuery with a custom UI build

Plugin

Enhancing the web client with JavaScript based implementations is supported by the unique plugin.js file.

A plugin that loads some type specific renderer and adds a custom command:

dm4c.add_plugin('de.deepamehta.example', function() {

    dm4c.load_page_renderer('/de.deepamehta.dm4-example/script/example_content_page_renderer.js')
    dm4c.load_field_renderer('/de.deepamehta.dm4-example/script/example_content_field_renderer.js')
    dm4c.load_stylesheet('/de.deepamehta.dm4-example/style/screen.css')

    function increaseExample() {
        var id = dm4c.selected_object.id,
            url = '/example/increase/' + id,
            example = dm4c.restc.request('GET', url)
        dm4c.do_select_topic(id)
    }

    var commands = [{
        context: ['context-menu', 'detail-panel-show'],
        label: 'Hit me!', handler: increaseExample
    }]

    dm4c.register_listener('topic_commands', function (topic) {
        return topic.type_uri === 'dm4.example.type' ? commands : []
    })
})

Type specific renderer can be assigned declarative or programmatically in a migration and manually in the web client.

Declarative renderer assignment:

{
    value: "Example Content",
    uri: "dm4.example.content",
    data_type_uri: "dm4.core.text",
    index_mode_uris: ["dm4.core.fulltext"],
    view_config_topics: [
        {
            type_uri: "dm4.webclient.view_config",
            composite: {
                dm4.webclient.js_field_renderer_class: "ExampleContentFieldRenderer",
                dm4.webclient.js_page_renderer_class: "ExampleContentPageRenderer"
            }
        }
    ]
}

FieldRenderer

A simple possibility to enhance the UI is the field renderer. The generic topic renderer creates a fresh object for each composite part, so please use the prototype approach.

// a simple field renderer that renders an example topic name with an additional CSS class
function ExampleContentFieldRenderer(field_model) {
    this.field_model = field_model
}

ExampleContentFieldRenderer.prototype.render_field = function(parent_element) {
    dm4c.render.field_label(this.field_model, parent_element)
    parent_element.append($("<span>").addClass('example').text(this.field_model.value))
}

ExampleContentFieldRenderer.prototype.render_form_element = function(parent_element) {
    var $content = dm4c.render.input(this.field_model)
    parent_element.append($content)
    return function() {
        return $.trim($content.val())
    }
}

}}}

PageRenderer

The page renderer can replace the whole page content.

// a page render that simply renders the value of an example content topic instance
function ExampleContentPageRenderer() {

    this.render_page = function(topic) {
        dm4c.render.field_label('Content')
        dm4c.render.page(topic.value)
    }

    this.render_form = function(topic) {
        var $content = dm4c.render.input(topic.value)
        dm4c.render.field_label('Content')
        dm4c.render.page($content)
        return function () {
            topic.value = $.trim($content.val())
            dm4c.do_update_topic(topic)
            dm4c.page_panel.refresh()
        }
    }
}

TopicMapRenderer

TBD describe additional topic map handling

Java

The server side environment is based on OSGi and Java, a detailed explanation can be found in the ArchitectureOverview.

TBD use some other JVM languages like Scala and Clojure

Plugin

A Java plugin can interact with predefined hook overwrites and provide some REST resources.

TBD rewrite hook calls in favour of listener interface per event

package de.deepamehta.plugins.example;

import java.util.logging.Logger;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;

import de.deepamehta.core.model.TopicModel;
import de.deepamehta.core.service.ClientState;
import de.deepamehta.core.service.Plugin;

@Path("/example")
@Produces("application/json")
public class ExamplePlugin extends Plugin implements ExampleService {

    private Logger log = Logger.getLogger(getClass().getName());

    @Override
    public void preCreateHook(TopicModel model, ClientState clientState) {
        log.info("init example count of " + model.getId());
        try {
            model.getCompositeValue().put("dm4.example.count", 0);
        } catch (Exception e) {
            log.warning("something went wrong");
        }
    }

    @GET
    @Path("/increase/{id}")
    @Override
    public Example increase(@PathParam("id") long id) {
        log.info("increase " + id);
        try {
            return new Example(id, dms).increase();
        } catch (Exception e) {
            throw new WebApplicationException(new RuntimeException("something went wrong", e));
        }
    }

}

Provider

To provide an automatically serialisation of your Java domain model classes, your can implement a specific provider or use the JSONEnabled interface.

TBD use of the JSONEnabled interface

package de.deepamehta.plugins.example.provider;

import javax.ws.rs.ext.Provider;

import org.codehaus.jettison.json.JSONObject;

import de.deepamehta.plugins.example.AbstractProvider;
import de.deepamehta.plugins.example.Example;

@Provider
public class ExampleProvider extends AbstractProvider<Example> {

    public ExampleProvider() {
        super(Example.class);
    }

    @Override
    protected JSONObject getJsonData(Example example) {
        return example.toJSON();
    }
}

Attachments