Tuesday, October 8, 2013

Alfresco Portlets in Liferay

Yesterday I wrote a short article about the integration of Alfresco into Liferay. One part of it focused on the Portlets those are part of Alfresco Share. This new article provides some deeper insights.

Let's assume that you deployed the Alfresco portlets to Liferay. So you now have a new web application ${LIFERAY_HOME}/tomcat/webapps/share (Let's name this path ${SHARE}). Under ${SHARE}/WEB-INF the file which is named 'portlet.xml' can be found. A portlet definition looks as the following one:



As you can see Alfresco provides a ProxyPortlet class in order to view a Share Web Script. The next interesting file is the one which is named 'liferay-portlet.xml'. It references the ShareMyDocLibs portlet:

<liferay-portlet-app>
        <portlet>
                <portlet-name>ShareMyDocLibs</portlet-name>
                <user-principal-strategy>screenName</user-principal-strategy>
        </portlet>
        ...
</liferay-portlet-app>

There is a third configuration file which is named 'liferay-display.xml'. It defines the Alfresco application category:

<display>
        <category name="Alfresco">
                <portlet id="ShareMyDocLibs"></portlet>
                ...
        </category>
</display>
To access the portlet also a servlet definition in the 'web.xml' configuration file is required:

...
       <servlet>
                <servlet-name>ShareMyDocLibs Servlet</servlet-name>
                <servlet-class>com.liferay.portal.kernel.servlet.PortletServlet</servlet-class>
                <init-param>
                        <param-name>portlet-class</param-name>
                        <param-value>org.alfresco.web.portlet.ProxyPortlet</param-value>
                </init-param>
                <load-on-startup>1</load-on-startup>
        </servlet>
...
       <servlet-mapping>
                <servlet-name>ShareMyDocLibs Servlet</servlet-name>
                <url-pattern>/ShareMyDocLibs/*</url-pattern>
        </servlet-mapping>


So in order to include your own Alfresco portlet, it should be sufficient to add the entries to the just mentioned descriptor files. But let's now investigate the ShareMyDocLibs script. It is located at '${SHARE}/WEB-INF/classes/alfresco/site-webscripts/org/alfresco/components/my-documentlibraries' and just follows the WebScript pattern. (More about WebScripts: http://ecmgeek.blogspot.de/2012/06/simple-pinboard-dashlet.html ). So it has a descriptor, a controller and in this case an HTML view. Let's create a very simple 'Hello World' Dashlet (Do not mix it up with Portlet: A dashlet is a mini app which is living on Alfresco Share's Dashboard. A Dashlet is also only a Web Script in Share. A Portlet is a mini app which is living in a Portlet Container, like Liferay. An JSR168 portlet can run in other Portlet Containers, too. Unlike Dashlets, Portlets are a kind of standardized. However, Alfresco provides this ProxyPortlet which allows to show Dashlets in Liferay.). So let's start by adding a Dashlet to '/WEB-INF/classes/alfresco/site-webscripts/org/alfresco/components/dashlets'. Let's name it 'my-liferay-portlet':

  • Create a WebScript descriptor file 'my-liferay-portlet.get.desc.xml'
<webscript>
    <shortname>A simple Liferay portlet</shortname>
    <description>Says Hello world!</description>
    <family>site-dashlet</family>
    <format default="html">extension</format>
    <url>/components/dashlets/my-liferay-portlet</url>
</webscript>
  • Create a Freemarker HTML template which has the name 'my-liferay-portlet.get.html.ftl'
<div>
<h3> My Liferay portlet </h3>
<br/>
<b> ${result} </b>
</div>
  • Create a JavaScript controller which sets the model. It should be named 'my-liferay-portlet.get.js'
model.result = "Hello world!";

  •  Navigate to 'http://localhost:8080/share/service/index' and click on the 'Refresh Web Scripts' button. If this does not work for you, just restart your Liferay installation.
  •  Your Liferay embedded Share is available via 'http://localhost:8080/share/' whereby 'http://localhost:8080' is the URL to reach your Liferay installation. (Further details in my previous article: http://ecmgeek.blogspot.de/2013/10/how-to-access-alfresco-repository-from.html ). So log-in to Share and add your Dashlet to a Site Dashboard to make sure that it works. The result in Share looks like that (As you can see, I did not add the Alfresco specific Dashlet border because that would look a little bit confusing in Liferay):
  • Now let's add the Dashlet as a portlet by editing the following files as mentioned above (Just copy the blocks of the ShareMyDocLibs portlet by mapping to the new Web Script's URL. Do not forget to restart your Liferay installation afterwards):
    • liferay-portlet.xml
 <portlet>
                <portlet-name>MyLiferayAlfrescoPortlet</portlet-name>
                <user-principal-strategy>screenName</user-principal-strategy>
 </portlet>
    • liferay-portlet.xml
 <portlet>
                <description>Alfresco: My Liferay Portlet</description>
                <portlet-name>MyLiferayAlfrescoPortlet</portlet-name>
                <portlet-class>org.alfresco.web.portlet.ProxyPortlet</portlet-class>
                <init-param>
                        <name>viewScriptUrl</name>
                        <value>/page/components/dashlets/my-liferay-portlet</value>
                </init-param>
                <supports>
                        <mime-type>text/html</mime-type>
                        <portlet-mode>VIEW</portlet-mode>
                </supports>
                <portlet-info>
                        <title>Share: My Document Libraries</title>
                        <short-title>My Document Libraries</short-title>
                </portlet-info>
                <security-role-ref>
                        <role-name>administrator</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>guest</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>power-user</role-name>
                </security-role-ref>
                <security-role-ref>
                        <role-name>user</role-name>
                </security-role-ref>
 </portlet>
    • liferay-display.xml
<display>
        <category name="Alfresco">
                <portlet id="ShareMyDocLibs"></portlet>
                <portlet id="ShareSiteDocLib"></portlet>
                <portlet id="ShareRepoBrowser"></portlet>
                <portlet id="MyLiferayAlfrescoPortlet"></portlet>
        </category>
</display>

    • web.xml

  • Then add the new portlet to a Liferay page. The resulting portlet looks like that:


So finally I can say, that integrating Alfresco with Liferay works good for you if you are an Alfresco Developer. Then Alfresco's ProxyPortlet already solves problems like the authentication via Liferay for you. If you know how to develop Alfresco Dashlets, then you also know how to develop Alfresco Portlets out of the box. I think that I would not recommend to use the existing Portlets like the 'MyDocumentLibraries' one, because it just breaks with Liferay at some points. It forwards you to the Alfresco Document Library and as soon as you click the wrong link, it then forwards you to standalone Share application. So a compromise could be:
  • Deploy Share to Liferay, but do not use the example Portlets
  • Create Dashlets for Share those work without back links to the rest of the Share UI
  • Make these Dashlets available as Portlets in Liferay

Monday, October 7, 2013

How to access the Alfresco repository from Liferay Portal

A few years ago I was responsible for some Partner Certfication projects for the database system vendor Ingres. Two of the partners were Alfresco and Liferay. So we investigated how to combine them best. The answer seemed to be a portlet which accesses Alfresco, by just using Alfresco as the Content Repository. A few years later I am now investigating it again from the point of view of a Consultant. So I evaluated some well documented ways to integrate the DMS Alfresco into the Portal System Liferay. The result is not that bad, but not really what I wanted.

There are basically two approaches. The first one is to embed Alfresco's UI (Alfresco Share) as a Dashlet. This has challenges and raises questions. For instance: What's about permissions within the Content Repository? If you know Alfresco, then you also know that it comes with 2 tiers aka 2 web applications. The application Share is repsobsible to show you the UI, whereby the application Alfresco is used to provide you the repository layer. The interesting part of this story is that it is just possible to deploy the applications seperated. So Alfresco Share is just a WebApp which performs REST to the service layer in order to show some data. Share's endpoint configuration allows you to configure which Alfresco Repository should be used. Exactly this, let's name it feauture, is helpful to integrate Share with Liferay. Basically, the whole Share applications gets deployed to Liferay. Share is then configured to access the repository layer which is hosted somewhere else. Alfresco Share is build on top of the Spring SURF framework. Which means that Web Scripts are used in order to show something (like Dashlets ... do not mix it up with Portlets). So as far as I understood it, several of those UI Web Scripts are made available as Portlets by having a Portlet descriptor within the Share application. This seems to be OK and also has advantages. Another more Portal System like idea would be to have a standard portlet (JSR 168, or maybe one which is a little bit more Liferay specific) which then performs the CMIS call to the underlying repository. The last metioned approach has some disadvantages regarding document focused customizations (So documents can have types with several properties and so on. So the portlet needs to be configured which properties of which document type should be presented to the end user. I guess this can become a kind of complex, and so Alfresco decided to just provide Share itself - which can be easily customized - to realize the portlets.). But to use Share as a Portlet causes another problem. Share requires that the user is authenticated and has a specific role (Site Manager, Site Contributor, ...) within the Alfresco repository in order to access documents. The way how this integration solved it, is to extend the Alfresco configuration by adding an extra external Authentication Subsystem. The Share application which is deployed into Liferay was configured to use this external and Cookie based authentication. When a Liferay user logs in the first time, a new Alfresco user with the same name is created within Alfresco, whereby the authentication itself happens in Liferay and not in Alfresco. I think this sufficient for demonstration or evaluation purposes, but not really an Enterprise scenario. In such a scenario you would have a standalone Alfresco repository which uses an LDAP or AD for authentication purposes. Every LDAP user would be already in Alfresco. So an interesting question would be if this external authentication integrates well with other authentication mechanisms. So the idea is that Liferay has exactly the same users synchronized as Alfresco, and so it is not required to recreate the user in Alfresco. This way, the Alfresco user could be also a kind of preconfigured regarding his/her site memberships in Alfresco. So here are the steps how to embed Alfresco Share into Liferay by accessing the Alfresco remote repository (https://www.liferay.com/de/web/navin.agarwal11/blog/-/blogs/integration-with-alfresco-4-x-and-liferay-6-1):


  • We assume that you installed Alfresco and Liferay not in the same Tomcat Application Container. So they have both different port settings. I installed at first Alfresco by choosing port 8081 and then Liferay by keeping port 8080.
  • In ${LIFERAY_HOME}/tomcat/conf/catalina.properties configure an additional classpath location ${LIFERAY_HOME}/tomcat/shared/classes in order to have a place where you can add Alfresco's overriding configuration files (The XML and property files are used to override the default configuration.). To do so add the following line: shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar
  • In Alfresco's global configuration file ${ALF_HOME}/tomcat/shared/classes/alfresco-global.properties extend the authentication chain by adding the external authentication configuration: 
authentication.chain=alfrescoNtlm1:alfrescoNtlm,external1:external
external.authentication.proxyUserName=
  • Copy the share.war file to ${LIFERAY_HOME}/deploy
  • Copy the folder '${ALF_HOME}/tomcat/shared/classes/alfresco/web-extension' and all it's contents to '${LIFERAY_HOME}/tomcat/shared/classes/alfresco'
  • Modify the file  '${LIFERAY_HOME}/tomcat/shared/classes/alfresco/share-config-custom.xml' in order to configure the endpoints.
<alfresco-config>
   <!-- Repository Library config section -->
   <config evaluator="string-compare" condition="RepositoryLibrary" replace="true">
      <!--
         Whether the link to the Repository Library appears in the header component or not.
      -->
      <visible>true</visible>
   </config>
   <config evaluator="string-compare" condition="Remote">
      <remote>
         <endpoint>
            <id>alfresco-noauth</id>
            <name>Alfresco - unauthenticated access</name>
            <description>Access to Alfresco Repository WebScripts that do not require authentication</description>
            <connector-id>alfresco</connector-id>
            <endpoint-url>http://localhost:8081/alfresco/s</endpoint-url>
            <identity>none</identity>
         </endpoint>
        <!-- Define a new connector -->
        <connector>
                <id>alfrescoCookie</id>
                <name>Alfresco Connector</name>
                <description>Connects to an Alfresco instance using cookie-based authentication</description>
                <class>org.springframework.extensions.webscripts.connector.AlfrescoConnector</class>
        </connector>
        <!-- Use the Cookie based authentication by default -->
         <endpoint>
            <id>alfresco</id>
            <name>Alfresco - user access</name>
            <description>Access to Alfresco Repository WebScripts that require user authentication</description>
            <connector-id>alfrescoCookie</connector-id>
            <endpoint-url>http://localhost:8081/alfresco/wcs</endpoint-url>
            <identity>user</identity>
            <external-auth>true</external-auth>
         </endpoint>
<endpoint>
            <id>alfresco-feed</id>
            <name>Alfresco Feed</name>
            <description>Alfresco Feed - supports basic HTTP authentication via the EndPointProxyServlet</description>
            <connector-id>http</connector-id>
            <endpoint-url>http://localhost:8081/alfresco/s</endpoint-url>
            <basic-auth>true</basic-auth>
            <identity>user</identity>
         </endpoint>
         <endpoint>
            <id>activiti-admin</id>
            <name>Activiti Admin UI - user access</name>
            <description>Access to Activiti Admin UI, that requires user authentication</description>
            <connector-id>activiti-admin-connector</connector-id>
            <endpoint-url>http://localhost:8081/alfresco/activiti-admin</endpoint-url>
            <identity>user</identity>
         </endpoint>
      </remote>
   </config>
</alfresco-config>

  • Restart Alfresco
  • Restart Liferay
  • Open Liferay, create a new Page and add a new Application for it. You can see that there is a new Application category which is named Alfresco. These are your Portlets.
  • BTW: You can find the portlet descriptor (portlet.xml) within Share at ${SHARE}/WEB-INF
The following YouTube video explains the result and the usage best: http://www.youtube.com/watch?v=On7SfssX8TI

If you know Liferay, then you may think 'Hey, Liferay already has DMS functionality. Why should I use another Content Repository?'. The answer is a statement like 'The right tool for the right job.'. Liferay is a Portal System. It is intended to provide you an unified view to your company's (or organization's) Tools, Documents and Web Content. Alfresco was orignally more a pure Document Management System. The purpose was to provide you easy access to your content/document together with structured information about it. (and other cool features like document oriented workflows, document behaviours, content rules, ...) So even if there was a convergence of the two systems, I would still say: 'Use Liferay for Portals and Intranets and use Alfresco to manage Documents (their Properties, their Life Cycles and the Processes of them)'. So it makes just sense to combine Liferay and Alfresco. But the question 'Why should I not use the Liferay Document Library?' is still valid in the sense of 'Why should Alfresco not be integrated into the Liferay Document Library?'. So the answer is 'It should!' and the following part of this article will show how. There is a very cool feature in Liferay which allows you to just add new CMIS repositories (You can find some details about CMIS in my blog, but it basically means a more or less standardized protocol to access Content Repositories). The way how it should work is described here: http://www.liferay.com/de/web/alexander.chow/blog/-/blogs/7670631 . Unfortunatly, it was impossible for me to test this feature because Liferay just refused to add my Repo with an error message like 'Please check your repository configuration.'. Maybe it works better in a later version. However, I got it running by adding my CMIS configuration to Liferay's external configuration file. Here are the steps:

  • In '${LIFERAY_HOME}/tomcat/webapps/ROOT/WEB-INF/classes/portal-ext.properties' add the following lines:

## CMIS
#
dl.store.impl=com.liferay.portlet.documentlibrary.store.CMISStore
dl.store.cmis.credentials.username=admin
dl.store.cmis.credentials.password=admin
dl.store.cmis.system.root.dir=Liferay Home
#dl.store.cmis.repository.url=http://localhost:8081/alfresco/cmisatom
dl.store.cmis.repository.url=http://localhost:8081/alfresco/service/cmis
session.store.password=true
company.security.auth=screenName

  • Important is that the screenName is used to authenticate. Alfresco uses normally not the email address as the user name. So the screen name should match the user id.
  • The CMIS AtomPub service is available via '/alfresco/service/cmis'. With Alfresco 4.x an additional JSON CMIS binding was introduced, and so there is now also the 'alfresco/cmisatom' service.
  • Log in to Liferay, open the Control Panel, choose the Global settings and add a new root folder which is named 'Liferay Home', then upload a test document.
  • Open Alfresco Share and navigate to the repository view. You can find a 'Company Home/Liferay Home' folder. 
The bad part of the story is that Liferay seems to use the CMIS repository the same way as a file system. It creates some cryptical folder names (maybe internal id-s) and places files with the extension 'bin' inside them. So the documents are not human readable in Alfresco. As far as I understood, this was not really the intend of CMIS (Here a short JavaDoc excerpt of the Java Client OpenCMIS: http://chemistry.apache.org/java/0.9.0/maven/apidocs/org/apache/chemistry/opencmis/client/api/Folder.html ).

So what are the options? There is still the option to develop an own Portlet. It could use CMIS or Alfresco's REST API. Another option is to develop an Alfresco Hook which goes beyond Liferay's CMIS Hook. A Hook (in Liferay) is an extension which uses an existing extension point. As far as I understood it is possible to replace the whole Document Library provider by using an own implementation.



What do you think? Any success stories about Alfresco & Liferay integrations?

Tuesday, October 1, 2013

Custom Share Actions with Forms

Let's imagine that you work on a task which should allow you to show a specific form by clicking on an action. So this tutorial shows how to achive this. For demo purposes we use an 'Edit title' action.

The action could look like the following one and needs to be configured in your 'share-config-custom.xml' file:

   <config evaluator="string-compare" condition="DocLibActions">
        <actions>
            <action id="my-edit" type="javascript" label="Edit title">
                <param name="function">onMyEdit</param>
            </action>
         </actions>
       
         <actionGroups>
            <actionGroup id="document-browse">
                <action index="101" id="my-edit"/>
            </actionGroup>
         </actionGroups>
   </config>

As you can see there is a JavaScript function bound to this Document Library Action. So we need to include a JavaScript file. Because we are currently focusing on the Document Library we could just override the 'actions-common.get.head.ftl' by copying it to '$TOMCAT_HOME/shared/classes/alfresco/web-extension/site-webscripts/org/alfresco/components/documentlibrary'. There we can see which JS files are already included. One of these files is for instance '${page.url.context}/res/js/documentlibrary-actions-min.js'. So it would be possible to add an addtional <script> tag to this head freemarker template. However, the better way is to use the following configuration in the 'share-config-custom.xml' file:

<config evaluator="string-compare" condition="DocLibCustom" replace="true">
      <dependencies>
            <js src="components/myactions/actions.js"/>
      </dependencies>
 </config>


In the next step, we have to provide the JavaScript function for the 'my-edit' action . For this purpose we just create a new JavaScript file in '$TOMCAT_HOME/webapps/share/components/myactions/actions.js'. It registers the 'onMyEdit' function.

(function()
{

    var mydlA_onMyEdit = function(record)
    {
         var scope = this,
            nodeRef = record.nodeRef,
            jsNode = record.jsNode;

         // Intercept before dialog show
         var doBeforeDialogShow = function mydlA_onMyEdit_doBeforeDialogShow(p_form, p_dialog)
         {
            // Dialog title
            var fileSpan = '<span class="light"> MyEdit </span>';

            //var dialogTitleSuffix = "-dialogTitle";
            var dialogTitleSuffix = "-form-container_h";
           
            Alfresco.util.populateHTML(
               [ p_dialog.id + dialogTitleSuffix , fileSpan ]
            );
         };

         var templateUrl = YAHOO.lang.substitute(Alfresco.constants.URL_SERVICECONTEXT + "components/form?itemKind={itemKind}&itemId={itemId}&destination={destination}&mode={mode}&submitType={submitType}&formId={formId}&showCancelButton=true",
         {
            itemKind: "node",
            itemId: nodeRef,
            mode: "edit",
            submitType: "json",
            formId: "my-edit-form"
         });

         // Using Forms Service, so always create new instance
         var myEdit = new Alfresco.module.SimpleDialog(this.id + "-myedit-" + Alfresco.util.generateDomId());

         myEdit.setOptions(
         {
            width: "auto",
            templateUrl: templateUrl,
            actionUrl: null,
            destroyOnHide: true,
            doBeforeDialogShow:
            {
               fn: doBeforeDialogShow,
               scope: this
            },
            onSuccess:
            {
               fn: function mydlA_onMyEdit_success(response)
               {
                  var successMsg = this.msg("Success!");
                 
                  Alfresco.util.PopupManager.displayMessage(
                  {
                     text: successMsg
                  });
               },
               scope: this
            },
            onFailure:
            {
               fn: function mydLA_onMyEdit_failure(response)
               {
                  var failureMsg = this.msg("Failure!");
                
                  Alfresco.util.PopupManager.displayMessage(
                  {
                     text: failureMsg
                  });
               },
               scope: this
            }
         });
       
         myEdit.show();
     };


    YAHOO.Bubbling.fire("registerAction",
    {
        actionName: "onMyEdit",
        fn: mydlA_onMyEdit
    });
})();

What we finally need is to define a form for our custom edit dialog. This happens also by using the configuration file 'share-config-custom.xml':

<config evaluator="node-type" condition="cm:content">
      <forms>
        <form id="my-edit-form">
            <field-visibility>
               <show id="cm:title" force="true" />
            </field-visibility>
            <appearance>
                <field id="cm:title">
                  <control template="/org/alfresco/components/form/controls/textfield.ftl" />
               </field>
            </appearance>
         </form>
      </forms>
</config>

Voila! Here a short summary:
  1. Define a Share Custom Action which calls JavaScript
  2. Implement a JavaScript function by registering it with a specific action name. The action name in your JavaScript file matches the function parameter in the Share configuration.
  3. Use the 'Alfresco.module.SimpleDialog' by passing it the form id as a parameter.
  4. Define a form with a specific id.