2014-09-18 17:22:18 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
final class PhabricatorConfigSchemaQuery extends Phobject {
|
|
|
|
|
2016-11-12 20:39:45 +01:00
|
|
|
private $refs;
|
|
|
|
private $apis;
|
|
|
|
|
|
|
|
public function setRefs(array $refs) {
|
|
|
|
$this->refs = $refs;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRefs() {
|
|
|
|
if (!$this->refs) {
|
|
|
|
return PhabricatorDatabaseRef::getMasterDatabaseRefs();
|
|
|
|
}
|
|
|
|
return $this->refs;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setAPIs(array $apis) {
|
|
|
|
$map = array();
|
|
|
|
foreach ($apis as $api) {
|
|
|
|
$map[$api->getRef()->getRefKey()] = $api;
|
|
|
|
}
|
|
|
|
$this->apis = $map;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
private function getDatabaseNames(PhabricatorDatabaseRef $ref) {
|
|
|
|
$api = $this->getAPI($ref);
|
|
|
|
$patches = PhabricatorSQLPatchList::buildAllPatches();
|
|
|
|
return $api->getDatabaseList(
|
|
|
|
$patches,
|
|
|
|
$only_living = true);
|
|
|
|
}
|
2014-09-18 17:22:18 +02:00
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
private function getAPI(PhabricatorDatabaseRef $ref) {
|
2016-11-12 20:39:45 +01:00
|
|
|
$key = $ref->getRefKey();
|
|
|
|
|
|
|
|
if (isset($this->apis[$key])) {
|
|
|
|
return $this->apis[$key];
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
return id(new PhabricatorStorageManagementAPI())
|
|
|
|
->setUser($ref->getUser())
|
|
|
|
->setHost($ref->getHost())
|
|
|
|
->setPort($ref->getPort())
|
|
|
|
->setNamespace(PhabricatorLiskDAO::getDefaultStorageNamespace())
|
|
|
|
->setPassword($ref->getPass());
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
public function loadActualSchemata() {
|
2016-11-12 20:39:45 +01:00
|
|
|
$refs = $this->getRefs();
|
2016-11-12 18:22:10 +01:00
|
|
|
|
|
|
|
$schemata = array();
|
|
|
|
foreach ($refs as $ref) {
|
|
|
|
$schema = $this->loadActualSchemaForServer($ref);
|
|
|
|
$schemata[$schema->getRef()->getRefKey()] = $schema;
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
return $schemata;
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
private function loadActualSchemaForServer(PhabricatorDatabaseRef $ref) {
|
|
|
|
$databases = $this->getDatabaseNames($ref);
|
2014-09-18 17:22:18 +02:00
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
$conn = $ref->newManagementConnection();
|
2014-09-18 17:22:18 +02:00
|
|
|
|
|
|
|
$tables = queryfx_all(
|
|
|
|
$conn,
|
2016-11-24 18:00:53 +01:00
|
|
|
'SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION, ENGINE
|
2014-09-18 17:22:18 +02:00
|
|
|
FROM INFORMATION_SCHEMA.TABLES
|
|
|
|
WHERE TABLE_SCHEMA IN (%Ls)',
|
|
|
|
$databases);
|
|
|
|
|
|
|
|
$database_info = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
|
|
|
|
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
|
|
WHERE SCHEMA_NAME IN (%Ls)',
|
|
|
|
$databases);
|
|
|
|
$database_info = ipull($database_info, null, 'SCHEMA_NAME');
|
|
|
|
|
2016-01-21 21:59:19 +01:00
|
|
|
// Find databases which exist, but which the user does not have permission
|
|
|
|
// to see.
|
|
|
|
$invisible_databases = array();
|
|
|
|
foreach ($databases as $database_name) {
|
|
|
|
if (isset($database_info[$database_name])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
queryfx($conn, 'SHOW TABLES IN %T', $database_name);
|
|
|
|
} catch (AphrontAccessDeniedQueryException $ex) {
|
|
|
|
// This database exists, the user just doesn't have permission to
|
|
|
|
// see it.
|
|
|
|
$invisible_databases[] = $database_name;
|
|
|
|
} catch (AphrontSchemaQueryException $ex) {
|
|
|
|
// This database is legitimately missing.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$sql = array();
|
|
|
|
foreach ($tables as $table) {
|
|
|
|
$sql[] = qsprintf(
|
|
|
|
$conn,
|
|
|
|
'(TABLE_SCHEMA = %s AND TABLE_NAME = %s)',
|
|
|
|
$table['TABLE_SCHEMA'],
|
|
|
|
$table['TABLE_NAME']);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($sql) {
|
|
|
|
$column_info = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME,
|
2014-10-01 17:24:51 +02:00
|
|
|
COLLATION_NAME, COLUMN_TYPE, IS_NULLABLE, EXTRA
|
2014-09-18 17:32:21 +02:00
|
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
2018-11-13 18:32:52 +01:00
|
|
|
WHERE %LO',
|
|
|
|
$sql);
|
2014-09-18 17:32:21 +02:00
|
|
|
$column_info = igroup($column_info, 'TABLE_SCHEMA');
|
|
|
|
} else {
|
|
|
|
$column_info = array();
|
|
|
|
}
|
|
|
|
|
2014-09-19 20:46:30 +02:00
|
|
|
// NOTE: Tables like KEY_COLUMN_USAGE and TABLE_CONSTRAINTS only contain
|
|
|
|
// primary, unique, and foreign keys, so we can't use them here. We pull
|
|
|
|
// indexes later on using SHOW INDEXES.
|
2014-09-18 17:32:21 +02:00
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
$server_schema = id(new PhabricatorConfigServerSchema())
|
|
|
|
->setRef($ref);
|
2014-09-18 17:22:18 +02:00
|
|
|
|
|
|
|
$tables = igroup($tables, 'TABLE_SCHEMA');
|
|
|
|
foreach ($tables as $database_name => $database_tables) {
|
|
|
|
$info = $database_info[$database_name];
|
|
|
|
|
|
|
|
$database_schema = id(new PhabricatorConfigDatabaseSchema())
|
|
|
|
->setName($database_name)
|
|
|
|
->setCharacterSet($info['DEFAULT_CHARACTER_SET_NAME'])
|
|
|
|
->setCollation($info['DEFAULT_COLLATION_NAME']);
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$database_column_info = idx($column_info, $database_name, array());
|
|
|
|
$database_column_info = igroup($database_column_info, 'TABLE_NAME');
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
foreach ($database_tables as $table) {
|
|
|
|
$table_name = $table['TABLE_NAME'];
|
|
|
|
|
|
|
|
$table_schema = id(new PhabricatorConfigTableSchema())
|
|
|
|
->setName($table_name)
|
2016-11-24 18:00:53 +01:00
|
|
|
->setCollation($table['TABLE_COLLATION'])
|
|
|
|
->setEngine($table['ENGINE']);
|
2014-09-18 17:22:18 +02:00
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$columns = idx($database_column_info, $table_name, array());
|
2014-09-18 17:22:18 +02:00
|
|
|
foreach ($columns as $column) {
|
2014-10-01 17:24:51 +02:00
|
|
|
if (strpos($column['EXTRA'], 'auto_increment') === false) {
|
|
|
|
$auto_increment = false;
|
|
|
|
} else {
|
|
|
|
$auto_increment = true;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
$column_schema = id(new PhabricatorConfigColumnSchema())
|
|
|
|
->setName($column['COLUMN_NAME'])
|
|
|
|
->setCharacterSet($column['CHARACTER_SET_NAME'])
|
|
|
|
->setCollation($column['COLLATION_NAME'])
|
2014-09-18 17:32:21 +02:00
|
|
|
->setColumnType($column['COLUMN_TYPE'])
|
2014-10-01 17:24:51 +02:00
|
|
|
->setNullable($column['IS_NULLABLE'] == 'YES')
|
|
|
|
->setAutoIncrement($auto_increment);
|
2014-09-18 17:22:18 +02:00
|
|
|
|
|
|
|
$table_schema->addColumn($column_schema);
|
|
|
|
}
|
|
|
|
|
2014-09-19 20:46:30 +02:00
|
|
|
$key_parts = queryfx_all(
|
|
|
|
$conn,
|
|
|
|
'SHOW INDEXES FROM %T.%T',
|
|
|
|
$database_name,
|
|
|
|
$table_name);
|
|
|
|
$keys = igroup($key_parts, 'Key_name');
|
2014-09-18 17:32:21 +02:00
|
|
|
foreach ($keys as $key_name => $key_pieces) {
|
2014-09-19 20:46:30 +02:00
|
|
|
$key_pieces = isort($key_pieces, 'Seq_in_index');
|
|
|
|
$head = head($key_pieces);
|
2014-09-19 20:46:44 +02:00
|
|
|
|
|
|
|
// This handles string indexes which index only a prefix of a field.
|
|
|
|
$column_names = array();
|
|
|
|
foreach ($key_pieces as $piece) {
|
|
|
|
$name = $piece['Column_name'];
|
|
|
|
if ($piece['Sub_part']) {
|
|
|
|
$name = $name.'('.$piece['Sub_part'].')';
|
|
|
|
}
|
|
|
|
$column_names[] = $name;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:32:21 +02:00
|
|
|
$key_schema = id(new PhabricatorConfigKeySchema())
|
|
|
|
->setName($key_name)
|
2014-09-19 20:46:44 +02:00
|
|
|
->setColumnNames($column_names)
|
Fix almost all remaining schemata issues
Summary:
Ref T1191. This fixes nearly every remaining blocker for utf8mb4 -- primarily, overlong keys.
Remaining issue is https://secure.phabricator.com/T1191#77467
Test Plan: I'll annotate inline.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley, hach-que
Maniphest Tasks: T6099, T6129, T6133, T6134, T6150, T6148, T6147, T6146, T6105, T1191
Differential Revision: https://secure.phabricator.com/D10601
2014-10-01 17:18:36 +02:00
|
|
|
->setUnique(!$head['Non_unique'])
|
|
|
|
->setIndexType($head['Index_type']);
|
2014-09-18 17:32:21 +02:00
|
|
|
|
|
|
|
$table_schema->addKey($key_schema);
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
$database_schema->addTable($table_schema);
|
|
|
|
}
|
|
|
|
|
|
|
|
$server_schema->addDatabase($database_schema);
|
|
|
|
}
|
|
|
|
|
2016-01-21 21:59:19 +01:00
|
|
|
foreach ($invisible_databases as $database_name) {
|
|
|
|
$server_schema->addDatabase(
|
|
|
|
id(new PhabricatorConfigDatabaseSchema())
|
|
|
|
->setName($database_name)
|
|
|
|
->setAccessDenied(true));
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
return $server_schema;
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
public function loadExpectedSchemata() {
|
2016-11-12 20:39:45 +01:00
|
|
|
$refs = $this->getRefs();
|
2016-11-12 18:22:10 +01:00
|
|
|
|
|
|
|
$schemata = array();
|
|
|
|
foreach ($refs as $ref) {
|
|
|
|
$schema = $this->loadExpectedSchemaForServer($ref);
|
|
|
|
$schemata[$schema->getRef()->getRefKey()] = $schema;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $schemata;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadExpectedSchemaForServer(PhabricatorDatabaseRef $ref) {
|
|
|
|
$databases = $this->getDatabaseNames($ref);
|
|
|
|
$info = $this->getAPI($ref)->getCharsetInfo();
|
2014-09-18 17:22:18 +02:00
|
|
|
|
2015-08-13 23:49:00 +02:00
|
|
|
$specs = id(new PhutilClassMapQuery())
|
2014-09-18 17:22:54 +02:00
|
|
|
->setAncestorClass('PhabricatorConfigSchemaSpec')
|
2015-08-13 23:49:00 +02:00
|
|
|
->execute();
|
2014-09-18 17:22:54 +02:00
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
$server_schema = id(new PhabricatorConfigServerSchema())
|
|
|
|
->setRef($ref);
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
foreach ($specs as $spec) {
|
2014-09-18 17:25:34 +02:00
|
|
|
$spec
|
2014-10-29 23:49:29 +01:00
|
|
|
->setUTF8Charset(
|
|
|
|
$info[PhabricatorStorageManagementAPI::CHARSET_DEFAULT])
|
|
|
|
->setUTF8BinaryCollation(
|
|
|
|
$info[PhabricatorStorageManagementAPI::COLLATE_TEXT])
|
|
|
|
->setUTF8SortingCollation(
|
|
|
|
$info[PhabricatorStorageManagementAPI::COLLATE_SORT])
|
2014-09-18 17:25:34 +02:00
|
|
|
->setServer($server_schema)
|
|
|
|
->buildSchemata($server_schema);
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $server_schema;
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:10 +01:00
|
|
|
public function buildComparisonSchemata(
|
|
|
|
array $expect_servers,
|
|
|
|
array $actual_servers) {
|
|
|
|
|
|
|
|
$schemata = array();
|
|
|
|
foreach ($actual_servers as $key => $actual_server) {
|
|
|
|
$schemata[$key] = $this->buildComparisonSchemaForServer(
|
|
|
|
$expect_servers[$key],
|
|
|
|
$actual_server);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $schemata;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function buildComparisonSchemaForServer(
|
2014-09-18 17:22:54 +02:00
|
|
|
PhabricatorConfigServerSchema $expect,
|
|
|
|
PhabricatorConfigServerSchema $actual) {
|
|
|
|
|
|
|
|
$comp_server = $actual->newEmptyClone();
|
|
|
|
|
|
|
|
$all_databases = $actual->getDatabases() + $expect->getDatabases();
|
|
|
|
foreach ($all_databases as $database_name => $database_template) {
|
|
|
|
$actual_database = $actual->getDatabase($database_name);
|
|
|
|
$expect_database = $expect->getDatabase($database_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_database, $actual_database);
|
|
|
|
|
|
|
|
$comp_database = $database_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
if (!$actual_database) {
|
|
|
|
$actual_database = $expect_database->newEmptyClone();
|
|
|
|
}
|
2016-01-21 21:59:19 +01:00
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
if (!$expect_database) {
|
|
|
|
$expect_database = $actual_database->newEmptyClone();
|
|
|
|
}
|
|
|
|
|
|
|
|
$all_tables =
|
|
|
|
$actual_database->getTables() +
|
|
|
|
$expect_database->getTables();
|
|
|
|
foreach ($all_tables as $table_name => $table_template) {
|
|
|
|
$actual_table = $actual_database->getTable($table_name);
|
|
|
|
$expect_table = $expect_database->getTable($table_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_table, $actual_table);
|
|
|
|
|
|
|
|
$comp_table = $table_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
if (!$actual_table) {
|
|
|
|
$actual_table = $expect_table->newEmptyClone();
|
|
|
|
}
|
|
|
|
if (!$expect_table) {
|
|
|
|
$expect_table = $actual_table->newEmptyClone();
|
|
|
|
}
|
|
|
|
|
|
|
|
$all_columns =
|
|
|
|
$actual_table->getColumns() +
|
|
|
|
$expect_table->getColumns();
|
|
|
|
foreach ($all_columns as $column_name => $column_template) {
|
|
|
|
$actual_column = $actual_table->getColumn($column_name);
|
|
|
|
$expect_column = $expect_table->getColumn($column_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_column, $actual_column);
|
|
|
|
|
|
|
|
$comp_column = $column_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
$comp_table->addColumn($comp_column);
|
|
|
|
}
|
2014-09-18 17:32:21 +02:00
|
|
|
|
|
|
|
$all_keys =
|
|
|
|
$actual_table->getKeys() +
|
|
|
|
$expect_table->getKeys();
|
|
|
|
foreach ($all_keys as $key_name => $key_template) {
|
|
|
|
$actual_key = $actual_table->getKey($key_name);
|
|
|
|
$expect_key = $expect_table->getKey($key_name);
|
|
|
|
|
|
|
|
$issues = $this->compareSchemata($expect_key, $actual_key);
|
|
|
|
|
|
|
|
$comp_key = $key_template->newEmptyClone()
|
|
|
|
->setIssues($issues);
|
|
|
|
|
|
|
|
$comp_table->addKey($comp_key);
|
|
|
|
}
|
|
|
|
|
2017-10-04 19:51:29 +02:00
|
|
|
$comp_table->setPersistenceType($expect_table->getPersistenceType());
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
$comp_database->addTable($comp_table);
|
|
|
|
}
|
|
|
|
$comp_server->addDatabase($comp_database);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $comp_server;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function compareSchemata(
|
|
|
|
PhabricatorConfigStorageSchema $expect = null,
|
|
|
|
PhabricatorConfigStorageSchema $actual = null) {
|
|
|
|
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$expect_is_key = ($expect instanceof PhabricatorConfigKeySchema);
|
|
|
|
$actual_is_key = ($actual instanceof PhabricatorConfigKeySchema);
|
|
|
|
|
|
|
|
if ($expect_is_key || $actual_is_key) {
|
|
|
|
$missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY;
|
|
|
|
$surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY;
|
|
|
|
} else {
|
|
|
|
$missing_issue = PhabricatorConfigStorageSchema::ISSUE_MISSING;
|
|
|
|
$surplus_issue = PhabricatorConfigStorageSchema::ISSUE_SURPLUS;
|
|
|
|
}
|
|
|
|
|
2014-09-18 17:22:54 +02:00
|
|
|
if (!$expect && !$actual) {
|
|
|
|
throw new Exception(pht('Can not compare two missing schemata!'));
|
|
|
|
} else if ($expect && !$actual) {
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$issues = array($missing_issue);
|
2014-09-18 17:22:54 +02:00
|
|
|
} else if ($actual && !$expect) {
|
Generate expected schemata for User/People tables
Summary:
Ref T1191. Some notes here:
- Drops the old LDAP and OAuth info tables. These were migrated to the ExternalAccount table a very long time ago.
- Separates surplus/missing keys from other types of surplus/missing things. In the long run, my plan is to have only two notice levels:
- Error: something we can't fix (missing database, table, or column; overlong key).
- Warning: something we can fix (surplus anything, missing key, bad column type, bad key columns, bad uniqueness, bad collation or charset).
- For now, retaining three levels is helpful in generating all the expected scheamta.
Test Plan:
- Saw ~200 issues resolve, leaving ~1,300.
- Grepped for removed tables.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T1191
Differential Revision: https://secure.phabricator.com/D10580
2014-10-01 16:36:47 +02:00
|
|
|
$issues = array($surplus_issue);
|
2014-09-18 17:22:54 +02:00
|
|
|
} else {
|
|
|
|
$issues = $actual->compareTo($expect);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $issues;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-09-18 17:22:18 +02:00
|
|
|
}
|