Wednesday, September 9, 2015

Make a JSR 286 Portlet work with AngularJS in WS Portal



In an ideal world, you would create a web application (an angularjs app for example) that connects to some REST services written by either yourself or some other team. No need to worry about other UIs rendering right next to you (*cough* Portal *cough*).

Portlet developers, however, have interesting challenges not often encountered by most web and java application developers. We know we can use JSP, JSF or some other server-side framework quite easily out of the box; we also know we can add some javascript (not a lot though :)). Things get more hairy if you're running EJBs or other non-REST services, need to have a responsive (async) ui and all the modern bells-and-whistles while still playing nicely with other portlets on the page.

In this article I'm going to demonstrate how to create a simple portlet, add angularJS, and make an ajax call to post some JSON to our portlet. We will therefore use our portlet as a simple REST-like service so we can call in to our backend.

AngularJS

AngularJS needs no introduction, but if you're new to it have a look at the site. It allows you to create some really cool single-page applications (SPAs) but can definitely also be used instead of JSF or custom-rolled js to build your portlet UI.

IBM Websphere Portal Script Portlets

A note on IBM Websphere Portal script portlets: script portlets essentially wraps your javascript in a portlet. This is relatively new, so please have a look here if you're interested.

Sample project ws-angular-portlet

I have created an example portlet WAR you can clone in github from here. Please have a look, as I'm going to use snippets from there to illustrate the key points. You can also use it as a starting point for your portlet application.

Note that the sample project does not contain the maven repositories needed to get the artifacts listed; this is because of the proprietary nature of Websphere Portal. Please add your own repos in the pom.xml.

Let's dig in

Make sure you've either copied the sample code, or have your project/portlet open with a javax.portlet.GenericPortlet and the relevant portlet.xml.

Handle Requests and Responses in the Portlet

 private ObjectMapper objectMapper = new ObjectMapper();  
      /**  
       * Simply forward to the JSP to bootstrap angularjs.  
       */  
      @Override  
      protected void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {            
           PortletRequestDispatcher dispatcher = getPortletConfig().getPortletContext().getRequestDispatcher("/WEB-INF/jsp/AngularPortlet.jsp");  
           dispatcher.forward(request, response);  
      }  
      /**  
       * Receives a JSON String (jsondata) that gets parsed into a codingglass.portlet.Request object.  
       * For illustration, this method then 'echoes' the request information in a codingglass.portlet.Response object as JSON.  
       */  
      @Override  
      public void serveResource(ResourceRequest request, ResourceResponse response) throws PortletException, IOException {  
           Request fromJson = objectMapper.readValue(request.getParameter("jsondata"), Request.class);  
           Response responseToClient = new Response("OK", "Call OK for ID " + fromJson.getId() + " with data " + fromJson.getData());            
           objectMapper.writeValue(response.getPortletOutputStream(), responseToClient);            
      }       


  1. In the doView, point to the jsp that will bootstrap your angular (code to follow);
  2. Override the serveResource() method in the portlet to handle ajax calls.
  3. For the incoming request, mapped to our custom parameter 'jsondata', parse it into a java object (Request.class in this case) using Jackson.
  4. Create the Response object, passed via Jackson to the response outputStream as JSON.

Create the JSP

Create the portlet JSP (as referenced from your portlet class) as follows.

 <%@ page contentType="text/html" pageEncoding="UTF-8" language="java"%>  
 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>  
 <%@ taglib prefix="portlet" uri="http://java.sun.com/portlet_2_0"%>  
 <portlet:defineObjects/>  
 <link rel="stylesheet" href="<%=request.getContextPath()%>/resources/css/app.css">   
 <!-- html container for your angularjs application. This container will be manually bootstrapped, as ng-app does not work for > 1 app  
 on a page  
  -->  
 <div id="<portlet:namespace/>angularjsApp">  
 <h1>Example AngularJS app</h1>  
      <call-portlet></call-portlet>  
 </div>  
 <!-- Create portlet/Portal constants (urls etc) to be used in the angular app via injection. -->  
 <c:set var="angularPortlet" scope="request">  
   <portlet:namespace />Portlet  
 </c:set>  
 <script type="text/javascript">  
      var ${angularPortlet} = (function() {  
                var portal = {};  
                portal.portletName = "${portletName}";  
                portal.portletNamespace = "<portlet:namespace/>";  
                portal.resourceURL = "<portlet:resourceURL/>";                 
                portal.contextPath = "<c:url value='/'/>";                 
                return portal;  
        })();         
 </script>  
 <script src="<%=request.getContextPath()%>/resources/js/jquery-1.11.3.min.js"></script>  
 <script src="<%=request.getContextPath()%>/resources/js/angular.min.js"></script>  
 <script src="<%=request.getContextPath()%>/resources/js/app.js"></script>  
 <!-- Manually bootstrap angularjs application. -->  
 <script type="text/javascript">  
           angular.module('angularjs-App').constant("PORTAL", ${angularPortlet});            
           angular.element(document).ready(function() {  
         angular.bootstrap(document.getElementById("<portlet:namespace/>angularjsApp"), ['angularjs-App']);   
       });                  
 </script>  


  1. After the normal scaffolding taglibs, define a div to be used as the application container, using the portlet:namespace tag to make it unique;
  2. Add our directive (can be angularJS view or whatever) inside the div;
  3. Set the portlet name as a variable so we can use it in our js;
  4. Create the constants to be used in our angular application. The constants include server-side-rendered URLs to the serverResource and other entry points;
  5. Manually bootstrap the angular application (ng-app doesn't work for more than one portlet on the page).

Create the angular application

Our example project consists of a single input box with a button. You type in what you want to send to the portlet and the result is echoed next to the button.

Disclaimer: for large apps you typically break down your controllers, services, directives etc into separate files; I grouped it for simplicity.

 angular.module('angularjs-App', [])  
 /**  
  * A simple service that'll call the portlet serveResource with our data.  
  * Note the setting of the Headers and data as parameters: this is required to make sure your data ends up in the resource request getParameter()  
  */  
 .factory('callPortletBackend', ['$http', 'PORTAL', function($http, PORTAL) {  
      return {  
           /**  
            * @param {String} an ID you want to pass to the portlet. This allows the portlet to handle >1 types of requests.  
            * @param {String} the data you want to pass in as a js object.  
            * @param {Object} The callback function once call is completed. The function takes in the result object etc (see angularjs $http).  
            */  
           submitToPortlet: function(id, data, handler) {                 
                var objectToSubmit = {id: id, data: data};  
                console.log('calling portlet with data ' + data);  
                $http({  
                     method: 'POST',  
                     url: PORTAL.resourceURL,  
                     headers: {'Content-Type': 'application/x-www-form-urlencoded'},  
                     data: $.param({                           
                          'jsondata': JSON.stringify(objectToSubmit)  
                     })  
                }).then(handler);  
           }  
      }  
 }])  
 /**  
  * Simple directive with input text and a button, to submit some stuff to the portlet.  
  */  
 .directive('callPortlet', ['callPortletBackend', 'PORTAL', function(callPortletBackend, PORTAL) {  
      return {  
           templateUrl: PORTAL.contextPath + '/resources/html/templates/callportlet.html',  
           restrict: 'E',  
           scope: false,  
           bindToController: true,  
           controllerAs: 'callportletCtrl',  
           controller: function() {  
                var thisCtrl = this;  
                thisCtrl.resultMessage = 'Nothing yet...';  
                thisCtrl.submitToPortlet = function() {  
                     callPortletBackend.submitToPortlet('myData', thisCtrl.dataToSubmit, function(result) {                                                    
                          thisCtrl.resultMessage = result.data;  
                     })  
                };  
           }  
      }  
 }]);  


  1. Create a service that calls the portlet's serveResource (resourceURL), passing in the data as JSON as a url parameter called 'jsondata'. Note how the PORTAL constant is used to get to the resourceURL.
  2. Create a directive that uses the service to post whatever you typed into the input (the template can be found here)
  3. Note the use of PORTAL.contextPath to load the directive's template from the portlet project serverside.

Deploy and test

That is it! The angular $http service will call the portlet serveResource with some JSON and display the JSON results back to the user. Forgive the styling.

ws-angular-portlet ui

Conclusion

We demonstrated how to create a portlet, running in (at least) Websphere Portal 8.x with an angularjs UI, using the portlet serveResource to make AJAX calls.

If you're running Liferay, please have a look at this project from planetsizebrain.

3 comments:

  1. Ty. Roan. theres a way to do JSR 286 Portlet work with Primefaces 6.0 ?

    ReplyDelete
    Replies
    1. I've done JSF with JSR286 Portlets before, but because it is essentially a serverside framework, you need to be able to tie in with the Portlet lifecycle. To do this, you need a portlet bridge.

      Have a look at MyFaces' bridge: https://myfaces.apache.org/portlet-bridge/

      or google for JSR-329.

      Also as an aside, PrimeFaces also has PrimeNG for angular :)

      Delete