20231106

How to make your Portlet reactive with Vue.js and Axios

Some people say, portlet development is out-of-date... but why should you care about that? Maybe you still have an integrated portal solutions like HCL (former IBM) WebSphere or Liferay running, while trying to not loose connection to modern frontend development..

I have found, that especially when it comes to building user-interfaces with a modern user-experience, portlet developers can easily make a paradigm-shift and start migrating towards another architecture with relatively little effort.

This post aims at helping You to integrate Vue.js into your legacy system based on IBM WebSphere Portal Server 8.5, but conceptually it should also work on any other portal framework. You can use it seamlessly in parallel to your existing code for serving Single Page Applications. From there you can move forward.

1. The container principle

Let's jump right into our framework.jsp, which is being rendered on doView:
<%@page session="false" contentType="text/html" pageEncoding="UTF-8"%>

<%@page import="de.myportal.pa_myportlet.PA_MyPortlet"%>

<%@include file="react.jspf"%>

<%@taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%><portlet:defineObjects>

<div id="<portlet:namespace />myportlet">
    <jsp:include page="<%=PA_MyPortlet.Apps.MY_APP_JSP %>"></jsp:include>
</div>

<style>
#<portlet:namespace />myportlet {
  box-sizing: border-box;
  font-family: "Consolas", Arial, sans-serif;
}
#<portlet:namespace />myportlet * {
  box-sizing: inherit; 
}
</style>

This is the framework for our reactive portlet. Essential functionality is provided by the Java Server Page (JSP) fragment react.jspf, which we will discuss next. For now, just note that we establish access to portlet objects by using <portlet:defineObjects>. With this we generate the portlet namespace on the server-side and use the DIV #<portlet:namespace />myportlet as a reusable container for which a basic CSS rule-set should hold.

We need to define a starting-point, otherwise the Portlet won't do much: Our component MY_APP_JSP is being included as a JSP SPA on the server-side.

2. Adding reactive functionality

Before we delve deeper, let's have a look at the react.jspf file that was statically included into our framework:
<%@taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%><portlet:defineObjects />

<script src='<%=renderResponse.encodeURL(renderRequest.getContextPath() + "/js/vue.js")%>' ></script>
<script src='<%=renderResponse.encodeURL(renderRequest.getContextPath() + "/js/axios.min.js")%>'></script>

<script type="text/javascript">
  function getAsync(url) {
    return axios({
      method: 'GET',
      url
    }).then((response) => response.data)
  }
  function postAsync(url, data) {
    return axios({
      method: 'POST',
      url,
      data
    }).then((response) => response.data)
  }
  function includeMarkup(targetID, markup) {
    var target = document.getElementById(targetID);
    target.innerHTML = markup;
    reloadScriptTags(target);
  }
  function reloadScriptTags(target) {
    [].map.call(target.getElementsByTagName('script'), function(oldscript) {
      var newscript = document.createElement('script');
      newscript.type = 'text/javascript';
      newscript.innerHTML = oldscript.innerHTML;
      if (oldscript.src) {
        newscript.src = oldscript.src;
      }
      oldscript.parentElement.removeChild(oldscript);
      target.appendChild(newscript);
    });
  }
</script>

This fragment includes the Vue.js and Axios library as well as a few JavaScript functions. They are used for injecting asynchronously served JSON or other server-side rendered JSP SPAs. Axios is used by the asynchronous GET and POST methods and returns a Promise itself. We will see how this works in the example app below. You can use the includeMarkup method to inject the markup to a target. Please note, that every script tag in the markup needs to be included by using the HTML DOM appendChild method to the target, otherwise the scripts won't work.

3. Creating reactive Single Page Applications

Now let's see how our component looks like. First we look at the frontend in myApp.jsp:
<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@page import="de.myportal.pa_myportlet.PA_MyPortlet"%>

<%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%><portlet:defineObjects />

<portlet:resourceURL id="<%=PA_MyPortlet.class.getSimpleName() %>" var="dataURL" />
<portlet:resourceURL id="<%=PA_MyPortlet.class.getSimpleName() %>" var="jspURL">
    <portlet:param name="jsp" value="<%=PA_MyPortlet.Apps.ANOTHER_APP_JSP%>" />
</portlet:resourceURL>

<div id="<portlet:namespace />myApp">
  <p>Hello, <span v-if="name">{{ name }}</span><span v-else>world</span>!</p>
  <label for="name">Name: </label><input id="name" v-model="name" type="text"><br />
  <input type="button" value="Get age" @click="send"> <input type="button" value="Load another app" @click="loadAnotherApp">
  <p v-if="age">You are {{ age }} years old.</p>
</div>

<script>
var myApp = new Vue({
  data: function() {
    return {
      name : null,
      age : null
    }
  },
  methods: {
    send: function() {
        postAsync('<%=dataURL.toString() %>', myApp.$data).then((json) => this.processResponse(json));
    },
    loadAnotherApp: function() {
        getAsync('<%=jspURL.toString()%>', myApp.$data).then((markup) => includeMarkup('<portlet:namespace />myportlet',markup));
    },
    processResponse: function(json) {
      if (json.age) {
        this.age = json.age;
      }
    }
  },
  el: '#<portlet:namespace />myApp'
})
</script>

</style>
  /* custom styles for the app go here */
</style>

Now we can use our asynchronous functions from 2. to POST requests with myApp's JSON data to the backend and inject the received data back into into our Vue.js component. We can also include markup of other JSPs to our framework container <portlet:namespace />myportlet. No more page reloads, no more blocking requests. Do you feel the flow already?

4.Building the backend

The working of this famework is expecially based on the JSR-286 serverResource method of our PA_MyPortlet.java class. Let's have a look at how this works.
package de.myportal.pa_myportlet;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;

import javax.portlet.GenericPortlet;
import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;

import com.ibm.json.java.JSONObject;

public class PA_MyPortlet
    extends GenericPortlet {

    private static final String PATH_TO_JSPS  = "/WEB-INF/jsp/";
    private static final String PATH_TO_APPS  = PATH_TO_JSPS + "apps/";
    private static final String ERROR_JSP     = PATH_TO_JSPS + "error.jsp";
    private static final String FRAMEWORK_JSP = PATH_TO_JSPS + "framework.jsp";

    public class Apps {
        public static final String MY_APP_JSP      = PATH_TO_APPS + "myApp.jsp";
        public static final String ANOTHER_APP_JSP = PATH_TO_APPS + "anotherApp.jsp";
    }

    public void init()
        throws PortletException {
        super.init();
    }

    public void doView(RenderRequest request, RenderResponse response)
        throws PortletException, IOException {
        getPortletContext().getRequestDispatcher(FRAMEWORK_JSP).include(request, response);
    }

    public void serveResource(ResourceRequest request, ResourceResponse response)
        throws PortletException, IOException {
        try {
            if (!(this.getClass().getSimpleName()).equals(request.getResourceID()))
                throw new Exception("The resourceID must fit the Portlet name.");

            final JSONObject json = poop(dope(request, response));

            final String jsp = request.getParameter("jsp");
            if (jsp != null) {
                response.setContentType(MediaType.TEXT_HTML);
                // request.setAttribute(JSONObject.class.getSimpleName(), json);
                getPortletContext().getRequestDispatcher(jsp).include(request, response);
            } else {
                response.setContentType(MediaType.APPLICATION_JSON);
                response.getWriter().println(json.toString());
            }
        } catch (Exception e) {
            // e.printStackTrace();
            request.setAttribute(Exception.class.getSimpleName(), e);
            getPortletContext().getRequestDispatcher(ERROR_JSP).include(request, response);
        }
    }

    private JSONObject dope(ResourceRequest resourceRequest, ResourceResponse resourceResponse)
        throws UnsupportedEncodingException, IOException {
        JSONObject json = new JSONObject();
        if (resourceRequest.getMethod().equals(HttpMethod.POST)) {
            String data = resourceRequest.getReader().readLine();
            json = JSONObject.parse(data);
        }
        final Enumeration<String> params = resourceRequest.getParameterNames();
        while (params.hasMoreElements()) {
            final String param = params.nextElement();
            json.put(param, resourceRequest.getParameter(param));
        }
        return json;
    }

    private JSONObject poop(JSONObject json) {
        json.put("age", 34);
        return json;
    }
}

Most frontend components will need at least some backend counter-part for serving user-data. In our portlet environment we have the two cases of serving data or serving JSPs. In the dope function we collect the params together with the POST-data from our request and pass it to the poop function afterwards.  After processing the requested data we send it back to the client as a JSON String. If the request has a "jsp" param, we serve the compiled JSP fragment instead.

So, that's all. If you are not dependent on portlet-security and the like, you could also use a servlet. Then implement authentication and authorization with JSON Web Token (JWT) and deliver data from a NoSQL database via GraphQL requests, but that's just one possible outlook.

No comments:

Post a Comment

Please stick to the Netiquette Guidelines and consider asking questions the smart way ..