wiki:AnotherPluginDevelopmentGuide

Version 10 (modified by dgf, 12 years ago) (diff)

fix topic scope

+++ 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:

What a plugin can do

  • Provide new topic types or extend extisting ones.
  • Provide and migrate topic instances and associations.
  • Provide application logic.
    • React upon server events.
    • Provide REST resources.
  • Extend 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 a 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.

Directories and files Explanation
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.
. . . . . renderer/ All client renderer implementations are separated here by directory.
. . . . . . canvas_renderers/
. . . . . . multi_renderers/
. . . . . . page_renderers/
. . . . . . simple_renderers/
. . . . . plugin.js The JavaScipt plugin "main" file.
. . . . style/ CSS stylesheets located here are picked up automatically.
. . . . . 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).

Directories and files Explanation
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 makes 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

Use of an in-memory database

Developing an topic importer or some other heavy data manipulations can stress you (on a HDD) or the lifetime of your static memory (in case of a SSD). On Linux systems it is very simple to move the DeepaMehta database into a memory file system with a mount like the following:

Copy your database into a in-memory file system:

$ mkdir offline-db
$ mv deepamehta-db/* offline-db
$ sudo mount -osize=100m tmpfs deepamehta-db -t tmpfs
$ cp -a offline-db/* deepamehta-db

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. A initially setup can be described in declarative style. To update existing 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.

TBD describe and exemplify migration*.properties file usage

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.service.Migration;

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

    @Override
    public void run() {
        // update web client icon configuration of the search topic
        dms.getTopicType("dm4.webclient.search", null)
                .getViewConfig()
                .addSetting("dm4.webclient.view_config", "dm4.webclient.icon",
                        "/de.deepamehta.dm4-example/images/bucket.png");
    }
}

Properties

DeepaMehta tries to find out most of the plugin behaviors by convention, nevertheless some configurations have to be done manually, 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.service.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 with some custom command callbacks:

dm4c.add_plugin('dm4.example.plugin', function() {

    // calls the alternative REST creation method with customized JSON format
    function createAnotherExample() {
        var name = prompt('Example name', 'Another Example'),
            topic = dm4c.restc.request('POST', '/example/create', { name: name })
        dm4c.show_topic(new Topic(topic), 'show', null, true)
    }

    // calls the server side increase method of the selected Example topic
    function increaseExample() {
        var url = '/example/increase/' + dm4c.selected_object.id,
            topic = dm4c.restc.request('GET', url)
        dm4c.show_topic(new Topic(topic), 'show', null, true)
    }

    // define type specific commands and register them 
    dm4c.add_listener('topic_commands', function (topic) {
        return topic.type_uri !== 'dm4.example.type' ? [] : [{
            context: ['context-menu', 'detail-panel-show'],
            label: 'Increase me!', handler: increaseExample
        }]
    })

    // register an additional create command
    dm4c.add_listener("post_refresh_create_menu", function(type_menu) {
        type_menu.add_separator()
        type_menu.add_item({ label: "New Example", handler: createAnotherExample })
    })
})

Type specific renderer can be assigned declarative or programmatically in a migration and manually by editing the view configuration of a type 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.simple_renderer_uri: "dm4.example.content_field_renderer",
                dm4.webclient.page_renderer_uri: "dm4.example.content_page_renderer"
            }
        }
    ]
}

SimpleRenderer

A simple possibility to enhance the UI is the simple renderer.

// a simple example that renders a topic name with a additional CSS class
dm4c.add_simple_renderer('dm4.example.content_field_renderer', {

    render_info: function(model, $parent) {
        dm4c.render.field_label(model, $parent)
        $parent.append($("<span>").addClass('example').text(model.value))
    },

    render_form: function(model, $parent) {
        var $content = dm4c.render.input(model)
        $parent.append($content)
        return function() { // return input value
            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
dm4c.add_page_renderer("dm4.example.content_page_renderer", {

    render_page: function(topic) {
        dm4c.render.field_label('Content')
        dm4c.render.page(topic.value)
    },

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

MultiRenderer

TBD describe and write a multi renderer implementation

CanvasRenderer

TBD describe additional canvas renderer

Stylesheets

All Stylesheets in the web resource folder are loaded by default.

Loading custom CSS files of a vendor plugin or something else:

dm4c.load_stylesheet('/de.deepamehta.dm4-example/vendor/library/css/screen.css')

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

Model

Writing the business logic close to the domain jargon is desirable and can be supported with a simple wrap of the underlying topic model.

A topic model wrapper:

package de.deepamehta.plugins.example.model;

import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import de.deepamehta.core.model.CompositeValue;
import de.deepamehta.core.model.TopicModel;

public class ExampleTopic extends TopicModel {

    public static final String COUNT = "dm4.example.count";
    public static final String NAME = "dm4.example.name";
    public static final String TYPE = "dm4.example.type";

    /**
     * @param model { name: "an example" }
     * @throws JSONException
     */
    public ExampleTopic(JSONObject json) throws JSONException {
        super(TYPE);
        setCompositeValue(new CompositeValue().put(NAME, json.getString("name")));
    }

}

Domain model class example:

package de.deepamehta.plugins.example.model;

import static de.deepamehta.plugins.example.model.ExampleTopic.*;

import org.codehaus.jettison.json.JSONObject;

import de.deepamehta.core.DeepaMehtaTransaction;
import de.deepamehta.core.JSONEnabled;
import de.deepamehta.core.Topic;
import de.deepamehta.core.model.SimpleValue;
import de.deepamehta.core.model.TopicModel;
import de.deepamehta.core.service.ClientState;
import de.deepamehta.core.service.DeepaMehtaService;

/**
 * A domain model class that wraps the underlying <code>Topic</code>.
 */
public class Example implements JSONEnabled {

    private final Topic topic;
    private final DeepaMehtaService dms;

    /**
     * Loads an existing <code>Example</code> topic.
     */
    public Example(long id, DeepaMehtaService dms, ClientState clientState) {
        topic = dms.getTopic(id, true, clientState);
        this.dms = dms;
    }

    /**
     * Creates a new <code>Example</code> topic from <code>ExampleTopic</code> model.
     */
    public Example(ExampleTopic model, DeepaMehtaService dms, ClientState clientState) {
        topic = dms.createTopic(model, clientState);
        this.dms = dms;
    }

    /**
     * Increase the count by one.
     * 
     * @return this
     */
    public Example increase() {
        DeepaMehtaTransaction tx = dms.beginTx();
        topic.setChildTopicValue(COUNT, new SimpleValue(getCount() + 1));
        tx.success();
        tx.finish();
        return this;
    }

    @Override
    public JSONObject toJSON() {
        return topic.toJSON();
    }

    // ------------------------------ simplified composite access

    public int getCount() {
        return getCountTopic().getSimpleValue().intValue();
    }

    public String getName() {
        return getNameTopic().getSimpleValue().toString();
    }

    // ------------------------------ private helper

    private TopicModel getCountTopic() {
        return topic.getCompositeValue().getTopic(COUNT);
    }

    private TopicModel getNameTopic() {
        return topic.getCompositeValue().getTopic(NAME);
    }

}

Plugin

A Java plugin can interact by implementing of listener and export a OSGi service with optional REST exposure. The listener contain topic CRUD (create, retype, update, delete) interaction and plugin lifecycle management, see package de.deepamehta.core.service.listener.

All public methods of your plugin service must be described in a service interface that extends PluginService:

package de.deepamehta.plugins.example.service;

import de.deepamehta.core.service.ClientState;
import de.deepamehta.core.service.PluginService;
import de.deepamehta.plugins.example.model.Example;
import de.deepamehta.plugins.example.model.ExampleTopic;

public interface ExampleService extends PluginService {

    Example create(ExampleTopic topic, ClientState clientState);

    Example increase(long id, ClientState clientState);

}

Implementing a plugin requires a derivation of PluginActivator, the plugin itself can expose methods with JAX-RS annotations:

package de.deepamehta.plugins.example;

import java.util.logging.Logger;

import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
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.osgi.PluginActivator;
import de.deepamehta.core.service.ClientState;
import de.deepamehta.core.service.listener.PreCreateTopicListener;
import de.deepamehta.plugins.example.model.Example;
import de.deepamehta.plugins.example.model.ExampleTopic;
import de.deepamehta.plugins.example.service.ExampleService;

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

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

    /**
     * Initially change the count value of the unattached
     * <code>TopicModel</code> to zero.
     */
    @Override
    public void preCreateTopic(TopicModel model, ClientState clientState) {
        if (model.getTypeUri().equals(ExampleTopic.COUNT)) {
            log.info("init Example count");
            model.setSimpleValue(0);
        }
    }

    /**
     * Creates a new <code>Example</code> instance based on the domain specific
     * REST call with a alternate JSON topic representation.
     */
    @POST
    @Path("/create")
    @Override
    public Example create(ExampleTopic topic, @HeaderParam("Cookie") ClientState clientState) {
        log.info("create Example " + topic);
        try {
            return new Example(topic, dms, clientState);
        } catch (Exception e) {
            throw new WebApplicationException(new RuntimeException("something went wrong", e));
        }
    }

    /**
     * Increase the count of an attached <code>Example</code> topic.
     */
    @GET
    @Path("/increase/{id}")
    @Override
    public Example increase(@PathParam("id") long id, @HeaderParam("Cookie") ClientState clientState) {
        log.info("increase Example " + id);
        try {
            return new Example(id, dms, clientState).increase();
        } catch (Exception e) {
            throw new WebApplicationException(new RuntimeException("something went wrong", e));
        }
    }
}

Provider

To provide a automatically serialisation of your Java domain model classes, you can implement a specific provider or use the JSONEnabled interface. There is no need for proprietary message body writer providers as long as the object implements JSONEnabled. Every object that is about to be send over the wire is supposed to do so. Of course, a generic reader provider is a different story and does not yet exist.

package de.deepamehta.plugins.example.provider;

import java.io.InputStream;

import javax.ws.rs.ext.Provider;

import org.codehaus.jettison.json.JSONObject;

import de.deepamehta.core.util.JavaUtils;
import de.deepamehta.plugins.example.AbstractJaxrsReader;
import de.deepamehta.plugins.example.model.ExampleTopic;

/**
 * Creates a <code>ExampleTopic</code> from JSON.
 * 
 * @see ExampleTopic#ExampleTopic(JSONObject)
 */
@Provider
public class ExampleTopicProvider extends AbstractJaxrsReader<ExampleTopic> {

    public ExampleTopicProvider() {
        super(ExampleTopic.class);
    }

    @Override
    protected ExampleTopic createInstance(InputStream entityStream) throws Exception {
        return new ExampleTopic(new JSONObject(JavaUtils.readText(entityStream)));
    }

}

Attachments