Merge branch 'MDL-29360' of git://github.com/timhunt/moodle
authorSam Hemelryk <sam@moodle.com>
Sun, 18 Sep 2011 23:56:34 +0000 (11:56 +1200)
committerSam Hemelryk <sam@moodle.com>
Sun, 18 Sep 2011 23:56:34 +0000 (11:56 +1200)
13 files changed:
admin/xmldb/actions/edit_field/edit_field.js
admin/xmldb/actions/edit_field_save/edit_field_save.class.php
lib/ddl/mysql_sql_generator.php
lib/ddl/oracle_sql_generator.php
lib/ddl/postgres_sql_generator.php
lib/ddl/simpletest/testddl.php
lib/ddl/sql_generator.php
lib/dml/oci_native_moodle_database.php
lib/dml/simpletest/testdml.php
lib/xmldb/xmldb_field.php
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_object.php
question/type/multianswer/renderer.php

index 5fcf76c..065828d 100644 (file)
@@ -99,7 +99,7 @@ function transformForm(event) {
             decimalsTip.innerHTML = ' 0...length or empty';
             break;
         case '4':  // XMLDB_TYPE_CHAR
-            lengthTip.innerHTML = ' 1...255';
+            lengthTip.innerHTML = ' 1...'.xmldb_field::CHAR_MAX_LENGTH;
             decimalsTip.innerHTML = '';
             decimalsField.disabled = true;
             decimalsField.value = '';
index b762063..710c36c 100644 (file)
@@ -192,7 +192,7 @@ class edit_field_save extends XMLDBAction {
     /// Char checks
         if ($type == XMLDB_TYPE_CHAR) {
             if (!(is_numeric($length) && !empty($length) && intval($length)==floatval($length) &&
-                  $length > 0 && $length <= 255)) {
+                  $length > 0 && $length <= xmldb_field::CHAR_MAX_LENGTH)) {
                 $errors[] = $this->str['charincorrectlength'];
             }
             if ($default !== NULL && $default !== '') {
index 38b2d7b..3004c76 100644 (file)
@@ -281,7 +281,7 @@ class mysql_sql_generator extends sql_generator {
     /// Change the name of the field to perform the change
         $xmldb_field_clone->setName($xmldb_field_clone->getName() . ' ' . $newname);
 
-        $fieldsql = $this->getFieldSQL($xmldb_field_clone);
+        $fieldsql = $this->getFieldSQL($xmldb_table, $xmldb_field_clone);
 
         $sql = 'ALTER TABLE ' . $this->getTableName($xmldb_table) . ' CHANGE ' . $fieldsql;
 
index 2e0b37c..9450388 100644 (file)
@@ -163,11 +163,14 @@ class oracle_sql_generator extends sql_generator {
                 }
                 break;
             case XMLDB_TYPE_CHAR:
+                // Do not use NVARCHAR2 here because it has hardcoded 1333 char limit,
+                // VARCHAR2 allows us to create larger fields that error out later during runtime
+                // only when too many non-ascii utf-8 chars present.
                 $dbtype = 'VARCHAR2';
                 if (empty($xmldb_length)) {
                     $xmldb_length='255';
                 }
-                $dbtype .= '(' . $xmldb_length . ')';
+                $dbtype .= '(' . $xmldb_length . ' CHAR)'; // CHAR is required because BYTE is the default
                 break;
             case XMLDB_TYPE_TEXT:
                 $dbtype = 'CLOB';
@@ -670,8 +673,8 @@ class oracle_sql_generator extends sql_generator {
             'increment', 'index', 'initial', 'insert',
             'integer', 'intersect', 'into', 'is', 'level',
             'like', 'lock', 'long', 'maxextents', 'minus',
-            'mlslabel', 'mode', 'modify', 'noaudit',
-            'nocompress', 'not', 'nowait', 'null', 'number',
+            'mlslabel', 'mode', 'modify', 'nchar', 'nclob', 'noaudit',
+            'nocompress', 'not', 'nowait', 'null', 'number', 'nvarchar2',
             'of', 'offline', 'on', 'online', 'option', 'or',
             'order', 'pctfree', 'prior', 'privileges',
             'public', 'raw', 'rename', 'resource', 'revoke',
index cb8e00b..5e2a3c8 100644 (file)
@@ -280,7 +280,7 @@ class postgres_sql_generator extends sql_generator {
                 $results[] = 'ALTER TABLE ' . $tablename . ' ALTER COLUMN ' . $fieldname . ' DROP DEFAULT'; /// Drop default clause
             }
             $alterstmt = 'ALTER TABLE ' . $tablename . ' ALTER COLUMN ' . $this->getEncQuoted($xmldb_field->getName()) .
-                         ' TYPE' . $this->getFieldSQL($xmldb_field, null, true, true, null, false);
+                         ' TYPE' . $this->getFieldSQL($xmldb_table, $xmldb_field, null, true, true, null, false);
         /// Some castings must be performed explicity (mainly from text|char to numeric|integer)
             if (($oldmetatype == 'C' || $oldmetatype == 'X') &&
                 ($xmldb_field->getType() == XMLDB_TYPE_NUMBER || $xmldb_field->getType() == XMLDB_TYPE_FLOAT)) {
index 2d3f56d..7ea1275 100644 (file)
@@ -1487,6 +1487,192 @@ class ddl_test extends UnitTestCase {
         $this->assertTrue(count($reserved) > 1);
     }
 
+    public function test_index_max_bytes() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $maxstr = '';
+        for($i=0; $i<255; $i++) {
+            $maxstr .= '言'; // random long string that should fix exactly the limit for one char column
+        }
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'));
+
+        // Drop if exists
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+        $dbman->create_table($table);
+        $tablename = $table->getName();
+        $this->tables[$tablename] = $table;
+
+        $rec = new stdClass();
+        $rec->name = $maxstr;
+
+        $id = $DB->insert_record($tablename, $rec);
+        $this->assertTrue(!empty($id));
+
+        $rec = $DB->get_record($tablename, array('id'=>$id));
+        $this->assertIdentical($rec->name, $maxstr);
+
+        $dbman->drop_table($table);
+
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, 255+1, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'));
+
+        try {
+            $dbman->create_table($table);
+            $this->assertTrue(false);
+        } catch (Exception $e) {
+            $this->assertTrue($e instanceof coding_exception);
+        }
+    }
+
+    public function test_index_composed_max_bytes() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $maxstr = '';
+        for($i=0; $i<200; $i++) {
+            $maxstr .= '言';
+        }
+        $reststr = '';
+        for($i=0; $i<133; $i++) {
+            $reststr .= '言';
+        }
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name1', XMLDB_TYPE_CHAR, 200, null, XMLDB_NOTNULL, null);
+        $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1','name2'));
+
+        // Drop if exists
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+        $dbman->create_table($table);
+        $tablename = $table->getName();
+        $this->tables[$tablename] = $table;
+
+        $rec = new stdClass();
+        $rec->name1 = $maxstr;
+        $rec->name2 = $reststr;
+
+        $id = $DB->insert_record($tablename, $rec);
+        $this->assertTrue(!empty($id));
+
+        $rec = $DB->get_record($tablename, array('id'=>$id));
+        $this->assertIdentical($rec->name1, $maxstr);
+        $this->assertIdentical($rec->name2, $reststr);
+
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name1', XMLDB_TYPE_CHAR, 201, null, XMLDB_NOTNULL, null);
+        $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1','name2'));
+
+        // Drop if exists
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+        try {
+            $dbman->create_table($table);
+            $this->assertTrue(false);
+        } catch (Exception $e) {
+            $this->assertTrue($e instanceof coding_exception);
+        }
+    }
+
+    public function test_char_size_limit() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, xmldb_field::CHAR_MAX_LENGTH, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Drop if exists
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+        $dbman->create_table($table);
+        $tablename = $table->getName();
+        $this->tables[$tablename] = $table;
+
+        // this has to work in all DBs
+        $maxstr = '';
+        for($i=0; $i<xmldb_field::CHAR_MAX_LENGTH; $i++) {
+            $maxstr .= 'a'; // ascii only
+        }
+
+        $rec = new stdClass();
+        $rec->name = $maxstr;
+
+        $id = $DB->insert_record($tablename, $rec);
+        $this->assertTrue(!empty($id));
+
+        $rec = $DB->get_record($tablename, array('id'=>$id));
+        $this->assertIdentical($rec->name, $maxstr);
+
+
+        // Following test is supposed to fail in oracle
+        $maxstr = '';
+        for($i=0; $i<xmldb_field::CHAR_MAX_LENGTH; $i++) {
+            $maxstr .= '言'; // random long string that should fix exactly the limit for one char column
+        }
+
+        $rec = new stdClass();
+        $rec->name = $maxstr;
+
+        try {
+            $id = $DB->insert_record($tablename, $rec);
+            $this->assertTrue(!empty($id));
+
+            $rec = $DB->get_record($tablename, array('id'=>$id));
+            $this->assertIdentical($rec->name, $maxstr);
+        } catch (dml_write_exception $e) {
+            if ($DB->get_dbfamily() === 'oracle') {
+                $this->fail('Oracle does not support text fields larger than 4000 bytes, this is not a big problem for mostly ascii based languages');
+            } else {
+                throw $e;
+            }
+        }
+
+
+        $table = new xmldb_table('testtable');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('name', XMLDB_TYPE_CHAR, xmldb_field::CHAR_MAX_LENGTH+1, null, XMLDB_NOTNULL, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Drop if exists
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+        $tablename = $table->getName();
+        $this->tables[$tablename] = $table;
+
+        try {
+            $dbman->create_table($table);
+            $this->assertTrue(false);
+        } catch (Exception $e) {
+            $this->assertTrue($e instanceof coding_exception);
+        }
+    }
+
  // Following methods are not supported == Do not test
 /*
     public function testRenameIndex() {
index 059c620..203ccf3 100644 (file)
@@ -240,7 +240,7 @@ abstract class sql_generator {
             if ($xmldb_field->getSequence()) {
                 $sequencefield = $xmldb_field->getName();
             }
-            $table .= "\n    " . $this->getFieldSQL($xmldb_field);
+            $table .= "\n    " . $this->getFieldSQL($xmldb_table, $xmldb_field);
             $table .= ',';
         }
     /// Add the keys, separated by commas
@@ -348,6 +348,9 @@ abstract class sql_generator {
      * needed to create it (in array)
      */
     public function getCreateIndexSQL($xmldb_table, $xmldb_index) {
+        if ($error = $xmldb_index->validateDefinition($xmldb_table)) {
+            throw new coding_exception($error);
+        }
 
         $unique = '';
         $suffix = 'ix';
@@ -367,7 +370,10 @@ abstract class sql_generator {
     /**
      * Given one correct xmldb_field, returns the complete SQL line to create it
      */
-    public function getFieldSQL($xmldb_field, $skip_type_clause = NULL, $skip_default_clause = NULL, $skip_notnull_clause = NULL, $specify_nulls_clause = NULL, $specify_field_name = true)  {
+    public function getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause = NULL, $skip_default_clause = NULL, $skip_notnull_clause = NULL, $specify_nulls_clause = NULL, $specify_field_name = true)  {
+        if ($error = $xmldb_field->validateDefinition($xmldb_table)) {
+            throw new coding_exception($error);
+        }
 
         $skip_type_clause = is_null($skip_type_clause) ? $this->alter_column_skip_type : $skip_type_clause;
         $skip_default_clause = is_null($skip_default_clause) ? $this->alter_column_skip_default : $skip_default_clause;
@@ -592,7 +598,7 @@ abstract class sql_generator {
         $tablename = $this->getTableName($xmldb_table);
 
     /// Build the standard alter table add
-        $sql = $this->getFieldSQL($xmldb_field, $skip_type_clause,
+        $sql = $this->getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause,
                                   $skip_default_clause,
                                   $skip_notnull_clause);
         $altertable = 'ALTER TABLE ' . $tablename . ' ADD ' . $sql;
@@ -639,7 +645,7 @@ abstract class sql_generator {
 
     /// Build de alter sentence using the alter_column_sql template
         $alter = str_replace('TABLENAME', $this->getTableName($xmldb_table), $this->alter_column_sql);
-        $colspec = $this->getFieldSQL($xmldb_field, $skip_type_clause,
+        $colspec = $this->getFieldSQL($xmldb_table, $xmldb_field, $skip_type_clause,
                                       $skip_default_clause,
                                       $skip_notnull_clause,
                                       true);
index 943dbbb..a007e66 100644 (file)
@@ -477,8 +477,11 @@ class oci_native_moodle_database extends moodle_database {
 
         $this->columns[$table] = array();
 
-        $sql = "SELECT CNAME, COLTYPE, WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
-                  FROM COL
+        // We give precedence to CHAR_LENGTH for VARCHAR2 columns over WIDTH because the former is always
+        // BYTE based and, for cross-db operations, we want CHAR based results. See MDL-29415
+        $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
+                  FROM COL c
+             LEFT JOIN USER_TAB_COLUMNS u ON (u.TABLE_NAME = c.TNAME AND u.COLUMN_NAME = c.CNAME AND u.DATA_TYPE = 'VARCHAR2')
                  WHERE TNAME = UPPER('{" . $table . "}')
               ORDER BY COLNO";
 
index f44e258..a4e6e93 100644 (file)
@@ -751,6 +751,7 @@ class dml_test extends UnitTestCase {
         $field = $columns['name'];
         $this->assertEqual('C', $field->meta_type);
         $this->assertFalse($field->auto_increment);
+        $this->assertEqual(255, $field->max_length);
         $this->assertTrue($field->has_default);
         $this->assertIdentical('lala', $field->default_value);
         $this->assertFalse($field->not_null);
index cd00b77..e272b51 100644 (file)
@@ -36,6 +36,17 @@ class xmldb_field extends xmldb_object {
     var $sequence;
     var $decimals;
 
+    /**
+     * Note:
+     *  - Oracle: VARCHAR2 has a limit of 4000 bytes
+     *  - SQL Server: NVARCHAR has a limit of 40000 chars
+     *  - MySQL: VARCHAR 65,535 chars
+     *  - PostgreSQL: no limit
+     *
+     * @const maximum length of text field
+     */
+    const CHAR_MAX_LENGTH = 1333;
+
     /**
      * Creates one new xmldb_field
      */
@@ -785,6 +796,53 @@ class xmldb_field extends xmldb_object {
 
         return $o;
     }
+
+    /**
+     * Validates the field restrictions.
+     *
+     * The error message should not be localised because it is intended for developers,
+     * end users and admins should never see these problems!
+     *
+     * @param xmldb_table $xmldb_table optional when object is table
+     * @return string null if ok, error message if problem found
+     */
+    function validateDefinition(xmldb_table $xmldb_table=null) {
+        if (!$xmldb_table) {
+            return 'Invalid xmldb_field->validateDefinition() call, $xmldb_table si required.';
+        }
+
+        switch ($this->getType()) {
+            case XMLDB_TYPE_INTEGER:
+                break;
+
+            case XMLDB_TYPE_NUMBER:
+                break;
+
+            case XMLDB_TYPE_FLOAT:
+                break;
+
+            case XMLDB_TYPE_CHAR:
+                if ($this->getLength() > self::CHAR_MAX_LENGTH) {
+                    return 'Invalid field definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_CHAR field "'.$this->getName().'" is too long.'
+                           .' Limit is '.self::CHAR_MAX_LENGTH.' chars.';
+                }
+                break;
+
+            case XMLDB_TYPE_TEXT:
+                break;
+
+            case XMLDB_TYPE_BINARY:
+                break;
+
+            case XMLDB_TYPE_DATETIME:
+                break;
+
+            case XMLDB_TYPE_TIMESTAMP:
+                break;
+        }
+
+        return null;
+    }
 }
 
 /// TODO: Delete for 2.1 (deprecated in 2.0).
index 11ed9c7..5763a43 100644 (file)
@@ -31,6 +31,22 @@ class xmldb_index extends xmldb_object {
     var $unique;
     var $fields;
 
+    /**
+     * Note:
+     *  - MySQL: MyISAM has a limit of 1000 bytes for any key including composed, InnoDB has limit 3500 bytes.
+     *
+     * @const max length of composed indexes, one utf-8 char is 3 bytes in the worst case
+     */
+    const INDEX_COMPOSED_MAX_BYTES = 999;
+
+    /**
+     * Note:
+     *  - MySQL: InnoDB limits size of index on single column to 767bytes (256 chars)
+     *
+     * @const single column index length limit, one utf-8 char is 3 bytes in the worst case
+     */
+    const INDEX_MAX_BYTES = 765;
+
     /**
      * Creates one new xmldb_index
      */
@@ -270,6 +286,75 @@ class xmldb_index extends xmldb_object {
 
         return $o;
     }
+
+    /**
+     * Validates the index restrictions.
+     *
+     * The error message should not be localised because it is intended for developers,
+     * end users and admins should never see these problems!
+     *
+     * @param xmldb_table $xmldb_table optional when object is table
+     * @return string null if ok, error message if problem found
+     */
+    function validateDefinition(xmldb_table $xmldb_table=null) {
+        if (!$xmldb_table) {
+            return 'Invalid xmldb_index->validateDefinition() call, $xmldb_table si required.';
+        }
+
+        $total = 0;
+        foreach ($this->getFields() as $fieldname) {
+            if (!$field = $xmldb_table->getField($fieldname)) {
+                // argh, we do not have the fields loaded yet, this should not happen during install
+                continue;
+            }
+
+            switch ($field->getType()) {
+                case XMLDB_TYPE_INTEGER:
+                    $total += 8; // big int
+                    break;
+
+                case XMLDB_TYPE_NUMBER:
+                    $total += 12; // this is just a guess
+                    break;
+
+                case XMLDB_TYPE_FLOAT:
+                    $total += 8; // double precision
+                    break;
+
+                case XMLDB_TYPE_CHAR:
+                    if ($field->getLength() > self::INDEX_MAX_BYTES / 3) {
+                        return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_CHAR field "'.$field->getName().'" can not be indexed because it is too long.'
+                                .' Limit is '.(self::INDEX_MAX_BYTES/3).' chars.';
+                    }
+                    $total += ($field->getLength() * 3); // the most complex utf-8 chars have 3 bytes
+                    break;
+
+                case XMLDB_TYPE_TEXT:
+                    return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_TEXT field "'.$field->getName().'" can not be indexed';
+                    break;
+
+                case XMLDB_TYPE_BINARY:
+                    return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: XMLDB_TYPE_BINARY field "'.$field->getName().'" can not be indexed';
+                    break;
+
+                case XMLDB_TYPE_DATETIME:
+                    $total += 8; // this is just a guess
+                    break;
+
+                case XMLDB_TYPE_TIMESTAMP:
+                    $total += 8; // this is just a guess
+                    break;
+            }
+        }
+
+        if ($total > self::INDEX_COMPOSED_MAX_BYTES) {
+            return 'Invalid index definition in table {'.$xmldb_table->getName(). '}: the composed index on fields "'.implode(',', $this->getFields()).'" is too long.'
+                    .' Limit is '.self::INDEX_COMPOSED_MAX_BYTES.' bytes / '.(self::INDEX_COMPOSED_MAX_BYTES/3).' chars.';
+        }
+
+        return null;
+    }
+
 }
 
 /// TODO: Delete for 2.1 (deprecated in 2.0).
index 5802110..4fd59ac 100644 (file)
@@ -478,4 +478,17 @@ class xmldb_object {
 
         return $arr;
     }
+
+    /**
+     * Validates the definition of objects and returns error message.
+     *
+     * The error message should not be localised because it is intended for developers,
+     * end users and admins should never see these problems!
+     *
+     * @param xmldb_table $xmldb_table optional when object is table
+     * @return string null if ok, error message if problem found
+     */
+    function validateDefinition(xmldb_table $xmldb_table=null) {
+        return null;
+    }
 }
index 36e1c08..29c23d6 100644 (file)
@@ -132,6 +132,10 @@ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
             $feedback[] = $state->default_string(true);
         }
 
+        if ($feedbacktext) { // Note $options->feedback is already checked above.
+            $feedback[] = $feedbacktext;
+        }
+
         if ($options->rightanswer) {
             $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
         }
@@ -144,6 +148,10 @@ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
             $feedback[] = get_string('markoutofmax', 'question', $a);
         }
 
+        if (!$feedback) {
+            return '';
+        }
+
         return html_writer::tag('span', implode('<br />', $feedback),
                 array('class' => 'feedbackspan accesshide'));
     }