diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 48ee728..cd3b334 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -8,7 +8,6 @@ use \InvalidArgumentException; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\EntityFieldQuery; -use Drupal\Core\Entity\EntityMalformedException; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityInterface; diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 77ffa8c..dc2c025 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -32,8 +32,9 @@ public function __construct(array $values, $entity_type) { parent::__construct($values, $entity_type); // Backup the original ID, if any. - if ($original_id = $this->id()) { - $this->originalID = $original_id; + $original_id = $this->id(); + if ($original_id !== NULL && $original_id !== '') { + $this->setOriginalID($original_id); } } @@ -45,23 +46,21 @@ public function getOriginalID() { } /** - * Overrides Entity::isNew(). - * - * EntityInterface::enforceIsNew() is not supported by configuration entities, - * since each configuration entity is unique. + * Implements ConfigEntityInterface::setOriginalID(). */ - final public function isNew() { - return !$this->id(); + public function setOriginalID($id) { + $this->originalID = $id; } /** - * Overrides Entity::bundle(). + * Overrides Entity::isNew(). * - * EntityInterface::bundle() is not supported by configuration entities, since - * a configuration entity is a bundle. + * EntityInterface::enforceIsNew() is only supported for newly created + * configuration entities but has no effect after saving, since each + * configuration entity is unique. */ - final public function bundle() { - return $this->entityType; + final public function isNew() { + return !empty($this->enforceIsNew) || $this->id() === NULL || $this->id() === ''; } /** diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php index a8b78e1..15ef4dd 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -22,4 +22,14 @@ */ public function getOriginalID(); + /** + * Sets the original ID. + * + * @param string $id + * The new ID to set as original ID. + * + * @return void + */ + public function setOriginalID($id); + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index 07849c6..cb93873 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -9,6 +9,7 @@ use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityMalformedException; use Drupal\Core\Entity\EntityStorageControllerInterface; /** @@ -222,6 +223,12 @@ public function create(array $values) { $class = isset($this->entityInfo['entity class']) ? $this->entityInfo['entity class'] : 'Drupal\Core\Entity\Entity'; $entity = new $class($values, $this->entityType); + // Mark this entity as new, so isNew() returns TRUE. This does not check + // whether a configuration entity with the same ID (if any) already exists. + // @todo If we expect isNew() to return FALSE when given an ID that exists + // already, wrap this in !config($prefix . $id)->getStorage()->exists(). + // In that case, do we expect existing values to be merged into $values? + $entity->enforceIsNew(); // Assign a new UUID if there is none yet. if (!isset($entity->{$this->uuidKey})) { @@ -260,22 +267,29 @@ public function delete($ids) { /** * Implements Drupal\Core\Entity\EntityStorageControllerInterface::save(). + * + * @throws EntityMalformedException + * When attempting to save a configuration entity that has no ID. */ public function save(EntityInterface $entity) { $prefix = $this->entityInfo['config prefix'] . '.'; + $id = $entity->id(); + if ($id === NULL || $id === '') { + throw new EntityMalformedException('The entity does not have an ID.'); + } + // Load the stored entity, if any. - if ($entity->getOriginalID()) { + if ($entity->getOriginalID() !== NULL) { $id = $entity->getOriginalID(); } - else { - $id = $entity->id(); - } $config = config($prefix . $id); $config->setName($prefix . $entity->id()); if (!$config->isNew() && !isset($entity->original)) { - $entity->original = entity_load_unchanged($this->entityType, $id); + $this->resetCache(array($id)); + $result = $this->load(array($id)); + $entity->original = reset($result); } $this->preSave($entity); @@ -295,6 +309,9 @@ public function save(EntityInterface $entity) { $config->save(); $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); + + // Immediately update the original ID. + $entity->setOriginalID($entity->id()); } else { $return = SAVED_NEW; diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigEntityListTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigEntityListTest.php index cd48f71..0cca791 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigEntityListTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigEntityListTest.php @@ -145,7 +145,7 @@ function testListUI() { $this->assertLink('Edit'); $this->clickLink('Edit'); $this->assertResponse(200); - $this->assertTitle('Edit test configuration | Drupal'); + $this->assertTitle('Edit Antelope | Drupal'); $edit = array('label' => 'Albatross', 'id' => 'albatross'); $this->drupalPost(NULL, $edit, t('Save')); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigEntityTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigEntityTest.php index 532979c..bb98ac0 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigEntityTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigEntityTest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Entity\EntityMalformedException; use Drupal\simpletest\WebTestBase; /** @@ -30,13 +31,163 @@ public static function getInfo() { } /** - * Tests basic CRUD operations through the UI. + * Tests CRUD operations. */ function testCRUD() { + // Verify default properties on a newly created empty entity. + $empty = entity_create('config_test', array()); + $this->assertIdentical($empty->id, NULL); + $this->assertTrue($empty->uuid); + $this->assertIdentical($empty->label, NULL); + $this->assertIdentical($empty->style, NULL); + $this->assertIdentical($empty->langcode, LANGUAGE_NOT_SPECIFIED); + + // Verify ConfigEntity properties/methods on the newly created empty entity. + $this->assertIdentical($empty->isNew(), TRUE); + $this->assertIdentical($empty->getOriginalID(), NULL); + $this->assertIdentical($empty->bundle(), 'config_test'); + $this->assertIdentical($empty->id(), NULL); + $this->assertTrue($empty->uuid()); + $this->assertIdentical($empty->label(), NULL); + + $this->assertIdentical($empty->get('id'), NULL); + $this->assertTrue($empty->get('uuid')); + $this->assertIdentical($empty->get('label'), NULL); + $this->assertIdentical($empty->get('style'), NULL); + $this->assertIdentical($empty->get('langcode'), LANGUAGE_NOT_SPECIFIED); + + // Verify Entity properties/methods on the newly created empty entity. + $this->assertIdentical($empty->isNewRevision(), FALSE); + $this->assertIdentical($empty->entityType(), 'config_test'); + $uri = $empty->uri(); + $this->assertIdentical($uri['path'], 'admin/structure/config_test/manage/'); + $this->assertIdentical($empty->isDefaultRevision(), TRUE); + + // Verify that an empty entity cannot be saved. + try { + $empty->save(); + $this->fail('EntityMalformedException was thrown.'); + } + catch (EntityMalformedException $e) { + $this->pass('EntityMalformedException was thrown.'); + } + + // Verify that an entity with an empty ID string is considered empty, too. + $empty_id = entity_create('config_test', array( + 'id' => '', + )); + $this->assertIdentical($empty_id->isNew(), TRUE); + try { + $empty_id->save(); + $this->fail('EntityMalformedException was thrown.'); + } + catch (EntityMalformedException $e) { + $this->pass('EntityMalformedException was thrown.'); + } + + // Verify properties on a newly created entity. + $config_test = entity_create('config_test', $expected = array( + 'id' => $this->randomName(), + 'label' => $this->randomString(), + 'style' => $this->randomName(), + )); + $this->assertIdentical($config_test->id, $expected['id']); + $this->assertTrue($config_test->uuid); + $this->assertNotEqual($config_test->uuid, $empty->uuid); + $this->assertIdentical($config_test->label, $expected['label']); + $this->assertIdentical($config_test->style, $expected['style']); + $this->assertIdentical($config_test->langcode, LANGUAGE_NOT_SPECIFIED); + + // Verify methods on the newly created entity. + $this->assertIdentical($config_test->isNew(), TRUE); + $this->assertIdentical($config_test->getOriginalID(), $expected['id']); + $this->assertIdentical($config_test->id(), $expected['id']); + $this->assertTrue($config_test->uuid()); + $expected['uuid'] = $config_test->uuid(); + $this->assertIdentical($config_test->label(), $expected['label']); + + $this->assertIdentical($config_test->isNewRevision(), FALSE); + $uri = $config_test->uri(); + $this->assertIdentical($uri['path'], 'admin/structure/config_test/manage/' . $expected['id']); + $this->assertIdentical($config_test->isDefaultRevision(), TRUE); + + // Verify that the entity can be saved. + try { + $status = $config_test->save(); + $this->pass('EntityMalformedException was not thrown.'); + } + catch (EntityMalformedException $e) { + $this->fail('EntityMalformedException was not thrown.'); + } + + // Verify that the correct status is returned and properties did not change. + $this->assertIdentical($status, SAVED_NEW); + $this->assertIdentical($config_test->id(), $expected['id']); + $this->assertIdentical($config_test->uuid(), $expected['uuid']); + $this->assertIdentical($config_test->label(), $expected['label']); + $this->assertIdentical($config_test->isNew(), FALSE); + $this->assertIdentical($config_test->getOriginalID(), $expected['id']); + + // Save again, and verify correct status and properties again. + $status = $config_test->save(); + $this->assertIdentical($status, SAVED_UPDATED); + $this->assertIdentical($config_test->id(), $expected['id']); + $this->assertIdentical($config_test->uuid(), $expected['uuid']); + $this->assertIdentical($config_test->label(), $expected['label']); + $this->assertIdentical($config_test->isNew(), FALSE); + $this->assertIdentical($config_test->getOriginalID(), $expected['id']); + + // Re-create the entity with the same ID and verify updated status. + $same_id = entity_create('config_test', array( + 'id' => $config_test->id(), + )); + $this->assertIdentical($same_id->isNew(), TRUE); + $status = $same_id->save(); + $this->assertIdentical($status, SAVED_UPDATED); + + // Verify that the entity was overwritten. + $same_id = entity_load('config_test', $config_test->id()); + $this->assertIdentical($same_id->id(), $config_test->id()); + // Note: Reloading loads from FileStorage, and FileStorage enforces strings. + $this->assertIdentical($same_id->label(), ''); + $this->assertNotEqual($same_id->uuid(), $config_test->uuid()); + + // Revert to previous state. + $config_test->save(); + + // Verify that renaming the ID returns correct status and properties. + $ids = array($expected['id'], 'second_' . $this->randomName(4), 'third_' . $this->randomName(4)); + for ($i = 1; $i < 3; $i++) { + $old_id = $ids[$i - 1]; + $new_id = $ids[$i]; + // Before renaming, everything should point to the current ID. + $this->assertIdentical($config_test->id(), $old_id); + $this->assertIdentical($config_test->getOriginalID(), $old_id); + + // Rename. + $config_test->id = $new_id; + $this->assertIdentical($config_test->id(), $new_id); + $status = $config_test->save(); + $this->assertIdentical($status, SAVED_UPDATED); + $this->assertIdentical($config_test->isNew(), FALSE); + + // Verify that originalID points to new ID directly after renaming. + $this->assertIdentical($config_test->id(), $new_id); + $this->assertIdentical($config_test->getOriginalID(), $new_id); + } + } + + /** + * Tests CRUD operations through the UI. + */ + function testCRUDUI() { $id = strtolower($this->randomName()); $label1 = $this->randomName(); $label2 = $this->randomName(); $label3 = $this->randomName(); + $message_insert = format_string('%label configuration has been created.', array('%label' => $label1)); + $message_update = format_string('%label configuration has been updated.', array('%label' => $label2)); + $message_delete = format_string('%label configuration has been deleted.', array('%label' => $label2)); // Create a configuration entity. $edit = array( @@ -44,27 +195,36 @@ function testCRUD() { 'label' => $label1, ); $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertUrl('admin/structure/config_test'); $this->assertResponse(200); - $message_insert = format_string('%label configuration has been created.', array('%label' => $label1)); $this->assertRaw($message_insert); + $this->assertNoRaw($message_update); + $this->assertLinkByHref("admin/structure/config_test/manage/$id/edit"); // Update the configuration entity. - $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); $edit = array( 'label' => $label2, ); - $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->drupalPost("admin/structure/config_test/manage/$id", $edit, 'Save'); + $this->assertUrl('admin/structure/config_test'); $this->assertResponse(200); - $message_update = format_string('%label configuration has been updated.', array('%label' => $label2)); $this->assertNoRaw($message_insert); $this->assertRaw($message_update); + $this->assertLinkByHref("admin/structure/config_test/manage/$id/edit"); + $this->assertLinkByHref("admin/structure/config_test/manage/$id/delete"); // Delete the configuration entity. - $this->assertLinkByHref('admin/structure/config_test/manage/' . $id . '/delete'); - $this->drupalPost('admin/structure/config_test/manage/' . $id . '/delete', array(), 'Delete'); + $this->drupalGet("admin/structure/config_test/manage/$id/edit"); + $this->drupalPost(NULL, array(), 'Delete'); + $this->assertUrl("admin/structure/config_test/manage/$id/delete"); + $this->drupalPost(NULL, array(), 'Delete'); + $this->assertUrl('admin/structure/config_test'); $this->assertResponse(200); + $this->assertNoRaw($message_update); + $this->assertRaw($message_delete); $this->assertNoText($label1); - $this->assertNoText($label2); + $this->assertNoLinkByHref("admin/structure/config_test/manage/$id"); + $this->assertNoLinkByHref("admin/structure/config_test/manage/$id/edit"); // Re-create a configuration entity. $edit = array( @@ -72,19 +232,26 @@ function testCRUD() { 'label' => $label1, ); $this->drupalPost('admin/structure/config_test/add', $edit, 'Save'); + $this->assertUrl('admin/structure/config_test'); $this->assertResponse(200); $this->assertText($label1); + $this->assertLinkByHref("admin/structure/config_test/manage/$id/edit"); // Rename the configuration entity's ID/machine name. - $this->assertLinkByHref('admin/structure/config_test/manage/' . $id); $edit = array( 'id' => strtolower($this->randomName()), 'label' => $label3, ); - $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save'); + $this->drupalPost("admin/structure/config_test/manage/$id", $edit, 'Save'); + $this->assertUrl('admin/structure/config_test'); $this->assertResponse(200); $this->assertNoText($label1); + $this->assertNoText($label2); $this->assertText($label3); + $this->assertNoLinkByHref("admin/structure/config_test/manage/$id"); + $this->assertNoLinkByHref("admin/structure/config_test/manage/$id/edit"); + $id = $edit['id']; + $this->assertLinkByHref("admin/structure/config_test/manage/$id/edit"); } } diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module index 44df4da..d73f64a 100644 --- a/core/modules/config/tests/config_test/config_test.module +++ b/core/modules/config/tests/config_test/config_test.module @@ -35,9 +35,7 @@ function config_test_config_import_change($name, $new_config, $old_config) { $GLOBALS['hook_config_import'] = __FUNCTION__; // @todo Make this less ugly. - list($entity_type) = explode('.', $name); - $entity_info = entity_get_info($entity_type); - $id = substr($name, strlen($entity_info['config prefix']) + 1); + list(, , $id) = explode('.', $name); $config_test = entity_load('config_test', $id); // Store the original config, and iterate through each property to store it. @@ -67,10 +65,9 @@ function config_test_config_import_delete($name, $new_config, $old_config) { $GLOBALS['hook_config_import'] = __FUNCTION__; // @todo Make this less ugly. - list($entity_type) = explode('.', $name); - $entity_info = entity_get_info($entity_type); - $id = substr($name, strlen($entity_info['config prefix']) + 1); - config_test_delete($id); + list(, , $id) = explode('.', $name); + $config_test = entity_load('config_test', $id); + $config_test->delete(); return TRUE; } @@ -83,6 +80,9 @@ function config_test_entity_info() { 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController', 'entity class' => 'Drupal\config_test\ConfigTest', 'list controller class' => 'Drupal\Core\Config\Entity\ConfigEntityListController', + 'form controller class' => array( + 'default' => 'Drupal\config_test\ConfigTestFormController', + ), 'uri callback' => 'config_test_uri', 'config prefix' => 'config_test.dynamic', 'entity keys' => array( @@ -117,15 +117,14 @@ function config_test_menu() { ); $items['admin/structure/config_test/add'] = array( 'title' => 'Add test configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('config_test_form'), + 'page callback' => 'config_test_add_page', 'access callback' => TRUE, 'type' => MENU_LOCAL_ACTION, ); $items['admin/structure/config_test/manage/%config_test'] = array( 'title' => 'Edit test configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('config_test_form', 4), + 'page callback' => 'config_test_edit_page', + 'page arguments' => array(4), 'access callback' => TRUE, ); $items['admin/structure/config_test/manage/%config_test/edit'] = array( @@ -154,26 +153,6 @@ function config_test_load($id) { } /** - * Saves a ConfigTest object. - * - * @param Drupal\config_test\ConfigTest $config_test - * The ConfigTest object to save. - */ -function config_test_save(ConfigTest $config_test) { - return $config_test->save(); -} - -/** - * Deletes a ConfigTest object. - * - * @param string $id - * The ID of the ConfigTest object to delete. - */ -function config_test_delete($id) { - entity_delete_multiple('config_test', array($id)); -} - -/** * Page callback; Lists available ConfigTest objects. */ function config_test_list_page() { @@ -182,79 +161,28 @@ function config_test_list_page() { } /** - * Form constructor to add or edit a ConfigTest object. + * Page callback: Presents the ConfigTest creation form. * - * @param Drupal\config_test\ConfigTest $config_test - * (optional) An existing ConfigTest object to edit. If omitted, the form - * creates a new ConfigTest. + * @return array + * A form array as expected by drupal_render(). */ -function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) { - // Standard procedure for handling the entity argument in entity forms, taking - // potential form caching and rebuilds properly into account. - // @see http://drupal.org/node/1499596 - if (!isset($form_state['config_test'])) { - if (!isset($config_test)) { - $config_test = entity_create('config_test', array()); - } - $form_state['config_test'] = $config_test; - } - else { - $config_test = $form_state['config_test']; - } - - $form['label'] = array( - '#type' => 'textfield', - '#title' => 'Label', - '#default_value' => $config_test->label(), - '#required' => TRUE, - ); - $form['id'] = array( - '#type' => 'machine_name', - '#default_value' => $config_test->id(), - '#required' => TRUE, - '#machine_name' => array( - 'exists' => 'config_test_load', - // @todo Update form_process_machine_name() to use 'label' by default. - 'source' => array('label'), - ), - ); - $form['style'] = array( - '#type' => 'select', - '#title' => 'Image style', - '#options' => array(), - '#default_value' => $config_test->get('style'), - '#access' => FALSE, - ); - if (module_exists('image')) { - $form['style']['#access'] = TRUE; - $form['style']['#options'] = image_style_options(); - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Save'); - - return $form; +function config_test_add_page() { + $entity = entity_create('config_test', array()); + return entity_get_form($entity); } /** - * Form submission handler for config_test_form(). + * Page callback: Presents the ConfigTest edit form. + * + * @param Drupal\config_test\ConfigTest $config_test + * The ConfigTest object to edit. + * + * @return array + * A form array as expected by drupal_render(). */ -function config_test_form_submit($form, &$form_state) { - form_state_values_clean($form_state); - - $config_test = $form_state['config_test']; - entity_form_submit_build_entity('config_test', $config_test, $form, $form_state); - - $status = $config_test->save(); - - if ($status == SAVED_UPDATED) { - drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $config_test->label()))); - } - else { - drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->label()))); - } - - $form_state['redirect'] = 'admin/structure/config_test'; +function config_test_edit_page(ConfigTest $config_test) { + drupal_set_title(format_string('Edit %label', array('%label' => $config_test->label())), PASS_THROUGH); + return entity_get_form($config_test); } /** @@ -280,5 +208,6 @@ function config_test_delete_form($form, &$form_state, ConfigTest $config_test) { */ function config_test_delete_form_submit($form, &$form_state) { $form_state['config_test']->delete(); + drupal_set_message(format_string('%label configuration has been deleted.', array('%label' => $form_state['config_test']->label()))); $form_state['redirect'] = 'admin/structure/config_test'; } diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestFormController.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestFormController.php new file mode 100644 index 0000000..ee66bbc --- /dev/null +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestFormController.php @@ -0,0 +1,90 @@ + 'textfield', + '#title' => 'Label', + '#default_value' => $entity->label(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $entity->id(), + '#required' => TRUE, + '#machine_name' => array( + 'exists' => 'config_test_load', + // @todo Update form_process_machine_name() to use 'label' by default. + 'source' => array('label'), + ), + ); + $form['style'] = array( + '#type' => 'select', + '#title' => 'Image style', + '#options' => array(), + '#default_value' => $entity->get('style'), + '#access' => FALSE, + ); + if (module_exists('image')) { + $form['style']['#access'] = TRUE; + $form['style']['#options'] = image_style_options(); + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + $form['actions']['delete'] = array( + '#type' => 'submit', + '#value' => 'Delete', + ); + + return $form; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $entity = $this->getEntity($form_state); + $status = $entity->save(); + + if ($status === SAVED_UPDATED) { + drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $entity->label()))); + } + else { + drupal_set_message(format_string('%label configuration has been created.', array('%label' => $entity->label()))); + } + + $form_state['redirect'] = 'admin/structure/config_test'; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::delete(). + */ + public function delete(array $form, array &$form_state) { + $entity = $this->getEntity($form_state); + $form_state['redirect'] = 'admin/structure/config_test/manage/' . $entity->id() . '/delete'; + } + +}