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.

Tuesday, September 17, 2013

How to install Alfresco Workdesk

The installation of Alfresco Workdesk is already explained in the Alfresco Workdesk documentation. So this article aims to be a short summary of it.

Let's assume that we already have a running Alfresco instance. Even the Alfresco Community Edition will do it. The default installation of Alfresco already comes with Apache Tomcat. So we can just use this container for evaluation purposes.

The Alfresco Workdesk bundle comes as an archive which contains the following folders:
  • bin: The Desinger and the Workdesk application
  • db: The database creation scripts
  • doc: Some basic documentation. Further documentation is available, but you have to request it at Alfresco.
  • javadoc: The source code documentation
  • lib: Additional dependencies
The 'bin' folder contains a script 'create_workdesk_war' which creates you a WAR-file from the 'bin/workdesk' folder. Actually it just zips the content of this folder up. However, let's create it. Then perform the follwing steps:
  • Copy the workdesk.war file to $TOMCAT_HOME/webapps
  • Restart your Tomcat Server
  • Open http://${host}:${port}/workdesk
As you can see Workdesk was already successfully deployed and can already be used BUT it is currently configured to use an Alfresco repository (via CMIS) which is hosted in the Cloud. So in the next steps we focus on changing the Workdesk configuration in order to access our local repository.
  • There are some prepared configuration profiles stored in '$TOMCAT_HOME/webapps/workdesk/WEB-INF/conf . Our current installation uses the 'opencmis_trial' profile. To connect to the local repository we need to use the 'opencmis' profile.
  • The profile's root foler 'opencmis' contains a file named 'owbootstrap.xml'.  It contains a section 'EcmAdapter. In order to use our local Repository, you have to adapt the URL in the subsection 'AtomPub'. It looks like the following configuration:
 <EcmAdapter>
 ...
                <AtomPub>http://${host}:${port}/alfresco/cmisatom</AtomPub>
 ...
</EcmAdapter>

  •  Now you have to tell Workdesk to use this configuration profile instead the default one. Therefore it is necessary to edit the file '$TOMCAT_HOME/webapps/workdesk/WEB-INF/web.xml'. The file contains a context parameter definition which is named 'OwResourceFilePath':
<context-param id="OwResourceFilePath">
        <param-name>OwResourceFilePath</param-name>
        <param-value>deploy#WEB-INF/conf/opencmis</param-value>
</context-param>
  •  Alfresco Workdesk seems to require own database tables. I just created the tables inside my Alfresco database. Here are the PostgreSQL statements:
-- CREATE TABLE "OW_ATTRIBUTE_BAG";
CREATE TABLE OW_ATTRIBUTE_BAG
(
  "username" character varying(128) NOT NULL,
  "bagname" character varying(128) NOT NULL,
  "attributename" character varying(256) NOT NULL,
  "attributevalue" character varying(1024) NOT NULL,
  CONSTRAINT "OW_ATTRIBUTE_BAG_pkey" PRIMARY KEY ("username" , "bagname" , "attributename" )
)
WITH (
  OIDS=FALSE
);
-- CREATE TABLE "OW_HISTORY";
CREATE TABLE OW_HISTORY
(
  "eventindex" SERIAL UNIQUE NOT NULL,
  "ow_hist_type" integer NOT NULL,
  "ow_hist_id" character varying(128) NOT NULL,
  "ow_hist_status" integer  DEFAULT NULL,
  "ow_hist_time" timestamp DEFAULT NULL,
  "ow_hist_user" character varying(255) NOT NULL,
  "ow_hist_summary" character varying(2048) DEFAULT NULL,
  "objectdmsid" character varying(255) DEFAULT NULL,
  "objectname" character varying(1024) DEFAULT NULL,
  "parentdmsid" character varying(255) DEFAULT NULL,
  "parentname" character varying(1024) DEFAULT NULL,
  "custom1" character varying(2048) DEFAULT NULL,
  "custom2" character varying(2048) DEFAULT NULL,
  "custom3" character varying(2048) DEFAULT NULL,
  CONSTRAINT "EventIndex_pkey" PRIMARY KEY ("eventindex" )
)
WITH (
  OIDS=FALSE
);
-- CREATE TABLE "OW_ROLE";
CREATE TABLE OW_ROLE
(
  "role_id" SERIAL UNIQUE NOT NULL,
  "role_name" character varying(255) NOT NULL,
  "role_resource" character varying(255) NOT NULL,
  "role_access" integer NOT NULL DEFAULT (-1),
  "role_access_mask" integer NOT NULL DEFAULT (-1),
  "category" integer NOT NULL,
  CONSTRAINT role_id_pkey PRIMARY KEY ("role_id")
)
WITH (
  OIDS=FALSE
);
  • Last but not least, it is required to tell Workdesk which database should be used. Therefore the file '$TOMCAT_HOME/webapps/workdesk/META-INF/context.xml needs to be edited. In my case it has the following content:
 <?xml version="1.0" encoding="UTF-8"?>

<Context path="/workdesk" debug="100" privileged="true" reloadable="true">
  
    <Resource name="java:/PostgreSQLDS"
        auth="Container"
        type="javax.sql.DataSource"
        factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql://localhost:5432/alfresco"
        username="${alf.db.user}"
        password="${alf.db.pwd}"
        removeAbandoned="true"
        maxActive="30"
        maxIdle="10"
        maxWait="1000"
        removeAbandonedTimeout="60"
        logAbandoned="true"/>         
   
</Context>

  • Just restart your Tomcat installation to get the changes applied. Then log-in by using one of your Alfresco users.

The more interesting part will follow. Another article will focus on how to customize Alfresco Workdesk in order manage specific cases.

    Friday, June 14, 2013

    Dynamic web pages with Python

    I just got a Raspberry Pi. This small box has just 700Mhz CPU speed and 512 MB main memory, so you can not expect to be able to run a full featured Enterprise Java Application on it. The Raspian Linux for the Pi is very Python focused and I think there are good reasons for that. Python is object oriented, easy to learn and very flexible and there are a lot of libraries and frameworks around it. This includes for instance Template Engines or Database Access. As a 'light' scripting language it seems also to be less resource consuming. So at least I like Python. So my first Raspberry project was to install an Apache Web Server and the Python Module in order to try out to develop dynamic web sites based on the Raspberry platform. First to say: the setup is very easy. Python comes with it's own distribution system (so with a kind of package manager) which allows you to install everything you need in a few minutes. For things like the Apache Web Server the Debian based package management (apt) should be usded:

    sudo apt-get install apache2
    sudo apt-get install libapache2-mod-python
    sudo apt-get install python-pip (This installs the Python Package Index)
    pip install Jinja2 (This installs the Template Engine Jinja2)
    

    All you need to do to enable your Apache for Python is to edit the configuration file /etc/apache2/sites-available/default by adding the following handler definition to the <Directory /var/www> - section:

     AddHandler mod_python .py
     PythonHandler mod_python.publisher
     PythonDebug On
    

    Now create a folder which is named 'templates' under /var/www . All your templates have to live inside this folder because we are using a File System Template Loader. Inside the template folder the hello.html fille was created for demonstration purposes:

       <html> Hello {{name}} ! </html>
    

    As you can see there is one placeholder 'name'. Now we want to create Python code which uses the template. The following code example explains it:

    ###
    # Apache could be used together with python. To have a clean
    # seperation between the script code behind and the html view
    # a template engine can be used. The following controller script
    # shows how to render a web page based on a model and an HTML
    # template
    ###
    
    #Import the Jinja2 template engine
    from jinja2 import Environment, FileSystemLoader
    
    # Function to handle the reqest
    def index(req):
    
      # Set the environment by using a file system loader
      env = Environment(loader = FileSystemLoader('/var/www/templates'))
    
      # Get the template hello.html
      template = env.get_template('hello.html')
    
      # Return the rendered result
      return template.render(name='Your Name')
    

    Your rendered output is now accessible via http://yourhost/hello.py and should look as the following one:

    Hello Your Name !
    





    Wednesday, March 6, 2013

    Alfresco's Auditing System

    Preamble

    I just had to think about the monitoring of Alfresco. Things like 'Which user logged in how often' or 'Which document was opened how often' were required. My first idea was to develop the following system:
    • Alfresco Share does communicate with the Repository layer via Web Scripts. So every action should cause an HTTP request.
    • A proxy in front of the Alfresco repository which filters requests those are relevant  (E.G. the AuthenticationService)
    • Each matching request should be logged to a database. So the database contains just the HTTP request text and the request parameters.
    • Then it is possible to use the request log to create specific reports
    By further investigating the requirements the question was raised if Alfresco not yet has such a functionality. This is the reason why this article spots some light on the Alfresco Auditing feature. From the first view, this feature includes the previous mentioned idea. I can define Extractors and Generators based on a by path filtering (whereby the path seems to refer to the RESTful service which is used). Further investigations may answer the question if the auditing is suitable to fit the above mentioned requirements.
    • Auditing needs to be enabled.
    • Configure filters.
    • There are DataProducers, DataExtractors and DataGenerators
    • It is possible to define custom AuditApplications
    • The Auditservice is used to retrieve the audit data.
    • The RecordValue element depends on a  DataExtractor by specifying which data trigger and which data source to use
    So now let's try to get behind it

    Configuration
    • To enable auditing you can set 'audit..enabled=true' in the alfresco-global.properties file. The web script under '/api/audit/control' then gives you further details about the state. To enable specific audit applications (see section below) you can set 'audit.${application id}.enabled = true'.
    • Set logging in audit-log.properties: org.alfresco.repo.audit.AuditComponentImpl=DEBUG
    Filters

    Filters are applied to events. The DataProducer is identified by a root path. A DataProducer calls a recordAuditValues method which uses the root path and a audit map. The map contains the information which is relevant for auditing purposes. So if the root path is "/alfresco-access/transactio" then the map contains the values 'action' (E.G. MOVE), 'node' (The target node of the action), 'move/from/node', 'move/to/node', 'sub-actions' and so on.

    It is now possible to define filters in the alfresco configuration file. The format is:
    • audit.filter.${application part of the root path}.${sub path of the root path}.${property in audit map} = ${; seperated list of regular expressions for values those should match}
    So an example is to audit every log-ins of the user jblogs and every user who has an user id which begins with 'd'.
    •  audit.filter.alfresco-access.login.user=jblogs; d.*
    Additionally it's required to enable the audting for a specifc filter
    • audit.filter.alfresco-access.login.enabled=true
    DataProducers

    There are several data producers out of the box available. The documentation says that the 'org.alfresco.repo.audit.access.AccessAuditor' does not resolve any event in detail (preview and download is one single event) whereby the 'AuditMethodInterceptor' producer records seperated events. There is property in the configuration 'audit.alfresco-access.sub-actions.enabled' which seems to be used to tell Alfresco which DataProducer should be used.

    DataGenerators

    A data generator produces output without any input. So data is produced when a specifc path is set as active. The AuthenticatedUserDataGenerator generates data as soon as a person gets authenticated. So the data generator is responsible for generating data dependent on specific events. Such a generator is not the same a DataProducer. It seems that a producer is used to implement the 'Which events should produce data?' and the DataGenerator is used to implement the 'Which data should generated?'.

    A data generator has a registered name or fully qualified class name. In the first case you can access it via the Spring bean id (audit-services-context.xml) in the last case you can reference it via its class name directly. (class or registeredName properties if defining them in the application configuration)

    The documentation says that the 'AccessAuditor' generator writes entries like:

    ${application part of the root path}.${sub path of the root path}.${property in audit map}= ${value in audit map}.
     
    DataExtractors

    It is a component which uses input data to produce some output. As a DataGenerator you can define it in your application configuration by using its registered name or fully qualified class name. Alfresco provides the SimpleValueExtractor (org.alfresco.repo.audit.extractor.SimpleValueDataExtractor). This default extractor just returns the input without any transofrmation. Another examle is the NodeNameDataExtractor which is able to extract the cm:name value of a node. So in summary the extractor is used to implement the 'How to store the previously generated data?'

    Path mappings

    We already mentioned the root path and we also know that our audit entry map contains paths as part of the keys. The path mapping can be used to rewrite these paths. So let's assume you want '/ecgaudit/login' as the path in you entry map instead '/alfresco-api/post/AuthenticationService/authenticate' then you can define the following path mapping:

    <PathMappings>
      <PathMap source="/alfresco-api/post/AuthenticationService/authenticate" target="/ecgaudit/login"
    </PathMappings>

    To following the path conventions, please keep in mind that the first part of the path is the application id.

    Audit Applications

    How exactly auditing behaves depends on the audit application. There is one application provided by Alfresco which is named 'alfresco-access'.

    You can add new audit application configurations to <tomcat>/shared/classes/alfresco/extension/audit directory. Just create an XML file ${application id}.xml inside it.

    Here an example application from the Alfresco Wiki:

      <Application name="DOD5015" key="DOD5015">
            <AuditPath key="login">
                <AuditPath key="args">
                    <AuditPath key="userName">
                        <RecordValue key="value" dataExtractor="simpleValue"/>
                    </AuditPath>
                </AuditPath>
                <AuditPath key="no-error">
                    <GenerateValue key="fullName" dataGenerator="personFullName"/>
                </AuditPath>
                <AuditPath key="error">
                    <RecordValue key="value" dataExtractor="nullValue"/>
                </AuditPath>
            </AuditPath>
        </Application>

    Audit trail

    As mentioned before there is the 'alfresco-access' application. So the default entries in the trail are coming from this application. Database tables are used to store the audit trail. The following columns are visible in the trail: 'user name', 'application', 'method' (which is similar to 'action'), timestamp, entry (as explained before such an entry contains a map of values. How the entry looks like depends on the data generation and extraction).

    AuditService

     The audit service implements the following interface: http://dev.alfresco.com/resource/docs/java/repository/org/alfresco/service/cmr/audit/AuditService.html . Eye catching is that you have to specify an audit query. A query is handled in an AuditQueryCallback (http://dev.alfresco.com/resource/docs/java/repository/org/alfresco/service/cmr/audit/AuditService.AuditQueryCallback.html). The callback has to implement the 'handleAuditEntry' method which gets passed the following parameters:

    • entryId
    • applicationName
    • user
    • time
    • the entry map
     Additionally it seens to be possible to just access the above defined applications RESTfully. If I understood it right, then the audit query is just the following HTTP call.
    • http://localhost:8080/alfresco/service/api/audit/query/${Application id}?${Parameters}
    The documentation mentions the following parameters:

    • verbose = true | false
    • limit = ${The number of last n values to return}
    • forward = true | false
    • toId = ${Which id-s should be included. This is not the node id, but the entry id}
    Summary

    In summary I think that the auditing feature is capable to fit my initial requirements. It is highly customizable. So it should be possible to extend it with own Producers, Generators and Extractors based on the specific requirements. It provides indeed a suiteable framework but by adding some complexity. What should be kept in mind is that this level of complexity may affect the performance of the system in a negative way, but this needs further evaluation. I will start by using the default audit application to get my hands on it.

    Thursday, February 21, 2013

    Custom form controls in Alfresco

    Basics

    It's possible to use custom form controls to show your properties. The following snippet explains how to use them:

    <config evaluator="node-type" condition="ecg:mytype">
    
     <forms>
          <form>
            <field-visibility>
    
                <!-- My property which is only visible in the view edit mode but not in the view mode-->
                <show id="ecg:myprop" for-mode="edit"/>
    
            </field-visibility>
    
            <appearance>
    
                 <!-- Define a panel on which my property should be shown -->
    
                 <set id="myPanel" appearance="bordered-panel" label="My Properties"/>
    
                 
    
                 <!-- Add the property to the panel by defining which control should be used -->
    
                 <field id="ecg:myProp" set="myPanel" label="My Property">
                    <control template="/de/ecmgeek/alfresco/mycontrol.ftl">
                        <control-param name="myFirstParam">myFirstParamValue</control-param>
                        <control-param name="mySecondParam">mySeceondParamValue</control-param>
                    </control>
                </field> 
    
            </appearance> 
    
         </form> 
    
    </forms> 
    
    </config> 
    
    



    Structure of a form control

     So a form control is just a freemarker template wich can be referenced in your form configuration. Therefore you need to store it under for instance 'web-extension/site-webscripts/de/ecmgeek/alfresco'. But how does such a form look like? Here the code of the textfield.ftl which is provided by Alfresco.


    <div class="form-field">
       <#if form.mode == "view">
          <div class="viewmode-field">
             <#if field.mandatory && !(field.value?is_number) && field.value == "">
                <span class="incomplete-warning"><img src="${url.context}/res/components/form/images/warning-16.png" title="${msg("form.field.incomplete")}" /><span>
             </#if>
             <span class="viewmode-label">${field.label?html}:</span>
             <#if field.control.params.activateLinks?? && field.control.params.activateLinks == "true">
                <#assign fieldValue=field.value?html?replace("((http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?\\^=%&:\\/~\\+#]*[\\w\\-\\@?\\^=%&\\/~\\+#])?)", "<a href=\"$1\" target=\"_blank\">$1</a>", "r")>
             <#else>
                <#if field.value?is_number>
                   <#assign fieldValue=field.value?c>
                <#else>
                   <#assign fieldValue=field.value?html>
                </#if>
             </#if>
             <span class="viewmode-value"><#if fieldValue == "">${msg("form.control.novalue")}<#else>${fieldValue}</#if></span>
          </div>
       <#else>
          <label for="${fieldHtmlId}">${field.label?html}:<#if field.mandatory><span class="mandatory-indicator">${msg("form.required.fields.marker")}</span></#if></label>
          <input id="${fieldHtmlId}" name="${field.name}" tabindex="0"
                 <#if field.control.params.password??>type="password"<#else>type="text"</#if>
                 <#if field.control.params.styleClass??>class="${field.control.params.styleClass}"</#if>
                 <#if field.control.params.style??>style="${field.control.params.style}"</#if>
                 <#if field.value?is_number>value="${field.value?c}"<#else>value="${field.value?html}"</#if>
                 <#if field.description??>title="${field.description}"</#if>
                 <#if field.control.params.maxLength??>maxlength="${field.control.params.maxLength}"</#if>
                 <#if field.control.params.size??>size="${field.control.params.size}"</#if>
                 <#if field.disabled && !(field.control.params.forceEditable?? && field.control.params.forceEditable == "true")>disabled="true"</#if> />
          <@formLib.renderFieldHelp field=field />
       </#if>
    </div>
    
    



    As you can see the following elements are out of the box available:
    • Parameters via E.G. 'field.control.params.myFirstParam'
    • The value of the field via 'field.value'. It makes sense to make it available as via ${fieldValue} in order to use it in your HTML or JavaScript. Therefore Freemarker's assign command can be used '<#assign fieldValue=field.value> . (The term 'field.value' is only available within Freemarker directives. But it is possible to assign them to a place holder (interpolation) by using the assign directive.)
    • The form mode via 'form.mode'
    • The enabled/disabled mode of the field by using 'field.disabled'.
    • The information if the field is a madatory one or not by using 'field.mandatory
    • The field description via 'field.description' 
    • The field name directly via ${fieldName}
    • The field id via ${fieldHtmlId}. The field id is important. It will be replaced by an unique id which depends on the form id and the prefix and name of your property. So you can later reference your field by using this unique id.  
     So from what we know it is easy to create static controls those are looking as we want by chaning the HTML code. But what if we want to have a more dynamic control, for instace an autocompletion one or a custom category picker?

    Within your control the Share API is available (http://sharextras.org/jsdoc/share/community-4.0.c/symbols/Alfresco.Share.html). Additionally you have access to the YUI stuff. (YUI is the UI framework behind Alfresco Share) (http://developer.yahoo.com/yui/2/). So you can mix the mentioned HTML with Java Script to perform AJAX requests in order to get data from the repository layer (provided by Web-Scripts). This client side Java Script can then directly manipulate your controls DOM tree. Important is that you run the script after your control was completely available (rendered). Here an example:


    
    <script type="text/javascript">//<![CDATA[
    
    (function()
    {
        YAHOO.util.Event.onContentReady("${fieldHtmlId}", 
        function ()
        {
            var successHandler = function (response)
            {
                 //Handle the AJAX request
    
                 var data = response.json;
    
    
    
                 //Use the data ... 
             };    
             
             var failureHandler = function (response) { 
                    Alfresco.logger.error("Could not perform the AJAX request: " +               response.serverResponse.responseText);
             };
             
    
             var myparamValue = "${field.control.params.myFirstParam}";
             var url = Alfresco.constants.PROXY_URI + "ecg/mywebscript.json?myparam=" + myparamValue ;
            
             var config =
             {
                method: "GET",
                url: url,
                successCallback: 
                { 
                    fn: successHandler, 
                    scope: this
                },
                failureCallback:
                {
                    fn: failureHandler,
                    scope: this
                }
             };
            
             Alfresco.util.Ajax.request(config);
        }
        ,this);
    })();
    
    //]]></script>
    
    
    
     <div class="form-field">
    
    ... 
    
    </div>
    
    

     
     How the form submission works

    So as you can see it is possible to get data from the repository layer by accessing RESTFul web services. Such a RESTFul web service. Such a service can be implemented as Web Script. You then can use the data which was provided by the service in order to manipulate the DOM of your control. An auto completion control could ask the service for matching words based on the input of the user by writing out a list into a DIV container with the id '${fieldHtmlId}-acc'. A user may select multiple (by the autocompletion recommended) values. But how would you submit the selected values. The answer is: Via the value attribute of the input field with the id ' ${fieldHtmlId}'. Dependent on the type of your field, the form processor will handle the values inside this field in a different way. So if you have a classification (or user) property then the value inside the field should be a node reference. If you have a text input then the value should be just the text to show. If you want to submit multiple node references then the value shoud be a comma seperated list of node references. Because it is often not really useful to show the user a field which shows a comma seperated list of values, the trick is to have one input field (or Picker, ...) which is visible but has a different id, E.G. '${fieldHtmlId}-show' whereby storing your seperated list of node references inside a hidden field with the id '${fieldHtml${fieldHtmlId}'. So you show one field for working purposes but you use the value of the on one with the id '${fieldHtml${fieldHtmlId}' during the submission. I used the following Java Script to add new node references to the value attribute of the hidden input field with the id '${fieldHtmlId}'${fieldHtmlId} '${fieldHtmlId}':}${fieldHtml${fieldHtmlId}${fieldHtml${fieldHtmlId}':


     //Add to the hidden field
      var hiddenField = document.getElementById("${fieldHtmlId}");
      var value = hiddenField.getAttributeNode("value");
               
       var bkpValue = value.nodeValue;
       var newValue = "";
                   
       if (bkpValue == "")
       {
          newValue = myNodeRef;
       }
       else
       {
            newValue = bkpValue + "," + myNodeRef;
        }
                   
        value.nodeValue    = newValue;
    
    


    Room for improvements

    You may use one control for multiple times on one single form. This causes that you can see tha above mentioned Java Script code for multiple times in the client side Java Script code (by using Firebug ...). To avoid this you can put the JavaScript into a custom Java Script library. Which then could be for instance accessed this way:  'MyLib.addToValues(fieldId, nodeRef)'.)nfiibavi