← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 10965: Analytics API. Impl support for getting responses with table layout. Useful for getting ready-mad...

 

------------------------------------------------------------
revno: 10965
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Thu 2013-05-23 18:57:03 +0200
message:
  Analytics API. Impl support for getting responses with table layout. Useful for getting ready-made reports based on dynamic input.
added:
  dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/BaseAnalyticalObjectTest.java
modified:
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObject.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/reporttable/ReportTable.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsService.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/DimensionItem.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.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/main/java/org/hisp/dhis/analytics/data/MockAnalyticsService.java
  dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/DimensionOptionTest.java
  dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java
  dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/chart/impl/DefaultChartService.java
  dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/reporttable/ReportTableGridTest.java
  dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AnalyticsController.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-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java	2013-05-23 16:57:03 +0000
@@ -35,10 +35,12 @@
 import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.CATEGORYOPTIONCOMBO_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.organisationunit.OrganisationUnit.KEY_USER_ORGUNIT;
 import static org.hisp.dhis.organisationunit.OrganisationUnit.KEY_USER_ORGUNIT_CHILDREN;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -46,6 +48,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.lang.StringUtils;
 import org.hisp.dhis.common.adapter.JacksonPeriodDeserializer;
 import org.hisp.dhis.common.adapter.JacksonPeriodSerializer;
 import org.hisp.dhis.common.annotation.Scanned;
@@ -406,21 +409,60 @@
         return categoryDims;
     }
     
+    /**
+     * Splits the keys of the given map on the dimension identifier separator, 
+     * sorts the identifiers, writes them out as a key and puts the key back into
+     * the map.
+     */
+    public static void sortKeys( Map<String, Double> valueMap )
+    {
+        Map<String, Double> map = new HashMap<String, Double>();
+        
+        for ( String key : valueMap.keySet() )
+        {
+            if ( key != null )
+            {
+                String[] ids = key.split( DIMENSION_SEP );
+                
+                Collections.sort( Arrays.asList( ids ) );
+                
+                String sortedKey = StringUtils.join( ids, DIMENSION_SEP );
+                
+                map.put( sortedKey, valueMap.get( key ) );
+            }
+        }
+        
+        valueMap.clear();
+        valueMap.putAll( map );
+    }
+    
+    /**
+     * Generates an identifier based on the given lists of NameableObjects. Uses
+     * the UIDs for each NameableObject, sorts them and writes them out as a key.
+     */
     public static String getIdentifer( List<NameableObject> column, List<NameableObject> row )
     {
-        StringBuilder id = new StringBuilder();
-        
-        for ( NameableObject item : column )
-        {
-            id.append( item.getUid() ).append( "-" );
-        }
-        
-        for ( NameableObject item : row )
-        {
-            id.append( item.getUid() ).append( "-" );
-        }
-        
-        return id.substring( 0, id.length() - 1 );
+        List<String> ids = new ArrayList<String>();
+        
+        if ( column != null )
+        {
+            for ( NameableObject item : column )
+            {
+                ids.add( item.getUid() );
+            }
+        }
+        
+        if ( row != null )
+        {
+            for ( NameableObject item : row )
+            {
+                ids.add( item.getUid() );
+            }
+        }
+        
+        Collections.sort( ids );
+        
+        return StringUtils.join( ids, DIMENSION_SEP );
     }
     
     public void mergeWith( BaseAnalyticalObject other )

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java	2013-05-23 16:57:03 +0000
@@ -172,7 +172,7 @@
     {
         this.displayDescription = displayDescription;
     }
-
+    
     @Override
     public void mergeWith( IdentifiableObject other )
     {

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObject.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObject.java	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObject.java	2013-05-23 16:57:03 +0000
@@ -44,6 +44,8 @@
     final String CATEGORYOPTIONCOMBO_DIM_ID = "co";
     final String PERIOD_DIM_ID = "pe";
     final String ORGUNIT_DIM_ID = "ou";
+    
+    final String DIMENSION_SEP = "-";
 
     final List<String> DATA_X_DIMS = Arrays.asList( INDICATOR_DIM_ID, DATAELEMENT_DIM_ID, DATASET_DIM_ID, DATAELEMENT_OPERAND_ID );
     

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/reporttable/ReportTable.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/reporttable/ReportTable.java	2013-05-23 12:59:00 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/reporttable/ReportTable.java	2013-05-23 16:57:03 +0000
@@ -616,6 +616,10 @@
         final String subtitle = StringUtils.trimToEmpty( getParentOrganisationUnitName() ) + SPACE
             + StringUtils.trimToEmpty( reportingPeriodName );
 
+        valueMap = new HashMap<String, Double>( valueMap );
+        
+        sortKeys( valueMap );
+        
         grid.setTitle( name + " - " + subtitle );
 
         // ---------------------------------------------------------------------
@@ -684,7 +688,7 @@
 
             for ( List<NameableObject> column : gridColumns )
             {
-                String key = BaseAnalyticalObject.getIdentifer( column, row );
+                String key = getIdentifer( column, row );
                 
                 Double value = valueMap.get( key );
                 

=== added file 'dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/BaseAnalyticalObjectTest.java'
--- dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/BaseAnalyticalObjectTest.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/BaseAnalyticalObjectTest.java	2013-05-23 16:57:03 +0000
@@ -0,0 +1,112 @@
+package org.hisp.dhis.common;
+
+/*
+ * Copyright (c) 2004-2012, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ *   list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * * Neither the name of the HISP project nor the names of its contributors may
+ *   be used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.hisp.dhis.dataelement.DataElementGroup;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+* @author Lars Helge Overland
+*/
+public class BaseAnalyticalObjectTest
+{
+    @Test
+    public void testSortKeys()
+    {
+        Map<String, Double> valueMap = new HashMap<String, Double>();
+        
+        valueMap.put( "b1-a1-c1", 1d );
+        valueMap.put( "a2-c2-b2", 2d );
+        valueMap.put( "c3-b3-a3", 3d );
+        valueMap.put( "a4-b4-c4", 4d );
+        
+        BaseAnalyticalObject.sortKeys( valueMap );
+        
+        assertEquals( 4, valueMap.size() );
+        assertTrue( valueMap.containsKey( "a1-b1-c1" ) );
+        assertTrue( valueMap.containsKey( "a2-b2-c2" ) );
+        assertTrue( valueMap.containsKey( "a3-b3-c3" ) );
+        assertTrue( valueMap.containsKey( "a4-b4-c4" ) );
+        
+        assertEquals( 1d, valueMap.get( "a1-b1-c1" ), 0.01 );
+        assertEquals( 2d, valueMap.get( "a2-b2-c2" ), 0.01 );
+        assertEquals( 3d, valueMap.get( "a3-b3-c3" ), 0.01 );
+        assertEquals( 4d, valueMap.get( "a4-b4-c4" ), 0.01 );
+        
+        valueMap = new HashMap<String, Double>();
+        
+        valueMap.put( "b1", 1d );
+        valueMap.put( "b2", 2d );
+
+        BaseAnalyticalObject.sortKeys( valueMap );
+
+        assertEquals( 2, valueMap.size() );
+        assertTrue( valueMap.containsKey( "b1" ) );
+        assertTrue( valueMap.containsKey( "b2" ) );
+        
+        assertEquals( 1d, valueMap.get( "b1" ), 0.01 );
+        assertEquals( 2d, valueMap.get( "b2" ), 0.01 );
+
+        valueMap = new HashMap<String, Double>();
+        
+        valueMap.put( null, 1d );
+        
+        BaseAnalyticalObject.sortKeys( valueMap );
+
+        assertEquals( 0, valueMap.size() );
+    }
+    
+    @Test
+    public void testGetIdentifier()
+    {
+        DataElementGroup oA = new DataElementGroup();
+        DataElementGroup oB = new DataElementGroup();
+        DataElementGroup oC = new DataElementGroup();
+        
+        oA.setUid( "a1" );
+        oB.setUid( "b1" );
+        oC.setUid( "c1" );
+        
+        List<NameableObject> column = new ArrayList<NameableObject>();
+        column.add( oC );
+        column.add( oA );
+        
+        List<NameableObject> row = new ArrayList<NameableObject>();
+        row.add( oB );
+        
+        assertEquals( "a1-b1-c1", BaseAnalyticalObject.getIdentifer( column, row ) );
+        assertEquals( "b1", BaseAnalyticalObject.getIdentifer( new ArrayList<NameableObject>(), row ) );
+        assertEquals( "b1", BaseAnalyticalObject.getIdentifer( null, row ) );
+    }
+}

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsService.java	2013-05-19 18:49:47 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsService.java	2013-05-23 16:57:03 +0000
@@ -27,6 +27,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -48,6 +49,20 @@
      * @return aggregated data as a Grid object.
      */
     Grid getAggregatedDataValues( DataQueryParams params );
+    
+    /**
+     * Generates an aggregated value grid for the given query. The grid will
+     * represent a table with dimensions used as columns and rows as specified
+     * in columnDimensions and rowDimensions arguments.
+     * 
+     * @param params the data query parameters.
+     * @param tableLayout whether to render the grid as a table with columns and rows,
+     *        or as a normalized plain data source.
+     * @param columns the identifiers of the dimensions to use as columns.
+     * @param rows the identifiers of the dimensions to use as rows.
+     * @return aggregated data as a Grid object.
+     */
+    Grid getAggregatedDataValues( DataQueryParams params, boolean tableLayout, List<String> columns, List<String> rows );
 
     /**
      * Generates a mapping where the key represents the dimensional item identifiers

=== 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	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DataQueryParams.java	2013-05-23 16:57:03 +0000
@@ -33,11 +33,13 @@
 import static org.hisp.dhis.common.DimensionType.ORGANISATIONUNIT_GROUPSET;
 import static org.hisp.dhis.common.DimensionalObject.CATEGORYOPTIONCOMBO_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.DATAELEMENT_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.DATAELEMENT_OPERAND_ID;
 import static org.hisp.dhis.common.DimensionalObject.DATASET_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.DATA_X_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.INDICATOR_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.common.NameableObjectUtils.asList;
 import static org.hisp.dhis.common.NameableObjectUtils.getList;
 import static org.hisp.dhis.system.util.CollectionUtils.emptyIfNull;
@@ -86,9 +88,8 @@
     
     private static final String DIMENSION_NAME_SEP = ":";
     private static final String OPTION_SEP = ";";
-    public static final String DIMENSION_SEP = "-";
 
-    public static final List<String> DATA_DIMS = Arrays.asList( INDICATOR_DIM_ID, DATAELEMENT_DIM_ID, DATASET_DIM_ID );
+    public static final List<String> DATA_DIMS = Arrays.asList( INDICATOR_DIM_ID, DATAELEMENT_DIM_ID, DATAELEMENT_OPERAND_ID, DATASET_DIM_ID );
     public static final List<String> FIXED_DIMS = Arrays.asList( DATA_X_DIM_ID, INDICATOR_DIM_ID, DATAELEMENT_DIM_ID, DATASET_DIM_ID, PERIOD_DIM_ID, ORGUNIT_DIM_ID );
     
     public static final int MAX_DIM_OPT_PERM = 10000;
@@ -571,7 +572,8 @@
     }
 
     /**
-     * Retrieves the options for the given dimension identifier.
+     * Retrieves the options for the given dimension identifier. Returns null if
+     * the dimension is not present.
      */
     public List<NameableObject> getDimensionOptions( String dimension )
     {
@@ -581,7 +583,8 @@
     }
     
     /**
-     * Retrieves the dimension with the given dimension identifier.
+     * Retrieves the dimension with the given dimension identifier. Returns null 
+     * if the dimension is not present.
      */
     public DimensionalObject getDimension( String dimension )
     {
@@ -707,6 +710,20 @@
     }
     
     /**
+     * Splits the given string on the ; character and returns the items in a 
+     * list. Returns null if the given string is null.
+     */
+    public static List<String> getDimensionsFromParam( String param )
+    {
+        if ( param == null )
+        {
+            return null;
+        }
+        
+        return Arrays.asList( param.split( OPTION_SEP ) );
+    }
+    
+    /**
      * Retrieves the measure criteria from the given string. Criteria are separated
      * by the option separator, while the criterion filter and value are separated
      * with the dimension name separator.
@@ -944,16 +961,56 @@
     // Get and set helpers for dimensions or filter
     // -------------------------------------------------------------------------
   
+    /**
+     * Retrieves the options for the the dimension or filter with the given 
+     * identifier.
+     */
     public List<NameableObject> getDimensionOrFilter( String key )
     {
         return getDimensionOptions( key ) != null ? getDimensionOptions( key ) : getFilterOptions( key );
     }
     
+    /**
+     * Retrieves the options for the given dimension identifier. If the dx dimension
+     * is specified, all concrete dimensions (in|de|dc|ds) are returned as a single
+     * dimension. Returns an empty array if the dimension is not present.
+     */
+    public NameableObject[] getDimensionArrayCollapseDx( String dimension )
+    {
+        List<NameableObject> items = new ArrayList<NameableObject>();
+        
+        if ( DATA_X_DIM_ID.equals( dimension ) )
+        {
+            items.addAll( getDimensionOptionsNullSafe( INDICATOR_DIM_ID ) );
+            items.addAll( getDimensionOptionsNullSafe( DATAELEMENT_DIM_ID ) );
+            items.addAll( getDimensionOptionsNullSafe( DATAELEMENT_OPERAND_ID ) );
+            items.addAll( getDimensionOptionsNullSafe( DATASET_DIM_ID ) );
+        }
+        else
+        {
+            items.addAll( getDimensionOptionsNullSafe( dimension ) );
+        }
+        
+        return items.toArray( new NameableObject[0] );
+    }
+
+    /**
+     * Retrieves the options for the given dimension identifier. Returns an empty
+     * list if the dimension is not present.
+     */
+    public List<NameableObject> getDimensionOptionsNullSafe( String dimension )
+    {
+        return getDimensionOptions( dimension ) != null ? getDimensionOptions( dimension ) : new ArrayList<NameableObject>();
+    }
+    
+    /**
+     * Indicates whether a dimension or filter with the given identifier exists.
+     */
     public boolean hasDimensionOrFilter( String key )
     {
         return dimensions.indexOf( new BaseDimensionalObject( key ) ) != -1 || filters.indexOf( new BaseDimensionalObject( key ) ) != -1;
     }
-    
+        
     // -------------------------------------------------------------------------
     // Get and set helpers for dimensions
     // -------------------------------------------------------------------------

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DimensionItem.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DimensionItem.java	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/DimensionItem.java	2013-05-23 16:57:03 +0000
@@ -34,6 +34,8 @@
 import org.hisp.dhis.common.NameableObject;
 import org.hisp.dhis.system.util.CollectionUtils;
 
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
+
 /**
  * @author Lars Helge Overland
  */
@@ -94,7 +96,7 @@
         {
             for ( DimensionItem item : items )
             {
-                builder.append( item.getItem().getUid() ).append( DataQueryParams.DIMENSION_SEP );
+                builder.append( item.getItem().getUid() ).append( DIMENSION_SEP );
             }
             
             builder.deleteCharAt( builder.length() - 1 );

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java	2013-05-07 08:37:33 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/QueryPlanner.java	2013-05-23 16:57:03 +0000
@@ -46,6 +46,19 @@
         throws IllegalQueryException;
     
     /**
+     * Validates whether the given table layout is valid for the given query. 
+     * Throws an IllegalQueryException if the query is not valid with a 
+     * descriptive message. Returns normally if the query is valid.
+     * 
+     * @param params the query.
+     * @param columns the column dimension identifiers.
+     * @param rows the row dimension identifiers.
+     * @throws IllegalQueryException if the query is invalid.
+     */
+    void validateTableLayout( DataQueryParams params, List<String> columns, List<String> rows )
+        throws IllegalQueryException;
+    
+    /**
      * Creates a list of DataQueryParams. It is mandatory to group the queries by
      * the following criteria: 1) partition / year 2) period type 3) organisation 
      * unit level. If the number of queries produced by this grouping is equal or

=== 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	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultAnalyticsService.java	2013-05-23 16:57:03 +0000
@@ -30,7 +30,6 @@
 import static org.hisp.dhis.analytics.AnalyticsTableManager.ANALYTICS_TABLE_NAME;
 import static org.hisp.dhis.analytics.AnalyticsTableManager.COMPLETENESS_TABLE_NAME;
 import static org.hisp.dhis.analytics.AnalyticsTableManager.COMPLETENESS_TARGET_TABLE_NAME;
-import static org.hisp.dhis.analytics.DataQueryParams.DIMENSION_SEP;
 import static org.hisp.dhis.analytics.DataQueryParams.DISPLAY_NAME_CATEGORYOPTIONCOMBO;
 import static org.hisp.dhis.analytics.DataQueryParams.DISPLAY_NAME_DATA_X;
 import static org.hisp.dhis.analytics.DataQueryParams.DISPLAY_NAME_ORGUNIT;
@@ -45,12 +44,15 @@
 import static org.hisp.dhis.common.DimensionalObject.INDICATOR_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.common.DimensionalObjectUtils.toDimension;
+import static org.hisp.dhis.common.IdentifiableObjectUtils.getUids;
 import static org.hisp.dhis.common.NameableObjectUtils.asList;
 import static org.hisp.dhis.common.NameableObjectUtils.asTypedList;
-import static org.hisp.dhis.common.IdentifiableObjectUtils.getUids;
 import static org.hisp.dhis.organisationunit.OrganisationUnit.KEY_USER_ORGUNIT;
 import static org.hisp.dhis.organisationunit.OrganisationUnit.KEY_USER_ORGUNIT_CHILDREN;
+import static org.hisp.dhis.reporttable.ReportTable.IRT2D;
+import static org.hisp.dhis.reporttable.ReportTable.addIfEmpty;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -76,6 +78,7 @@
 import org.hisp.dhis.analytics.QueryPlanner;
 import org.hisp.dhis.common.BaseAnalyticalObject;
 import org.hisp.dhis.common.BaseDimensionalObject;
+import org.hisp.dhis.common.CombinationGenerator;
 import org.hisp.dhis.common.DimensionType;
 import org.hisp.dhis.common.DimensionalObject;
 import org.hisp.dhis.common.Grid;
@@ -105,6 +108,7 @@
 import org.hisp.dhis.period.RelativePeriodEnum;
 import org.hisp.dhis.period.RelativePeriods;
 import org.hisp.dhis.period.comparator.AscendingPeriodComparator;
+import org.hisp.dhis.reporttable.ReportTable;
 import org.hisp.dhis.system.grid.ListGrid;
 import org.hisp.dhis.system.util.ConversionUtils;
 import org.hisp.dhis.system.util.DebugUtils;
@@ -357,6 +361,54 @@
         
         return grid;
     }
+
+    @Override
+    public Grid getAggregatedDataValues( DataQueryParams params, boolean tableLayout, List<String> columns, List<String> rows )
+    {
+        if ( !tableLayout )
+        {
+            return getAggregatedDataValues( params );
+        }
+        
+        queryPlanner.validateTableLayout( params, columns, rows );
+        
+        Map<String, Double> valueMap = getAggregatedDataValueMapping( params );
+
+        ReportTable reportTable = new ReportTable();
+        
+        List<NameableObject[]> tableColumns = new ArrayList<NameableObject[]>();
+        List<NameableObject[]> tableRows = new ArrayList<NameableObject[]>();
+
+        if ( columns != null )
+        {            
+            for ( String dimension : columns )
+            {
+                reportTable.getColumnDimensions().add( dimension );
+                
+                tableColumns.add( params.getDimensionArrayCollapseDx( dimension ) );
+            }
+        }
+        
+        if ( rows != null )
+        {
+            for ( String dimension : rows )
+            {
+                reportTable.getRowDimensions().add( dimension );
+                
+                tableRows.add( params.getDimensionArrayCollapseDx( dimension ) );
+            }
+        }
+
+        reportTable.setGridColumns( new CombinationGenerator<NameableObject>( tableColumns.toArray( IRT2D ) ).getCombinations() );
+        reportTable.setGridRows( new CombinationGenerator<NameableObject>( tableRows.toArray( IRT2D ) ).getCombinations() );
+
+        addIfEmpty( reportTable.getGridColumns() ); 
+        addIfEmpty( reportTable.getGridRows() );
+
+        Grid grid = reportTable.getGrid( new ListGrid(), valueMap, false );
+        
+        return grid;
+    }
     
     @Override
     public Map<String, Double> getAggregatedDataValueMapping( DataQueryParams params )

=== 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	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultQueryPlanner.java	2013-05-23 16:57:03 +0000
@@ -140,6 +140,45 @@
         }
     }
     
+    public void validateTableLayout( DataQueryParams params, List<String> columns, List<String> rows )
+    {
+        String violation = null;
+        
+        if ( ( columns == null || columns.isEmpty() ) && ( rows == null || rows.isEmpty() ) )
+        {
+            violation = "Cannot generate table layout when columns and rows are empty";
+        }
+        
+        if ( columns != null )
+        {
+            for ( String column : columns )
+            {
+                if ( params.getDimensionArrayCollapseDx( column ).length == 0 )
+                {
+                    violation = "Column must be present as dimension in query: " + column;
+                }
+            }
+        }
+        
+        if ( rows != null )
+        {
+            for ( String row : rows )
+            {
+                if ( params.getDimensionArrayCollapseDx( row ).length == 0 )
+                {
+                    violation = "Row must be present as dimension in query: " + row;
+                }
+            }
+        }
+        
+        if ( violation != null )
+        {
+            log.warn( "Validation failed: " + violation );
+            
+            throw new IllegalQueryException( violation );
+        }
+    }
+    
     public List<DataQueryParams> planQuery( DataQueryParams params, int optimalQueries, String tableName )
     {
         validate( params );

=== 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	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManager.java	2013-05-23 16:57:03 +0000
@@ -31,7 +31,7 @@
 import static org.hisp.dhis.analytics.AggregationType.AVERAGE_INT;
 import static org.hisp.dhis.analytics.AggregationType.AVERAGE_INT_DISAGGREGATION;
 import static org.hisp.dhis.analytics.AggregationType.COUNT;
-import static org.hisp.dhis.analytics.DataQueryParams.DIMENSION_SEP;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID;
 import static org.hisp.dhis.analytics.MeasureFilter.EQ;
 import static org.hisp.dhis.analytics.MeasureFilter.GE;

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/MockAnalyticsService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/MockAnalyticsService.java	2013-05-19 18:49:47 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/MockAnalyticsService.java	2013-05-23 16:57:03 +0000
@@ -27,6 +27,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -56,7 +57,13 @@
     {
         throw new NotImplementedException();
     }
-    
+
+    @Override
+    public Grid getAggregatedDataValues( DataQueryParams params, boolean tableLayout, List<String> columns, List<String> rows )
+    {
+        throw new NotImplementedException();
+    }
+
     @Override
     public Map<String, Double> getAggregatedDataValueMapping( DataQueryParams params )
     {

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/DimensionOptionTest.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/DimensionOptionTest.java	2013-05-07 08:37:33 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/DimensionOptionTest.java	2013-05-23 16:57:03 +0000
@@ -27,6 +27,14 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import static org.hisp.dhis.common.DimensionalObject.DATAELEMENT_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
+import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID;
+import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID;
+import static org.hisp.dhis.system.util.TextUtils.EMPTY;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -37,11 +45,6 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
-import static org.hisp.dhis.analytics.DataQueryParams.*;
-import static org.hisp.dhis.system.util.TextUtils.EMPTY;
-import static org.hisp.dhis.common.DimensionalObject.*;
-
 /**
  * @author Lars Helge Overland
  */

=== 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	2013-05-23 13:38:20 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java	2013-05-23 16:57:03 +0000
@@ -28,7 +28,7 @@
  */
 
 import static org.hisp.dhis.analytics.AnalyticsTableManager.ANALYTICS_TABLE_NAME;
-import static org.hisp.dhis.analytics.DataQueryParams.DIMENSION_SEP;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.common.DimensionalObject.DATA_X_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID;
 import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID;

=== modified file 'dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/chart/impl/DefaultChartService.java'
--- dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/chart/impl/DefaultChartService.java	2013-05-21 11:39:31 +0000
+++ dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/chart/impl/DefaultChartService.java	2013-05-23 16:57:03 +0000
@@ -34,6 +34,7 @@
 import static org.hisp.dhis.chart.Chart.TYPE_PIE;
 import static org.hisp.dhis.chart.Chart.TYPE_STACKED_BAR;
 import static org.hisp.dhis.chart.Chart.TYPE_STACKED_COLUMN;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.hisp.dhis.system.util.ConversionUtils.getArray;
 
 import java.awt.BasicStroke;
@@ -53,7 +54,6 @@
 import org.apache.commons.math.analysis.UnivariateRealInterpolator;
 import org.apache.commons.math.stat.regression.SimpleRegression;
 import org.hisp.dhis.analytics.AnalyticsService;
-import org.hisp.dhis.analytics.DataQueryParams;
 import org.hisp.dhis.chart.Chart;
 import org.hisp.dhis.chart.ChartService;
 import org.hisp.dhis.chart.ChartStore;
@@ -735,7 +735,7 @@
             {
                 categoryIndex++;
 
-                String key = series.getUid() + DataQueryParams.DIMENSION_SEP + category.getUid();
+                String key = series.getUid() + DIMENSION_SEP + category.getUid();
 
                 Double value = valueMap.get( key );
                 

=== modified file 'dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/reporttable/ReportTableGridTest.java'
--- dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/reporttable/ReportTableGridTest.java	2013-05-20 06:10:29 +0000
+++ dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/reporttable/ReportTableGridTest.java	2013-05-23 16:57:03 +0000
@@ -27,7 +27,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-import static org.hisp.dhis.analytics.DataQueryParams.DIMENSION_SEP;
+import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
 import static org.junit.Assert.assertEquals;
 
 import java.util.ArrayList;

=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AnalyticsController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AnalyticsController.java	2013-04-04 18:06:19 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AnalyticsController.java	2013-05-23 16:57:03 +0000
@@ -28,6 +28,7 @@
  */
 
 import static org.hisp.dhis.analytics.AnalyticsService.NAMES_META_KEY;
+import static org.hisp.dhis.analytics.DataQueryParams.getDimensionsFromParam;
 
 import java.util.Map;
 import java.util.Set;
@@ -75,13 +76,16 @@
         @RequestParam(required = false) Set<String> filter,
         @RequestParam(required = false) AggregationType aggregationType,
         @RequestParam(required = false) String measureCriteria,
+        @RequestParam(required = false) boolean tableLayout,
+        @RequestParam(required = false) String columns,
+        @RequestParam(required = false) String rows,
         Model model,
         HttpServletResponse response ) throws Exception
     {
         DataQueryParams params = analyticsService.getFromUrl( dimension, filter, aggregationType, measureCriteria, i18nManager.getI18nFormat() );
 
         contextUtils.configureResponse( response, ContextUtils.CONTENT_TYPE_JSON, CacheStrategy.RESPECT_SYSTEM_SETTING );
-        Grid grid = analyticsService.getAggregatedDataValues( params );        
+        Grid grid = analyticsService.getAggregatedDataValues( params, tableLayout, getDimensionsFromParam( columns ), getDimensionsFromParam( rows ) );
         model.addAttribute( "model", grid );
         model.addAttribute( "viewClass", "detailed" );
         return "grid";
@@ -93,13 +97,16 @@
         @RequestParam(required = false) Set<String> filter,
         @RequestParam(required = false) AggregationType aggregationType,
         @RequestParam(required = false) String measureCriteria,
+        @RequestParam(required = false) boolean tableLayout,
+        @RequestParam(required = false) String columns,
+        @RequestParam(required = false) String rows,
         Model model,
         HttpServletResponse response ) throws Exception
     {
         DataQueryParams params = analyticsService.getFromUrl( dimension, filter, aggregationType, measureCriteria, i18nManager.getI18nFormat() );
 
         contextUtils.configureResponse( response, ContextUtils.CONTENT_TYPE_XML, CacheStrategy.RESPECT_SYSTEM_SETTING );
-        Grid grid = analyticsService.getAggregatedDataValues( params );
+        Grid grid = analyticsService.getAggregatedDataValues( params, tableLayout, getDimensionsFromParam( columns ), getDimensionsFromParam( rows ) );
         GridUtils.toXml( grid, response.getOutputStream() );
     }
 
@@ -109,13 +116,16 @@
         @RequestParam(required = false) Set<String> filter,
         @RequestParam(required = false) AggregationType aggregationType,
         @RequestParam(required = false) String measureCriteria,
+        @RequestParam(required = false) boolean tableLayout,
+        @RequestParam(required = false) String columns,
+        @RequestParam(required = false) String rows,
         Model model,
         HttpServletResponse response ) throws Exception
     {
         DataQueryParams params = analyticsService.getFromUrl( dimension, filter, aggregationType, measureCriteria, i18nManager.getI18nFormat() );
 
         contextUtils.configureResponse( response, ContextUtils.CONTENT_TYPE_HTML, CacheStrategy.RESPECT_SYSTEM_SETTING );
-        Grid grid = analyticsService.getAggregatedDataValues( params );
+        Grid grid = analyticsService.getAggregatedDataValues( params, tableLayout, getDimensionsFromParam( columns ), getDimensionsFromParam( rows ) );
         GridUtils.toHtml( substituteMetaData( grid ), response.getWriter() );
     }
 
@@ -125,13 +135,16 @@
         @RequestParam(required = false) Set<String> filter,
         @RequestParam(required = false) AggregationType aggregationType,
         @RequestParam(required = false) String measureCriteria,
+        @RequestParam(required = false) boolean tableLayout,
+        @RequestParam(required = false) String columns,
+        @RequestParam(required = false) String rows,
         Model model,
         HttpServletResponse response ) throws Exception
     {
         DataQueryParams params = analyticsService.getFromUrl( dimension, filter, aggregationType, measureCriteria, i18nManager.getI18nFormat() );
 
         contextUtils.configureResponse( response, ContextUtils.CONTENT_TYPE_CSV, CacheStrategy.RESPECT_SYSTEM_SETTING, "data.csv", true );
-        Grid grid = analyticsService.getAggregatedDataValues( params );
+        Grid grid = analyticsService.getAggregatedDataValues( params, tableLayout, getDimensionsFromParam( columns ), getDimensionsFromParam( rows ) );
         GridUtils.toCsv( substituteMetaData( grid ), response.getOutputStream() );
     }
     
@@ -141,13 +154,16 @@
         @RequestParam(required = false) Set<String> filter,
         @RequestParam(required = false) AggregationType aggregationType,
         @RequestParam(required = false) String measureCriteria,
+        @RequestParam(required = false) boolean tableLayout,
+        @RequestParam(required = false) String columns,
+        @RequestParam(required = false) String rows,
         Model model,
         HttpServletResponse response ) throws Exception
     {
         DataQueryParams params = analyticsService.getFromUrl( dimension, filter, aggregationType, measureCriteria, i18nManager.getI18nFormat() );
 
         contextUtils.configureResponse( response, ContextUtils.CONTENT_TYPE_EXCEL, CacheStrategy.RESPECT_SYSTEM_SETTING, "data.xls", true );
-        Grid grid = analyticsService.getAggregatedDataValues( params );
+        Grid grid = analyticsService.getAggregatedDataValues( params, tableLayout, getDimensionsFromParam( columns ), getDimensionsFromParam( rows ) );
         GridUtils.toXls( substituteMetaData( grid ), response.getOutputStream() );
     }