Session replication in Tomcat with Hazelcast

When running Alfresco Share in a clustered environment, it’s recommended to use sticky sessions. Sticky sessions make sure that a user stays on the same server throughout the users interaction with Alfresco Share. There is one occasion where the user experience fails and that is when the server the user currently uses goes down for some reason. The next action in Alfresco Share the user executes will lead to a login screen for the user. This can be prevented if the session is replicated to all the other Alfresco Share servers in the cluster. There are multiple ways to achieve this. One way is to use memcached or couchbase as a session storage. Another is to use Hazelcast, the clustering solution already used by Alfresco.

In order to simplify deployment and upgrades, injecting stuff into the web.xml file for Alfresco Share is done as a post-processing step when installing Alfresco. This is achieved with an ant task, xmltask.

Installation steps (Ubuntu 12.04)

  • sudo apt-get install ant
  • sudo curl -o /usr/share/java/xmltask-1.16.1.jar -L http://downloads.sourceforge.net/project/xmltask/xmltask/1.16.1/xmltask.jar?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fxmltask%2Ffiles%2Fxmltask%2F1.16.1%2F&ts=1428909584&use_mirror=softlayer-ams
  • sudo ln -s /usr/share/java/xmltask-1.16.1.jar /usr/share/java/xmltask.jar
  • sudo ln -s /usr/share/java/xmltask.jar /usr/share/ant/lib/xmltask.jar
  • Create an ant build script that injects the correct filter, filter-mapping and session-listener elements into the web.xml.
<project name="post-process-share" default="modify" basedir=".">

  <taskdef name="xmltask" classname="com.oopsconsultancy.xmltask.ant.XmlTask" />

  <target name="modify">
    <xmltask source="./tomcat-share/webapps/share/WEB-INF/web.xml"
             dest="./tomcat-share/webapps/share/WEB-INF/web.xml"
             failWithoutMatch="true">

      <insert path="/:web-app/:filter[1]" position="before">
        <![CDATA[
        <filter>
          <filter-name>hazelcast-filter</filter-name>
          <filter-class>com.hazelcast.web.WebFilter</filter-class>
          <init-param>
            <param-name>map-name</param-name>
            <param-value>slingshot-sessions</param-value>
          </init-param>
          <init-param>
            <param-name>sticky-session</param-name>
            <param-value>false</param-value>
          </init-param>
          <init-param>
            <param-name>debug</param-name>
            <param-value>true</param-value>
          </init-param>
          <init-param>
            <param-name>use-client</param-name>
            <param-value>true</param-value>
          </init-param>
          <init-param>
            <param-name>instance-name</param-name>
            <param-value>slingshot</param-value>
          </init-param>
          <init-param>
            <param-name>client-config-location</param-name>
            <param-value>hazelcast-custom-client.properties</param-value>
          </init-param>
        </filter>
        ]]>
      </insert>

      <insert path="/:web-app/:filter-mapping[1]" position="before">
        <![CDATA[
        <filter-mapping>
          <filter-name>hazelcast-filter</filter-name>
          <url-pattern>/*</url-pattern>
          <dispatcher>FORWARD</dispatcher>
          <dispatcher>INCLUDE</dispatcher>
          <dispatcher>REQUEST</dispatcher>
        </filter-mapping>
        ]]>
      </insert>

      <insert path="/:web-app/:listener" position="before">
        <![CDATA[
        <listener>
          <listener-class>com.hazelcast.web.SessionListener</listener-class>
        </listener>
        ]]>
      </insert>

    </xmltask>
  </target>

</project>
  • In the install script, do something like
echo "Executing post-process steps . . ."
# unpack the share.war file in order to to post-process the web.xml
unzip -qq $ALFRESCO_TOMCAT_HOME_SHARE/webapps/share.war -d $ALFRESCO_TOMCAT_HOME_SHARE/webapps/share
# execute the ant build script
ant -q -f build-post-process.xml
echo ""
  • Create a hazelcast client settings file (hazelcast-custom-client.properties) and place it in the tomcat/shared/classes directory.
# the group name, password and port configured in the custom-slingshot-application-context.xml
hazelcast.client.group.name = slingshot
hazelcast.client.group.pass = alfresco
hazelcast.client.addresses  = localhost:5801
  • Two new dependencies must be added in order for the WebFilter and SessionListener to work with the above configuration. If using Maven, do this:
<!-- Hazelcast VM and Client added to have session replication -->
<dependency>
  <groupId>com.hazelcast</groupId>
  <artifactId>hazelcast-wm</artifactId>
  <version>2.4</version>
</dependency>

<!-- There is a bug in ClientConfigBuilder that's fixed in 2.4.1 -->
<dependency>
  <groupId>com.hazelcast</groupId>
  <artifactId>hazelcast-client</artifactId>
  <version>2.4.1</version>
</dependency>

With this setup in place, each and every server that’s a member in the cluster will replicate it’s sessions to all the other members. If (when…) one server goes down all users are transparently moved to a new server.

Posted in Alfresco, cluster, share | Leave a comment

Specialize type upon content creation within behaviour can result in invalid version history

A customer today reported a problem where version history was not what you would expect between the 1.0 and the 1.1 versions in Alfresco Share. A bit of background history to start with:

As soon as content is dropped into a site, it will get its type changed to rl:customType. This type adds a new properties such as rl:documentNumber and others. The rl:documentNumber is generated for all new content added to the site. All this is done in a behaviour listening at onCreateNode on Transaction commit.

@Override
public void afterPropertiesSet() throws Exception {
	super.afterPropertiesSet();
	Assert.notNull(policyComponent);
	if (!isInitialized()) {
	  if (LOG.isTraceEnabled())
	    LOG.trace("Initialized " + this.getClass().getName());
	  policyComponent.bindClassBehaviour(OnCreateNodePolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCreateNode", NotificationFrequency.TRANSACTION_COMMIT));
	}
}

The problem that appeared when a version 1.1 was created through editing the uploaded document. When browsing the version history you could see that the 1.0 version did not have a rl:documentNumber set, however, the 1.1 version did. So the conclusion was that the version 1.0 was actually created before transaction commit in the version store. If this is a feature or a bug, I don’t know, but the solution is simple. In your behaviour, bind on the first event instead of transaction commit as this will correctly make sure that everything written to the node in the behaviour also ends up in the versioning history.

@Override
public void afterPropertiesSet() throws Exception {
	super.afterPropertiesSet();
	Assert.notNull(policyComponent);
	if (!isInitialized()) {
	  if (LOG.isTraceEnabled())
	    LOG.trace("Initialized " + this.getClass().getName());
	  policyComponent.bindClassBehaviour(OnCreateNodePolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onCreateNode", NotificationFrequency.FIRST_EVENT));
	}
}
Posted in Alfresco, Behaviours, Version History | 17 Comments

Update a date of a task in a running workflow

I had the need to change a reminder date of a workflow task in order to test a cron job sending out email reminders.

Using the workflow console (http://localhost:8080/alfresco/faces/jsp/admin/workflow-console.jsp)

First query to find running workflows.

show workflows all

Identify the workflow you are interested in.

Using the output of the previous commands we drill down to what we need:

show paths activiti$710
desc path activiti$788

Update our reminider date using ISO8601 date format:
update task activiti$791 lysewf:reminderDate=2014-10-06T12:00:00+01:00

Check that the date has changed:
desc task activiti$791

Posted in Alfresco, workflow | 189 Comments

Alfresco Mobile for iOS & reverse proxy

For all of you that has a reverse proxy in front of Alfresco and has experienced problems with the new Alfresco Mobile 2.0 for iOS, there is a solution for this. The CMIS endpoint has changed in the new app and now uses /cmisatom instead. Because of this the app uses the response generated by Alfresco for the CMIS calls, not the configured service URL. The initial login call (and some other ones) uses the configured service URL but later in the process the app picks up URL’s from the response and uses them. As a standard the response URL includes references to the local server name, not the reverse proxy address. In order to fix this some Tomcat configuration has to be done. In some cases it’s enough with one connector, but in my specific case I have to use two.


<Connector
    port="8080"
    protocol="HTTP/1.1"
    connectionTimeout="20000"
    URIEncoding="UTF-8"
    redirectPort="8443" />

<Connector
    port="8090"
    protocol="HTTP/1.1"
    connectionTimeout="20000"
    URIEncoding="UTF-8"
    redirectPort="8443"
    scheme="https"
    proxyName="alfrescoapp.example.com"
    proxyPort="443" />

So now the reverse proxy address ‘alfresco.example.com’ uses the first connector and the reverse proxy address ‘alfrescoapp.example.com’ uses the second connector.

For references please look in this forum thread:

https://forums.alfresco.com/comment/149481#comment-149481

Posted in Uncategorized | 17 Comments

Creating a new multiple action for a specific datalist

In the current project I’m working on there is a requirement that for one type of datalist the user should be able to select some of the items and based upon those items a pdf will be constructed and placed in a predefined folder in the document library.

One challenge in the implementation was to make the menu alternative only show up for one datalist type.

The server side javascript file responsible for displaying the drop-down menu is called data-list/toolbar.get.js, when putting this in the correct path mapped by your extension module to “site-webscripts/org/alfresco/components/data-lists/” our extended file will be processed after the alfresco original toolbar.get.js

First we need to make sure we add the menu alternative for the correct list. We will look at the query string to find out the name of the clicked list.

   var queryString = url.queryString;
   var listName = getQueryVariable('list', queryString);

   function getQueryVariable(variable, queryString) {
	var vars = queryString.split("&");
	for (var i = 0; i < vars.length; i++) {
		var pair = vars[i].split("=");
		if (pair[0] == variable) {
			return pair[1];
		}
	}
	return (false);
   }

Then we make a remote call to the Alfresco repository and ask for all datalists in the current site, looping them through and when we find the correct type of list and the name of the list matches we will push the new menu alternative onto the actionSet array.

   var site = page.url.templateArgs.site,
      theUrl = "/slingshot/datalists/lists/site/" + site + "/dataLists",
      result = remote.call(theUrl),
      canCreate = false,
      lists = [];
   
   if (result.status == 200)
   {
      response = JSON.parse(result.response);
      lists = response.datalists;
      
      var arrayLength = lists.length;
      for (var i = 0; i < arrayLength; i++) {
          if(lists[i].itemType=='somecodl:issueList' && lists[i].name==listName){
    		// add action for our list type
			model.actionSet.push({
				id : 'onActionCreateMeetingMinutes',
				type : 'action-link',
				permission : 'create',
				asset : '',
				href : '',
				label : 'menu.selected-items.meetingMinutes'
			});
          }
      }
   }

The final code will then look like this:


function extended_main(url){
	
   var queryString = url.queryString;
   var listName = getQueryVariable('list', queryString);

   var site = page.url.templateArgs.site,
      theUrl = "/slingshot/datalists/lists/site/" + site + "/dataLists",
      result = remote.call(theUrl),
      canCreate = false,
      lists = [];
   
   if (result.status == 200)
   {
      response = JSON.parse(result.response);
      lists = response.datalists;
      
      var arrayLength = lists.length;
      for (var i = 0; i < arrayLength; i++) {
          if(lists[i].itemType=='somecodl:issueList' && lists[i].name==listName){
    		// add action for Action List
			model.actionSet.push({
				id : 'onActionCreateMeetingMinutes',
				type : 'action-link',
				permission : 'create',
				asset : '',
				href : '',
				label : 'menu.selected-items.meetingMinutes'
			});
          }
      }
   }
	
   // Widget instantiation metadata...
   var toolbar = {
      id : "DataListToolbar",
      name : "Alfresco.component.DataListToolbar",
      options : {
         siteId : (page.url.templateArgs.site != null) ? page.url.templateArgs.site : ""
      }
   };
   
   model.widgets = [toolbar];
}

function getQueryVariable(variable, queryString) {
	var vars = queryString.split("&");
	for (var i = 0; i < vars.length; i++) {
		var pair = vars[i].split("=");
		if (pair[0] == variable) {
			return pair[1];
		}
	}
	return (false);
}

extended_main(url);

When the new menu alternative is selected we will present a metadataform, and upon submission create a pdf in the documentLibrary, but thats another (yet to come) blogpost.

Posted in Alfresco, datalists, Javascript | 34 Comments

Alfresco logging with Logstash and Kibana. Part 1: The basics

This is the first post in a series about how to integrate Alfresco with Elasticsearch using Logstash and Kibana. The overall purpose is to get a better view of the events happening in Alfresco. The purpose of this post is to show the basic set up of the needed components.

As any system Alfresco could produce enormous amounts of log data. It is also usually spread out across several application servers (for example one for the repository, one for Share and one for Solr). In a clustered environment there could also be multiple instances of a particular type. Logs are text files with some common patterns. For example, the standard Alfresco logs contains a time stamp, the log level, name of logger and of course the message. It is not unusual that the combined log files in a production system contains tens of thousands of lines and it could be very hard to get an overview and map lines between different log files to see the full context. To the rescue comes Logstash and Kibana! Basically Logstash collect data from one or several sources, filters it and sends it to an output. In this case the output will be Elasticsearch and Kibana acts as the web user interface towards the (log) data stored in Elasticsearch. There are several excellent tutorials on how to set up these components. Very briefly this is how I did it:

Preparations

Download Logstash:

curl -O https://download.elasticsearch.org/logstash/logstash/logstash-1.4.2.tar.gz
tar zxvf logstash-1.4.2.tar.gz

Download Elasticsearch

curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.1.1.tar.gztar zxvf elasticsearch-1.1.1.tar.gz

Download and install Kibana

curl -O https://download.elasticsearch.org/kibana/kibana/kibana-3.1.0.tar.gz
tar zxvf kibana-3.1.0.tar.gz
cp -r kibana-3.1.10 /var/www/kibana

You also need to point Kibana to the Elasticsearch host. Locate config.js inside the kibana directory and change

elasticsearch: "http://"+window.location.hostname+":9200",

to

elasticsearch: "http://yourelasticsearchserver:9200",

Configure Logstash for Alfresco

Now you need to do some basic config in Logstash. The configuration file consist of inputs, filters and outputs. We will start with a very simple configuration that just reads a file input (alfresco.log) and forwards it to Elasticsearch. Create the server.conf in logstash-1.4.2/ with the following content:

input {
 file {
  path => "/opt/alfresco/tomcat-repo/logs/alfresco.log"
  type => "alfresco" # a type to identify those logs
  start_position => "end"
 }
}
filter {
}
output {
 stdout { }
 elasticsearch {
  cluster => "elasticsearch"
 }
}

It just tells Logstash to forward what is written to your alfresco.log (assuming it is located in /opt/alfresco) to Elasticsearch without any filtering

Fire it up!

Now you are ready to start up the services. Start with Elasticsearch:

./elasticsearch-1.1.1/bin/elasticsearch

Then Kibana, in my case I use the nginx web server so I simply do

nginx restart

Finally you start Logstash and point out the configuration file defined above:

logstash-1.4.2/bin/logstash --verbose -f ./logstash-1.4.2/server.conf

After you have started Logstash you should see some logging in Elasticsearch indicating that Logstash has been connected. Now start Alfresco and you should see all the alfresco logging also appear in the Logstash console. Open the Logstash dashboard at http://localhost/index.html#/dashboard/file/logstash.json It will look something like this:

Kibana screenshot showing the Alfresco repository startup logging

Kibana screenshot showing the Alfresco repository startup logging.

Basic Logstash configuration for Alfresco

So far we have made a generic setup of Logstash and Kibana. Now it’s time for some Alfresco specifics.

1. Multiline filter (really not Alfresco specific but for Java logs in general)

You might have noticed that each row in alfresco.log was interpreted as a single item in Kibana. This is not suitable as Java logs often contains line breaks (for example stack traces). Add a filter in your Logstash configuration that merges each row of a stack trace into a single item:

filter {
 multiline {
   pattern => "^\s"
   what => "previous"
 } 
}

2. Configure types for Alfresco, Share and Solr

Another advantage of using Logstash is that you could combine several logs into a single Elasticsearch index. With Alfresco you would probably like to do this for at least the repository, Share and Solr but it is of course also possible to add system logs, access logs or even Alfresco audit logs to Logstash to analyze them at once. In the example you simply add another log using the input directive. Below is an example for all three Alfresco logs:

input {
 file {
  path => "/opt/alfresco/tomcat-repo/logs/alfresco.log"
  type => "alfresco"
  start_position => "end"
 }

 file {
  path => "/opt/alfresco/tomcat-share/logs/share.log"
  type => "share"
  start_position => "end"
 }

 file {
  path => "/opt/alfresco/tomcat-solr/logs/solr.log"
  type => "solr"
  start_position => "end"
 }
}

With this new config the Kibana dashboard will look like (I also enabled full debug logging in Alfresco to get some more data):

Kibana dashboard with repo, share and solr logging

Note the three different types. Here alfresco produces the majority output as DEBUG log level is enabled.

I added a query each for the types I defined in my Logstash config. This is just a very simple example on how to filter and search in the logs.

What is next?

There are numerous ways to configure Logstash for your needs. In the following posts I will explain

  • How to use the log4j socket appender instead of files to transmit messages to Logstash. Is is normally not the best idea to run it on the Alfresco repository server.
  • How to use a json log4j layout to auto parse log messages in Logstash.
  • How to view Alfresco audit data in Kibana using Logstash and Elasticsearch.
Posted in Alfresco, Logging | Tagged , , , , | 2 Comments

Edit metadata of multiple documents

check what fields to edit

Background
A customer of ours had a need to change properties to the same value for a bulk of documents at once. So we created an action in the document library where you could select a bunch of documents of the same content type, and then choose “edit multiple” from the actions menu. You are then presented with the gui shown to the left. Tick the checkbox next to the field to enable it for bulk edit. When hitting the save button all the documents selected are updated with the values at once.

Implementation
First we need to indicate the document type in the document library listing, otherwise it will be hard for the user to know which of the documents that share the same metadata, and therefore could be subject to a bulk change. This is done by registering a new renderer in javascript:

(function () {
    if (Alfresco.DocumentList) {
        YAHOO.Bubbling.fire("registerRenderer", {
            propertyName: "type",
            renderer: function (record, label) {
                var key = 'type.' + record.jsNode.type.replace(":", "_");

                return '<span class="item">' + Alfresco.util.message('label.nodeType') +
                       ': ' + Alfresco.util.message(key) + '</span>';
            }
        });
    }
})();

Include the javascript file in a DocLibCustom directive:

<config evaluator="string-compare" condition="DocLibCustom">
  <dependencies>
    <js src="components/documentlibrary/documentlist-display-type.js" />
  </dependencies>
</config>

The result will be something like this (with our customisation highlighted):

Selection_041

Configure our new bulk action

<config evaluator="string-compare" condition="DocumentLibrary">
 
 <multi-select>
  <action type="action-link" id="onActionEditMultipleDocumentMetadata" asset="document" permission="Write" label="actions.editMultipleDocumentMetadata" />
 </multi-select>
 
</config>
window.RPLP = window.RPLP || {};
window.RPLP.globalNodeRefs = [];
(function () {
    /**
     * YUI Library aliases
     */
    var Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event;

    /**
     * Alfresco Slingshot aliases
     */
    var $html = Alfresco.util.encodeHTML,
        $combine = Alfresco.util.combinePaths,
        $siteURL = Alfresco.util.siteURL,
        $isValueSet = Alfresco.util.isValueSet;

    YAHOO.Bubbling.fire("registerAction", {
        actionName: "onActionEditMultipleDocumentMetadata",
        fn: function rplp_onActionEditMultipleDocumentMetadata(record) {
            var type = record[0].jsNode.type;

            // Check that all the nodes are of same type
            var allSame = true;
            var nodeRefs = [];
            for (var i = 0, ii = record.length; i < ii; i++) {
                jsNode = record[i].jsNode;
                if (type !== jsNode.type) {
                    allSame = false;
                    break;
                }
                nodeRefs.push(record[i].nodeRef);
            }
            window.RPLP.globalNodeRefs = nodeRefs;

            if (allSame === false) {
                Alfresco.util.PopupManager.displayMessage({
                    text: this.msg("rplp.actions.editMultipleDocumentMetadata.multipleTypes"),
                    displayTime: 2.0
                });
            } else { // All are of same type

                // Spawn form
                var scope = this,
                    nodeRef = record[0].nodeRef,
                    jsNode = record[0].jsNode;

                // Intercept before dialog show
                var doBeforeDialogShow = function dlA_onActionDetails_doBeforeDialogShow(p_form, p_dialog) {
                    // Dialog title
                    var fileSpan = '<span class="light">' + this.msg("rplp.actions.editMultipleDocumentMetadata.dialogTitle") + '</span>';

                    Alfresco.util.populateHTML(
                        [p_dialog.id + "-dialogTitle", scope.msg("edit-details.title", fileSpan)]
                    );

                    // Edit metadata link button
                    this.widgets.editMetadata = Alfresco.util.createYUIButton(p_dialog, "editMetadata", null, {
                        type: "link",
                        label: scope.msg("edit-details.label.edit-metadata"),
                        href: $siteURL("edit-metadata?nodeRef=" + nodeRef)
                    });
                };

                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: "multiple-edit-metadata"
                });

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

                editDetails.setOptions({
                    width: "auto",
                    templateUrl: templateUrl,
                    actionUrl: null,
                    destroyOnHide: true,
                    doBeforeDialogShow: {
                        fn: doBeforeDialogShow,
                        scope: this
                    },
                    onSuccess: {
                        fn: function dlA_onActionDetails_success(response) {
                            // Reload the node's metadata
                            var webscriptPath = "components/documentlibrary/data";
                            if ($isValueSet(this.options.siteId)) {
                                webscriptPath += "/site/" + encodeURIComponent(this.options.siteId)
                            }
                            Alfresco.util.Ajax.request({
                                url: $combine(Alfresco.constants.URL_SERVICECONTEXT, webscriptPath, "/node/", jsNode.nodeRef.uri) + "?view=" + this.actionsView,
                                successCallback: {
                                    fn: function dlA_onActionDetails_refreshSuccess(response) {

                                        // Display success message
                                        Alfresco.util.PopupManager.displayMessage({
                                            text: this.msg("message.details.success")
                                        });

                                        // Refresh the document list...
                                        YAHOO.Bubbling.fire("metadataRefresh");
                                    },
                                    scope: this
                                },
                                failureCallback: {
                                    fn: function dlA_onActionDetails_refreshFailure(response) {
                                        Alfresco.util.PopupManager.displayMessage({
                                            text: this.msg("message.details.failure")
                                        });
                                    },
                                    scope: this
                                }
                            });
                        },
                        scope: this
                    },
                    onFailure: {
                        fn: function dLA_onActionDetails_failure(response) {
                            var failureMsg = this.msg("message.details.failure");
                            if (response.json && response.json.message.indexOf("Failed to persist field 'prop_cm_name'") !== -1) {
                                failureMsg = this.msg("message.details.failure.name");
                            }
                            Alfresco.util.PopupManager.displayMessage({
                                text: failureMsg
                            });
                        },
                        scope: this
                    }
                }).show();
            }
        }
    });

})();

Our new action first declares a global javascript variable to hold an array of all nodeRefs (window.RPLP.globalNodeRefs = [];) that should be bulk edited. I found no other convenient way to pass them on to the ftl-template that later on will render the form and place those in a hidden form field. When the action gets fired it first checks that all nodes requested for bulk edit are of the same type before moving on to call the forms engine to render the form. When calling the forms engine we will do this with a new formId, “multiple-edit-metadata”. This way we can configure our form with xml the regular way.

Don’t forget to configure the message keys in the message resource bundle as well.

Configure multiple-edit-metadata forms for types

For every content-type you want to bulk-edit, create an xml config with “multiple-edit-metadata” as formId:

<config evaluator="string-compare" condition="DocumentLibrary">
      <!-- Form used when editing multiple nodes of same type at the same time. -->
      <form id="multiple-edit-metadata">
        <edit-form template="/se/redpill/alfresco/share/global/components/form/multiple-node-simple-metadata.ftl" />
        <field-visibility>
          <show id="rplp:externalReference" />
          <hide id="rplp:documentStatusType" />
          <hide id="rplp:reviewStatusType" />
          <show id="rplp:securityClassificationType" />
          <show id="rplp:applicationRecipient" />
          <show id="rplp:applicationType" />
        </field-visibility>
        <appearance>
          <field id="rplp:externalReference">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/textfield-cb.ftl">
              <control-param name="styleClass">autoWidth</control-param>
            </control>
          </field>

          <field id="rplp:documentStatusType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>
            </control>
          </field>

          <field id="rplp:reviewStatusType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>
            </control>
          </field>

          <field id="rplp:securityClassificationType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>
            </control>
          </field>
          <field id="rplp:applicationRecipient">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/textfield-cb.ftl">
              <control-param name="styleClass">autoWidth</control-param>
            </control>
          </field>

          <field id="rplp:applicationType">
            <control template="/se/redpill/alfresco/share/global/components/form/controls/multi-edit/selectone-cb.ftl">
              <control-param name="addEmptyOption">true</control-param>
              <control-param name="styleClass">selectWidth</control-param>
            </control>
          </field>
        </appearance>
      </form>
</config>

We will use a custom freemarker template for the edit-form to be able to pass the array of nodes on to the backend service. Notice the javascript at the end of the template poplulating the hidden input value.

<#if formUI == "true">
   <@formLib.renderFormsRuntime formId=formId />
</#if>

<div id="${args.htmlid}-dialog">
   <div id="${args.htmlid}-dialogTitle" class="hd"></div>
   <div class="bd">

      <div id="${formId}-container" class="form-container">
   
         <#if form.showCaption?exists && form.showCaption>
            <div id="${formId}-caption" class="caption"><span class="mandatory-indicator">*</span>${msg("form.required.fields")}</div>
         </#if>
      
         <form id="${formId}" method="${form.method}" accept-charset="utf-8" enctype="${form.enctype}" action="${form.submissionUrl}">
            <input type="hidden" id="muliple-edit-nodeRefs" />
            <div id="${formId}-fields" class="form-fields">
               <#list form.structure as item>
                  <#if item.kind == "set">
                     <@formLib.renderSet set=item />
                  <#else>
                     <@formLib.renderField field=form.fields[item.id] />
                  </#if>
               </#list>
            </div>

            <div class="bdft">
               <input id="${formId}-submit" type="submit" value="${msg("form.button.submit.label")}" />
               &nbsp;<input id="${formId}-cancel" type="button" value="${msg("form.button.cancel.label")}" />
            </div>
      
         </form>

      </div>
   </div>
</div>

<script type="text/javascript">
YAHOO.util.Event.onAvailable('muliple-edit-nodeRefs', function(){
  // Set which nodeRefs are involved in the multiple update.
  YAHOO.util.Dom.get('muliple-edit-nodeRefs').value = window.RPLP.globalNodeRefs.join();
});

</script>

One of the drawbacks with this solution is that every form component have been duplicated. This is done because of the checkbox that sits next to every input type. If you want to edit the field you need to first tick the box. It would problaby be possible to use javascript instead to post-process the DOM-tree and dynamically add a checkbox next to every input. That way there will be no need to add a form component for every input type. However we only had a few input types and were a bit short of time, so I made one whenever needed. Below is the ftl-code for the regular textbox.

  <div class="yui-g">
    <div class="yui-u first">
      <div class="form-field">
          <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}"<#else>maxlength="1024"</#if> 
                 <#if field.control.params.size??>size="${field.control.params.size}"</#if> 
                 disabled="true" />
          <@formLib.renderFieldHelp field=field />
      </div>
    </div>
    <div class="yui-u">
      <div class="form-field">
        <br/>
        <input class="formsCheckBox" id="${fieldHtmlId}-entry" type="checkbox" tabindex="0"  
                   onchange='disableSiblingInputField("${fieldHtmlId}");' />
        <label for="${fieldHtmlId}-entry" class="checkbox">Redigera</label>
      </div>
    </div>
 </div>

<script type="text/javascript">//<![CDATA[
function disableSiblingInputField(fieldId){
  var fieldToDisable = YAHOO.util.Dom.get(fieldId);
  if (fieldToDisable.disabled === true){
    fieldToDisable.disabled = false;
  }else {
    fieldToDisable.disabled = true;
  }
}
//]]></script>

The backend
The last thing needed to make all the parts work, was to subclass the NodeFormProcessor on the repository side and call the persist method in a loop for all nodes in our array:

<!-- override the form processor to be able to edit multiple nodes at once. -->  
  <bean id="nodeFormProcessor"
      class="se.redpill.alfresco.repo.forms.RplpNodeFormProcessor"
      parent="baseFormProcessor">
      <property name="filterRegistry" ref="nodeFilterRegistry" />
      <property name="matchPattern">
         <value>node</value>
      </property>
   </bean>
package se.redpill.alfresco.repo.forms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.forms.FormData;
import org.alfresco.repo.forms.FormData.FieldData;
import org.alfresco.repo.forms.processor.node.NodeFormProcessor;
import org.alfresco.service.cmr.repository.NodeRef;
import org.apache.log4j.Logger;

/**
 * The node form processor is overridden in order to be able to meet 
 * the requirement to update properties of muliple nodes at the same
 * time. This implementation will try to save every requested node, 
 * and collect info on failed ones. If there was one failure an exception
 * will be thrown after successful persistance of the other ones in order
 * to inform the user.
 *  
 * @author erik.billerby@redpill-linpro.com
 *
 */
public class RplpNodeFormProcessor extends NodeFormProcessor {
  private static final Logger logger = Logger.getLogger(RplpNodeFormProcessor.class);
  protected static String MULTIPLE_NODE_REFS_FIELD_NAME = "muliple-edit-nodeRefs";

  @Override
  protected NodeRef internalPersist(NodeRef item, FormData data) {

    List<String> nodeRefs = null;
    List<NodeRef> failedNodeRefs = new ArrayList<>();
    for (FieldData fieldData : data) {

      String fieldName = fieldData.getName();
      if (MULTIPLE_NODE_REFS_FIELD_NAME.equals(fieldName)) {
        if (logger.isDebugEnabled()){
          logger.debug("This is a call to update properties for multiple nodes at once.");
        }
        String value = (String) fieldData.getValue();
        nodeRefs = Arrays.asList(value.split("\\s*,\\s*"));
        // we found it, no need to continue the loop
        break;
      }
    }
    if (nodeRefs != null) {
      for (String nodeRefString : nodeRefs) {
        NodeRef nodeRef = new NodeRef(nodeRefString);
        try {
          super.internalPersist(nodeRef, data);
        }catch(Exception e){
          failedNodeRefs.add(nodeRef);
        }
      }
      
      // After saving all nodes possible check if we got any error and throw exception
      // to inform user on which nodes we could not update.
      if (!failedNodeRefs.isEmpty()){
        StringBuilder sb = new StringBuilder();
        for (NodeRef failedNode: failedNodeRefs){
          String name = (String) nodeService.getProperty(failedNode, ContentModel.PROP_NAME);
          sb.append(name).append(",");
        }
        throw new AlfrescoRuntimeException("rplp.exception.update-multiple-nodes.failedNodes", new String[] { sb.toString() });
      }
      return item;
    } else {
      // This is a normal single node call.
      return super.internalPersist(item, data);
    }
    
  }

}
Posted in Alfresco, Javascript | Tagged , , | 13 Comments

Resetting a users dashboard in Alfresco Share

It may happen, in some peculiar circumstances, that a users dashboard becomes corrupted. The signs for when this happens is that the users dashboard can’t load or that there’s an error when the user tries to customize the user  dashboard. One way of handling this is to delete all information about the dashboard, that is delete the chosen page layout and dashlets and reset the dashboard to the default state it has when a new user is created. Following is a small script that can be executed from the Javascript Console (https://github.com/share-extras/js-console).


var userid = 'this_is_a_user_id';

// get the spring web application context
var ctx = Packages.org.springframework.web.context.ContextLoader.getCurrentWebApplicationContext();

// get hold of the Spring bean surfConfigCleaner from the remote-api-context.xml file
var surfConfigCleaner = ctx.getBean('surfConfigCleaner');

// find the person instance for the particular user id
var person = people.getPerson(userid);

// execute the behaviour method which does the cleaning
surfConfigCleaner.beforeDeleteNode(person.nodeRef);

Using the Javascript Console in this manner (getting hold of Spring Beans) is, as the creator of it says, pretty powerful. Quoting Florian Maul (fme AG), “With great power comes great responsibility!”.

Posted in Alfresco, Javascript | 15 Comments

Introducing the Alfresco Gamification Addon

by Bhagya Nirmaan Silva at Redpill-Linpro, Karlstad, Sweden

Before my first visit to Stockholm to attend an Alfresco Share workshop, I was asked to come up with something interesting and useful to be developed during the workshop. I was giving it a thought and suddenly, it felt that Alfresco is missing something that I have become much familiar through the last few years. What is it? Gamification.

Of course, you already might have been awarded a few badges on your social sites like Stack-Overflow, foursquare, gowalla, getglue and many more . If you are still unfamiliar with the concept, try the below introduction.

Simply explained, gamification makes engagement more fun. It is the bigger and virtual version of being awarded a candy whenever you do something good when you were small. The users who positively engage with the system will be rewarded with virtual badges for their activity with the system. It also brings excitement and a sense of appreciation for the users.


Continue reading

Posted in Alfresco | Tagged , , , , , | 16 Comments

Publishing an external link to Alfresco content with Expiry Date and Transformation Support

by Bhagya Nirmaan Silva at Redpill Linpro, Karlstad, Sweden.

This customization allows users to publish documents externally and to acquire a link which enables non-alfresco users to view the documents which exist in the system. At the stage of publishing the document, the document public link can get an an expiry date set. It will also allow transformations of the content available within the document by simply specifying the file extension at the end of the external link.

This blog post covers the steps from a slide deck that was done as a part of the training for Alfresco Share and Repository Customization work. The final source code can be found on github at the following link , and you are welcome to suggest and improve the code on your own.

https://github.com/bhagyas/alfresco-external-link

Special thanks goes to Jeff Potts and Share-extras project authors. The content available from their sites served as the base for creating the project. Each step covers an iteration which was  during the development of the final result, hence there will be certain steps which modify actions taken in earlier steps.

You will find screen-shots along the way. :)

Note: You should have a basic understanding on Alfresco Share and Alfresco Repository development in order to continue with this post.

Continue reading

Posted in Alfresco | Tagged , , , , | 263 Comments