← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 9416: Analytics, impl support for disaggregation for data elements with average aggregation operator

 

------------------------------------------------------------
revno: 9416
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Sun 2012-12-30 17:23:41 +0100
message:
  Analytics, impl support for disaggregation for data elements with average aggregation operator
modified:
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java
  dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/TextUtils.java


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsManager.java	2012-12-18 16:01:44 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsManager.java	2012-12-30 16:23:41 +0000
@@ -30,9 +30,30 @@
 import java.util.Map;
 import java.util.concurrent.Future;
 
+import org.hisp.dhis.system.util.ListMap;
+
 public interface AnalyticsManager
 {
-    static final String SEP = "-";
+    static final char SEP = '-';
     
-    Future<Map<String, Double>> getAggregatedDataValues(  DataQueryParams params );
+    /**
+     * Retrieves aggregated data values for the given query. The data is returned
+     * as a mapping where the key is concatenated from the dimension options for
+     * all dimensions, and the value is the data value.
+     * 
+     * @param params the query to retrieve aggregated data for.
+     * @return a map.
+     */
+    Future<Map<String, Double>> getAggregatedDataValues( DataQueryParams params );
+
+    /**
+     * Inserts entries for the aggregation periods mapped to each data period
+     * in the given data value map. Removes the original entry for the data period.
+     * 
+     * @param dataValueMap map with entries for all data values produced for the query.
+     * @param params the query.
+     * @param dataPeriodAggregationPeriodMap the mapping between data periods and
+     *        aggregation periods for this query.
+     */
+    void replaceDataPeriodsWithAggregationPeriods( Map<String, Double> dataValueMap, DataQueryParams params, ListMap<String, String> dataPeriodAggregationPeriodMap );
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java	2012-12-27 19:15:15 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java	2012-12-30 16:23:41 +0000
@@ -27,6 +27,8 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import static org.hisp.dhis.analytics.AggregationType.AVERAGE_DISAGGREGATION;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -34,7 +36,10 @@
 import java.util.Map;
 
 import org.hisp.dhis.common.Dxf2Namespace;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.period.PeriodType;
 import org.hisp.dhis.system.util.CollectionUtils;
+import org.hisp.dhis.system.util.ListMap;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@@ -67,7 +72,9 @@
     
     private transient int organisationUnitLevel;
     
-    private AggregationType aggregationType;
+    private transient AggregationType aggregationType;
+    
+    private transient PeriodType dataPeriodType;
     
     // -------------------------------------------------------------------------
     // Constructors
@@ -94,17 +101,23 @@
         this.periodType = params.getPeriodType();
         this.organisationUnitLevel = params.getOrganisationUnitLevel();
         this.aggregationType = params.getAggregationType();
+        this.dataPeriodType = params.getDataPeriodType();
     }
 
     // -------------------------------------------------------------------------
     // Logic
     // -------------------------------------------------------------------------
 
+    /**
+     * Creates a list of the names of all dimensions for this query. If the period
+     * type property is set, the period dimension name will be replaced by the name
+     * of the period type, if present. If the organisation unit level property
+     * is set, the organisation unit dimension name will be replaced by the name
+     * of the organisation unit level column.
+     */
     public List<String> getDimensionNames()
     {
-        List<String> list = new ArrayList<String>();
-
-        list.addAll( dimensions.keySet() );
+        List<String> list = getDimensionNamesAsList();
         
         if ( categories )
         {
@@ -124,11 +137,28 @@
         return list;
     }
     
+    /**
+     * Returns the index of the period dimension in the index list.
+     */
+    public int getPeriodDimensionIndex()
+    {
+        return getDimensionNamesAsList().indexOf( PERIOD_DIM_ID );
+    }
+    
+    /**
+     * Returns a list of the names of all filters.
+     */
     public List<String> getFilterNames()
     {
         return new ArrayList<String>( filters.keySet() );
     }
-        
+    
+    /**
+     * Returns a mapping between the dimension names and dimension values. Inserts
+     * keys and values for the current period type column name and organisation 
+     * unit level name, if the period type property and organisation unit level
+     * property are set.
+     */
     public Map<String, List<String>> getDimensionMap()
     {
         Map<String, List<String>> map = new HashMap<String, List<String>>();
@@ -173,7 +203,62 @@
     {
         return this.aggregationType != null && this.aggregationType.equals( aggregationType );
     }
-    
+
+    /**
+     * Creates a mapping between the data periods, based on the data period type
+     * for this query, and the aggregation periods for this query.
+     */
+    public ListMap<String, String> getDataPeriodAggregationPeriodMap()
+    {
+        ListMap<String, String> map = new ListMap<String, String>();
+
+        if ( dataPeriodType != null )
+        {
+            for ( String period : this.getPeriods() )
+            {
+                Period aggregatePeriod = PeriodType.getPeriodFromIsoString( period );
+                
+                Period dataPeriod = dataPeriodType.createPeriod( aggregatePeriod.getStartDate() );
+                
+                map.putValue( dataPeriod.getIsoDate(), period );
+            }
+        }
+        
+        return map;
+    }
+    
+    /**
+     * Replaces the periods of this query with the corresponding data periods.
+     * Sets the period type to the data period type. This method is relevant only 
+     * when then the data period type has lower frequency than the aggregation 
+     * period type.
+     */
+    public void replaceAggregationPeriodsWithDataPeriods( ListMap<String, String> dataPeriodAggregationPeriodMap )
+    {
+        if ( isAggregationType( AVERAGE_DISAGGREGATION ) &&  dataPeriodType != null )
+        {
+            this.periodType = this.dataPeriodType.getName();
+            
+            setPeriods( new ArrayList<String>( getDataPeriodAggregationPeriodMap().keySet() ) );
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // Logic
+    // -------------------------------------------------------------------------
+
+    /**
+     * Returns the dimension names as a list.
+     */
+    private List<String> getDimensionNamesAsList()
+    {
+        return new ArrayList<String>( dimensions.keySet() );
+    }
+    
+    // -------------------------------------------------------------------------
+    // hashCode, equals and toString
+    // -------------------------------------------------------------------------
+
     @Override
     public int hashCode()
     {
@@ -242,7 +327,7 @@
     {
         return "[Dimensions: " + dimensions + ", Filters: " + filters + "]";
     }
-        
+    
     // -------------------------------------------------------------------------
     // Get and set methods for serialize properties
     // -------------------------------------------------------------------------
@@ -412,4 +497,14 @@
     {
         this.aggregationType = aggregationType;
     }
+
+    public PeriodType getDataPeriodType()
+    {
+        return dataPeriodType;
+    }
+
+    public void setDataPeriodType( PeriodType dataPeriodType )
+    {
+        this.dataPeriodType = dataPeriodType;
+    }
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java	2012-12-27 19:15:15 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java	2012-12-30 16:23:41 +0000
@@ -45,6 +45,8 @@
 import org.hisp.dhis.system.util.Timer;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import static org.hisp.dhis.analytics.AnalyticsManager.SEP;
+
 public class DefaultAnalyticsService
     implements AnalyticsService
 {
@@ -76,7 +78,7 @@
         for ( Map.Entry<String, Double> entry : map.entrySet() )
         {
             grid.addRow();
-            grid.addValues( entry.getKey().split( AnalyticsManager.SEP ) );
+            grid.addValues( entry.getKey().split( String.valueOf( SEP ) ) );
             grid.addValue( entry.getValue() );
         }
         
@@ -115,5 +117,5 @@
         t.getTime( "Got aggregated values" );
         
         return map;
-    }
+    } 
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java	2012-12-27 19:15:15 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java	2012-12-30 16:23:41 +0000
@@ -29,6 +29,7 @@
 
 import static org.hisp.dhis.dataelement.DataElement.AGGREGATION_OPERATOR_AVERAGE;
 import static org.hisp.dhis.dataelement.DataElement.AGGREGATION_OPERATOR_SUM;
+import static org.hisp.dhis.analytics.AggregationType.*;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -93,13 +94,28 @@
                     
                     for ( DataQueryParams byAggregationType : groupedByAggregationType )
                     {
-                        byAggregationType.setTableName( byPartition.getTableName() );
-                        byAggregationType.setOrganisationUnitLevel( byOrgUnitLevel.getOrganisationUnitLevel() );
-                        byAggregationType.setPeriodType( byPeriodType.getPeriodType() );
-                        
-                        //TODO split on data element period type for average disaggregation
-                        
-                        queries.add( byAggregationType );
+                        if ( AVERAGE_DISAGGREGATION.equals( byAggregationType.getAggregationType() ) )
+                        {
+                            List<DataQueryParams> groupedByDataPeriodType = groupByDataPeriodType( byAggregationType );
+                            
+                            for ( DataQueryParams byDataPeriodType : groupedByDataPeriodType )
+                            {
+                                byDataPeriodType.setTableName( byPartition.getTableName() );
+                                byDataPeriodType.setOrganisationUnitLevel( byOrgUnitLevel.getOrganisationUnitLevel() );
+                                byDataPeriodType.setPeriodType( byPeriodType.getPeriodType() );
+                                byDataPeriodType.setAggregationType( byAggregationType.getAggregationType() );
+                                
+                                queries.add( byDataPeriodType );
+                            }
+                        }
+                        else
+                        {
+                            byAggregationType.setTableName( byPartition.getTableName() );
+                            byAggregationType.setOrganisationUnitLevel( byOrgUnitLevel.getOrganisationUnitLevel() );
+                            byAggregationType.setPeriodType( byPeriodType.getPeriodType() );
+                            
+                            queries.add( byAggregationType );
+                        }
                     }
                 }
             }
@@ -267,6 +283,17 @@
         return queries;    
     }
     
+    /**
+     * Groups the given query in sub queries based on the aggregation type of its
+     * data elements. The aggregation type can be sum, average aggregation or
+     * average disaggregation. Sum means that the data elements have sum aggregation
+     * operator. Average aggregation means that the data elements have the average
+     * aggregation operator and that the period type of the data elements have 
+     * higher or equal frequency than the aggregation period type. Average disaggregation
+     * means that the data elements have the average aggregation operator and
+     * that the period type of the data elements have lower frequency than the
+     * aggregation period type.
+     */
     private List<DataQueryParams> groupByAggregationType( DataQueryParams params )
     {
         List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
@@ -293,6 +320,33 @@
     }
     
     /**
+     * Groups the given query in sub queries based on the period type of its
+     * data elements. Sets the data period type on each query.
+     */
+    private List<DataQueryParams> groupByDataPeriodType( DataQueryParams params )
+    {
+        List<DataQueryParams> queries = new ArrayList<DataQueryParams>();
+
+        if ( params.getDatElements() == null || params.getDatElements().isEmpty() )
+        {
+            queries.add( new DataQueryParams( params ) );
+            return queries;
+        }
+        
+        ListMap<PeriodType, String> periodTypeDataElementMap = getPeriodTypeDataElementMap( params.getDatElements() );
+        
+        for ( PeriodType periodType : periodTypeDataElementMap.keySet() )
+        {
+            DataQueryParams query = new DataQueryParams( params );
+            query.setDataElements( periodTypeDataElementMap.get( periodType ) );
+            query.setDataPeriodType( periodType );
+            queries.add( query );
+        }
+        
+        return queries;
+    }
+    
+    /**
      * Replaces the period filter with individual filters for each period type.
      */
     private List<DataQueryParams> setFilterByPeriodType( List<DataQueryParams> queries )
@@ -379,7 +433,7 @@
         
         return map;
     }
-    
+        
     /**
      * Creates a mapping between the aggregation type and data element for the
      * given data elements and period type.
@@ -413,4 +467,22 @@
         
         return map;
     }
+
+    /**
+     * Creates a mapping between the period type and the data element for the
+     * given data elements.
+     */
+    private ListMap<PeriodType, String> getPeriodTypeDataElementMap( Collection<String> dataElements )
+    {
+        ListMap<PeriodType, String> map = new ListMap<PeriodType, String>();
+        
+        for ( String element : dataElements )
+        {
+            DataElement dataElement = dataElementService.getDataElement( element );
+            
+            map.putValue( dataElement.getPeriodType(), element );
+        }
+        
+        return map;
+    }
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java	2012-12-27 19:15:15 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java	2012-12-30 16:23:41 +0000
@@ -27,14 +27,17 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import static org.hisp.dhis.analytics.AggregationType.AVERAGE_AGGREGATION;
+import static org.hisp.dhis.analytics.AggregationType.AVERAGE_DISAGGREGATION;
 import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID;
 import static org.hisp.dhis.system.util.TextUtils.getCommaDelimitedString;
 import static org.hisp.dhis.system.util.TextUtils.getQuotedCommaDelimitedString;
-import static org.hisp.dhis.analytics.AggregationType.*;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Future;
 
 import org.apache.commons.logging.Log;
@@ -42,12 +45,15 @@
 import org.hisp.dhis.analytics.AnalyticsManager;
 import org.hisp.dhis.analytics.DataQueryParams;
 import org.hisp.dhis.period.PeriodType;
+import org.hisp.dhis.system.util.ListMap;
 import org.hisp.dhis.system.util.SqlHelper;
+import org.hisp.dhis.system.util.TextUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.support.rowset.SqlRowSet;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.AsyncResult;
+import org.springframework.util.Assert;
 
 /**
  * This class is responsible for producing aggregated data values. It reads data
@@ -59,6 +65,8 @@
 public class JdbcAnalyticsManager
     implements AnalyticsManager
 {
+    //TODO optimize when all options in dimensions are selected
+    
     private static final Log log = LogFactory.getLog( JdbcAnalyticsManager.class );
     
     @Autowired
@@ -68,11 +76,12 @@
     // Implementation
     // -------------------------------------------------------------------------
 
-    //TODO optimize when all options in dimensions are selected
-    
     @Async
     public Future<Map<String, Double>> getAggregatedDataValues( DataQueryParams params )
     {
+        ListMap<String, String> dataPeriodAggregationPeriodMap = params.getDataPeriodAggregationPeriodMap();
+        params.replaceAggregationPeriodsWithDataPeriods( dataPeriodAggregationPeriodMap );
+        
         List<String> dimensions = params.getDimensionNames();
         Map<String, List<String>> dimensionMap = params.getDimensionMap();
         
@@ -85,7 +94,7 @@
         sql += params.isAggregationType( AVERAGE_AGGREGATION ) ? "sum(daysxvalue) / " + days : "sum(value)";
         
         sql += " as value from " + params.getTableName() + " ";
-            
+        
         for ( String dim : dimensions )
         {
             sql += sqlHelper.whereAnd() + " " + dim + " in (" + getQuotedCommaDelimitedString( dimensionMap.get( dim ) ) + " ) ";
@@ -113,13 +122,47 @@
                 key.append( rowSet.getString( dim ) + SEP );
             }
             
-            key.deleteCharAt( key.length() - SEP.length() );
+            key.deleteCharAt( key.length() - 1 );
             
             Double value = rowSet.getDouble( VALUE_ID );
 
             map.put( key.toString(), value );
         }
         
+        replaceDataPeriodsWithAggregationPeriods( map, params, dataPeriodAggregationPeriodMap );
+        
         return new AsyncResult<Map<String, Double>>( map );
-    }    
+    }
+
+    public void replaceDataPeriodsWithAggregationPeriods( Map<String, Double> dataValueMap, DataQueryParams params, ListMap<String, String> dataPeriodAggregationPeriodMap )
+    {
+        if ( params.isAggregationType( AVERAGE_DISAGGREGATION ) )
+        {
+            int periodIndex = params.getPeriodDimensionIndex();
+            
+            Set<String> keys = new HashSet<String>( dataValueMap.keySet() );
+            
+            for ( String key : keys )
+            {
+                String[] keyArray = key.split( String.valueOf( SEP ) );
+                
+                Assert.notNull( keyArray[periodIndex], keyArray.toString() );
+                
+                List<String> periods = dataPeriodAggregationPeriodMap.get( keyArray[periodIndex] );
+                
+                Assert.notNull( periods, dataPeriodAggregationPeriodMap.toString() );
+                
+                Double value = dataValueMap.get( key );
+                
+                for ( String period : periods )
+                {
+                    String[] keyCopy = keyArray.clone();
+                    keyCopy[periodIndex] = period;                    
+                    dataValueMap.put( TextUtils.toString( keyCopy, SEP ), value );
+                }
+                
+                dataValueMap.remove( key );
+            }
+        }
+    }
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java	2012-12-27 18:15:23 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java	2012-12-30 16:23:41 +0000
@@ -44,6 +44,9 @@
 import org.hisp.dhis.organisationunit.OrganisationUnitService;
 import org.hisp.dhis.period.Cal;
 import org.hisp.dhis.period.PeriodType;
+import org.hisp.dhis.period.QuarterlyPeriodType;
+import org.hisp.dhis.period.YearlyPeriodType;
+import org.hisp.dhis.system.util.ListMap;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -104,6 +107,34 @@
     // Tests
     // -------------------------------------------------------------------------
     
+    public void testGetDataPeriodAggregationPeriodMap()
+    {
+        DataQueryParams params = new DataQueryParams();
+        params.setDataElements( Arrays.asList( deA.getUid(), deB.getUid(), deC.getUid(), deD.getUid() ) );
+        params.setOrganisationUnits( Arrays.asList( ouA.getUid(), ouB.getUid(), ouC.getUid(), ouD.getUid(), ouE.getUid() ) );
+        params.setPeriods( Arrays.asList( "2000Q1", "2000Q2", "2000Q3", "2000Q4", "2001Q1", "2001Q2" ) );
+        params.setPeriodType( QuarterlyPeriodType.NAME );
+        params.setDataPeriodType( new YearlyPeriodType() );
+        
+        ListMap<String, String> map = params.getDataPeriodAggregationPeriodMap();
+        
+        assertEquals( 2, map.size() );
+        
+        assertTrue( map.keySet().contains( "2000" ) );
+        assertTrue( map.keySet().contains( "2001" ) );
+        
+        assertEquals( 4, map.get( "2000" ).size() );
+        assertEquals( 2, map.get( "2001" ).size() );
+        
+        assertTrue( map.get( "2000" ).contains( "2000Q1" ) );
+        assertTrue( map.get( "2000" ).contains( "2000Q2" ) );
+        assertTrue( map.get( "2000" ).contains( "2000Q3" ) );
+        assertTrue( map.get( "2000" ).contains( "2000Q4" ) );
+
+        assertTrue( map.get( "2001" ).contains( "2001Q1" ) );
+        assertTrue( map.get( "2001" ).contains( "2001Q2" ) );
+    }
+    
     /**
      * Query spans 2 partitions. Splits in 2 queries for each partition, then
      * splits in 2 queries on organisation units to satisfy optimal for a total 

=== modified file 'dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/TextUtils.java'
--- dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/TextUtils.java	2012-12-18 16:01:44 +0000
+++ dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/TextUtils.java	2012-12-30 16:23:41 +0000
@@ -306,4 +306,29 @@
     {
         return string != null ? string.toLowerCase() : null;
     }
+    
+    /**
+     * Null-safe method for writing the items of a string array out as a string
+     * separated by the given char separator.
+     * 
+     * @param array the array.
+     * @param separator the separator of the array items.
+     * @return a string.
+     */
+    public static String toString( String[] array, char separator )
+    {
+        StringBuilder builder = new StringBuilder();
+        
+        if ( array != null && array.length > 0 )
+        {
+            for ( String string : array )
+            {
+                builder.append( string ).append( separator );
+            }
+            
+            builder.deleteCharAt( builder.length() - 1 );
+        }
+        
+        return builder.toString();
+    }
 }