'Error', E_WARNING => 'Warning', E_PARSE => 'Parse Error', E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', E_CORE_WARNING => 'Core Warning', E_COMPILE_ERROR => 'Compile Error', E_COMPILE_WARNING => 'Compile Warning', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_STRICT => 'Strict Notice', E_RECOVERABLE_ERROR => 'Recoverable Error' ]; public function __construct( ApiProvider $provider, $outputDir, $template, $baseUrl, array $quickLinks, array $sources, $issueLoggingEnabled = false ) { $this->apiProvider = $provider; $this->outputDir = $outputDir; $this->template = $template; $this->baseUrl = $baseUrl; $this->quickLinks = $quickLinks; $this->sources = $sources; $this->issueLoggingEnabled = $issueLoggingEnabled; } public function build() { $this->skipMembers = new \SplObjectStorage(); fwrite(STDOUT, "Parsing available service API versions...\n"); // Collect versions $services = []; $aliases = []; foreach ($this->gatherServiceVersions() as $name => $data) { // Skip "latest" unset($data['versions']['latest']); $ns = $data['namespace']; foreach ($data['versions'] as $alias => $version) { list($api, $docModel) = call_user_func( "Aws\\{$ns}\\{$ns}Client::applyDocFilters", ApiProvider::resolve($this->apiProvider, 'api', $name, $version), ApiProvider::resolve($this->apiProvider, 'docs', $name, $version) ); $service = new Service($api, $docModel); $title = isset($service->shortTitle) && $service->shortTitle !== '' ? $service->shortTitle : $service->title; if (isset($services[$title][$version])) { if (empty($aliases[$title][$version])) { $aliases[$title][$version] = []; } $aliases[$title][$version] []= $alias; continue; } $examples = $this->loadExamples($name, $version); if (method_exists("Aws\\{$ns}\\{$ns}Client", 'addDocExamples')) { $examples = call_user_func( "Aws\\{$ns}\\{$ns}Client::addDocExamples", $examples ); } $this->renderService($service, $examples); $services[$title][$version] = $service; } } uasort($services, function($a, $b) { $serviceA = current($a); $serviceB = current($b); return strcasecmp($serviceA->namespace, $serviceB->namespace); }); $this->updateHomepage($services); $this->updateClients($services); $this->updateExceptions($services); $this->updateAliases($services, $aliases); $this->updateSitemap(); $this->updateSearch($services); if ($this->issueLoggingEnabled) { $this->updateIssues(); } } private function updateHomepage(array $services) { $this->updateServiceTable($services); $this->updateQuickLinks($services); } private function updateServiceTable(array $services) { fwrite(STDOUT, "Building homepage service table\n"); // Build up the list of services for the homepage. $servicesTable = ''; foreach ($services as $versions) { krsort($versions); $service = reset($versions); $servicesTable .= "serviceLink}\">{$service->title}"; $servicesTable .= "clientLink}\">{$service->client}"; $servicesTable .= ""; } $this->replaceInner('index', $servicesTable, ':services:'); } private function updateQuickLinks(array $services) { fwrite(STDOUT, "Updating homepage quick links\n"); // Determine which services in the provided array should have a quick link $services = array_filter($services, function (array $versions) { return 0 < count(array_filter($versions, function (Service $service) { return in_array($service->name, $this->quickLinks); })); }); // Drop all but the latest version of each service from the array $services = array_map(function (array $versions) { return array_shift($versions); }, $services); $serviceMap = []; foreach ($services as $service) { $isOlderVersion = isset($serviceMap[$service->name]) && $service->version < $serviceMap[$service->name]->version; if ($isOlderVersion) { continue; } $serviceMap[$service->name] = $service; } $services = array_values($serviceMap); // Sort the services in the order provided in the config usort($services, function (Service $a, Service $b) { return array_search($a->name, $this->quickLinks) - array_search($b->name, $this->quickLinks); }); // Build up the quick links for the home page $quickLinks = ''; foreach ($services as $service) { $title = $service->shortTitle ?: $service->title; $quickLinks .= << {$title} EOT; } $this->replaceInner('index', $quickLinks, ':quickLinks:'); } private function updateIssues() { $text = ''; foreach ($this->issues as $level=>$levelIssues) { foreach ($levelIssues as $serviceName => $versions) { foreach ($versions as $serviceVersion => $messages) { foreach (array_keys($messages) as $message) { if (!empty($text)) { $text .= PHP_EOL; } $levelName = isset(DocsBuilder::$ERROR_PRINT_NAMES[$level]) ? DocsBuilder::$ERROR_PRINT_NAMES[$level] : 'Unknown'; $text .= '[' . date("Y-m-d H:i:s (T)") . '] ' . '[' . $levelName . '] ' . $serviceName . '-' . $serviceVersion . ': ' . $message; } } } } return (bool) file_put_contents("{$this->outputDir}/build-issues.log", $text); } private function renderService(Service $service, $examples) { $html = new HtmlDocument; $html->open('div', 'page-header'); $html->elem('h1', null, "$service->title {$service->version}"); $html->close(); $metadata = <<Client: {$service->client}
Service ID: {$service->name}
Version: {$service->version}
EOT; $html->elem('dl', 'tree well', $metadata); $desc = <<fullTitle}, and shows how to use the {$service->client} object to call the described operations. This documentation is specific to the {$service->version} API version of the service. EOT; $html->elem('p', null, $desc); $html->elem('h2', null, 'Operation Summary'); $desc = <<\$client->getCommand('CommandName'), where "CommandName" is the name of one of the following operations. Note: a command is a value that encapsulates an operation and the parameters used to create an HTTP request. EOT; $html->elem('p', null, $desc); $desc = <<\$client->commandName(/* parameters */). You can send the command asynchronously (returning a promise) by appending the word "Async" to the operation name: \$client->commandNameAsync(/* parameters */). EOT; $html->elem('p', null, $desc); $html->append($this->createHtmlForToc($service, $service->api->getOperations())); $this->createHtmlForPaginators($html, $service->api); $this->createHtmlForWaiters($html, $service->api); $html->section(2, 'Operations'); foreach ($service->api->getOperations() as $opName => $operation) { $html->append($this->createHtmlForOperation( $service, $opName, $operation, isset($examples[$opName]) ? $examples[$opName] : [] )); } $html->section(2, 'Shapes'); $map = $service->api->getShapeMap(); $keys = array_keys($service->api['shapes']); sort($keys); foreach ($keys as $name) { $shape = $map->resolve(['shape' => $name]); // Do not render top-level input or output shapes. if ($shape['type'] == 'structure' && !isset($this->skipMembers[$shape]) ) { $html->section(3, $name, 'shape', 'method-title'); $html->append($this->renderShape($service->docs, $shape)); } } $this->writeThemeFile($service->serviceLink, $html->render()); } private function createHtmlForToc(Service $service, array $operations) { $html = new HtmlDocument(); $html->open('ul', 'methods-summary'); foreach ($operations as $opName => $operation) { $item = '' . "{$opName} ( array \$params = [] )"; if ($description = $service->docs->getOperationDocs($opName)) { $shortened = strip_tags($description); $shortened = strpos($shortened, '.') === false ? $shortened : substr($shortened, 0, strpos($shortened, '.') + 1); $item .= '

' . $shortened . '

'; } $html->elem('li', null, $item); } $html->close(); return $html; } private function writeThemeFile($name, $contents) { $name = str_replace('.html', '', $name); $name .= '.html'; fwrite(STDOUT, "Writing file: {$name}.\n"); $html = str_replace('{{ contents }}', $contents, $this->template); return (bool) file_put_contents("{$this->outputDir}/{$name}", $html); } private function replaceInner($name, $replace, $search = '{{ contents }}') { $name = str_replace('.html', '', $name); fwrite(STDOUT, "Updating file: {$name}.\n"); $path = "{$this->outputDir}/{$name}.html"; $contents = file_get_contents($path); $contents = str_replace($search, $replace, $contents); file_put_contents($path, $contents); } private function gatherServiceVersions() { $manifest = __DIR__ . '/../../../src/data/manifest.json'; return json_decode(file_get_contents($manifest), true); } private function loadExamples($name, $version) { $path = __DIR__ . "/../../../src/data/{$name}/{$version}/examples-1.json"; try { return \Aws\load_compiled_json($path)['examples']; } catch (\InvalidArgumentException $e) { return []; } } private function updateClients(array $services) { fwrite(STDOUT, "Updating client pages with service links\n"); foreach ($services as $versions) { krsort($versions); $service = reset($versions); $html = '

Supported API Versions

'; $html .= <<This class uses a service description model that is associated at runtime based on the version option given when constructing the client. The version option will determine which API operations, waiters, and paginators are available for a client. Creating a command or a specific API operation can be done using magic methods (e.g., \$client->commandName(/** parameters */), or using the $\client->getCommand method of the client.

EOT; $html .= '
'; $this->replaceInner($service->clientLink, $html, ''); } } private function updateExceptions(array $services) { fwrite(STDOUT, "Updating exception pages with modeled exception data...\n"); foreach ($services as $versions) { krsort($versions); $service = reset($versions); $shapes = $service->api->getErrorShapes(); if (count($shapes) > 0) { $html = new HtmlDocument; $html->section(2, 'Expected Exception Codes'); $desc = <<elem('div', null, $desc); foreach ($shapes as $shape) { if ($shape['type'] == 'structure' && !isset($this->skipMembers[$shape]) ) { $html->section(3, $shape->getName(), 'shape', 'method-title'); // Add error syntax $outputShapes = new ShapeIterator($shape, $service->docs); $outputExample = new ExampleBuilder($shape->getName(), false); foreach ($outputShapes as $outputShape) { $outputExample->addShape($outputShape); } $html->elem('pre', null, htmlentities($outputExample->getCode())); // Add member details $html->append($this->renderShape($service->docs, $shape)); } } $this->replaceInner($service->exceptionLink, $html->render(), ''); } } } private function updateAliases(array $services, array $compatibleVersions) { fwrite(STDOUT, "Updating redirects for forward-compatible service versions\n"); foreach ($compatibleVersions as $service => $aliasedVersions) { foreach ($aliasedVersions as $version => $aliases) { $redirectPage = << EOHTML; foreach ($aliases as $alias) { $redirectFrom = str_replace($version, $alias, $services[$service][$version]->serviceLink); file_put_contents("{$this->outputDir}/$redirectFrom", $redirectPage); } } } } private function updateSearch(array $services) { fwrite(STDOUT, "Updating search index\n"); $broker = new Broker(new Broker\Backend\Memory()); foreach ($this->sources as $sourceFile) { $broker->processFile($sourceFile); } $index = array_merge( $this->getServiceAutocompleteIndex($services), $this->getClassAutocompleteIndex($broker), $this->getFunctionAutocompleteIndex($broker) ); $jsonIndex = json_encode($this->utf8Encode($index)); $js = <<outputDir}/searchIndex.js", $js); } private function utf8Encode($mixed) { if (is_array($mixed)) { return array_map([$this, 'utf8Encode'], $mixed); } elseif (is_string($mixed)) { return utf8_encode($mixed); } elseif (empty($mixed)) { return ''; } throw new \InvalidArgumentException('Expecting string or array, got ' . gettype($mixed)); } private function getServiceAutocompleteIndex(array $services) { $autoComplete = []; // Drop all but the latest version of each service from the array $services = array_map(function (array $versions) { return array_shift($versions); }, $services); // Add operations from latest version of each service to autocomplete index foreach ($services as $service) { foreach ($service->api->getOperations() as $operation => $def) { $autoComplete []= [ 'name' => $service->namespace . '::' . lcfirst($operation), 'match' => $operation, 'link' => $service->serviceLink . '#' . strtolower($operation), 'description' => strip_tags($service->docs->getOperationDocs($operation)), ]; } } return $autoComplete; } private function getClassAutocompleteIndex(Broker $broker) { $methodsToSkip = $this->getMethodsToSkip(); $autoComplete = []; $classes = array_filter( array_values($broker->getClasses()), [$this, 'filterVisible'] ); foreach ($classes as $class) { // Add class to autocomplete index $autoComplete []= [ 'name' => $class->getName(), 'match' => $class->getShortName(), 'link' => 'class-' . str_replace('\\', '.', $class->getName()) . '.html', 'description' => $this->shiftDocCommentLine($class->getDocComment()), ]; $methods = array_filter( $class->getOwnMethods(\ReflectionMethod::IS_PUBLIC|\ReflectionMethod::IS_PROTECTED), [$this, 'filterVisible'] ); // Skip over methods implementing base interfaces or that start with // an underscore $methods = array_filter($methods, function (ReflectionMethod $method) use ($methodsToSkip) { $name = $method->getName(); return !in_array($name, $methodsToSkip) && $name{0} !== '_'; }); foreach ($methods as $method) { $autoComplete []= [ 'name' => $class->getName() . '::' . $method->getName(), 'match' => $method->getName(), 'link' => 'class-' . str_replace('\\', '.', $class->getName()) . '.html' . '#_' . $method->getName(), 'description' => '', ]; } } return $autoComplete; } private function getMethodsToSkip() { $interfacesToSkip = [ '\\ArrayAccess', '\\Countable', '\\Iterator', '\\IteratorAggregate', '\\JsonSerializable', '\\Serializable', ]; $methodsToSkip = []; foreach ($interfacesToSkip as $interfaceToSkip) { $methodsToSkip = array_merge( $methodsToSkip, array_map(function (\ReflectionMethod $method) { return $method->getName(); }, (new \ReflectionClass($interfaceToSkip))->getMethods()) ); } return $methodsToSkip; } private function getFunctionAutocompleteIndex(Broker $broker) { $functions = array_filter( array_values($broker->getFunctions()), [$this, 'filterVisible'] ); return array_map(function (ReflectionFunction $function) { return [ 'name' => $function->getName(), 'match' => $function->getShortName(), 'link' => 'function-' . str_replace('\\', '.', $function->getName()) . '.html', 'description' => $this->shiftDocCommentLine($function->getDocComment()), ]; }, $functions); } private function filterVisible(ReflectionBase $reflected) { $annotations = $reflected->getAnnotations(); return empty($annotations['internal']) && empty($annotations['deprecated']); } private function shiftDocCommentLine($comment) { $comment = array_map(function ($line) { return ltrim($line, '/* '); }, explode("\n", $comment)); $comment = array_filter($comment, function ($line) { return !empty($line); }); return array_shift($comment); } private function createHtmlForWaiters(HtmlDocument $html, Api $service) { $waiters = $service->getWaiters(); if (!$waiters) { return; } $desc = << getWaiter(\$waiterName, \$operationParameters) method of a client object. This client supports the following waiters: EOT; $html->section(2, 'Waiters'); $html->open('div', 'element-summary'); $html->elem('p', null, $desc); $html->open('table', 'table table-condensed'); $html->open('thead'); $html->open('tr'); $html->elem('th', null, 'Waiter name'); $html->elem('th', null, 'API Operation'); $html->elem('th', null, 'Delay'); $html->elem('th', null, 'Max Attempts'); $html->close(); $html->close(); $html->open('tbody'); foreach ($waiters as $name => $config) { $html->open('tr'); $html->elem('td', null, $name); $html->elem('td', null, '' . $config['operation'] . ''); $html->elem('td', null, $config['delay']); $html->elem('td', null, $config['maxAttempts']); $html->close(); } $html->close(); $html->close(); $html->close(); } private function createHtmlForPaginators(HtmlDocument $html, Api $service) { $paginators = $service->getPaginators(); if (!$paginators) { return; } $desc = << getPaginator(\$paginatorName, \$operationParameters). This client supports the following paginators: EOT; $html->section(2, 'Paginators'); $html->open('div', 'element-summary'); $html->elem('p', null, $desc); $html->open('ul'); foreach ($paginators as $name => $config) { $html->open('li'); $attr = ['href' => '#' . strtolower($name)]; $html->elem('a', $attr, $name); $html->close(); } $html->close(); $html->close(); } private function createHtmlForOperation(Service $service, $name, Operation $operation, $examples) { $html = new HtmlDocument; $html->open('div', 'operation-container'); // Name $html->section( 3, $html->glyph('cog') . ' ' . $name, null, 'method-title', $name ); // Code $html->elem( 'pre', 'opcode', '$result = $client->' . lcfirst($name) . '([/* ... */]);' . "\n" . '$promise = $client->' . lcfirst($name) . 'Async([/* ... */]);' . "\n" ); // Description if ($description = $service->docs->getOperationDocs($name)) { $html->elem('div', 'operation-docs', $description); } // Parameters $input = $operation->getInput(); $this->skipMembers->attach($input); $output = $operation->getOutput(); $this->skipMembers->attach($output); $inputShapes = new ShapeIterator($input, $service->docs); $inputExample = new ExampleBuilder($name); foreach ($inputShapes as $shape) { $inputExample->addShape($shape); } $html ->elem('h4', null, 'Parameter Syntax') ->elem('pre', null, htmlentities($inputExample->getCode())) ->elem('h4', null, 'Parameter Details') ->append($this->renderShape($service->docs, $input, false)); // Results $html->elem('h4', null, 'Result Syntax'); if (!count($output->getMembers())) { $html->elem('pre', null, '[]'); $html->elem('h4', null, 'Result Details'); $html->elem('div', 'alert alert-info', 'The results for this operation are always empty.'); } else { $outputShapes = new ShapeIterator($output, $service->docs); $eventStreamExample = null; foreach ($outputShapes as $shape) { if (!empty($shape['eventstream'])) { $eventStreamExample = new EventStreamExampleBuilder($shape['param']); } } $outputExample = new ExampleBuilder($name, false); foreach ($outputShapes as $shape) { $outputExample->addShape($shape); if ($eventStreamExample) { $eventStreamExample->addShape($shape); } } $html->elem('pre', null, htmlentities($outputExample->getCode())) ->elem('h4', null, 'Result Details') ->append($this->renderShape($service->docs, $output, false)); if ($eventStreamExample) { $desc = <<elem('h5', null, 'Using an EventParsingIterator') ->elem('p', null, $desc) ->elem('pre', null, htmlentities($eventStreamExample->getCode())); } } // Errors $html->elem('h4', null, 'Errors'); $errors = $operation->getErrors(); if (!$errors) { $html->elem('p', null, 'There are no errors described for this operation.'); } else { $html->open('ul'); foreach ($errors as $error) { $desc = $service->docs->getErrorDocs($error->getName()) ?: 'This error does not currently have a description.'; $html ->open('li') ->open('p') ->elem( 'a', [ 'href' => $service->exceptionLink . '#shape-' . strtolower($error->getName()) ], $error['name'] . ': ') ->elem('p', null, $desc) ->close() ->close(); } $html->close(); } // Examples if (!empty($examples)) { $generator = new CodeSnippetGenerator($service->api); $html->elem('h4', null, 'Examples'); foreach ($examples as $number => $example) { $exampleNumber = $number + 1; $exampleId = $this->exampleSlug($name, $exampleNumber); $html->open('h5', ['id' => $exampleId]); $html->elem('span', null, 'Example ' . $exampleNumber . ': ' . $example['title']); $html->elem('a', ['href' => '#' . $exampleId], $html->glyph('link')); $html->close(); $html->elem('p', null, $example['description']); $comments = $example['comments']; $html->elem('pre', null, $generator->generateInput( $name, isset($example['input']) ? $example['input'] : [], isset($comments['input']) ? $comments['input'] : [] )); if (isset($example['output'])) { $html->elem('p', null, 'Result syntax:'); $html->elem('pre', null, $generator->generateOutput( $name, $example['output'], isset($comments['output']) ? $comments['output'] : [] )); } } $generatorIssues = $generator->getIssues(); foreach ($generatorIssues as $shapeName => $levelIssues) { foreach ($levelIssues as $level => $messages) { foreach (array_keys($messages) as $message) { $generatorIssues[$shapeName][$level][$message] = $name; } } } $this->logIssues( $service->api->getServiceName(), $service->api->getApiVersion(), $generatorIssues ); } $html->close(); // operation-container return $html; } private function logIssues($serviceName, $serviceVersion, $issuesToLog) { if (!isset($this->issues[$serviceName][$serviceVersion])) { $this->issues[$serviceName][$serviceVersion] = []; } foreach ($issuesToLog as $shapeName=>$shapeIssues) { foreach ($shapeIssues as $level => $messages) { foreach ($messages as $message => $exampleName) { $this->issues[$level][$serviceName][$serviceVersion][ $exampleName . ' has an issue - ' . $message . ' on ' . $shapeName ] = true; } } } } private function renderShape( DocModel $docs, StructureShape $shape, $showTitle = true ) { $html = new HtmlDocument(); $html->open('div', 'shape-container'); // Disable show title for input and output parameters. if ($showTitle) { $desc = $docs->getShapeDocs($shape['name'], null, null); if ($desc) { $html->elem('h5', null, 'Description'); $html->elem('div', 'shape-description', $desc); } } $html->elem('h5', null, 'Members'); $html->open('dl', 'shape-members'); $members = $shape->getMembers(); ksort($members); foreach ($members as $name => $member) { $html->open('dt', 'param-def'); $html->elem('span', 'term', $name); $html->close(); $html->open('dd', 'param-def'); $required = !empty($shape['required']) && in_array($name, $shape['required']); $html->append($this->describeParam($member, $required)); $desc = $docs->getShapeDocs($member['name'], $shape['name'], $name); $html->elem('div', 'param-def-doc', $desc); $html->close(); } $html->close(); $html->close(); return $html; } private function describeParam(AbstractModel $member, $required = false) { $html = new HtmlDocument(); if ($member instanceof StructureShape) { $typeDesc = $this->getMemberText($member); } elseif ($member instanceof ListShape) { $typeDesc = 'Array of ' . $this->getMemberText($member->getMember()) . 's'; } elseif ($member instanceof MapShape) { $typeDesc = 'Associative array of custom strings keys (' . $member->getKey()->getName() . ') to ' . $this->getMemberText($member->getValue()) . 's'; } else { $typeDesc = $this->getPrimitivePhpType($member); } $html->open('div', 'param-attributes')->open('ul'); if ($required) { $html->elem('li', 'required', 'Required: Yes'); } $html->elem('li', '', 'Type: ' . $typeDesc); $html->close(); $html->close(); return $html; } private function getMemberText(AbstractModel $member) { if ($member instanceof StructureShape) { if (!empty($member['eventstream'])) { return $this->getEventStreamMemberText($member); } if (!empty($member['document'])) { return $this->getDocumentText($member); } return $this->memberLink($member->getName()) . ' structure'; } elseif ($member instanceof ListShape) { switch ($member->getMember()['type']) { case 'string': return 'strings'; case 'double': return 'floats'; case 'integer': return 'integers'; case 'boolean': return 'booleans'; case 'structure': return $this->getMemberText($member->getMember()) . 's'; } } elseif ($member instanceof MapShape) { switch ($member->getValue()['type']) { case 'string': return 'strings'; case 'double': return 'floats'; case 'integer': return 'integers'; case 'boolean': return 'booleans'; case 'structure': return $this->getMemberText($member->getValue()) . 's'; } } return $this->getPrimitivePhpType($member); } private function getEventStreamMemberText(StructureShape $member) { return 'EventParsingIterator supplying the following structures: ' . implode(', ', array_map( [$this, 'memberLink'], array_reduce( $member->getMembers(), function ($carry, $item) { $carry []= $item['name']; return $carry; }, [] ) ) ); } private function getDocumentText(StructureShape $member) { return 'document (null|bool|string|numeric) or an (array|associative array)' . ' whose members are all valid documents'; } private function getPrimitivePhpType($member) { switch ($member['type']) { case 'long': return 'long (int|float)'; case 'integer': return 'int'; case 'blob': return 'blob (string|resource|Psr\Http\Message\StreamInterface)'; case 'char': return 'char (string)'; case 'timestamp': return 'timestamp (string|DateTime or anything parsable by strtotime)'; case 'string': if ($member['jsonvalue']){ return 'string (string|number|array|map or anything parsable by json_encode)'; } default: return $member['type']; } } private function exampleSlug($name, $number) { return strtolower($name) . '-example-' . $number; } private function memberSlug($name) { return 'shape-' . strtolower($name); } private function memberLink($name) { return '' . $name . ''; } private function updateSitemap() { fwrite(STDOUT, "Updating sitemap\n"); $writer = new \SimpleXMLElement(""); $writer->addAttribute('xmlns', "http://www.sitemaps.org/schemas/sitemap/0.9"); $linksToIndex = new \GlobIterator("{$this->outputDir}/*.html", \FilesystemIterator::CURRENT_AS_FILEINFO); foreach ($linksToIndex as $link) { $url = $writer->addChild('url'); $url->addChild('loc', "{$this->baseUrl}{$link->getBasename()}"); } $writer->asXML("{$this->outputDir}/sitemap.xml"); } }