diff --git a/lib/classes/dml/table.php b/lib/classes/dml/table.php new file mode 100644 index 0000000000000..3cf86d01b0e8d --- /dev/null +++ b/lib/classes/dml/table.php @@ -0,0 +1,133 @@ +. + +/** + * Helpers and methods relating to DML tables. + * + * @since Moodle 3.7 + * @package core + * @category dml + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\dml; + +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Helpers and methods relating to DML tables. + * + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class table { + + /** @var string Name of the table that this class represents */ + protected $tablename; + + /** @var string Table alias */ + protected $tablealias; + + /** @var string Prefix to place before each field */ + protected $fieldprefix; + + /** @var array List of fields */ + protected $fields; + + /** + * Constructor for the table class. + * + * @param string $tablename The name of the table that this instance represents. + * @param string $tablealias The alias to use when selecting the table + * @param string $fieldprefix The prefix to use when selecting fields. + */ + public function __construct(string $tablename, string $tablealias, string $fieldprefix) { + $this->tablename = $tablename; + $this->tablealias = $tablealias; + $this->fieldprefix = $fieldprefix; + } + + /** + * Get the from TABLE ALIAS part of the FROM/JOIN string. + * + * @return string + */ + public function get_from_sql() : string { + return "{{$this->tablename}} {$this->tablealias}"; + } + + /** + * Get the list of fields in a table for use in preloading fields. + * + * @return array The list of columns in a table. The array key is the column name with an applied prefix. + */ + protected function get_fieldlist() : array { + global $DB; + + if (null === $this->fields) { + $fields = []; + foreach (array_keys($DB->get_columns($this->tablename)) as $fieldname) { + $fields["{$this->fieldprefix}{$fieldname}"] = $fieldname; + } + + $this->fields = $fields; + } + + return $this->fields; + } + + /** + * Get the SELECT SQL to select a set of columns for this table. + * + * This function is intended to be used in combination with extract_from_result(). + * + * @return string The SQL to use in the SELECT + */ + public function get_field_select() : string { + $fieldlist = $this->get_fieldlist(); + + return implode(', ', array_map(function($fieldname, $fieldalias) { + return "{$this->tablealias}.{$fieldname} AS {$fieldalias}"; + }, $fieldlist, array_keys($fieldlist))); + } + + /** + * Extract fields from the specified result. The fields are removed from the original object. + * + * This function is intended to be used in combination with get_field_select(). + * + * @param stdClass $result The result retrieved from the database with fields to be extracted + * @return stdClass The extracted result + */ + public function extract_from_result(stdClass $result) : stdClass { + $record = new stdClass(); + + $fieldlist = $this->get_fieldlist(); + foreach ($fieldlist as $fieldalias => $fieldname) { + if (property_exists($result, $fieldalias)) { + $record->$fieldname = $result->$fieldalias; + unset($result->$fieldalias); + } else { + debugging("Field '{$fieldname}' not found", DEBUG_DEVELOPER); + } + } + + return $record; + } +} diff --git a/lib/dml/moodle_database.php b/lib/dml/moodle_database.php index 72d78b2ba6522..9e781cd895209 100644 --- a/lib/dml/moodle_database.php +++ b/lib/dml/moodle_database.php @@ -828,65 +828,6 @@ public function get_in_or_equal($items, $type=SQL_PARAMS_QM, $prefix='param', $e return array($sql, $params); } - /** - * Get the SELECT SQL to preload columns for the specified fieldlist and table alias. - * - * This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields. - * - * @param array $fieldlist The list of fields from get_preload_columns - * @param string $tablealias The table alias used in the FROM/JOIN field - * @return string The SQL to use in the SELECT - */ - public function get_preload_columns_sql(array $fieldlist, string $tablealias) : string { - return implode(', ', array_map(function($fieldname, $alias) use ($tablealias) { - return "{$tablealias}.{$fieldname} AS {$alias}"; - }, $fieldlist, array_keys($fieldlist))); - } - - /** - * Extract fields from the specified data. - * The fields are removed from the original object. - * - * This function is intended to be used in combination with get_preload_columns and get_preload_columns_sql. - * - * @param array $fieldlist The list of fields from get_preload_columns - * @param \stdClass $data The data retrieved from the database with fields to be extracted - * @return string The SQL to use in the SELECT - */ - public function extract_fields_from_object(array $fieldlist, \stdClass $data) : \stdClass { - $newdata = (object) []; - foreach ($fieldlist as $alias => $fieldname) { - if (property_exists($data, $alias)) { - $newdata->$fieldname = $data->$alias; - unset($data->$alias); - } else { - debugging("Field '{$fieldname}' not found", DEBUG_DEVELOPER); - } - } - - return $newdata; - } - - /** - * Get the preload columns for the specified table and use the specified prefix in the column alias. - * - * This function is intended to be used in combination with get_preload_columns_sql and extract_from_fields. - * - * @param string $table - * @param string $prefix - * @return array The list of columns in a table. The array key is the column name with an applied prefix. - */ - public function get_preload_columns(string $table, string $prefix) : array { - global $DB; - - $fields = []; - foreach (array_keys($this->get_columns($table)) as $fieldname) { - $fields["{$prefix}{$fieldname}"] = $fieldname; - } - - return $fields; - } - /** * Converts short table name {tablename} to the real prefixed table name in given sql. * @param string $sql The sql to be operated on. diff --git a/lib/dml/tests/dml_table_test.php b/lib/dml/tests/dml_table_test.php new file mode 100644 index 0000000000000..4d3cd8511e541 --- /dev/null +++ b/lib/dml/tests/dml_table_test.php @@ -0,0 +1,228 @@ +. + +/** + * DML Table tests. + * + * @package core_dml + * @category phpunit + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use \core\dml\table; + +/** + * DML Table tests. + * + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\dml\table + * @covers :: + */ +class core_dml_table_testcase extends database_driver_testcase { + + /** + * Data provider for various \core\dml\table method tests. + * + * @return array + */ + public function get_field_select_provider() : array { + return [ + 'single field' => [ + 'tablename' => 'test_table_single', + 'fieldlist' => [ + 'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null], + ], + 'primarykey' => 'id', + 'fieldprefix' => 'ban', + 'tablealias' => 'banana', + 'banana.id AS banid', + ], + 'multiple fields' => [ + 'tablename' => 'test_table_multiple', + 'fieldlist' => [ + 'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null], + 'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'], + 'name' => ['name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'], + ], + 'primarykey' => 'id', + 'fieldprefix' => 'ban', + 'tablealias' => 'banana', + 'banana.id AS banid, banana.course AS bancourse, banana.name AS banname', + ], + ]; + } + + /** + * Ensure that \core\dml\table::get_field_select() works as expected. + * + * @dataProvider get_field_select_provider + * @covers ::get_field_select + * @param string $tablename The name of the table + * @param array $fieldlist The list of fields + * @param string $primarykey The name of the primary key + * @param string $fieldprefix The prefix to use for each field + * @param string $tablealias The table AS alias name + * @param string $expected The expected SQL + */ + public function test_get_field_select( + string $tablename, + array $fieldlist, + string $primarykey, + string $fieldprefix, + string $tablealias, + string $expected + ) { + $dbman = $this->tdb->get_manager(); + + $xmldbtable = new xmldb_table($tablename); + $xmldbtable->setComment("This is a test'n drop table. You can drop it safely"); + + foreach ($fieldlist as $args) { + call_user_func_array([$xmldbtable, 'add_field'], $args); + } + $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]); + $dbman->create_table($xmldbtable); + + $table = new table($tablename, $tablealias, $fieldprefix); + $this->assertEquals($expected, $table->get_field_select()); + } + + /** + * Data provider for \core\dml\table::extract_from_result() tests. + * + * @return array + */ + public function extract_from_result_provider() : array { + return [ + 'single table' => [ + 'fieldlist' => [ + 'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null], + 'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'], + 'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'], + ], + 'primarykey' => 'id', + 'prefix' => 's', + 'result' => (object) [ + 'sid' => 1, + 'scourse' => 42, + 'sflag' => 'foo', + ], + 'expectedrecord' => (object) [ + 'id' => 1, + 'course' => 42, + 'flag' => 'foo', + ], + ], + 'single table amongst others' => [ + 'fieldlist' => [ + 'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null], + 'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'], + 'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'], + ], + 'primarykey' => 'id', + 'prefix' => 's', + 'result' => (object) [ + 'sid' => 1, + 'scourse' => 42, + 'sflag' => 'foo', + 'oid' => 'id', + 'ocourse' => 'course', + 'oflag' => 'flag', + ], + 'expectedrecord' => (object) [ + 'id' => 1, + 'course' => 42, + 'flag' => 'foo', + ], + ], + ]; + } + + /** + * Ensure that \core\dml\table::extract_from_result() works as expected. + * + * @dataProvider extract_from_result_provider + * @covers ::extract_from_result + * @param array $fieldlist The list of fields + * @param string $primarykey The name of the primary key + * @param string $fieldprefix The prefix to use for each field + * @param stdClass $result The result of the get_records_sql + * @param stdClass $expected The expected output + */ + public function test_extract_fields_from_result( + array $fieldlist, + string $primarykey, + string $fieldprefix, + stdClass $result, + stdClass $expected + ) { + $dbman = $this->tdb->get_manager(); + + $tablename = 'test_table_extraction'; + $xmldbtable = new xmldb_table($tablename); + $xmldbtable->setComment("This is a test'n drop table. You can drop it safely"); + + foreach ($fieldlist as $args) { + call_user_func_array([$xmldbtable, 'add_field'], $args); + } + $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]); + $dbman->create_table($xmldbtable); + + $table = new table($tablename, 'footable', $fieldprefix); + $this->assertEquals($expected, $table->extract_from_result($result)); + } + + /** + * Ensure that \core\dml\table::get_from_sql() works as expected. + * + * @dataProvider get_field_select_provider + * @covers ::get_from_sql + * @param string $tablename The name of the table + * @param array $fieldlist The list of fields + * @param string $primarykey The name of the primary key + * @param string $fieldprefix The prefix to use for each field + * @param string $tablealias The table AS alias name + * @param string $expected The expected SQL + */ + public function test_get_from_sql( + string $tablename, + array $fieldlist, + string $primarykey, + string $fieldprefix, + string $tablealias, + string $expected + ) { + $dbman = $this->tdb->get_manager(); + + $tablename = 'test_table_extraction'; + $xmldbtable = new xmldb_table($tablename); + $xmldbtable->setComment("This is a test'n drop table. You can drop it safely"); + + foreach ($fieldlist as $args) { + call_user_func_array([$xmldbtable, 'add_field'], $args); + } + $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]); + $dbman->create_table($xmldbtable); + + $table = new table($tablename, $tablealias, $fieldprefix); + + $this->assertEquals("{{$tablename}} {$tablealias}", $table->get_from_sql()); + } +} diff --git a/lib/dml/tests/dml_test.php b/lib/dml/tests/dml_test.php index f89378aa8a247..fb144e9c7368a 100644 --- a/lib/dml/tests/dml_test.php +++ b/lib/dml/tests/dml_test.php @@ -794,136 +794,6 @@ public function test_get_columns() { $this->assertFalse($columns['id']->auto_increment); } - public function test_get_preload_columns() { - $DB = $this->tdb; - $dbman = $this->tdb->get_manager(); - - $table = $this->get_test_table(); - $tablename = $table->getName(); - - $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); - $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); - $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'); - $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - $dbman->create_table($table); - - $expected = [ - 'aid' => 'id', - 'acourse' => 'course', - 'aname' => 'name', - ]; - $columns = $DB->get_preload_columns($tablename, 'a'); - $this->assertCount(3, $columns); - $this->assertEquals($expected, $columns); - } - - /** - * Ensure that get_preload_columns_sql works as expected. - * - * @dataProvider get_preload_columns_sql_provider - * @param array $fieldlist The list of fields - * @param string $tablealias The alias to use - * @param string $expected The string to match - */ - public function test_get_preload_columns_sql(array $fieldlist, string $tablealias, string $expected) { - $this->assertEquals($expected, $this->tdb->get_preload_columns_sql($fieldlist, $tablealias)); - } - - /** - * Data provider for get_preload_columns_sql tests. - * - * @return array - */ - public function get_preload_columns_sql_provider() : array { - return [ - 'single field' => [ - [ - 'xid' => 'id', - ], - 'x', - 'x.id AS xid', - ], - 'multiple fields' => [ - [ - 'bananaid' => 'id', - 'bananacourse' => 'course', - 'bananafoo' => 'foo', - ], - 'banana', - 'banana.id AS bananaid, banana.course AS bananacourse, banana.foo AS bananafoo', - ], - ]; - } - - /** - * Ensure that extract_fields_from_object works as expected. - * - * @dataProvider extract_fields_from_object_provider - * @param array $fieldlist The list of fields - * @param stdClass $in Input values for the test - * @param stdClass $out The expected output - * @param stdClass $modified Expected value of $in after it's been modified - */ - public function test_extract_fields_from_object(array $fieldlist, \stdClass $in, \stdClass $out, \stdClass $modified) { - $result = $this->tdb->extract_fields_from_object($fieldlist, $in); - $this->assertEquals($out, $result); - $this->assertEquals($modified, $in); - } - - /** - * Data provider for extract_fields_from_object tests. - * - * @return array - */ - public function extract_fields_from_object_provider() : array { - return [ - 'single table' => [ - [ - 'sid' => 'id', - 'scourse' => 'course', - 'sflag' => 'flag', - ], - (object) [ - 'sid' => 1, - 'scourse' => 42, - 'sflag' => 'foo', - ], - (object) [ - 'id' => 1, - 'course' => 42, - 'flag' => 'foo', - ], - (object) [ - ], - ], - 'single table amongst others' => [ - [ - 'sid' => 'id', - 'scourse' => 'course', - 'sflag' => 'flag', - ], - (object) [ - 'sid' => 1, - 'scourse' => 42, - 'sflag' => 'foo', - 'oid' => 'id', - 'ocourse' => 'course', - 'oflag' => 'flag', - ], - (object) [ - 'id' => 1, - 'course' => 42, - 'flag' => 'foo', - ], - (object) [ - 'oid' => 'id', - 'ocourse' => 'course', - 'oflag' => 'flag', - ], - ], - ]; - } - public function test_get_manager() { $DB = $this->tdb; $dbman = $this->tdb->get_manager(); diff --git a/mod/forum/classes/local/vaults/discussion_list.php b/mod/forum/classes/local/vaults/discussion_list.php index 19c6087b95166..a4228bf9dd3a9 100644 --- a/mod/forum/classes/local/vaults/discussion_list.php +++ b/mod/forum/classes/local/vaults/discussion_list.php @@ -29,6 +29,7 @@ use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor; use mod_forum\local\vaults\preprocessors\extract_user as extract_user_preprocessor; use mod_forum\local\renderers\discussion_list as discussion_list_renderer; +use core\dml\table as dml_table; use stdClass; /** @@ -89,22 +90,22 @@ protected function generate_get_records_sql(string $wheresql = null, ?string $so // - First post // - Author // - Most recent editor. - $tablefields = $db->get_preload_columns(self::TABLE, $alias); - $postfields = $db->get_preload_columns('forum_posts', 'p_'); + $thistable = new dml_table(self::TABLE, $alias, $alias); + $posttable = new dml_table('forum_posts', 'fp', 'p_'); $firstauthorfields = \user_picture::fields('fa', null, self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS); $latestuserfields = \user_picture::fields('la', null, self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS); $fields = implode(', ', [ - $db->get_preload_columns_sql($tablefields, $alias), - $db->get_preload_columns_sql($postfields, 'fp'), + $thistable->get_field_select(), + $posttable->get_field_select(), $firstauthorfields, $latestuserfields, ]); - $tables = '{' . self::TABLE . '} ' . $alias; + $tables = $thistable->get_from_sql(); $tables .= ' JOIN {user} fa ON fa.id = ' . $alias . '.userid'; $tables .= ' JOIN {user} la ON la.id = ' . $alias . '.usermodified'; - $tables .= ' JOIN {forum_posts} fp ON fp.id = ' . $alias . '.firstpost'; + $tables .= ' JOIN ' . $posttable->get_from_sql() . ' ON fp.id = ' . $alias . '.firstpost'; $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; @@ -139,8 +140,8 @@ protected function get_preprocessors() : array { return array_merge( parent::get_preprocessors(), [ - 'discussion' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()), - 'firstpost' => new extract_record_preprocessor($this->get_db(), 'forum_posts', 'p_'), + 'discussion' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()), + 'firstpost' => new extract_record_preprocessor('forum_posts', 'p_'), 'firstpostauthor' => new extract_user_preprocessor(self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS), 'latestpostauthor' => new extract_user_preprocessor(self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS), ] diff --git a/mod/forum/classes/local/vaults/forum.php b/mod/forum/classes/local/vaults/forum.php index 5804514fa64c9..b224466340aba 100644 --- a/mod/forum/classes/local/vaults/forum.php +++ b/mod/forum/classes/local/vaults/forum.php @@ -29,6 +29,7 @@ use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\vaults\preprocessors\extract_context as extract_context_preprocessor; use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor; +use core\dml\table as dml_table; use context_helper; /** @@ -65,22 +66,23 @@ protected function get_table_alias() : string { protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null) : string { $db = $this->get_db(); $alias = $this->get_table_alias(); - $tablefields = $db->get_preload_columns(self::TABLE, $alias); - $coursemodulefields = $db->get_preload_columns('course_modules', 'cm_'); - $coursefields = $db->get_preload_columns('course', 'c_'); + + $thistable = new dml_table(self::TABLE, $alias, $alias); + $cmtable = new dml_table('course_modules', 'cm', 'cm_'); + $coursetable = new dml_table('course', 'c', 'c_'); $fields = implode(', ', [ - $db->get_preload_columns_sql($tablefields, $alias), + $thistable->get_field_select(), context_helper::get_preload_record_columns_sql('ctx'), - $db->get_preload_columns_sql($coursemodulefields, 'cm'), - $db->get_preload_columns_sql($coursefields, 'c'), + $cmtable->get_field_select(), + $coursetable->get_field_select(), ]); - $tables = '{' . self::TABLE . '} ' . $alias; + $tables = $thistable->get_from_sql(); $tables .= " JOIN {modules} m ON m.name = 'forum'"; - $tables .= " JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = {$alias}.id"; + $tables .= " JOIN " . $cmtable->get_from_sql() . " ON cm.module = m.id AND cm.instance = {$alias}.id"; $tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id'; - $tables .= " JOIN {course} c ON c.id = {$alias}.course"; + $tables .= " JOIN " . $coursetable->get_from_sql() . " ON c.id = {$alias}.course"; $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; @@ -99,9 +101,9 @@ protected function get_preprocessors() : array { return array_merge( parent::get_preprocessors(), [ - 'forum' => new extract_record_preprocessor($this->get_db(), self::TABLE, $this->get_table_alias()), - 'course_module' => new extract_record_preprocessor($this->get_db(), 'course_modules', 'cm_'), - 'course' => new extract_record_preprocessor($this->get_db(), 'course', 'c_'), + 'forum' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()), + 'course_module' => new extract_record_preprocessor('course_modules', 'cm_'), + 'course' => new extract_record_preprocessor('course', 'c_'), 'context' => new extract_context_preprocessor(), ] ); diff --git a/mod/forum/classes/local/vaults/preprocessors/extract_record.php b/mod/forum/classes/local/vaults/preprocessors/extract_record.php index 0943d21458eed..2bccefd93a8db 100644 --- a/mod/forum/classes/local/vaults/preprocessors/extract_record.php +++ b/mod/forum/classes/local/vaults/preprocessors/extract_record.php @@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die(); use moodle_database; +use core\dml\table as dml_table; /** * Extract record vault preprocessor. @@ -38,24 +39,17 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class extract_record { - /** @var moodle_database $db A moodle database */ - private $db; - /** @var string $table The table name where the records were loaded from */ + /** @var \core\dml\table $table The table object relating to the table that the records were loaded from */ private $table; - /** @var string $alias The table alias used as the record property prefix */ - private $alias; /** * Constructor. * - * @param moodle_database $db A moodle database * @param string $table The table name where the records were loaded from * @param string $alias The table alias used as the record property prefix */ - public function __construct(moodle_database $db, string $table, string $alias) { - $this->db = $db; - $this->table = $table; - $this->alias = $alias; + public function __construct(string $table, string $alias) { + $this->table = new dml_table($table, $alias, $alias); } /** @@ -66,11 +60,8 @@ public function __construct(moodle_database $db, string $table, string $alias) { * @return stdClass[] The extracted records */ public function execute(array $records) : array { - $db = $this->db; - $fields = $this->db->get_preload_columns($this->table, $this->alias); - - return array_map(function($record) use ($db, $fields) { - return $db->extract_fields_from_object($fields, $record); + return array_map(function($record) { + return $this->table->extract_from_result($record); }, $records); } }