Ephesoft – Missing Images in BatchList.html

Version 3.0.3.4 SP2

A client contacted me today with a support issue. When attempting to view documents over their network in Review or Validate the documents would fail to load. The same documents would load fine over the localhost URL when viewing on the server.

After a consultation with the fine support staff at Ephesoft it was a potential cross domain scripting issue. The following steps resolved the issue:

1. Navigate to <Ephesoft install folder>\Application\WEB-INF\classes\META-INF\dcma-batch\

2. Backup the existing file

3. Update the batch.base_http_url parameter to the URL base used by the end users

4. Restart the server

 

Support staff noted “the http path will look unique. It is supposed to. It will have back slashes before the colons and that is because they are needed by the application to interpret the path properly.”

 

Upon restarting the server, Ephesoft responded as expected.

Alfresco ScriptNodes and jsonUtils

I’m using Alfresco Enterprise v4.1.1 for a project and I found a bit of a hole in the jsonUtils object.

Here’s what I tried to do in a webscript:

var users = people.getMembers(people.getGroup("GROUP_MyGroup"));
var json = jsonUtils.toJSONString(users);

 

The first line returns a javascript array of org.alfresco.repo.jscript.ScriptNodes (3 persons). These are the node objects you use in your webscripts.
The second line is supposed to provide a valid JSON string representing the user array.
The “jsonUtils” object available in webscripts uses org.springframework.extensions.webscripts.json.JSONUtils.

What I get for my array of 3 users looks something like this:

"Node Type: {http://www.alfresco.org/model/content/1.0}person, Node Aspects: [...]",
"Node Type: {http://www.alfresco.org/model/content/1.0}person, Node Aspects: [...]",
"Node Type: {http://www.alfresco.org/model/content/1.0}person, Node Aspects: [...]"

 

I’ve done some slight editing to save space here but it’s not valid JSON and not very useful at all. It turns out this is the output from ScriptNode.toString() for each user.

However ScriptNode also provides a handy toJSON() method.
Here’s some Java I wrote to add support for both a single ScriptNode and an array of ScriptNodes:

public String toJSONString(Object object) throws IOException {

  JSONUtils jsonUtils = new JSONUtils();

  if (object instanceof ScriptNode) {
    // the true tells it to use short QNames
    // eg. cm:name instead of {http://www.alfresco.org/model/content/1.0}name
    return ((ScriptNode) object).toJSON(true);

  } else {
    // is it an array of ScriptNodes?
    if (object instanceof NativeArray) {

      NativeArray array = (NativeArray) object;

      // get first item and see if it's a ScriptNode
      Object firstItem = array.get(0, array);

      if (firstItem instanceof ScriptNode) {
        StringBuffer json = new StringBuffer("[");
        ScriptNode node = (ScriptNode) firstItem;
        json.append(node.toJSON(true));

        for (int i = 1; i < array.getLength(); i++) {

          json.append(",");
          Object value = array.get(i, array);
          node = (ScriptNode) value;
          json.append(node.toJSON(true));
        }
        json.append("]");
        return json.toString();
      }
    }

    // if it's not a ScriptNode or array of ScriptNodes let jsonUtils handle it
    return this.jsonUtils.toJSONString(object);
  }
}

 

I expose this code to javascript with an extension name of ‘jsonUtilsTim’ and now the webscript looks like this:

var users = people.getMembers(people.getGroup("GROUP_MyGroup"));
var json = jsonUtilsTim.toJSONString(users);

 

Now I get the following which is valid JSON and actually useful:

[
 {
 "mimetype": "application/octet-stream",
 "aspects": [
 "app:configurable",
 "cm:ownable",
 "sys:referenceable",
 "sys:localized"
 ],
 "nodeRef": "workspace://SpacesStore/770df4a0-78ff-4c46-aca9-0de5ff12345",
 "properties": {
 "cm:name": "770df4a0-78ff-4c46-aca9-0de5ff512345",
 "sys:node-dbid": 27,
 "cm:email": "admin@alfresco.com",
 "cm:organizationId": "",
 "sys:store-identifier": "SpacesStore",
 "sys:locale": "en_US",
 "cm:homeFolderProvider": "bootstrapHomeFolderProvider",
 "cm:userName": "admin",
 "cm:sizeCurrent": null,
 "cm:owner": "admin",
 "sys:node-uuid": "770df4a0-78ff-4c46-aca9-0de5ff512345",
 "cm:lastName": "",
 "sys:store-protocol": "workspace",
 "cm:homeFolder": "workspace://SpacesStore/4f4ff00d-c94f-4a99-a304-cbe2e2912345",
 "cm:firstName": "Administrator"
 },
 "type": "cm:person"
 },
 {
 "mimetype": "application/octet-stream",
 "aspects": [
 "cm:ownable",
 "sys:referenceable",
 "sys:localized",
 "cm:personDisabled",
 "cm:preferences"
 ],
 "nodeRef": "workspace://SpacesStore/b6d80d49-21cc-4f04-9c92-e70630312345",
 "properties": {
 "sys:locale": "en_US",
 "cm:companyaddress3": "UK",
 "cm:homeFolderProvider": "userHomesHomeFolderProvider",
 "cm:companytelephone": "",
 "cm:owner": "admin",
 "cm:jobtitle": "Web Site Manager",
 "cm:preferenceValues": "contentUrl=store://2012/10/26/15/16/146c1e27-3980-4c21-b66c-397d97512345.bin|mimetype=text/plain|size=817|encoding=UTF-8|locale=en_US_|id=127",
 "cm:homeFolder": "workspace://SpacesStore/5da679f6-e7f3-4f8d-9506-554ed7312345",
 "cm:instantmsg": "",
 "cm:sizeQuota": -1,
 "cm:googleusername": "",
 "cm:firstName": "Jane",
 "cm:emailFeedId": 442,
 "cm:name": "b6d80d49-21cc-4f04-9c92-e70630312345",
 "cm:userStatusTime": "Tue Feb 15 13:13:09 MST 2011",
 "sys:node-dbid": 562,
 "cm:email": "jane.doe@example.com",
 "sys:store-identifier": "SpacesStore",
 "cm:companyfax": "",
 "sys:node-uuid": "b6d80d49-21cc-4f04-9c92-e70630312345",
 "cm:lastName": "Doe",
 "cm:persondescription": "contentUrl=store://2012/10/26/15/16/8f13b84b-f797-4176-bcce-cdff90912345.bin|mimetype=application/octet-stream|size=54|encoding=UTF-8|locale=en_US_|id=128",
 "cm:companyemail": "",
 "cm:sizeCurrent": null,
 "cm:userName": "jane.doe",
 "sys:store-protocol": "workspace"
 },
 "type": "cm:person"
 },
 {
 "mimetype": "application/octet-stream",
 "aspects": [
 "app:configurable",
 "cm:ownable",
 "sys:referenceable",
 "sys:localized"
 ],
 "nodeRef": "workspace://SpacesStore/c530d479-9a78-4c14-94c7-933b46412345",
 "properties": {
 "cm:name": "c530d479-9a78-4c14-94c7-933b46412345",
 "sys:node-dbid": 6939,
 "cm:organizationId": "",
 "cm:email": "john.doe@example.com",
 "sys:store-identifier": "SpacesStore",
 "sys:locale": "en_US",
 "cm:presenceUsername": "",
 "cm:homeFolderProvider": "userHomesHomeFolderProvider",
 "cm:owner": "john.doe",
 "cm:organization": "",
 "cm:jobtitle": "",
 "sys:node-uuid": "c530d479-9a78-4c14-94c7-933b46412345",
 "cm:lastName": "Doe",
 "cm:homeFolder": "workspace://SpacesStore/15a8f7fb-87ee-4a7e-9b4d-c2f3a8f12345",
 "cm:presenceProvider": "",
 "cm:location": "",
 "cm:sizeQuota": -1,
 "cm:sizeCurrent": null,
 "cm:userName": "john.doe",
 "sys:store-protocol": "workspace",
 "cm:firstName": "John"
 },
 "type": "cm:person"
 }
 ]

Scheduling a “nag” email using Activiti Workflow and Alfresco

Sometimes users need to be reminded of outstanding tasks that have been neglected for a specified period of time. A helpful way to do this is to send them a nice email. For a recent project, I cobbled together an Activiti workflow that had various such “nag” steps that would fire off an email if a task became stagnant for too long, and I thought I’d share some code snippets on how I did it.

Workflow

I’ve simplified the workflow to a single step, plus the “nag” task for brevity:

You could easily imagine this as a part of a much more complex workflow, perhaps with multiple steps requiring timed email notifications.

Classify Task

If the Classify task is idle for more than 14 days, we kick off a friendly reminder email. Here’s the Classify Article workflow step.

<userTask id=”classifyArticle” name=”Classify Article” activiti:candidateGroups= “${ATCWF_classifierGroup}” activiti:formKey=”ATCWF:classifyArticle”>
    <extensionElements>
      <activiti:taskListener event=”complete”>
        <activiti:field name=”script”>
          <activiti:string>classification = task.getVariable(‘ATCWF_classification’);
              execution.setVariable(‘ATCWF_classification’,classification);
          </activiti:string>
        </activiti:field>
      </activiti:taskListener>
    </extensionElements>
 </userTask>
 <boundaryEvent id=”classifyTimer” name=”Classify Nag Timer” cancelActivity=”false” attachedToRef=”classifyArticle”>
    <timerEventDefinition>
      <timeCycle>${ATCWF_nagTimerDuration}</timeCycle>
    </timerEventDefinition>
</boundaryEvent>

So when this task is completed, it grabs the ATCWF_classification variable chosen by the user on the workflow’s form, and stores it in an Activiti workflow variable for later use. Not overly useful, but good enough for demonstration.

I’ve attached a boundary event, “classifyTimer”, to this task which will fire after a timeCycle determined by the ATCWF_nagTimerDuration Activiti workflow variable.

For the required 14 days, the value will be R/P14D (“repeat every period of 14 days”, according to ISO 8601 format.) Make sure cancelActivity is set to false so that the workflow won’t be suspended while we’re waiting.

A Brief Aside

I’d like to make a brief aside to comment on a useful “trick” I’ve used in my code snippets that might be useful to others, although it is not necessary for our task of creating a “nag” email.

Both ${ATCWF_nagTimerDuration} and ${ATCWF_classifierGroup} are Activiti workflow variables that I declared and set in a step prior to this Classify Article task.

This let me store those values in a configuration file in the underlying Alfresco repository, allowing them to be changed without having to redeploy the workflow. Which is nice, since deploying a new workflow doesn’t impact in-flight workflows, but it is possible by altering the value of some workflow variables.

This “trick” could be applied to change what group a later workflow step is assigned to, depending on choices made by a user in an earlier step, cutting down on extraneous exclusive gateways and tasks needed to account for all possible choices.


Email Task

Now let’s look at the step that’ll send a reminder email every 14 days.

<serviceTask id=”classifyNagEmail” name=”Nag Email” activiti:class=”org.alfresco.repo.workflow.activiti.script.AlfrescoScriptDelegate” activiti:async=”true” activiti:exclusive=”false”>
    <extensionElements>
      <activiti:field name=”script”>
        <activiti:string>
            var article = bpm_package.children[0];

                   // construct notification email
                  var subject = “An article needs to be classified”;
                  var emailTemplatePath = “”Data Dictionary/Email Templates/Notify Email Templates/ATC”;
                  var emailTemplate = companyhome.childByNamePath(emailTemplatePath+”/notify_reminder.ftl”);
                  var emailFrom = “reminders@abstractive.ca”;

                  var recipient = ATCWF_classifierGroup;
     
                  var mail = actions.create(“mail”);
                  mail.parameters.to_many  = recipient;
                  mail.parameters.subject   = subject;
                  mail.parameters.from = emailFrom;
                  mail.parameters.template = emailTemplate;
                  // placeholder body text; the template will have the message
                  mail.parameters.text = “Task requires action”;

           // pass article properties to the template layer for easy email substitution
           var templateArgs = new Array();
           var articleId = article.properties['sys:node-dbid'];
           templateArgs['articleId'] = articleId + “”;
           templateArgs['articleTitle']    = article.properties['cm:title'];
           templateArgs['reviewPeriod']    = ATCWF_nagTimerValue;

                  var templateModel = new Array();
                  templateModel['args'] = templateArgs;
                  mail.parameters.template_model = templateModel;

           mail.execute(article);
       </activiti:string>
    </activiti:field>
  </extensionElements>
</serviceTask>

Here we see the construction of an Alfresco mail action, the retrieval of a FreeMarker template from the repository for the message body, and passing the Activiti workflow variables and package properties to the template so the email can give more specific information about the article needing attention.

I simplified things a bit in the above snippet, as I normally put static variables such as the email subject, email template path etc. as properties in a configuration file for the same reasons I mentioned earlier.

Hopefully it’s not too difficult to see how you could extend the concepts and “tricks” I’ve shown in these two steps to make an intelligent workflow capable of making decisions and notifying pesky users to take care of neglected tasks.

Custom Share role breaks existing sites solution

ADDED: Your new group is added to the ACL of both site and document library spaces with the role taken from your new group name i.e. new group is  GROUP_site_SOMESITEID_SiteCollaboratorPlus, SiteCollaboratorPlus is the role.

When you create a custom role in Share you must know up front what roles you need – before your project begins because it breaks your existing sites if you deploy the role after the fact. This isn’t always how things work out so here is a quick little webscript that will fix existing sites when deploying a new custom role.

This is a java backed webscript that will search all existing sites that do not have your custom role security group. It will then take your new role name via Spring and dynamically add it to all sites in share (I put it in spring because we don’t want people creating new roles to ALL sites via a URL parameter).

If you are un-familiar with how the files talk to eachother or where to put things please see the Alfresco Wiki on Java backed webscripts. Once the webscript is installed, simply go to http://localhost/alfresco/wcs/ecm/sites/fixrole (Make sure you change the spring context to add YOUR custom role name)

The context file (webscript-context.xml):

This descriptor file is where you want to put your custom group name that its complaining about. Just replace your siteid with the :SITE param you see below.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN 2.0//EN' 'http://www.springframework.org/dtd/spring-beans-2.0.dtd'>

<beans>

    <bean id="webscript.ca.abstractive.ecm.alfresco.sitefix.fix-role.get" 

      parent="webscript">

        <property name="serviceRegistry" ref="ServiceRegistry" />
        <property name="searchService" ref="SearchService" /> 
        <property name="nodeService" ref="NodeService" />
        <property name="authorityService" ref="AuthorityService" /> 

        <!-- Custom role SiteCollaboratorPlus -->
        <property name="authorityName">
            <value>GROUP_site_:SITE_SiteCollaboratorPlus</value>
        </property>  
    </bean>
</beans>

The webscript descriptor (fix-role.get.desc.xml):

<webscript>
  <shortname>Fix Share Roles</shortname>
  <description>Adds new role to all existing sites</description>
  <url>/ecm/sites/fixrole</url>
  <authentication>user</authentication>
  <format default="">argument</format>
  <family>Toolkit</family> 
</webscript>

The Java (UtilityRoleCreation.java):

package ca.abstractive.ecm.alfresco.sites;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.alfresco.model.ContentModel;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.ResultSetRow;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.web.bean.repository.Repository;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

public class UtilityRoleCreation extends AbstractWebScript {
 
    protected ServiceRegistry serviceRegistry = null;
    protected SearchService searchService = null; 
    protected NodeService nodeService = null;
    protected AuthorityService authorityService = null;
    protected String authorityName = null;
    protected PermissionService permissionService = null;
    
    
    /**
     * Returns a list of sites in Alfresco
     *   
     * @return a list of site nodes 
     */  
    protected List<NodeRef> getSites(){
    
        //Get all sites
        String nodeQuery = "TYPE:\"st:site\"";

        SearchParameters params = new SearchParameters();
        params.setLanguage(SearchService.LANGUAGE_LUCENE);
        params.addStore(Repository.getStoreRef());
        params.setQuery(nodeQuery);
        ResultSet results = null;
        List<NodeRef> siteNodeRefs = new ArrayList<NodeRef>();
        
        try {
            results = this.searchService.query(params);
            if (results == null) {
                return null;

            } else {

                Iterator<ResultSetRow> siteNodes = results.iterator();
                while(siteNodes.hasNext()){
                     
                     ResultSetRow siteRow = siteNodes.next();
                     NodeRef nodeRef = siteRow.getNodeRef();
                     
                     siteNodeRefs.add(nodeRef);
                }
                
                return siteNodeRefs;
            }
            
        } finally {
            if (results != null) {
                results.close();
            }
        } 
         
    }    
    
    /**
     *  Webscript execute method
     *  
     */
    public void execute(WebScriptRequest req, WebScriptResponse res) 
            throws IOException  {
         
        try
        { 
            
            // build a json object
            JSONObject obj = new JSONObject();
            
            List<NodeRef> sites = this.getSites();
              
            Iterator<NodeRef> siteNodes = sites.iterator();
            while(siteNodes.hasNext()){
                 
                NodeRef nodeRef = siteNodes.next();
                  
                 //Get the sitename
                 String siteid = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);  
                 String siteGroup = new StringBuilder().append("GROUP_site_").append(siteid).toString();
                       
                 if(this.authorityService.authorityExists(siteGroup)){
                    
                    Set<String> authorities = this.authorityService.getContainedAuthorities(AuthorityType.GROUP, siteGroup, true);
                    String customRoleAuthorityName = this.authorityName.replace(":SITE", siteid);
                    
                    if (authorities.contains(customRoleAuthorityName) == false)
                     { 
                        obj.put(siteGroup, customRoleAuthorityName);
                        
                        //create the authority
                        String newAuthName = this.authorityService.createAuthority(AuthorityType.GROUP, customRoleAuthorityName.replace("GROUP_", "")); 
                        this.authorityService.addAuthority(siteGroup, newAuthName);
                         
                        // Assign the group the relevant permission on the site and document library
                        String[] bits = customRoleAuthorityName.split("_");
                        String role = bits[bits.length-1];
                        
                        permissionService.setPermission(nodeRef, customRoleAuthorityName, role, true);                            
                        NodeRef libraryNodeRef = nodeService.getChildByName(nodeRef, ContentModel.ASSOC_CONTAINS, "documentLibrary");
                        permissionService.setPermission(libraryNodeRef, customRoleAuthorityName, role, true);
                     }
                 }
            }
             
            // build a JSON string and send it back
            String jsonString = obj.toString();
            res.getWriter().write(jsonString);
        }
        catch(JSONException e)
        {
            throw new WebScriptException("Unable to serialize JSON");
        } 
    }
    
     
    
    /* ------------------------------------------------------------ */
    /* Getter/Setter methods */
    /* ------------------------------------------------------------ */
    
    /**
     * @return the serviceRegistry
     */
    public ServiceRegistry getServiceRegistry() {
        return serviceRegistry;
    }

    /**
     * @param serviceRegistry the serviceRegistry to set
     */
    public void setServiceRegistry(ServiceRegistry serviceRegistry) {
        this.serviceRegistry = serviceRegistry;
        this.searchService = serviceRegistry.getSearchService(); 
        this.nodeService = serviceRegistry.getNodeService();
        this.permissionService = serviceRegistry.getPermissionService();
    }

    /**
     * @return the searchService
     */
    public SearchService getSearchService() {
        return searchService;
    }

    /**
     * @param searchService the searchService to set
     */
    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }
    
    /**
     * @return the authorityService
     */
    public AuthorityService getAuthorityService() {
        return authorityService;
    }

    /**
     * @param authorityService the searchService to set
     */
    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }
      

    /**
     * @return the nodeService
     */
    public NodeService getNodeService() {
        return nodeService;
    }

    /**
     * @param nodeService the nodeService to set
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }


    public String getAuthorityName() {
        return authorityName;
    }


    public void setAuthorityName(String authorityName) {
        this.authorityName = authorityName;
    }
    
    /**
     * Set permission service
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }  
    public PermissionService getPermissionService()
    {
        return permissionService;
    }
}

Creating Layer Overlays for Google Maps and WebFOCUS

BACKGROUND

 

Over the past several years WebFOCUS mapping functionality has come a long way down a darkly lit, sometimes hopeless hallway. I recall in my early years of learning the focexec ropes receiving a request for me to plot call center information on a map to visualize relationships in call types and their geography.

 

Not having the current Google Map integration available to me, I began my journey towards what today is a fully functional mapping tool. The solution consisted of XML output from WebFOCUS being consumed by what by today’s standards is considered to be a rather primitive Yahoo map. The data, displayed as pins on the map was determined by the user’s selection of criteria via an HTML form.

 

Today mapping has spread throughout the Enterprise and is used in a multitude of ways. From call center applications used to visualize the location of clients and providers to managerial visualization of trending, mapping has become as popular as charting and trending.

 

THE HOOK

 

Recently, I was tasked with the requirement of pushing mapping further into the daily lives of the executive and management teams within our organization. As we know, the style of reporting required at this level is different than at the operational level, dealing more with summarizations and high level data than with individual clients.

 

I recalled a presentation I had been in with a colleague of mine at a previous Summit conference. In it Mr. Dirk Kuerbig, all around IBI genius and good guy, presented a solution for presenting data in layers overlaid on a Google Map. This was to be the basis point of my solution.

 

THE REQUIRMENTS

 

  • WebFOCUS 7.7.2
  • Google Map Integration
  • Application must allow for the selection of the following basic criteria
    • The statistic the user wished to see (Total Dollars, Total Clients, etc)
    • The overlay (City, Constituency, Province, etc)
    • The application must then prompt for subsequent parameters according to the statistic selected
    • The map would be presented based the selected layers. The layer color would be shown as a heat map (lower value =lighter color, higher value = higher colors)

 

THE ISSUES

 

BUILT IN INTEGRATION INSUFFICIENT

Looking at the solution requirements it became immediately clear that the built in Google Integration with WebFOCUS would not be sufficient. The following list of issues were identified:

  • Current to 7.7.2, Google map integration only allows for layers based on individual points, not a layer based on a zone or region or series of points.
  • No capability to do heat mapping (Although you can have dynamic pin colors based on a defined field)

 

GOOGLE MAP/BROWSER DEFICIENCIES

Once the build began we quickly found that in simple cases, where shapes being drawn as a layer only included a few points there were no issues. We also noted that in the case of complex shapes, like those of city or constituency boundaries, Google Maps would crash and cause serious performance issues.

 

CONSUMING WEBFOCUS DATA

Out of the box, WebFOCUS best option for consuming data from a webpage is XML. I don’t like XML. JSON was the preferred option for this project.

 

THE SOLUTION

 

THE DATA

Thanks to a tip and some code from Mr. Darin Lee, and a properly configured XSL template provided by my colleague, Mr. Nathan Wong of Abstractive, we were able to extend WebFOCUS allowing us to specify JSON output. This data is dynamically served through WebFOCUS based on the selected layer overlay and criteria.

 

THE LAYERS

This was the big time-muncher! To properly configure the overlay information to be able to display it on a Google Map I had followed the process outlined below. It is important to note that I began with a shape file in the .shp file extension (Typically ArcGIS). If you have the shape file in KML format, you may skip the initial steps.

 

Convert File to KML

*Owning a licence to ArcGIS client tools I utilized the tools available to me.

  1. Open ArcMap
  2. Import the desired layer
  3. Using the “Export to KML” Extension (http://arcscripts.esri.com/details.asp?dbid=14273) export the file to XML

 

Encode the Layer Information

Open the KML document in a text editor. Review its structure and notice the boundary information included. Each boundary may include thousands of points which make up the shape. Eack boundary was encoded in order to aid Google Maps in their timely creation.

 

*I am sure that someone smarter than I will think of a way to automate this, but I encoded each boundary manually.

 

  1. Copy each coordinate within a boundary “<coordinates>” to “</coordinates>”, keeping in mind that there may be inner and outer rings as well.
  2. Copy latitude and longitude comma delimited values into the form at http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/encodeForm.html
  3. Run the Form and save the results.

 

Build JavaScript Object to Display the Layers and Heat Map

Once we had collected all the necessary information I was then able to begin the fun part, painting the picture including:

 

  • Dynamic display of layer based on selection
  • Dynamic display of background color based on predefined color ranges (mine are yellow to blue), minimum and maximum data values (based on the data) and the value of the given boundary.
  • Dynamic tooltips and popup messages based on area of the map the user is viewing

 

SUMMARY

 

Today, within our enterprise we have another WebFOCUS based application ready to visualize data geographically. Whether it is a program manager wanting to know where his client base resides or an executive wanting to know the average age of his cliental within a specific geographic location, information is available and ready to aid in vital business decisions.

 

Please contact me at any time should you have any questions or corrections.

Alfresco Sub-groups

My goal: add a group as a sub-group of another group – ie. nest groups.

Alfresco Version: Enterprise – v3.4.7 (572)

My first try – the Alfresco Explorer UI

The problem with this is that although it will show you sub-groups it will only let you add users to groups, not other groups.

My second try – the Alfresco Share UI

I started by searching for my parent group by name.
Found it – great. Next I clicked the Edit icon.
Turns out this only lets you edit the display name of the group.
So I clicked the Browse button.
In the browse interface when you click on a group you can add sub-groups and users – perfect.
BUT…for some reason my parent group did not show up on the list. It’s not a system list or anything special, just a local Alfresco group I created.

My third try – script it

I took a look at my trusty Alfresco javascript API (http://wiki.alfresco.com/wiki/3.4_JavaScript_API)
Adding a sub-group this way is pretty simple:

  1. Find the parent group
  2. Find the child group
  3. Add the child to the parent

Here is the javascript (NOTE the “GROUP_” prefix when finding a group):

var parentGrp = people.getGroup("GROUP_My Parent Group"); 
var childGrp = people.getGroup("GROUP_My Child Group"); 
people.addAuthority(parentGrp, childGrp);

 

I don’t really understand why, in Share, searching for a specific group and browsing groups offer different options on the results.

But scripting it is a quick, easy work-around.

Our New Home

Welcome to the new and improved Abstractive Technology Consulting website. Over the past few months our in house design guru, Mr. Mike Priest, myself and the rest of the Abstractive team have been working to overhaul the previous Abstractive site.

 

Our small consulting company has travelled a long road since our inception just a few short years ago. We have been able to form strong partnerships between coworkers, partners and our amazing clients, and we look forward with great anticipation towards the growth and opportunity that will present itself in the weeks, months and years to come.

 

I wanted to take a few moments to introduce you to some of the features of our new home.:

 

The Design (Via Mike Priest)

The design used in throughout the website is dynamic and elegant. By utilizing the responsive web design concept, modern web frameworks and key user interaction methods; we created a site that feels like it was built specifically for the device the user is using.

The home page was built into one page. This allowed us to deliver a message from all parts of our company without having to navigate to those sections individually. As you know from mobile browsers, it can be slow and tedious to move between numerous pages on a website; especially if you are on the train. To make the usability a bit more fun we added auto scrolling on the main navigation for the home page. Since mobile devices have a limited viewport it makes it much easier to scroll to sections if it is automatically done for you, and as you can see there is a link to scroll right back to where you started. Simple.

On a computer or laptop the home page gives more of a pamphlet look and feel. As the page is quite long we move the navigation with you. This again allows for better usability eliminating the need  to scroll all the way to the top
of the page to get back home.

We are a big fan of cufon as it renders custom font much crisper than @font-face. Hopefully you can see, all in all we have more of an “abstract” art kind of delivery.

 

The Content

We have configured our new site to present to you information on our services and products. Please take a minute to read up on the various services we provide and contact us with any questions you might have.

 

We also wanted to provide our clients and potential employees with more insight into how we function as an organization. Our new site has added sections on:

  • The Abstractive Consulting Community Fund (ACCF), formed in 2011 to aid us in giving back to our community,
  • Our Partners – We have been lucky enough to form great relationships with industry leading technology providers
  • Careers – Our company is always looking for bright, creative minds to join with us in providing quality consulting services. Here we highlight some of our employee benefits and positions which may have openings
  • Blog – A new journey for us into the social media universe, we are happy to share our experiences within the ECM and BI world.

 

Abstractive Analytics

We are proud at Abstractive Technology Consulting not to be your typical consulting firm. We know the world of technology is driven by those who are willing to lead and innovate. Armed with that understanding and with minds suitable to the task, our team has developed Abstractive Analytics, an unique analytical tool capable of capturing and analyzing user interactions across your web-based systems, allowing you to improve your business flow. For more information or a demo please contact us at info@abstractive.ca.

 

Thanks for Visiting

Thanks for visiting the site. We certainly appreciate your interest in our company. If you have any questions or comments please feel free to contact us.

 

Gary Weller

Senior Business Intelligence Consultant

 

 

 

Implementing a complete high availability Alfresco solution using open source technologies

As a proof of concept, I have done some research and experimenting to determine the best way of clustering Alfresco using completely open source components. I wanted a solution that offered load balancing as well as fault tolerance. There are three components outside of Alfresco that are needed to achieve this.

  1. Load balancer
  2. File system
  3. Database

The load balancer is the simplest component of the three, and the one with the most options available. We just need a load balancer that is able to handle sticky sessions. A dumb load balancer which round robins connections will not work for this scenario.

Alfresco stores all the content as regular files. (Unlike Sharepoint putting content in the database. Yikes!) In order to achieve HA on the content repository we need some sort of clustered or replicating file system. It was not long ago when clustered file systems were out of reach from the open source community. It is great that we now have some viable open source options now.

The last component needed, of course, is the database. Unfortunately, there is no viable multi-master open source option. There are many projects that are working towards this, such as Bucardo. But there is nothing currently that is a drop in replacement and/or production ready. The good news is we still have a master-slave(s) setup that can still achieve HA and some sort of load balancing.

Here is the complete solution I implemented:

 

Alfresco: Alfresco Enterprise 4.0.2

I used the latest version of Alfresco Enterprise at the time of writing this, just since it is what I deal with the most. I believe the Community Edition would work just fine as well in this scenario since the heart of Alfresco clustering is within Ehcache.

 

Load balancer: HAProxy

HAProxy is known to be very stable and currently used on some very high traffic web sites. It also gives us the functionality to keep track of sessions via the JSESSIONID cookie. Another great feature is we can take the fault detection further, and test a web script page in Alfresco to determine if Alfresco is currently running. (http://admin:passwd@server1/alfresco/wcs/s is a great page to check.)

There will be a small portion of people that were looking at this diagram and saying to themselves, “But there is a single point of failure!” HAProxy is a very simple component, and it would be easy to set up an active/passive automatic fail over. Also very stable physical and virtual options exist.

I should also note that we have tested HAProxy using single sign on authentication via Active Directory Kerberos. I assume NTLM would work just fine as well.

 

Clustered file system: GlusterFS

I have read good things about GlusterFS, but this was my first hands on experience with it. I was shocked how simple and quick this was to get up and running. A command to add the second server, and another to get the replication going. No messing with configuration files. You can even have 4 servers and enable replication and striping. Similar to the way RAID 10 (or 0+1) works, but across servers. This is a perfect fit for putting Alfresco’s content. Load balancing and seamless fault tolerance.

 

Replicating database: PostgreSQL + pgpool-II

MySQL is still an option, but I chose to go with Postgres here. I liked some of the HA features Postgres provided that seemed lacking in MySQL.  Unfortunately, either way we have to use a master-slave replication configuration.

In order to achieve load balancing and fault tolerance we need to put pgpool-II in front on the databases. It will take read only queries and load balance them between the master and slave(s). Commands that involve any kind of updates, or writes will be forwarded to the master which in turn get streamed to the slaves. This makes writes slower than a standalone database, but most Alfresco installs should be primarily reads for the average implementation. Pgpool can also be configured to use parallel queries. This means large queries can be split up amongst servers.

Pgpool will also detect any faults, so if any of the slaves go down it will just take them out of the pool. And if the master goes down, it will take one of the slaves and promote it to the new master. For the chance of a problem with Pgpool, a similar configuration with HAProxy, an active/passive configuration can be used to add some redundancy.

 

Enjoy your content management uptime! And feel free to drop me a comment.