-
Notifications
You must be signed in to change notification settings - Fork 3
Developer Documentation And Examples
This document is intended to be used as a practical guide in order to help developers adjust to the somewhat-complex OpenWIS 4 architecture. It will describe a general approach that "should" be used when attempting to modify Core Geonetwork content, and also provide concrete examples for specific scenarios.
The architectural concept of OpenWIS 4 is to build-around (or plug-into, if you prefer) Core Geonetwork. This means that a release of CGN is originally built, and on top of it OpenWIS:
- Enhances the web resource management done by Wro4j.
- Hacks the original Angular framework in order to provide easier access and inheritence to existing controllers.
- Adds some Javascript general-utility code.
- Uses the enhanced Wro4j module and hacked Angular in order to dynamically add web content.
- Provides Spring components that bind with the existing (somewhat-custom) Spring implementation in CGN. In this fashion, it is possible to write code external to CGN itself and only have to worry about how to actually plug it into the application.
A more elaborate description of the internals of CGN and OW4 can be found here: Developing OpenWIS v4 functionality. This section will only provide a set of instructions which - if followed - will assist greatly with web content.
The usual situation we find ourselves in when we want to modify web content is the following:
- One of the few root pages has loaded a root Angular module.
- Somewhere inside the DOM there exists a child Angular module we wish to modify, and its HTML content was most probably loaded using an ng-include (or an ng-route, but not likely).
- The above child module was declared in a specific Javascript file and that file was loaded using Wro4j.
Inspect the page in the browser and try to find a unique element (or element attribute) around the area of interest. Finding a unique element ID in CGN is not easy so you might have to settle for an Angular attribute or a combination of elements. What must be achieved in the end is to find an angular element whose scope we can borrow and "lives close by" to where we want to add our code.
Once that unique element is identified, search for it in the application sources. Usually the content resides in HTML files, but there are a few cases where HTML is generated purely by Javascript. This instruction may appear trivial but know that CGN has a very large number of included "tiles" and it features very deep nesting of these includes, making it tricky to correctly identify the target.
If an Angular controller is targeted then its much easier to find as it should only be bound once, by name.
At this point you should have a pretty clear understanding of what changes you wish to perform, and where exactly to perform them.
Please note that the even though the code in the following steps is real, the files/modules/etc. used are not, so this cannot be expected to run.
The target content will reside inside an angular module. Find that module's name and search for it in the Javascript sources. You will find a file with content similar to this:
/**
* File: DashboardController.js
*/
(function() {
goog.provide('gn_dashboard_controller');
goog.require('gn_dashboard_content_stat_controller');
goog.require('gn_dashboard_search_stat_controller');
goog.require('gn_dashboard_status_controller');
goog.require('gn_vcs_controller');
var module = angular.module('gn_dashboard_controller',
['gn_dashboard_status_controller',
'gn_dashboard_search_stat_controller',
'gn_dashboard_content_stat_controller',
'gn_vcs_controller']);
.
.
.The declaration of the Angular module is obvious and holds no secrets. However, the Closure statements goog.provide and goog.require are no Closure at all.
Instead, they are instructions for CGN's (and OW4's) custom Wro4j that inform the tool of the Javascript files it must load.
After Wro4j processing they will be removed from the file, so you won't find them in the web-page.
As explained in [Developing OpenWIS v4 functionality](Developing OpenWIS v4 functionality), CGN (and OW4, as a result) uses a very complex approach to auto-load resources through Wro4j. In short, it searches for the "Closure" statements mentioned above and builds a dependency tree, whose files it later includes in its resources. The extended OW4 Wro4j module allows us to use multile goog.provide statements, and in this way load new files not included in CGN.
Thus, our first concern is to create the provider Javascript file.
/**
* File: provider.my_example_module.js
*/
(function() {
goog.provide('gn_dashboard_controller');
goog.require('injector.my_example_module');
})();In the above code we told Wro4j that the gn_dashboard_controller module requires injector.my_example_module in order to operate. This requirement was appended to the original requirements declared in CGN. The effect of the above is that Wro4j will search for a file that provides injector.my_example_module and load it into its resources. This way, we achieved to load whatever code exists in injector.my_example_module and execute it.
The above technique only helps when we want to add new Javascript files to the application.
If - for any reason - we wish to remove a file already "required" by CGN, then we have to overlay the original file (DashboardController.js) with our own version that won't contain the module we wish to remove.
In order to understand the need for the injector component, it is important to visualize the following. When a page loads, Wro4j minifies (unless instructed otherwise) the modules in its dependency tree and adds them in the HTML in an upside-down pyramid fashion. This is because if "module1" has a dependency on "module1_dep" on the Angular level, it would be declared as follows:
var module1 = angular.module('module1',['module1_dep']);As a result, when "module1" is declared the "module1_dep" must already have been initialized, or an error will be thrown. Our new "my_example_module" will be inserted somewhere in that pyramid and (please note that we are still at the point that the document is being created):
- Any static Javascript will be executed right away (or ASAP).
- Anything Angular-related will execute when Angular bootstraps, after the document is ready.
Now, at this point, if our code was "native" to CGN, we would be good to go. But what we want to do is actually modify existing CGN components, so we have to select between:
- Stopping the original component from loading, and loading our own instead: This is extremely difficult to achieve. Most of the HTML content loads by using ng-includes and the components therein are not available to "grab" until it is too late and they are already active in the DOM.
- Allowing the original page to fully load, and replacing/removing the component: This is feasible and with the help of some utility Javascript functions quite easy to do.
- If simply dealing with replacing an Angular controller, create a new one that extends the original and overrides certain functionality: This is very easy to do using the Angular hack.
Whatever approach we select, we will need to write some Javascript that will interfere with the DOM and do its magic. That Javascript, external to our actual Angular module (but necessary to bind it to the page), is written in the injector.
/**
* File: injector.my_example_module.js
*/
(function() {
goog.provide('injector.my_example_module');
var module = angular.module('injector.my_example_module', []);
})();
/**
* Push a function to be executed on document.ready. This function will load an
* html fragment containing a script and append it to the body. That script
* contains further instructions for angular that could not be loaded because of
* the wro4j limitations.
*
*/
JsUtil.FunctionsToExecuteOnReady
.push(function() {
JsUtil.appendHtmlContentIntoElement(
$(document.body),
'/geonetwork/catalog/components/openwis-extensions/my_example_module/script.my_example_module.html');
});What the injector does is declare its precense to Wro4j, create an angular module, and then add a function to execute "onReady". Now, this onReady is after the document is ready but before Angular has a chance to bootstrap. This is a must because:
- We probably need the Angular element we will be referring-to already present on the page (or a Javascript error will occur).
- We need to write some extra module-defining code before Angular bootstraps.
Our last component for the job is the script which is actually an HTML file containing a <script>....</script> tag. It is in HTML form because that way it's easier to append it to the document body.
<!--
- File: script.my_example_module.html
-->
<script type="text/javascript">
JsUtil.addFunctionToExecuteOnIncludeLoadSuccess('path1/path2/target-template.html',function(){
var element = $('[id="target-element"]');
var elementScope = element.scope();
var parent = element.parent();
var clone = element.clone();
clone.attr('id','new-id');
element.remove();
parent.append(JsUtil.Angular.Compile(clone.prop('outerHTML'))(elementScope));
});
// This is to make chrome add this code to its debug sources (doesn't handle dynamic content that well)
//@ sourceURL=my_example_module.js
angular.module('gn_adminmetadata_controller').requires.push('injector.my_example_module');
</script>This is the place where the actual plugin-into-CGN-magic happens. Using a utility function (JsUtil.addFunctionToExecuteOnIncludeLoadSuccess) we declare that once our target-template.html has successfully loaded, execute a the following code..... This ensures that all components have successfully bootstrapped and all scopes have initialized.
There are several Javascript "helpers" available. Among them, JsUtil.addFunctionToExecuteOnIncludeLoadSuccess allows to execute something when an ng-include loads successfully, JsUtil.addFunctionToExecuteOnRouteLoadSuccess does the same when an ng-route loads, and JsUtil.Angular.Compile allows to Angular-compile HTML anywhere. More may be added if the need arises.
What we are actually doing in this example is quite simple:
- We are searching for an element with id="target-element".
- We get its Angular scope.
- We get the original element's parent.
- We clone the element.
- We modify the id of the clone.
- We remove the original element.
- We append the clone to the parent element, after compiling it with the scope of the original element.
We could have done anything we wanted with any element at this point, as long as we have a scope that will help us compile our new HTML. The only realistic difficulty with this approach is that sometimes it's quite hard to actually get the element you want. This is because CGN generally doesn't use HTML IDs, or it some times has many elements with the same ID (!). In that case (as mentioned earlier) we are searching for anything unique "in the vicinity".
Finally, one more thing that the script does is dynamically add a new Angular dependency into the existing ones of gn_adminmetadata_controller. This is necessary when we want to declare/use Angular components because without it there is no connection of our code with the Angular app.
To sum-up, our approach was the following:
- Identify the target element (id="target-element") and its associated file (path1/path2/target-template.html).
- Find the target Angular module (gn_dashboard_controller) and its associated file (DashboardController.js).
- Write the Provider (provider.my_example_module.js) that will allow us to load our code with the target Angular module.
- Write the Injector (injector.my_example_module.js) that will allow us to append code at the document body.
- Write the Script (script.my_example_module.html) that will allow us to actually replace the element.
It may appear like a lot of steps, but this is a common approach that works for every single situation encountered so far.
There are no definitive instructions for server-side code. A modular design is genrally preferred but its use is not enforced by any means. Still, the following is worth mentioning. It appears that CGN features its own Spring implementation that has gone a long way to include Jeeves (here and here), and may behave unexpectedly in features such as web-services.
Nevertheless, when using spring-security-saml everything worked smoothly. It appears that only certain Spring features have been tampered-with.
The code for the following examples is available in the wiki under the example-sources directory and you will need to clone the wiki locally in order to get to them. In there you will find the full sources of a modified version of the application that includes all the examples mentioned here.
In every example we will be using the same steps we defined earlier.
The code for this example can be found here: /openwis-war/src/main/webapp/catalog/components/openwis-extensions/example_add_html_block.
One of the easier examples is to add some HTML content. What we have seen the application do it load content through ng-includes, so we will do the same.
We will be adding a block of static HTML content on the navigation bar. It will not look pretty, but the goal is to show that we can modify anything.
So now, to follow the 5 steps mentioned earlier:
Our target content file has an id="navbar" and resides in the file views/default/templates/home.html.
The module we can plug-into is gn_cat_controller.
We create our provider in file provider.example_add_html_block.js. In there we define the name of the injector module.
We create our injector in file injector.example_add_html_block.js. In there we append the content of the script HTML file when the document is ready (but before Angular bootstraps).
We create our script in file script.example_add_html_block.html. In its javascript we do the following:
- Register a listener that will execute a function when the target html is loaded by an ng-include.
- Our function will find the target element and also get a reference of its Angular scope.
- Then it will add an ng-include that will load another HTML file (calendar.html) which holds the actual HTML content to be added.
- Finaly, in order for the new Angular content to actually become part of the application, we add our new module to the dependencies of the module we tied ourselved in on step 2.
Once everything is in place, you should see the following when opening http://hostname:port/geonetwork:
The code for this example can be found here: /openwis-war/src/main/webapp/catalog/components/openwis-extensions/example_add_menu_item.
This example will add a menu item in the settings menu. In CGN menus are usually created dynamically by controllers, so we will extend one and overrride its original menu.
Our steps:
Our target content is now a Javascript file, GnSettingsController: /web-ui/src/main/resources/catalog/js/admin/SettingsController.js. It holds the menu we want to modify.
The module we can plug-into is gn_settings_controller.
We create our provider in file provider.example_add_menu_item.js. In there we define the name of the injector module.
We create our injector in file injector.example_add_menu_item.js. In there we append the content of the script HTML file when the document is ready (but before Angular bootstraps).
We create our script in file script.example_add_menu_item.html. In its javascript we do the following:
- Create a new Angular controller that extends the one we want to replace. This way we can keep the functionality we want and only change specific things
- Use JsUtil.registerReplacementController to replace the original controller with our own, by utilizing the hack we performed on the Angular sources.
- Finaly, in order for the new Angular content to actually become part of the application, we add our new module to the dependencies of the module we tied ourselved in on step 2.
Once everything is in place, you should see the following when opening the Admin Console->Settings page (must be logged-in):
The code for this example can be found here: /openwis-war/src/main/webapp/catalog/components/openwis-extensions/example_add_angular_element.
In the first example we added an ng-include that only loaded static content. In this one we will add a button that actually does something when clicked.
The fastest way to do that is to clone an existing button, so that's what we are going to do.
Our steps:
Our target content is a button in the file admin/report/report-updated-metadata.html.
The module we can plug-into is gn_report_controller.
We create our provider in file provider.example_add_angular_element.js. In there we define the name of the injector module.
We create our injector in file injector.example_add_angular_element.js. In there we append the content of the script HTML file when the document is ready (but before Angular bootstraps).
We create our script in file script.example_add_angular_element.html. In its javascript we do the following:
- In most cases CGN doesn't provide unique element IDs, so we will need to grab the button by its data-ng-disabled attribute.
- Get the element's scope for compilation later.
- Get the element's parent.
- Clone the element, change it's "label", and add an 'onclick' handler.
- Append it to the parent.
- Finaly, in order for the new Angular content to actually become part of the application, we add our new module to the dependencies of the module we tied ourselved in on step 2.
Once everything is in place, you should see the new button when opening the Admin Console->Reports page (must be logged-in):
And here is the alert when the button is clicked:
The code for this example can be found here: /openwis-war/src/main/webapp/catalog/components/openwis-extensions/example_change_angular_element_functionality.
In this example we will simply change what a button does when it is clicked. In order to do that we will clone, modify, remove, and rebind the button.
Our steps:
Our target content is the 'Save' button in the file admin/metadata/metadata-identifier-templates.html.
The module we can plug-into is gn_adminmetadata_controller.
We create our provider in file provider.example_change_angular_element_functionality.js. In there we define the name of the injector module.
We create our injector in file injector.example_change_angular_element_functionality.js. In there we append the content of the script HTML file when the document is ready (but before Angular bootstraps).
We create our script in file script.example_change_angular_element_functionality.html. In its javascript we do the following:
- Grab the button by its data-ng-click attribute.
- Get the element's scope for compilation later.
- Get the element's parent.
- Clone the element, change its 'data-ng-click' and set the one from the 'Delete' button near-by. This will effectively call the code for deletion when we click on 'Save'.
- Remove the original button.
- Append the cloned element to the parent.
- Finaly, in order for the new Angular content to actually become part of the application, we add our new module to the dependencies of the module we tied ourselved in on step 2.
Once everything is in place, go to Admin Console->Metadata & Templates->Metadata Identifier Templates(must be logged-in). Create a new template and click on 'Save'. You will see an error message as below, because the application tried to delete a template that it couldn't find:
_The code for this example can be found in the following directories:
- /openwis-war/src/main/webapp/catalog/components/openwis-extensions/example_end_to_end
- /openwis-rest-service
This example will add a button to the navigation bar. We will not extend the controller like in example 2, but manipulate the DOM directly. When that button is clicked, it will talk to a new REST service we will create and display the response in an alert. Our steps:
Our target content is the 'Save' button in the file views/default/templates/home.html.
The module we can plug-into is gn_cat_controller.
We create our provider in file provider.example_end_to_end.js. In there we define the name of the injector module.
We create our injector in file injector.example_end_to_end.js. In there we append the content of the script HTML file when the document is ready (but before Angular bootstraps).
We create our script in file script.example_end_to_end.html. In its javascript we do the following:
- Grab a menu item, and clone it.
- Grab the item's parent, which is a 'ul', and also get its scope.
- Change the clone's content text, some of its attributes, and make it look like a button.
- Create a new 'li' element, and append the clone inside it.
- Append the 'li' to the parent 'ul'.
- Write a click handler for the new button that will call our new REST service and alert its response.
- Finaly, in order for the new Angular content to actually become part of the application, we add our new module to the dependencies of the module we tied ourselved in on step 2.
We created a new maven module for the REST service (openwis-rest-service) containing org.fao.geonet.service.rest.api.DateService.java. The code itself simply returns the server date in a Json string, and we use annotations to set it up.
One very important thing to note here is the package name in this class. It is set to org.fao.geonet because the custom Spring implementation in CGN doesn't seem to bind the web-service correctly if it is declared in an external package.
Once everything is in place, going to the main page after having logged-in will show the new button:
Clicking on it will show the service response in an alert: