Changes between Version 34 and Version 35 of PluginDevelopmentGuide
- Timestamp:
- 29.10.2016 10:54:37 (8 years ago)
Legend:
- Unmodified
- Added
- Removed
- Modified
-
PluginDevelopmentGuide
v34 v35 94 94 <parent> 95 95 <groupId>de.deepamehta</groupId> 96 <artifactId>deepamehta-plugin -parent</artifactId>97 <version>4. 1.1-SNAPSHOT</version>96 <artifactId>deepamehta-plugin</artifactId> 97 <version>4.8</version> 98 98 </parent> 99 99 … … 130 130 { 131 131 type_uri: "dm4.webclient.view_config", 132 c omposite: {132 childs: { 133 133 dm4.webclient.show_in_create_menu: true 134 134 } … … 144 144 {{{ 145 145 #!txt 146 requiredPluginMigrationNr=1 147 importModels=de.deepamehta.webclient 146 dm4.plugin.activate_after=de.deepamehta.webclient 147 dm4.plugin.model_version=1 148 148 }}} 149 149 150 150 === Setup for Hot-Deployment === 151 151 152 In order to let DeepaMehta hot-deploy the plugin you must include it in DeepaMehta's hot-deployment list.153 154 In DeepaMehta's **`pom.xml`**: add the plugin's `target` directory (here: `/home/myhome/deepamehta-dev/dm4-tagging/target`) to the `felix.fileinstall.dir` property. Important: make sure your line ends with a comma:152 The easiest way to let DeepaMehta hot-deploy the plugin is to develop it within the **`bundle-dev`** directory. To start doing so move the plugin directory on your hard disc into DeepaMehta's hot-deployment folder called **`bundle-dev`**. The next step is to build your plugin. 153 154 Another way to let DeepaMehta hot-deploy the plugin is in DeepaMehta's **`pom.xml`**: add the plugin's `target` directory (here: `/home/myhome/deepamehta-dev/dm4-tagging/target`) to the `felix.fileinstall.dir` property. Important: make sure your line ends with a comma: 155 155 156 156 {{{ … … 427 427 === Plugin configuration === 428 428 429 If your plugin comes with its own data model you must tell DeepaMehta the data model version it relies on. To do so, set the ` requiredPluginMigrationNr` configuration property in the `plugin.properties` file, e.g.:430 431 {{{ 432 #!txt 433 requiredPluginMigrationNr=2434 }}} 435 436 DeepaMehta's migration machinery takes charge of running the plugin's migrations up to that configured number. If your plugin comes with no data model, you can specify `0` resp. omit the ` requiredPluginMigrationNr` property as `0` is its default value.429 If your plugin comes with its own data model you must tell DeepaMehta the data model version it relies on. To do so, set the `dm4.plugin.model_version` configuration property in the `plugin.properties` file, e.g.: 430 431 {{{ 432 #!txt 433 dm4.plugin.model_version=2 434 }}} 435 436 DeepaMehta's migration machinery takes charge of running the plugin's migrations up to that configured number. If your plugin comes with no data model, you can specify `0` resp. omit the `dm4.plugin.model_version` property as `0` is its default value. 437 437 438 438 Usually each plugin has its own `plugin.properties` file. It allows the developer to configure certain aspects of the plugin. The name of the `plugin.properties` file and its path within the plugin directory is fixed: … … 489 489 }}} 490 490 491 This example plugin would have set ` requiredPluginMigrationNr` to 6 (configured in `plugin.properties`), so 6 migrations are involved. 4 are declarative and 2 are imperative here.492 493 Important: for each number between 1 and ` requiredPluginMigrationNr` exactly one migration file must exist. That is //either// a declarative migration file //or// an imperative migration file.491 This example plugin would have set `dm4.plugin.model_version` to 6 (configured in `plugin.properties`), so 6 migrations are involved. 4 are declarative and 2 are imperative here. 492 493 Important: for each number between 1 and `dm4.plugin.model_version` exactly one migration file must exist. That is //either// a declarative migration file //or// an imperative migration file. 494 494 495 495 It would be invalid if for a given number a) no migration file exists, or b) two migration files exist (one declarative and one imperative). In these cases the DeepaMehta migration machinery throws an error and the plugin is not activated. … … 556 556 { 557 557 type_uri: "dm4.webclient.view_config", 558 c omposite: {558 childs: { 559 559 dm4.webclient.icon: "/de.deepamehta.notes/images/yellow-ball.png", 560 560 dm4.webclient.show_in_create_menu: true … … 573 573 An imperative migration is a Java class that is derived from `de.deepamehta.core.service.Migration` and that overrides the `run()` method. The `run()` method is called by DeepaMehta to run the migration. 574 574 575 Within the migration you have access to the //DeepaMehta Core Service// through the `dms` object. By the means of the DeepaMehtaCore Service you can perform arbitrary database operations. Typically this involves importing further objects from the `de.deepamehta.core` API.575 Within the migration you have access to the DeepaMehta //Core Service// through the `dm4` object. By the means of the Core Service you can perform arbitrary database operations. Typically this involves importing further objects from the `de.deepamehta.core` API. 576 576 577 577 As an example see a migration that comes with the //DeepaMehta 4 Topicmaps// plugin: … … 579 579 {{{ 580 580 #!java 581 package de.deepamehta. plugins.topicmaps.migrations;581 package de.deepamehta.topicmaps.migrations; 582 582 583 583 import de.deepamehta.core.TopicType; 584 import de.deepamehta.core.model.AssociationDefinitionModel;585 584 import de.deepamehta.core.service.Migration; 586 585 … … 589 588 @Override 590 589 public void run() { 591 TopicType type = dm s.getTopicType("dm4.topicmaps.topicmap", null);592 type.addAssocDef( newAssociationDefinitionModel("dm4.core.composition_def",590 TopicType type = dm4.getTopicType("dm4.topicmaps.topicmap"); 591 type.addAssocDef(mf.newAssociationDefinitionModel("dm4.core.composition_def", 593 592 "dm4.topicmaps.topicmap", "dm4.topicmaps.state", "dm4.core.one", "dm4.core.one")); 594 593 } … … 608 607 * **Consuming services provided by other plugins**. Example: in order to investigate a topic's workspace assignments and the current user's memberships the //DeepaMehta 4 Access Control// plugin consumes the service provided by the //DeepaMehta 4 Workspaces// plugin. 609 608 610 * **Access the DeepaMehta Core Service**. The DeepaMehta Core Serviceprovides the basic database operations (create, retrieve, update, delete) to deal with the DeepaMehta Core objects: Topics, Associations, Topic Types, Association Types.609 * **Access the DeepaMehta Core Service**. The DeepaMehta //Core Service// provides the basic database operations (create, retrieve, update, delete) to deal with the DeepaMehta Core objects: Topics, Associations, Topic Types, Association Types. 611 610 612 611 Weather a DeepaMehta plugin has a server side part at all depends on the nature of the plugin. Plugins without a server side part include those which e.g. just define a data model or just provide a custom (JavaScript) renderer. … … 653 652 654 653 Furthermore when writing a plugin main file you must add 2 entries in the plugin's **`pom.xml`**: 655 1. a < dependencies> element to include the `deepamehta-core` dependency. This brings youthe `PluginActivator` class.654 1. a <parent> element to declare the artifactId **`deepamehta-plugin`**. This brings you necessary dependenies and the `PluginActivator` class. 656 655 2. a <build> element to configure the Maven Bundle Plugin. It needs to know what your plugin main class is. You must specify the fully-qualified class name. 657 656 … … 669 668 <parent> 670 669 <groupId>de.deepamehta</groupId> 671 <artifactId>deepamehta-plugin -parent</artifactId>672 <version>4. 1.1-SNAPSHOT</version>670 <artifactId>deepamehta-plugin</artifactId> 671 <version>4.8</version> 673 672 </parent> 674 675 <dependencies>676 <dependency>677 <groupId>de.deepamehta</groupId>678 <artifactId>deepamehta-core</artifactId>679 <version>4.1.1-SNAPSHOT</version>680 </dependency>681 </dependencies>682 673 683 674 <build> … … 724 715 import de.deepamehta.core.model.TopicModel; 725 716 import de.deepamehta.core.osgi.PluginActivator; 726 import de.deepamehta.core.service.ClientState;727 717 import de.deepamehta.core.service.Directives; 728 718 import de.deepamehta.core.service.event.PostCreateTopicListener; … … 738 728 739 729 @Override 740 public void postCreateTopic(Topic topic , ClientState clientState, Directives directives) {730 public void postCreateTopic(Topic topic) { 741 731 log.info("### Topic created: " + topic); 742 732 } 743 733 744 734 @Override 745 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState, 746 Directives directives) { 735 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 747 736 log.info("### Topic updated: " + topic + "\nOld topic: " + oldModel); 748 737 } … … 766 755 For a plugin to provide a service you must define a //service interface//. The service interface contains all the method signatures that make up the service. When other plugins consume your plugin's service they do so via the service interface. 767 756 768 The service interface //must// be located in the plugin's `src/main/java/<your plugin package>/service/` directory. By convention the name of the service interface ends with `...Service`. The service interface must be declared `public`. 769 770 The service interface is a regular Java interface that extends `de.deepamehta.core.service.PluginService`. PluginService is a common base interface for all plugin services. It's just a marker interface. 757 To be recogbized the service interface //must// by convention end its name on `...Service`. The service interface must be declared `public` and is a regular Java interface. 771 758 772 759 A DeepaMehta plugin can define //one// service interface at most. More than one service interface is not supported. … … 782 769 de/ 783 770 deepamehta/ 784 plugins/ 785 topicmaps/ 786 service/ 787 TopicmapsService.java 788 }}} 789 790 The service interface of the //Topicmaps// plugin is named `TopicmapsService`. The plugin package is `de.deepamehta.plugins.topicmaps`. 771 topicmaps/ 772 TopicmapsService.java 773 }}} 774 775 The service interface of the //Topicmaps// plugin is named `TopicmapsService`. The plugin package is `de.deepamehta.topicmaps`. 791 776 792 777 The //Topicmaps// service interface looks like this: … … 794 779 {{{ 795 780 #!java 796 package de.deepamehta. plugins.topicmaps.service;797 798 import de.deepamehta. plugins.topicmaps.TopicmapRenderer;799 import de.deepamehta. plugins.topicmaps.model.ClusterCoords;800 import de.deepamehta. plugins.topicmaps.model.Topicmap;781 package de.deepamehta.topicmaps.service; 782 783 import de.deepamehta.topicmaps.TopicmapRenderer; 784 import de.deepamehta.topicmaps.model.ClusterCoords; 785 import de.deepamehta.topicmaps.model.Topicmap; 801 786 802 787 import de.deepamehta.core.Topic; 803 import de.deepamehta.core.service.ClientState; 804 import de.deepamehta.core.service.PluginService; 805 806 807 808 public interface TopicmapsService extends PluginService { 809 810 Topic createTopicmap(String name, String topicmapRendererUri, ClientState clientState); 811 Topic createTopicmap(String name, String uri, String topicmapRendererUri, ClientState clientState); 788 789 790 public interface TopicmapsService { 791 792 Topic createTopicmap(String name, String topicmapRendererUri); 793 Topic createTopicmap(String name, String uri, String topicmapRendererUri); 812 794 813 795 // --- 814 796 815 Topicmap getTopicmap(long topicmapId , ClientState clientState);797 Topicmap getTopicmap(long topicmapId); 816 798 817 799 // --- … … 837 819 }}} 838 820 839 You see the Topicmaps service consist of methods to create topicmaps, retrieve topicmaps, and manipulate topicmaps. As any plugin service the Topicmaps service must be derived from `PluginService`.821 You see the Topicmaps service consist of methods to create topicmaps, retrieve topicmaps, and manipulate topicmaps. 840 822 841 823 ==== Implementing the service ==== … … 849 831 {{{ 850 832 #!java 851 package de.deepamehta. plugins.topicmaps;852 853 import de.deepamehta. plugins.topicmaps.model.Topicmap;854 import de.deepamehta. plugins.topicmaps.service.TopicmapsService;833 package de.deepamehta.topicmaps; 834 835 import de.deepamehta.topicmaps.model.Topicmap; 836 import de.deepamehta.topicmaps.TopicmapsService; 855 837 856 838 import de.deepamehta.core.Topic; 857 839 import de.deepamehta.core.osgi.PluginActivator; 858 import de.deepamehta.core.service.ClientState;859 840 860 841 … … 865 846 866 847 @Override 867 public Topic createTopicmap(String name, String topicmapRendererUri , ClientState clientState) {868 ... 869 } 870 871 @Override 872 public Topic createTopicmap(String name, String uri, String topicmapRendererUri , ClientState clientState) {848 public Topic createTopicmap(String name, String topicmapRendererUri) { 849 ... 850 } 851 852 @Override 853 public Topic createTopicmap(String name, String uri, String topicmapRendererUri) { 873 854 ... 874 855 } … … 877 858 878 859 @Override 879 public Topicmap getTopicmap(long topicmapId , ClientState clientState) {860 public Topicmap getTopicmap(long topicmapId) { 880 861 ... 881 862 } … … 897 878 Your plugin can consume the services provided by other plugins. To do so your plugin must get hold of the //service object// of the other plugin. Through the service object your plugin can call all the service methods declared in the other's plugin service interface. 898 879 880 To tell the DeepaMehta Core which plugin service your plugin wants to consume you need to declare an instance variable in your plugin like using the @Inject notation: 881 882 {{{ 883 #!java 884 @Inject 885 private AccessControlService acService; 886 }}} 887 888 Make sure to add your interest in building on the respective plugin service as dependencies to your **`pom.xml`** file. In the case of using the AccessControlService we would need to add the following: 889 890 {{{ 891 #!xml 892 <dependencies> 893 <dependency> 894 <groupId>de.deepamehta</groupId> 895 <artifactId>deepamehta-accesscontrol</artifactId> 896 <version>4.8</version> 897 </dependency> 898 </dependencies> 899 }}} 900 899 901 Behind the scenes the DeepaMehta Core handles a plugin service as an OSGi service. Because of the dynamic nature of an OSGi environment DeepaMehta plugin services can arrive and go away at any time. Your plugin must deal with that. However, you as a plugin developer must not care about DeepaMehta's OSGi foundation. The DeepaMehta Core hides the details from you and provides an easy-to-use API for consuming plugin services. 900 902 901 To consume a plugin service your plugin must override 2 hooks: `serviceArrived` and `serviceGone`. Through the former your plugin gets hold of a service object and in the latter your plugin must release that service object. These 2 hooks are called by the DeepaMehta Core as soon as a desired plugin becomes available resp. goes away. 902 903 To tell the DeepaMehta Core which plugin service your plugin wants to consume use the `@ConsumesService` annotation at the `serviceArrived` hook. State the fully qualified name (string) of the respective service interface as the annotation argument. To consume more than one service use the array notation: `{"...", "..."}`. 903 To deal with other plugin services coming and going your plugin can override 2 hooks: `serviceArrived` and `serviceGone`. These 2 hooks are called by the DeepaMehta Core as soon as a desired plugin becomes available resp. goes away. 904 904 905 905 The single argument of the 2 `serviceArrived` and `serviceGone` hooks is the respective service object, declared generically just as `PluginService`. (Remember, `PluginService` is the common base interface for all plugin services.) So casting is required. In `serviceArrived` you typically store the service object in a private instance variable. In `serviceGone` you typically set the instance variable to `null` in order to release the service object. … … 909 909 {{{ 910 910 #!java 911 package de.deepamehta. plugins.workspaces;912 913 import de.deepamehta. plugins.facets.service.FacetsService;911 package de.deepamehta.workspaces; 912 913 import de.deepamehta.facets.FacetsService; 914 914 915 915 import de.deepamehta.core.osgi.PluginActivator; … … 921 921 public class WorkspacesPlugin extends PluginActivator { 922 922 923 @Inject 923 924 private FacetsService facetsService; 924 925 … … 926 927 927 928 @Override 928 @ConsumesService("de.deepamehta.plugins.facets.service.FacetsService")929 929 public void serviceArrived(PluginService service) { 930 facetsService = (FacetsService) service; 930 if (service instanceof FacetsService) { 931 // do something when the facet service comes around 932 } 931 933 } 932 934 933 935 @Override 934 936 public void serviceGone(PluginService service) { 935 facetsService = null; 936 } 937 }}} 938 939 You see the Workspaces plugin consumes just one plugin service: the //Facets// service. Its service interface is specified as the `@ConsumesService` argument. The `PluginService` object passed to the 2 hooks needs not being further investigated. 940 941 In contrast lets see how it looks like when a plugin consumes more than one service. As an example here is an extraction of the //Geomaps// plugin (part of the DeepaMehta Standard Distribution): 937 // do something when a service goes away 938 } 939 }}} 940 941 You see the Workspaces plugin consumes a plugin service: the //Facets// service. The `PluginService` object passed to the 2 hooks needs not being further investigated. 942 943 In this way your plugin could also consume more than one service. 944 945 === Providing a RESTful web service === 946 947 Until here your plugin service is accessible from within the OSGi environment only. You can make the service accessible from //outside// the OSGi environment as well by promoting it to a RESTful web service. Your plugin service is then accessible from external applications via HTTP. (External application here means both, the client-side portion of a DeepaMehta plugin, or an arbitrary 3rd-party application). 948 949 To provide a RESTful web service you must provide a generic plugin service first (as described above in [[#Providingaservice|Providing a service]]) and then make it RESTful by using JAX-RS annotations. With JAX-RS annotations you basically control how HTTP requests will be mapped to your service methods. 950 951 To make your plugin service RESTful you must: 952 953 * Annotate the plugin main class with `@Path` to anchor the plugin service in URI space. 954 955 * Annotate the plugin main class with `@Consumes` and `@Produces` to declare the supported HTTP request and response media types. You can use these annotations also at a particular service method to override the class-level defaults. 956 957 * Annotate each service method with one of `@GET`, `@POST`, `@PUT`, or `@DELETE` to declare the HTTP method that will invoke that service method. 958 959 * Annotate each service method with `@Path` to declare the URI template that will invoke that service method. The URI template can contain parameters, notated with curly braces `{...}`. 960 961 * Annotate service method parameters with `@PathParam` to map URI template parameters to service method parameters. 962 963 As an example let's see how the //Topicmaps// plugin (part of the DeepaMehta Standard Distribution) annotates its main class and service methods: 942 964 943 965 {{{ 944 966 #!java 945 public class GeomapsPlugin extends PluginActivator { 946 947 private TopicmapsService topicmapsService; 948 private FacetsService facetsService; 949 950 // *** Hook Implementations *** 951 952 @Override 953 @ConsumesService({ 954 "de.deepamehta.plugins.topicmaps.service.TopicmapsService", 955 "de.deepamehta.plugins.facets.service.FacetsService" 956 }) 957 public void serviceArrived(PluginService service) { 958 if (service instanceof TopicmapsService) { 959 topicmapsService = (TopicmapsService) service; 960 } else if (service instanceof FacetsService) { 961 facetsService = (FacetsService) service; 962 } 963 } 964 965 @Override 966 public void serviceGone(PluginService service) { 967 if (service == topicmapsService) { 968 topicmapsService = null; 969 } else if (service == facetsService) { 970 facetsService = null; 971 } 972 } 973 }}} 974 975 You see the Geomaps plugin consumes 2 plugin services: the //Topicmaps// service and the //Facets// service. Both service interfaces are specified as the `@ConsumesService` argument. Note the array notation with the curly braces `{...}`. Note also the `if` statements in the `serviceArrived` and `serviceGone` hooks. 976 977 === Providing a RESTful web service === 978 979 Until here your plugin service is accessible from within the OSGi environment only. You can make the service accessible from //outside// the OSGi environment as well by promoting it to a RESTful web service. Your plugin service is then accessible from external applications via HTTP. (External application here means both, the client-side portion of a DeepaMehta plugin, or an arbitrary 3rd-party application). 980 981 To provide a RESTful web service you must provide a generic plugin service first (as described above in [[#Providingaservice|Providing a service]]) and then make it RESTful by using JAX-RS annotations. With JAX-RS annotations you basically control how HTTP requests will be mapped to your service methods. 982 983 To make your plugin service RESTful you must: 984 985 * Annotate the plugin main class with `@Path` to anchor the plugin service in URI space. 986 987 * Annotate the plugin main class with `@Consumes` and `@Produces` to declare the supported HTTP request and response media types. You can use these annotations also at a particular service method to override the class-level defaults. 988 989 * Annotate each service method with one of `@GET`, `@POST`, `@PUT`, or `@DELETE` to declare the HTTP method that will invoke that service method. 990 991 * Annotate each service method with `@Path` to declare the URI template that will invoke that service method. The URI template can contain parameters, notated with curly braces `{...}`. 992 993 * Annotate service method parameters with `@PathParam` to map URI template parameters to service method parameters. 994 995 As an example let's see how the //Topicmaps// plugin (part of the DeepaMehta Standard Distribution) annotates its main class and service methods: 996 997 {{{ 998 #!java 999 package de.deepamehta.plugins.topicmaps; 1000 1001 import de.deepamehta.plugins.topicmaps.model.Topicmap; 1002 import de.deepamehta.plugins.topicmaps.service.TopicmapsService; 967 package de.deepamehta.topicmaps; 968 969 import de.deepamehta.topicmaps.model.Topicmap; 970 import de.deepamehta.topicmaps.TopicmapsService; 1003 971 1004 972 import de.deepamehta.core.Topic; 1005 973 import de.deepamehta.core.osgi.PluginActivator; 1006 import de.deepamehta.core.service.ClientState;1007 974 1008 975 import javax.ws.rs.GET; … … 1029 996 @Override 1030 997 public Topic createTopicmap(@PathParam("name") String name, 1031 @PathParam("topicmap_renderer_uri") String topicmapRendererUri, 1032 @HeaderParam("Cookie") ClientState clientState) { 998 @PathParam("topicmap_renderer_uri") String topicmapRendererUri) { 1033 999 ... 1034 1000 } … … 1037 1003 @Path("/{id}") 1038 1004 @Override 1039 public Topicmap getTopicmap(@PathParam("id") long topicmapId , @HeaderParam("Cookie") ClientState clientState) {1005 public Topicmap getTopicmap(@PathParam("id") long topicmapId) { 1040 1006 ... 1041 1007 } … … 1085 1051 @Path("/{id}") 1086 1052 @Override 1087 public Topicmap getTopicmap(@PathParam("id") long topicmapId , @HeaderParam("Cookie") ClientState clientState) {1088 ... 1089 } 1090 }}} 1091 1092 Now you know how exactly the JAX-RS implementation extracts the `topicmapId` and `clientState` parameter valuesfrom the HTTP request:1053 public Topicmap getTopicmap(@PathParam("id") long topicmapId) { 1054 ... 1055 } 1056 }}} 1057 1058 Now you know how exactly the JAX-RS implementation extracts the `topicmapId` parameter value from the HTTP request: 1093 1059 1094 1060 The `topicmapId` value is extracted from the request's URI path and then converted to a `long`. Here criterion 1 is satisfied and the conversion is straight-forward. 1095 1061 1096 The `clientState` value is extracted from the request's `Cookie` header value. The actual `ClientState` object is created by passing the value to the `ClientState(String)` constructor. Here criterion 2 is satisfied.1097 1098 1062 ==== Parsing the HTTP request body ==== 1099 1063