Business Blog Directory  Business  Business blogs  Blog Directory  Blog Directory & Search engine 

Thursday, September 1, 2011

Content Management - Internationalising Alfrescos LIST Constraints


EOSSOnline incorporates an Enterprise Content Management System into its EOSS stack to manage the creation and retrieval of unstructured information, mostly in the form of documents, and the ECM of choice for the EOSS stack is the rather excellent Open Source Alfresco 3.4.d Community edition.

Alfresco is much more than just a CMS but that's all we're interested in in this particular post. If you install Alfresco directly from one of the pre-packaged zip bundles you can download from their site you will have a fairly competent CMS at you disposal that you can even expose to your users as if it were a standard shared network drive.

The EOSS platform mainly uses Alfresco internally to provide content as a service to other applications and internal business processes in the EOSS stack as well as to customer defined business processes in general.

The EOSS stack has been designed to be completely internationalisable and in general Alfresco meets this requirement with it's language pack concept. However, there are a couple of areas with regards it's UI that it doesn't handle this so well. One of those areas is the rendering of drop down lists in the Alfresco Explorer UI.

So in this post we will take a look at creating a new, reusable, internationalised, drop down list component that can handle the users locale, as well as a few other bonuses. This can usually be done in one of two ways, you can either create a custom Constraint object or you can create a custom Renderer that works at the UI level, you could also employ a mix of both principles if you wanted to. There are already some articles on creating custom constraints available on the internet, one of which deals with dynamic lists that you can find here . We are going to look at the other option, creating a Custom JSF renderer to do the job instead.
There are pros and cons to each of these approaches but I just happen to like the pure renderer approach because if done right it has all of the benefits and few if any of the problems. In-fact one of those other bonuses I mentioned earlier is that our new list renderer supports language collation as well, so we can define our lists to render in Ascending, Descending or LIST constraint order.

A word of caution here about using list constraints for properties in general, they should only be used to represent a known list of statuses and if you must insist on modifying this list after content has already been created in your repository, be very careful that you only append new options to the list and DO NOT remove existing options as you may well cause existing content with properties set to those now missing values to be invalid and may even cause issue when rendering the property in the UI.

With the previous words of caution fresh in our mind this is how we are going to advocate using our new renderer, to represent a selection of key values.
Using the standard Alfresco LIST constraint approach we would normally do something like the following:

Create a new model file and call it something like test-model.xml to include our new LIST constraint element and our test aspect. Place this file in the directory [tomcat-install]/shared/classes/alfresco/extension/eossonline/models.
<constraints>
  <constraint name="eoss:projectStatus" type="LIST">
            <parameter name="allowedValues">
                <list>
                    <value>Red</value>
                    <value>Yellow</value>
                    <value>Green</value>
                </list>
            </parameter>
       </constraint>
</constraints>
Actually, with our new way of thinking we will change these to be states represented as keys instead. So we would instead do something like the following.
<constraints>
  <constraint name="eoss:projectStatus" type="LIST">
            <parameter name="allowedValues">
                <list>
                    <value>status_red</value>
                    <value>status_yellow</value>
                    <value>status_green</value>
                </list>
            </parameter>
       </constraint>
</constraints>

Lets also create our new aspect with a couple of properties constrained by our LIST
<aspects>
  <aspect name="eoss:projectRelated">
   <title>Project Aspect</title>
   <properties>
    <property name="eoss:status">
     <type>d:text</type>
     <mandatory>true</mandatory>
     <default>status_green</default>
     <constraints>
      <constraint ref="eoss:projectStatus" />
     </constraints>
    </property>
    <property name="eoss:statusAsc">
     <type>d:text</type>
     <mandatory>true</mandatory>
     <default>status_green</default>
     <constraints>
      <constraint ref="eoss:projectStatus" />
     </constraints>
    </property>
    <property name="eoss:statusDesc">
     <type>d:text</type>
     <mandatory>true</mandatory>
     <default>status_green</default>
     <constraints>
      <constraint ref="eoss:projectStatus" />
     </constraints>
    </property>
   </properties>
  </aspect>
 </aspects>
Nothing special here and if left like this these would be the actual values our users would see in the drop down lists, no matter what locale they were logged in as.


As you can see here, we have created an aspect with three properties we will use to test out our new constraints renderer. Obviously nothing here will change the sort orders of the drop down lists or map these keys to values in any resource bundles but the properties have been named according to our intent.
We now need to tell Alfresco that we want to be able to add this aspect to existing content and that we would also like the properties to be displayed and editable. We do this by updating the web-client-config-custom.xml file located in the [tomcat-install]/shared/classes/alfresco/extension directory like so.
<alfresco-config>
	<config evaluator="string-compare" condition="Action Wizards"> 
		<aspects> 
			<aspect name="eoss:projectRelated"/> 
		</aspects> 
	</config>
   
	<config evaluator="aspect-name" condition="eoss:projectRelated">
      	<property-sheet>
         		<show-property name="eoss:statusAsc"/>
         		<show-property name="eoss:statusDesc"/>
         		<show-property name="eoss:status"/>
		</property-sheet>
	</config>
</alfresco-config>

Time for a little gripe me thinks..... Most things in Alfresco are fairly easy to customise in an isolated way. However, the rigid naming and location of this file makes customising Alfresco a pain. It stops me from being able to just unzip my extensions onto an exploded Alfresco instance. Basically, most extensions you come across will want to modify this file and if you unzip multiple extensions then this file will most likely only contain the details needed for the last one you unzipped. I'm not even sure that packaging customisations as AMP modules alleviate this issue either. Anyway, back to the implementation.

We also need to bootstrap our new model component when Alfresco starts up. As the boot strapping is handled by Spring configuration you can pretty much name this file anything you like as long as it end in -context.xml. In our case we will call it eossonline-model-context.xml and put it in the [tomcat-install]/shared/classes/alfresco/extension directory with the following contents.
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
   <!--
      EOSSOnline Bootstrap Extension Sequence. This file specifies the
      initialisation (and order of initialisation) to perform during Repository
      startup for beans.
   -->
<beans>
 	<bean id="eossonline.dictionaryBootstrap" parent="dictionaryModelBootstrap" depends-on="dictionaryBootstrap">
		<property name="models">
      		<list>
        <value>alfresco/extension/eossonline/models/test-model.xml</value>
      		</list>
    		</property>
	</bean>
	
	<bean id="eossonline.resourceBundle" class="org.alfresco.i18n.ResourceBundleBootstrapComponent">
		<property name="resourceBundles">
			<list>
	<value>alfresco.extension.eossonline.eossonline-messages</value>
			</list>
		</property>
	</bean>

</beans>

Here we declare two beans to be boostrapped at startup time. The bean id's don't actually matter, we just want to make sure that they won't clash with any other bean id's that may be declared else where. The second of these beans allows us to add a resource bundle to the existing resources already loaded into Alfresco, we will use this in a while to define our property labels and more importantly out Internationalised List values.

If I now start up Alfresco we will be able to add our new aspect to existing content in the repository using the Run Action Wizard and once added we will be able to see and edit the properties just like normal.

Lets first take a look at the internationalisation problem. If I log in to Alfresco now using a Spanish locale I get presented with the same list of options as I would as when logging in using an English locale i.e status_red, status_green and status_yellow, I would prefer my Spanish users to be able to select from Roco, Amarillo and Verde and my English users to select from Red, Yellow and Green instead. What I don't want to do is save these internationalised words in the properties because first of all the constraint would fail and I want to use these values in business processes, workflows and rules defined elsewhere in my solution.

Alfresco Explorer uses JSF to define it's UI controls, actually JSF is a standard so technically Alfresco uses the IceFaces JSF implementation to create it's UI controls but lets forget the semantics here and get on with it shall we.
To configure a new Renderer you have to define your renderers and converters in one of two places. Either you can add them to the faces-config-custom.xml in the the exploded Alfresco WEB-INF directory or you can add a faces-config.xml file to the META-INF directory of a jar file. If you choose the first of these options you will be in the same situation as we are in with the web-client-config-custom.xml. Not all changes regarding the faces config can be bundled with your jar file in the META-INF directory but in our case they can. As we are also going to create a couple of java class files we will take the opportunity to package them up into a jar file along with our faces-config.xml file.

This is what our file looks like.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
                              "http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
   <!-- ==================== COMPONENT GENERATOR BEANS ==================== -->
   <managed-bean>
      <managed-bean-name>EossAscendingListGenerator</managed-bean-name>
      <managed-bean-class>com.eossonline.alfresco.web.ui.generators.EOSSI18nListGenerator</managed-bean-class>
      <managed-bean-scope>session</managed-bean-scope>
      <managed-property>
         <property-name>sortOrder</property-name>
         <value>ASCENDING</value>
      </managed-property>
   </managed-bean>
   
   <managed-bean>
      <managed-bean-name>EossDescendingListGenerator</managed-bean-name>
      <managed-bean-class>com.eossonline.alfresco.web.ui.generators.EOSSI18nListGenerator</managed-bean-class>
      <managed-bean-scope>session</managed-bean-scope>
      <managed-property>
         <property-name>sortOrder</property-name>
         <value>DESCENDING</value>
      </managed-property>
   </managed-bean>
   
   <managed-bean>
      <managed-bean-name>EossListGenerator</managed-bean-name>
      <managed-bean-class>com.eossonline.alfresco.web.ui.generators.EOSSI18nListGenerator</managed-bean-class>
      <managed-bean-scope>session</managed-bean-scope>
   </managed-bean>
   
      <!-- ==================== CONVERTERS ==================== -->
   <converter>
      <converter-id>com.eossonline.faces.ResourceConverter</converter-id>
      <converter-class>com.eossonline.alfresco.web.ui.repo.converters.ResourceConverter</converter-class>
   </converter>
</faces-config>
Basically we have created three JSF managed beans with different names and sort orders as well as defined a converter class that will be used by our renderer class. We have also set the scope of these beans to be session scoped so that they only need to be instantiated once per session by the JSF framework. Once the two java classes are written and the class files are packed up into a jar along with this configuration file we we will be in a position where we can tell Alfresco to use these renderers. To do that we will have to modify the web-client-config-custom.xml file we updated earlier, like so.
...	
<config evaluator="aspect-name" condition="eoss:projectRelated">
      <property-sheet>
		<show-property name="eoss:statusAsc" component-generator= "EossAscendingListGenerator"/>
      	<show-property name="eoss:statusDesc" component-generator= "EossDescendingListGenerator"/>
      	<show-property name="eoss:status" component-generator= "EossListGenerator"/>
	</property-sheet>
</config>
...
Notice how we have added the component-generator property to each show-property element. These names match up with the names of the JSF managed beans in the faces-config.xml file we just create.

Were almost done as we just need to write the java code, create our resource files, package up our jar and drop it into our Alfresco WEB-INF\lib directory and restart Alfresco. I'm not going to explain the java code or the packaging of the class files and JSF config into a jar file in this post, you can take a look at the source code and work out what it does and how to create a jar for your self.

But before I list the java source there,s still one thing left to do. We need to create out resource bundle that will give us our internationalised drop down lists.

We already named it alfresco.extension.eossonline.eossonline-messages earlier in out -context.xml bootstrap file. But just to be clear this represents our base bundle which is named eossonline-messages and thus the property file names will be eossonline-messages.properties, for the default language bundle and any internationalised versions will be named eossonline-messages_[languageCode]_[countryCode].properties for example the Spanish version of the translations would live in a file named eossonline-messages_es_ES.properties and they would all live together in the [tomcat-install]/shared/classes/alfresco/extension/eossonline directory.

# Project Aspect properties
# This defines what Alfresco will display for the aspect in the Action Wizard drop down
eoss_filingModel.aspect.eoss_projectRelated.title=Project

# These values will be displayed as the labels of the properties in the UI
eoss_filingModel.property.eoss_status.title=Project Status
eoss_filingModel.property.eoss_statusAsc.title=Project Status Ascending
eoss_filingModel.property.eoss_statusDesc.title=Project Status Descending
# end of Project Aspect properties

# New statuses can be appended to the end of the list.
# The keys here match the entries defined in the model Constraint LIST
status_green=Green
status_yellow=Yellow
status_red=Red
# end of project statuses
OK here's the Java Code and were done.....

package com.eossonline.alfresco.web.ui.generators;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;

import javax.faces.component.UIComponent;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;

import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint;
import org.alfresco.service.cmr.dictionary.Constraint;
import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.web.app.servlet.FacesHelper;
import org.alfresco.web.bean.generator.BaseComponentGenerator;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.ui.repo.component.property.PropertySheetItem;
import org.alfresco.web.ui.repo.component.property.UIPropertySheet;
import org.springframework.extensions.surf.util.I18NUtil;

import com.eossonline.alfresco.web.ui.repo.converters.ResourceConverter;

public class EOSSI18nListGenerator extends BaseComponentGenerator {

	protected SelectItem[] items = null;

	// This is the default sort order for the list
	protected SortOrder sortOrder = SortOrder.NONE;

	protected Node node;
	
	public EOSSI18nListGenerator() {
	}

	@SuppressWarnings("unchecked")
	public UIComponent generate(FacesContext context, String id) {
	
		UIComponent component = context.getApplication().createComponent(
				UISelectOne.COMPONENT_TYPE);
		FacesHelper.setupComponentId(context, component, id);

		// create the list of choices
		UISelectItems itemsComponent = (UISelectItems) context.getApplication()
				.createComponent("javax.faces.SelectItems");

		itemsComponent.setValue(items);

		// add the items as a child component
		component.getChildren().add(itemsComponent);

		return component;
	}

	@Override
	protected UIComponent createComponent(FacesContext context,
			UIPropertySheet propertySheet, PropertySheetItem item) {
		this.node = propertySheet.getNode();

		return super.createComponent(context, propertySheet, item);
	}

	@Override
	protected void setupConverter(FacesContext context,
			UIPropertySheet propertySheet, PropertySheetItem property,
			PropertyDefinition propertyDef, UIComponent component) {
		
		items = getResourceItems(propertyDef);
		
		createAndSetConverter(context, ResourceConverter.CONVERTER_ID,
				component);
	}

	@Override
	protected void setupMandatoryValidation(FacesContext context,
			UIPropertySheet propertySheet, PropertySheetItem item,
			UIComponent component, boolean realTimeChecking, String idSuffix) {
	}

	protected SelectItem[] getResourceItems(PropertyDefinition propertyDef) {
		if(items != null) {
			return items;
		}
		// First time called, generate the list of options
		
		// Determine sort order
		switch(sortOrder) {
		case ASCENDING:
			getSortOrderAssending(propertyDef);
			break;
		
		case DESCENDING:
			getSortOrderDescending(propertyDef);
			break;
		
		case NONE:
		default:
			getSortOrderNone(propertyDef);
			
		}		
		return items; 
	}
	
	private void getSortOrderAssending(PropertyDefinition propertyDef) {
		List<String> allItems = getListOfValuesConstraint(propertyDef);
		
		// We now have all the possible values that will be used as keys to lookup resources
		SortedMap<String, String> itemsWithValues = new TreeMap<String, String>(new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				return o1.compareTo(o2);
			}
		});

		// The problem we now have is that Sorting is naturally done by key and what we want in our case is sorting by value 
		for(String item : allItems) {
			String value = I18NUtil.getMessage(item);
			// If there is no resource matching this key then use the key as the value
			// This will result in a very similar output as already displayed by the standard LIST constraint drop down
			itemsWithValues.put(value == null ? item : value, item);
		}
		// We now have a SortedMap where the values have been used as the keys for sorting reasons
		
		items = new SelectItem[itemsWithValues.size()];
		int i = 0;
		for(String item : itemsWithValues.keySet()) {			
			String value = itemsWithValues.get(item);
			
			// Were now going to create the SelectItem object by flipping the key and value again. 
			// This gives us the output we expected
			items[i++] = new SelectItem(value == null ? item : value, item);
		}
	}

	private void getSortOrderDescending(PropertyDefinition propertyDef) {
		List<String> allItems = getListOfValuesConstraint(propertyDef);
		
		// We now have all the possible values that will be used as keys to lookup resources
		SortedMap<String, String> itemsWithValues = new TreeMap<String, String>(new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				return o2.compareTo(o1);
			}
		});

		// The problem we now have is that Sorting is naturally done by key and what we want in our case is sorting by value 
		for(String item : allItems) {
			String value = I18NUtil.getMessage(item);
			// If there is no resource matching this key then use the key as the value
			// This will result in a very similar output as already displayed by the standard LIST constraint drop down
			itemsWithValues.put(value == null ? item : value, item);
		}
		// We now have a SortedMap where the values have been used as the keys for sorting reasons
		
		items = new SelectItem[itemsWithValues.size()];
		int i = 0;
		for(String item : itemsWithValues.keySet()) {			
			String value = itemsWithValues.get(item);
			
			// Were now going to create the SelectItem object by flipping the key and value again. 
			// This gives us the output we expected
			items[i++] = new SelectItem(value == null ? item : value, item);
		}
	}

	private void getSortOrderNone(PropertyDefinition propertyDef) {
		Collection<String> allItems = getListOfValuesConstraint(propertyDef);
		
		// As we don't intend to sort this list we are basically done... 
		// so just iterate over the list and create the SelectItem array
		items = new SelectItem[allItems.size()];
		int i = 0;
		for(String item : allItems) {			
			String value = I18NUtil.getMessage(item);
			
			// If there is no resource matching this key then use the key as the value
			// This will result in a very similar output as already displayed by the standard LIST constraint drop down
			items[i++] = new SelectItem(item, value == null ? item : value);
		}
	}
	
	// Let Spring property editors convert from injected String to Enum 
	public void setSortOrder(String sortOrder) {
		if("ascending".compareToIgnoreCase(sortOrder) == 0 ) {
			this.sortOrder = SortOrder.ASCENDING;
		} else if("descending".compareToIgnoreCase(sortOrder) == 0) {
			this.sortOrder = SortOrder.DESCENDING;
		}
	}

    public List<String> getListOfValuesConstraint(PropertyDefinition propertyDef) {

        List<String> lovConstraint = new Vector<String>();

        if (propertyDef != null) {
            // go through the constraints and see if it has any
            // list of values constraint
            List<ConstraintDefinition> constraints = propertyDef.getConstraints();

            for (ConstraintDefinition constraintDef : constraints) {
                Constraint constraint = constraintDef.getConstraint();

                if (constraint instanceof ListOfValuesConstraint) {
                    lovConstraint.addAll(((ListOfValuesConstraint) constraint).getAllowedValues());
                }
            }
        }

        return lovConstraint;
    }
    
	enum SortOrder {
	    NONE, ASCENDING,DESCENDING 
	}	
}

package com.eossonline.alfresco.web.ui.repo.converters;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

import org.springframework.extensions.surf.util.I18NUtil;

public class ResourceConverter implements Converter
{
   /**
    * <p>The standard converter id for this converter.</p>
    */
   public static final String CONVERTER_ID = "com.eossonline.faces.ResourceConverter";

   /**
    * @see javax.faces.convert.Converter#getAsObject(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.String)
    */
   public Object getAsObject(FacesContext context, UIComponent component, String value)
   {   
      if(value == null)
      {
         throw new IllegalArgumentException(I18NUtil.getMessage("error_locale_null"));
      } 
      else
      {
         return value;
      }
   }

   /**
    * @see javax.faces.convert.Converter#getAsString(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.Object)
    */
   public String getAsString(FacesContext context, UIComponent component, Object value)
   {
      if(value == null)
      {
         throw new IllegalArgumentException(I18NUtil.getMessage("error_locale_null"));
      }
      
      //    if the component's renderer type is  javax.faces.Text, return 
      //    the language label corresponding to the received language code (as string) or the received locale
      else if(component.getRendererType().equalsIgnoreCase("javax.faces.Text"))
      {
    	  String ret = I18NUtil.getMessage((String)value);
    	  if(ret == null) {
    		  ret = (String)value;
    	  }
    	  return ret;
      }
      //    else don't modify 
      else
      {
         return value.toString();
      }                            
   }   
}

Thats all for today folks. We hope you get some millage out of this.

Steven McArdle CTO
On behalf of the eossonline team.

No comments:

Post a Comment