As an experiment I tried to deploy an existing AngularJS Application on an Liferay Portal Server. Just to see if it would be possible to use HTML 5 + JSON Services for Portlet development too.
It turned out to be quite simple, but it feels a bit weird of course. For example AngularJS uses hashbangs and they make browser navigation suddenly available on the portlet level.
The approach is also limited, because it might be difficult to have multiple HTML 5 portlets on a single portal page. The DOM manipulation would most likely not work properly. But it could be interesting if you provide portlets for mobile devices that run maximized anyway.

Below a step by step guide:

1. Convert the front-end

Let’s say your index.html looks like this:

<!doctype html>
<html ng-app="my-angularjs-app">
  <head>
    <link rel="stylesheet" href="css/app.cs">
  </head>
  <body>
    <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

    <script type="text/javascript" src="js/app.js"></script>
  </body>
</html>

To run this as a portlet you have to do the following:

  1. Create a Java Web-Application as usual and copy all your resources into it
  2. In the document root create a view.jsp and copy everything within the body of index.html into it
  3. Wrap the content of view.jsp with an additional div layer and add the attribute ng-app=”my-angularjs-app”
  4. Create the usual portlet configuration files under WEB-INF
  5. Configure Liferay to add the Javascript to the footer of every portal page that contains the portlet
  6. Configure Liferay to add the Stylesheet to the header section of every portal page that contains the portlet

Here the view.jsp:

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

<portlet:defineObjects />

<div ng-app="my-angularjs-app">

  <!-- The body of index.html -->
  <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

</div>

The portlet.xml and liferay-display.xml config files are straightforward:

<?xml version="1.0"?>
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0">
    <portlet>
        <portlet-name>my-angularjs-app-portlet</portlet-name>        
        <display-name>my-angularjs-app-portlet</display-name>
        <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>        
        <init-param>
            <name>view-jsp</name>
            <value>/view.jsp</value>
        </init-param>
        <expiration-cache>0</expiration-cache>
        <supports>
            <mime-type>text/html</mime-type>
        </supports>
    </portlet>
</portlet-app>
<?xml version="1.0"?>
<!DOCTYPE display PUBLIC "-//Liferay//DTD Display 6.1.0//EN" "http://www.liferay.com/dtd/liferay-display_6_1_0.dtd">
<display>
    <category name="AngularJSPortlets">
        <portlet id="my-angularjs-app-portlet" />
    </category>
</display>

In liferay-portlet.xml we need to include the javascripts and stylesheets from the index.html:

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">
<liferay-portlet-app>
	<portlet>
		<portlet-name>my-angularjs-app-portlet</portlet-name>
		<header-portlet-css>/css/app.css</header-portlet-css>
		<footer-portlet-javascript>/js/app.js</footer-portlet-javascript>		
	</portlet>
</liferay-portlet-app>

If you have multiple template files in your application you might have to fix the path to them, because relative URLs won’t work in Portal environment. The URLs need to point to the context of your deployed application. For example, if you have a route like this:

$routeProvider.when('/page1',
  {templateUrl: 'page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: 'page2.html', controller: 'Page2Ctrl'});

They should now look like this:

$routeProvider.when('/page1', 
  {templateUrl: '/my-application/page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: '/my-application/page2.html', controller: 'Page2Ctrl'});

I know, that’s a bit odd, to hardcode an application context, but you could make the path configurable.

That’s basically it for the front-end. If you deploy your application within a WAR on Liferay you should be able to add your portlet to a portal page.

2. Integrate the back-end

Essentially, there are two possibilities here:

  1. Call remote REST services. Here you could get troubles with the Same-Origin-Policy, but fortunately, AngularJS supports JSONP. Is you choose this solution you’ve nothing else to do.
  2. Deploy the back-end (the services) together with your portlet. You cannot get problems with the Same-Origin-Policy and it is even possible to obtain the currently authenticated portal user in the service code.

I chose option 2, and deployed the Spring MVC REST back-end as part of the portlet. To hide the application context of the portlet and to be able to obtain the shared session attributes of Liferay (e.g. the authenticated user) I’ve used Liferay’s PortalDelegateServlet:

In web.xml of the portlet application:

<servlet>
  <servlet-name>portalDelegateServlet</servlet-name>
  <servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
    <init-param>
      <param-name>servlet-class</param-name>
      <param-value>org.springframework.web.servlet.DispatcherServlet</param-value>
    </init-param>
    <init-param>
      <param-name>sub-context</param-name>
      <param-value>rest-services</param-value>
    </init-param>
    <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:/META-INF/webapp-context.xml</param-value>
    </init-param>        
  <load-on-startup>1</load-on-startup>
</servlet>

If you’ve a service defined like this in Spring MVC:

@RequestMapping("/rest-services")
public class MyService {
  @RequestMapping("/customers")
  public List<Customer> customers() {
    //...
  }
}

You can now access your the service under:

http://<liferay-portal>/delegate/rest-services/customers

Please note: This only works, if the sub-context in the delegate definition matches the path prefix of the services.

The advantage of using the delegate (instead of directly accessing the service in the context of the webapp containing your portlet) is that could can obtain the currently authenticated Liferay user like this:

request.getSession().getAttribute("USER_ID")

3. Alternative approach with Spring Portlet MVC

I also tried to use Spring Portlet MVC to get a more traditional portlet setup. It would also have the advantage that the back-end is actually container authenticated and e.g. request.isInRole() could be used.

The problem was, it required a lot of additional configuration and a specific Portlet controller. Also, “normal” URLs couldn’t be used for service calls, they needed to be constructed as Action URLs like this:

<portlet:actionURL var="serviceURL" >
    <portlet:param name="serviceName" value="myServiceName"/>
</portlet:actionURL>
   // Javascript
   var url = "${serviceURL}";

At the end it was too much effort for me to get it running.

5 thoughts on “Convert an existing AngularJS App into a Liferay Portlet

  1. I am trying to accomplish something similar and need some guidance. I have back-end packaged as rest.war file and I copied it into Liferay deploy directory. I could then call these REST services from Lifray portlets. But rest.war deployed in this way has its own session and Portal authentication obviously cannot be used. Can you share a working ‘hello world’ sample which shows how you “deployed the Spring MVC REST back-end as part of the portlet” ?

    1. I cannot hand out the code since it is based on an app we made for a customer.
      You can of course not share the authentication but as I mentioned in the article it is at least possible to obtain the shared portal attributes such as USER_ID.
      Usually a REST API has its own authentication mechanism, either token based or HTTP digest or something like that. In our case the security it is based on Spring Security and we could just write a custom AuthenticationProvider that automatically authenticates the Liferay portal user. That’s maybe also something you could do.

  2. Hi Juergen,
    I think, what you have posted is enough for someone to recreate a working application. But in my case the application is broken into a single SpringMVC REST project (with Spring security e.t.c.) and several Backbone sub-projects which make http calls to the Controllers in the Spring project. I know that I need to manually convert the Backbone sub-projects into Portlets. But for the Spring project I was hoping to take advantage of the fact that Liferay already does it for you: When you copy a .war into int Liferay’s deploy folder, the necessary artifacts to turn your Spring servlet into a portlet are generated. Unfortunately, this auto-conversion does not include the PortalDelegatServlet stuff. As a result, I can make REST calls to /rest/myService but have to secure these calls myself. I could certainly re-wire my Spring Security to tap into Liferay session but this would still leave me with 2 sessions to maintain. I would rather drop Security from the SpringMVC REST project and have the Service accessible via /delegate/rest/myService which is secured by the Liferay session. I was thinking that this would be accomplished if in the web.xml of my Spring project I changed the tag as described above, added few Liferay jar to my WEB-INF/lib, and then copied the .war into Liferay’s deploy folder. Do you think that something along these lines is feasible?

    1. Hi,
      I’m afraid there is no way to automatically acquire the Liferay authentication in your backend webapp.
      The first problem is that Liferay has its own authentication mechanism and doesn’t use container authentication. Otherwise you could use a SSO facility that comes with most application servers.
      The second problem is that the Liferay authentication is designed to secure portlets and not the web applications that host these portlets. That means, as long as your backend doesn’t implement javax.portlet.GenericPortlet you’ve no chance to see the principal or the roles of the Liferay user.
      What you actually need is SSO mechanism between Liferay and your backend webapp. Liferay supports a few of them (see https://www.liferay.com/de/community/wiki/-/wiki/Main/Single+Sign-on). If you’re using Tomcat OpenSSO could be an option.

Leave a Reply

Your email address will not be published. Required fields are marked *


*