← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 11732: Analytics, impl generation of event analytics tables

 

Merge authors:
  Lars Helge Øverland (larshelge)
------------------------------------------------------------
revno: 11732 [merge]
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Wed 2013-08-21 13:57:43 +0200
message:
  Analytics, impl generation of event analytics tables
added:
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java
modified:
  dhis-2/dhis-services/dhis-service-analytics/pom.xml
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTable.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTableManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/scheduling/AnalyticsTableTask.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcCompletenessTargetTableManager.java
  dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml
  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/pom.xml'
--- dhis-2/dhis-services/dhis-service-analytics/pom.xml	2013-05-31 08:27:38 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/pom.xml	2013-08-21 10:08:02 +0000
@@ -27,6 +27,10 @@
     </dependency>
     <dependency>
       <groupId>org.hisp.dhis</groupId>
+      <artifactId>dhis-service-patient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hisp.dhis</groupId>
       <artifactId>dhis-service-administration</artifactId>
     </dependency>
     <dependency>

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTable.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTable.java	2013-08-20 22:48:23 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTable.java	2013-08-21 11:56:16 +0000
@@ -27,6 +27,8 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.List;
+
 import org.hisp.dhis.period.Period;
 import org.hisp.dhis.program.Program;
 
@@ -36,6 +38,8 @@
 public class AnalyticsTable
 {
     private String baseName;
+
+    private List<String[]> dimensionColumns;
     
     private Period period;
     
@@ -45,20 +49,23 @@
     {
     }
 
-    public AnalyticsTable( String baseName )
+    public AnalyticsTable( String baseName, List<String[]> dimensionColumns )
     {
         this.baseName = baseName;
+        this.dimensionColumns = dimensionColumns;
     }
     
-    public AnalyticsTable( String baseName, Period period )
+    public AnalyticsTable( String baseName, List<String[]> dimensionColumns, Period period )
     {
         this.baseName = baseName;
+        this.dimensionColumns = dimensionColumns;
         this.period = period;
     }
     
-    public AnalyticsTable( String baseName, Period period, Program program )
+    public AnalyticsTable( String baseName, List<String[]> dimensionColumns, Period period, Program program )
     {
         this.baseName = baseName;
+        this.dimensionColumns = dimensionColumns;
         this.period = period;
         this.program = program;
     }
@@ -113,6 +120,16 @@
         this.baseName = baseName;
     }
 
+    public List<String[]> getDimensionColumns()
+    {
+        return dimensionColumns;
+    }
+
+    public void setDimensionColumns( List<String[]> dimensionColumns )
+    {
+        this.dimensionColumns = dimensionColumns;
+    }
+
     public Period getPeriod()
     {
         return period;

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTableManager.java	2013-08-20 23:17:09 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/AnalyticsTableManager.java	2013-08-21 11:56:16 +0000
@@ -107,17 +107,6 @@
     Future<?> populateTableAsync( ConcurrentLinkedQueue<AnalyticsTable> tables );    
 
     /**
-     * Returns a list of string arrays in where the first index holds the database
-     * column name, the second index holds the database column data type and the 
-     * third column holds a table alias and name, i.e.:
-     * 
-     * 0 = database column name
-     * 1 = database column data type
-     * 2 = column alias and name
-     */
-    List<String[]> getDimensionColumns( AnalyticsTable table );
-    
-    /**
      * Retrieves the start date of the period of the earliest data value row.
      */
     Date getEarliestData();

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/scheduling/AnalyticsTableTask.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/scheduling/AnalyticsTableTask.java	2013-03-15 16:33:34 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/scheduling/AnalyticsTableTask.java	2013-08-21 11:56:16 +0000
@@ -52,6 +52,9 @@
     @Resource(name="org.hisp.dhis.analytics.CompletenessTargetTableService")
     private AnalyticsTableService completenessTargetTableService;
     
+    @Resource(name="org.hisp.dhis.analytics.EventAnalyticsTableService")
+    private AnalyticsTableService eventAnalyticsTableService;
+    
     @Autowired
     private ResourceTableService resourceTableService;
     
@@ -95,6 +98,10 @@
         
         completenessTargetTableService.update( last3Years, taskId );
         
+        notifier.notify( taskId, "Updating event analytics tables" );
+        
+        eventAnalyticsTableService.update( last3Years, taskId );
+        
         notifier.notify( taskId, INFO, "Analytics tables updated", true );
     }
 }

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java	2013-08-20 23:17:09 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java	2013-08-21 11:56:16 +0000
@@ -51,6 +51,7 @@
 import org.springframework.jdbc.BadSqlGrammarException;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.transaction.annotation.Transactional;
 
 /**
  * @author Lars Helge Overland
@@ -83,9 +84,25 @@
     protected JdbcTemplate jdbcTemplate;
 
     // -------------------------------------------------------------------------
+    // Abstract methods
+    // -------------------------------------------------------------------------
+
+    /**
+     * Returns a list of string arrays in where the first index holds the database
+     * column name, the second index holds the database column data type and the 
+     * third column holds a table alias and name, i.e.:
+     * 
+     * 0 = database column name
+     * 1 = database column data type
+     * 2 = column alias and name
+     */
+    protected abstract List<String[]> getDimensionColumns( AnalyticsTable table );
+    
+    // -------------------------------------------------------------------------
     // Implementation
     // -------------------------------------------------------------------------
-  
+
+    @Transactional
     public List<AnalyticsTable> getTables( boolean last3YearsOnly )
     {
         Date threeYrsAgo = new Cal().subtract( Calendar.YEAR, 2 ).set( 1, 1 ).time();
@@ -95,6 +112,7 @@
         return getTables( earliest, latest );
     }
 
+    @Transactional
     public List<AnalyticsTable> getTables( Date earliest, Date latest )
     {
         String baseName = getTableName();
@@ -105,7 +123,7 @@
         
         for ( Period period : periods )
         {
-            tables.add( new AnalyticsTable( baseName, period ) );
+            tables.add( new AnalyticsTable( baseName, getDimensionColumns( null ), period ) );
         }
         
         return tables;

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java	2013-08-20 23:17:09 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/DefaultAnalyticsTableService.java	2013-08-21 11:56:16 +0000
@@ -147,7 +147,7 @@
     // -------------------------------------------------------------------------
     // Supportive methods
     // -------------------------------------------------------------------------
-  
+
     private void createTables( List<AnalyticsTable> tables )
     {
         for ( AnalyticsTable table : tables )
@@ -218,7 +218,7 @@
         
         for ( AnalyticsTable table : tables )
         {
-            List<String[]> columns = tableManager.getDimensionColumns( table );
+            List<String[]> columns = table.getDimensionColumns();
             
             for ( String[] column : columns )
             {

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java	2013-08-20 23:17:09 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java	2013-08-21 10:08:02 +0000
@@ -80,7 +80,7 @@
     {
         return "analytics";
     }
-        
+    
     public void createTable( AnalyticsTable table )
     {
         final String tableName = table.getTempTableName();

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcCompletenessTargetTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcCompletenessTargetTableManager.java	2013-08-20 23:17:09 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcCompletenessTargetTableManager.java	2013-08-21 11:56:16 +0000
@@ -38,6 +38,7 @@
 import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet;
 import org.hisp.dhis.organisationunit.OrganisationUnitLevel;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.transaction.annotation.Transactional;
 
 /**
  * @author Lars Helge Overland
@@ -46,10 +47,11 @@
     extends AbstractJdbcTableManager
 {
     @Override
+    @Transactional
     public List<AnalyticsTable> getTables( boolean last3YearsOnly )
     {
         List<AnalyticsTable> tables = new ArrayList<AnalyticsTable>();
-        tables.add( new AnalyticsTable( getTableName() ) );
+        tables.add( new AnalyticsTable( getTableName(), getDimensionColumns( null ) ) );
         return tables;
     }
     

=== added file 'dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java	2013-08-21 11:56:16 +0000
@@ -0,0 +1,221 @@
+package org.hisp.dhis.analytics.table;
+
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+
+import org.hisp.dhis.analytics.AnalyticsTable;
+import org.hisp.dhis.dataelement.DataElement;
+import org.hisp.dhis.organisationunit.OrganisationUnitLevel;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.program.Program;
+import org.hisp.dhis.program.ProgramService;
+import org.hisp.dhis.system.util.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.hisp.dhis.system.util.TextUtils.removeLast;
+
+/**
+ * @author Lars Helge Overland
+ */
+public class JdbcEventAnalyticsTableManager
+    extends AbstractJdbcTableManager
+{
+    @Autowired
+    private ProgramService programService;
+        
+    // -------------------------------------------------------------------------
+    // Implementation
+    // -------------------------------------------------------------------------
+    
+    @Override
+    @Transactional
+    public List<AnalyticsTable> getTables( Date earliest, Date latest )
+    {
+        String baseName = getTableName();
+        
+        List<Period> periods = PartitionUtils.getPeriods( earliest, latest );
+
+        List<AnalyticsTable> tables = new ArrayList<AnalyticsTable>();
+        
+        for ( Period period : periods )
+        {
+            for ( Program program : programService.getAllPrograms() )
+            {
+                AnalyticsTable table = new AnalyticsTable( baseName, null, period, program );
+                List<String[]> dimensionColumns = getDimensionColumns( table );
+                table.setDimensionColumns( dimensionColumns );                
+                tables.add( table );
+            }
+        }
+        
+        return tables;
+    }    
+    
+    public boolean validState()
+    {
+        return jdbcTemplate.queryForRowSet( "select dataelementid from patientdatavalue limit 1" ).next();
+    }
+    
+    public String getTableName()
+    {
+        return "analytics_event";
+    }
+    
+    public void createTable( AnalyticsTable table )
+    {
+        final String tableName = table.getTempTableName();
+        
+        final String sqlDrop = "drop table " + tableName;
+        
+        executeSilently( sqlDrop );
+        
+        String sqlCreate = "create table " + tableName + " (";
+        
+        for ( String[] col : getDimensionColumns( table ) )
+        {
+            sqlCreate += col[0] + " " + col[1] + ",";
+        }
+        
+        sqlCreate = removeLast( sqlCreate, 1 ) + ")";
+        
+        log.info( "Create SQL: " + sqlCreate );
+        
+        executeSilently( sqlCreate );
+    }
+    
+    @Async
+    @Override
+    public Future<?> populateTableAsync( ConcurrentLinkedQueue<AnalyticsTable> tables )
+    {
+        taskLoop : while ( true )
+        {
+            AnalyticsTable table = tables.poll();
+                
+            if ( table == null )
+            {
+                break taskLoop;
+            }
+            
+            final String start = DateUtils.getMediumDateString( table.getPeriod().getStartDate() );
+            final String end = DateUtils.getMediumDateString( table.getPeriod().getEndDate() );
+
+            String sql = "insert into " + table.getTempTableName() + " (";
+
+            for ( String[] col : getDimensionColumns( table ) )
+            {
+                sql += col[0] + ",";
+            }
+            
+            sql = removeLast( sql, 1 ) + ") select ";
+
+            for ( String[] col : getDimensionColumns( table ) )
+            {
+                sql += col[2] + ",";
+            }
+            
+            sql = removeLast( sql, 1 ) + " ";
+            
+            sql += 
+                "from programstageinstance psi " +
+                "left join programinstance pi on psi.programinstanceid=pi.programinstanceid " +
+                "left join programstage ps on psi.programstageid=ps.programstageid " +
+                "left join program pr on pi.programid=pr.programid " +
+                "left join _orgunitstructure ous on psi.organisationunitid=ous.organisationunitid " +
+                "where psi.executiondate >= '" + start + "' " +
+                "and psi.executiondate <= '" + end + "' " +
+                "and pr.programid=" + table.getProgram().getId() + ";";
+
+            log.info( "Populate SQL: "+ sql );
+            
+            jdbcTemplate.execute( sql );
+        }
+    
+        return null;
+    }
+    
+    public List<String[]> getDimensionColumns( AnalyticsTable table )
+    {
+        List<String[]> columns = new ArrayList<String[]>();
+
+        Collection<OrganisationUnitLevel> levels =
+            organisationUnitService.getOrganisationUnitLevels();
+        
+        for ( OrganisationUnitLevel level : levels )
+        {
+            String column = PREFIX_ORGUNITLEVEL + level.getLevel();
+            String[] col = { column, "character(11)", "ous." + column };
+            columns.add( col );
+        }
+        
+        for ( DataElement dataElement : table.getProgram().getAllDataElements() )
+        {
+            String select = "(select value from patientdatavalue where programstageinstanceid=" +
+                "psi.programstageinstanceid and dataelementid=" + dataElement.getId() + ") as " + dataElement.getUid();
+            
+            String[] col = { dataElement.getUid(), "character(255)", select };
+            columns.add( col );
+        }
+        
+        String[] psi = { "psi", "character(11) not null", "psi.uid" };
+        String[] ps = { "ps", "character(11) not null", "ps.uid" };
+        String[] ed = { "executiondate", "date", "psi.executiondate" };
+        
+        columns.addAll( Arrays.asList( psi, ps, ed ) );
+        
+        return columns;
+    }
+    
+    public Date getEarliestData()
+    {
+        final String sql = "select min(pdv.timestamp) from patientdatavalue pdv";
+        
+        return jdbcTemplate.queryForObject( sql, Date.class );
+    }
+
+    public Date getLatestData()
+    {
+        final String sql = "select max(pdv.timestamp) from patientdatavalue pdv";
+        
+        return jdbcTemplate.queryForObject( sql, Date.class );
+    }
+    
+    @Async
+    public Future<?> applyAggregationLevels( ConcurrentLinkedQueue<AnalyticsTable> tables, Collection<String> dataElements, int aggregationLevel )
+    {
+        return null; // Not relevant
+    }
+}

=== modified file 'dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml	2013-03-04 14:44:15 +0000
+++ dhis-2/dhis-services/dhis-service-analytics/src/main/resources/META-INF/dhis/beans.xml	2013-08-21 10:08:02 +0000
@@ -8,6 +8,8 @@
   
   <bean id="org.hisp.dhis.analytics.CompletenessTargetTableManager" class="org.hisp.dhis.analytics.table.JdbcCompletenessTargetTableManager" />
   
+  <bean id="org.hisp.dhis.analytics.EventAnalyticsTableManager" class="org.hisp.dhis.analytics.table.JdbcEventAnalyticsTableManager" />
+  
   <bean id="org.hisp.dhis.analytics.AnalyticsTableService" class="org.hisp.dhis.analytics.table.DefaultAnalyticsTableService">
     <property name="tableManager" ref="org.hisp.dhis.analytics.AnalyticsTableManager" />
   </bean>
@@ -20,6 +22,10 @@
     <property name="tableManager" ref="org.hisp.dhis.analytics.CompletenessTargetTableManager" />
   </bean>
   
+  <bean id="org.hisp.dhis.analytics.EventAnalyticsTableService" class="org.hisp.dhis.analytics.table.DefaultAnalyticsTableService">
+    <property name="tableManager" ref="org.hisp.dhis.analytics.EventAnalyticsTableManager" />
+  </bean>
+  
   <bean id="org.hisp.dhis.analytics.AnalyticsManager" class="org.hisp.dhis.analytics.data.JdbcAnalyticsManager" />
   
   <bean id="org.hisp.dhis.analytics.AnalyticsService" class="org.hisp.dhis.analytics.data.DefaultAnalyticsService" />

=== 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	2013-01-16 18:23:37 +0000
+++ dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/TextUtils.java	2013-08-21 11:56:16 +0000
@@ -136,6 +136,30 @@
     }
     
     /**
+     * Removes the last given number of characters from the given string. Returns
+     * null if the string is null. Returns an empty string if characters is less
+     * than zero or greater than the length of the string.
+     * 
+     * @param string the string.
+     * @param characters number of characters to remove.
+     * @return the substring.
+     */
+    public static String removeLast( String string, int characters )
+    {
+        if ( string == null )
+        {
+            return null;
+        }
+        
+        if ( characters < 0 || characters > string.length() )
+        {
+            return EMPTY;
+        }
+        
+        return string.substring( 0, string.length() - characters );
+    }
+    
+    /**
      * Trims the given string from the end.
      * 
      * @param value the value to trim.