OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/autocomplete/keyword_provider.h" | 5 #include "chrome/browser/autocomplete/keyword_provider.h" |
6 | 6 |
7 #include <algorithm> | 7 #include <algorithm> |
8 #include <vector> | 8 #include <vector> |
9 | 9 |
10 #include "base/string16.h" | 10 #include "base/string16.h" |
(...skipping 24 matching lines...) Expand all Loading... |
35 provider_->MaybeEndExtensionKeywordMode(); | 35 provider_->MaybeEndExtensionKeywordMode(); |
36 } | 36 } |
37 | 37 |
38 void StayInKeywordMode() { | 38 void StayInKeywordMode() { |
39 provider_ = NULL; | 39 provider_ = NULL; |
40 } | 40 } |
41 private: | 41 private: |
42 KeywordProvider* provider_; | 42 KeywordProvider* provider_; |
43 }; | 43 }; |
44 | 44 |
| 45 // static |
| 46 string16 KeywordProvider::SplitReplacementStringFromInput( |
| 47 const string16& input, |
| 48 bool trim_leading_whitespace) { |
| 49 // The input may contain leading whitespace, strip it. |
| 50 string16 trimmed_input; |
| 51 TrimWhitespace(input, TRIM_LEADING, &trimmed_input); |
| 52 |
| 53 // And extract the replacement string. |
| 54 string16 remaining_input; |
| 55 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace, |
| 56 &remaining_input); |
| 57 return remaining_input; |
| 58 } |
| 59 |
45 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) | 60 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) |
46 : AutocompleteProvider(listener, profile, "Keyword"), | 61 : AutocompleteProvider(listener, profile, "Keyword"), |
47 model_(NULL), | 62 model_(NULL), |
48 current_input_id_(0) { | 63 current_input_id_(0) { |
49 // Extension suggestions always come from the original profile, since that's | 64 // Extension suggestions always come from the original profile, since that's |
50 // where extensions run. We use the input ID to distinguish whether the | 65 // where extensions run. We use the input ID to distinguish whether the |
51 // suggestions are meant for us. | 66 // suggestions are meant for us. |
52 registrar_.Add(this, | 67 registrar_.Add(this, |
53 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, | 68 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, |
54 content::Source<Profile>(profile->GetOriginalProfile())); | 69 content::Source<Profile>(profile->GetOriginalProfile())); |
(...skipping 30 matching lines...) Expand all Loading... |
85 } | 100 } |
86 }; | 101 }; |
87 | 102 |
88 // We need our input IDs to be unique across all profiles, so we keep a global | 103 // We need our input IDs to be unique across all profiles, so we keep a global |
89 // UID that each provider uses. | 104 // UID that each provider uses. |
90 static int global_input_uid_; | 105 static int global_input_uid_; |
91 | 106 |
92 } // namespace | 107 } // namespace |
93 | 108 |
94 // static | 109 // static |
95 string16 KeywordProvider::SplitKeywordFromInput( | |
96 const string16& input, | |
97 bool trim_leading_whitespace, | |
98 string16* remaining_input) { | |
99 // Find end of first token. The AutocompleteController has trimmed leading | |
100 // whitespace, so we need not skip over that. | |
101 const size_t first_white(input.find_first_of(kWhitespaceUTF16)); | |
102 DCHECK_NE(0U, first_white); | |
103 if (first_white == string16::npos) | |
104 return input; // Only one token provided. | |
105 | |
106 // Set |remaining_input| to everything after the first token. | |
107 DCHECK(remaining_input != NULL); | |
108 const size_t remaining_start = trim_leading_whitespace ? | |
109 input.find_first_not_of(kWhitespaceUTF16, first_white) : first_white + 1; | |
110 | |
111 if (remaining_start < input.length()) | |
112 remaining_input->assign(input.begin() + remaining_start, input.end()); | |
113 | |
114 // Return first token as keyword. | |
115 return input.substr(0, first_white); | |
116 } | |
117 | |
118 // static | |
119 string16 KeywordProvider::SplitReplacementStringFromInput( | |
120 const string16& input, | |
121 bool trim_leading_whitespace) { | |
122 // The input may contain leading whitespace, strip it. | |
123 string16 trimmed_input; | |
124 TrimWhitespace(input, TRIM_LEADING, &trimmed_input); | |
125 | |
126 // And extract the replacement string. | |
127 string16 remaining_input; | |
128 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace, | |
129 &remaining_input); | |
130 return remaining_input; | |
131 } | |
132 | |
133 // static | |
134 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput( | 110 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput( |
135 Profile* profile, | 111 Profile* profile, |
136 const AutocompleteInput& input, | 112 const AutocompleteInput& input, |
137 string16* remaining_input) { | 113 string16* remaining_input) { |
138 if (!input.allow_exact_keyword_match()) | 114 if (!input.allow_exact_keyword_match()) |
139 return NULL; | 115 return NULL; |
140 | 116 |
141 string16 keyword; | 117 string16 keyword; |
142 if (!ExtractKeywordFromInput(input, &keyword, remaining_input)) | 118 if (!ExtractKeywordFromInput(input, &keyword, remaining_input)) |
143 return NULL; | 119 return NULL; |
144 | 120 |
145 // Make sure the model is loaded. This is cheap and quickly bails out if | 121 // Make sure the model is loaded. This is cheap and quickly bails out if |
146 // the model is already loaded. | 122 // the model is already loaded. |
147 TemplateURLService* model = TemplateURLServiceFactory::GetForProfile(profile); | 123 TemplateURLService* model = TemplateURLServiceFactory::GetForProfile(profile); |
148 DCHECK(model); | 124 DCHECK(model); |
149 model->Load(); | 125 model->Load(); |
150 | 126 |
151 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); | 127 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); |
152 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; | 128 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; |
153 } | 129 } |
154 | 130 |
155 string16 KeywordProvider::GetKeywordForText( | |
156 const string16& text) const { | |
157 const string16 keyword(TemplateURLService::CleanUserInputKeyword(text)); | |
158 | |
159 if (keyword.empty()) | |
160 return keyword; | |
161 | |
162 TemplateURLService* url_service = GetTemplateURLService(); | |
163 if (!url_service) | |
164 return string16(); | |
165 | |
166 // Don't provide a keyword if it doesn't support replacement. | |
167 const TemplateURL* const template_url = | |
168 url_service->GetTemplateURLForKeyword(keyword); | |
169 if (!TemplateURL::SupportsReplacement(template_url)) | |
170 return string16(); | |
171 | |
172 // Don't provide a keyword for inactive/disabled extension keywords. | |
173 if (template_url->IsExtensionKeyword()) { | |
174 const Extension* extension = profile_->GetExtensionService()-> | |
175 GetExtensionById(template_url->GetExtensionId(), false); | |
176 if (!extension || | |
177 (profile_->IsOffTheRecord() && | |
178 !profile_->GetExtensionService()->IsIncognitoEnabled(extension->id()))) | |
179 return string16(); | |
180 } | |
181 | |
182 return keyword; | |
183 } | |
184 | |
185 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( | |
186 const string16& text, | |
187 const string16& keyword, | |
188 const AutocompleteInput& input) { | |
189 return CreateAutocompleteMatch(GetTemplateURLService(), keyword, input, | |
190 keyword.size(), SplitReplacementStringFromInput(text, true), 0); | |
191 } | |
192 | |
193 void KeywordProvider::Start(const AutocompleteInput& input, | 131 void KeywordProvider::Start(const AutocompleteInput& input, |
194 bool minimal_changes) { | 132 bool minimal_changes) { |
195 // This object ensures we end keyword mode if we exit the function without | 133 // This object ensures we end keyword mode if we exit the function without |
196 // toggling keyword mode to on. | 134 // toggling keyword mode to on. |
197 ScopedEndExtensionKeywordMode keyword_mode_toggle(this); | 135 ScopedEndExtensionKeywordMode keyword_mode_toggle(this); |
198 | 136 |
199 matches_.clear(); | 137 matches_.clear(); |
200 | 138 |
201 if (!minimal_changes) { | 139 if (!minimal_changes) { |
202 done_ = true; | 140 done_ = true; |
(...skipping 13 matching lines...) Expand all Loading... |
216 // whatever we do here! | 154 // whatever we do here! |
217 // | 155 // |
218 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for | 156 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for |
219 // keywords, we might suggest keywords that haven't even been partially typed, | 157 // keywords, we might suggest keywords that haven't even been partially typed, |
220 // if the user uses them enough and isn't obviously typing something else. In | 158 // if the user uses them enough and isn't obviously typing something else. In |
221 // this case we'd consider all input here to be query input. | 159 // this case we'd consider all input here to be query input. |
222 string16 keyword, remaining_input; | 160 string16 keyword, remaining_input; |
223 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) | 161 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) |
224 return; | 162 return; |
225 | 163 |
226 TemplateURLService* model = GetTemplateURLService(); | 164 // Make sure the model is loaded. This is cheap and quickly bails out if |
| 165 // the model is already loaded. |
| 166 TemplateURLService* model = |
| 167 profile_ ? |
| 168 TemplateURLServiceFactory::GetForProfile(profile_) : |
| 169 model_; |
| 170 DCHECK(model); |
| 171 model->Load(); |
227 | 172 |
228 // Get the best matches for this keyword. | 173 // Get the best matches for this keyword. |
229 // | 174 // |
230 // NOTE: We could cache the previous keywords and reuse them here in the | 175 // NOTE: We could cache the previous keywords and reuse them here in the |
231 // |minimal_changes| case, but since we'd still have to recalculate their | 176 // |minimal_changes| case, but since we'd still have to recalculate their |
232 // relevances and we can just recreate the results synchronously anyway, we | 177 // relevances and we can just recreate the results synchronously anyway, we |
233 // don't bother. | 178 // don't bother. |
234 // | 179 // |
235 // TODO(pkasting): http://b/893701 We should remember the user's use of a | 180 // TODO(pkasting): http://b/893701 We should remember the user's use of a |
236 // search query both from the autocomplete popup and from web pages | 181 // search query both from the autocomplete popup and from web pages |
237 // themselves. | 182 // themselves. |
238 std::vector<string16> keyword_matches; | 183 std::vector<string16> keyword_matches; |
239 model->FindMatchingKeywords(keyword, | 184 model->FindMatchingKeywords(keyword, |
240 !remaining_input.empty(), | 185 !remaining_input.empty(), |
241 &keyword_matches); | 186 &keyword_matches); |
242 | 187 |
| 188 // Prune any extension keywords that are disallowed in incognito mode (if |
| 189 // we're incognito), or disabled. |
243 for (std::vector<string16>::iterator i(keyword_matches.begin()); | 190 for (std::vector<string16>::iterator i(keyword_matches.begin()); |
244 i != keyword_matches.end(); ) { | 191 i != keyword_matches.end(); ) { |
245 const TemplateURL* template_url(model->GetTemplateURLForKeyword(*i)); | 192 const TemplateURL* template_url(model->GetTemplateURLForKeyword(*i)); |
246 | |
247 // Prune any extension keywords that are disallowed in incognito mode (if | |
248 // we're incognito), or disabled. | |
249 if (profile_ && | 193 if (profile_ && |
250 input.matches_requested() == AutocompleteInput::ALL_MATCHES && | 194 input.matches_requested() == AutocompleteInput::ALL_MATCHES && |
251 template_url->IsExtensionKeyword()) { | 195 template_url->IsExtensionKeyword()) { |
252 ExtensionService* service = profile_->GetExtensionService(); | 196 ExtensionService* service = profile_->GetExtensionService(); |
253 const Extension* extension = service->GetExtensionById( | 197 const Extension* extension = service->GetExtensionById( |
254 template_url->GetExtensionId(), false); | 198 template_url->GetExtensionId(), false); |
255 bool enabled = | 199 bool enabled = |
256 extension && (!profile_->IsOffTheRecord() || | 200 extension && (!profile_->IsOffTheRecord() || |
257 service->IsIncognitoEnabled(extension->id())); | 201 service->IsIncognitoEnabled(extension->id())); |
258 if (!enabled) { | 202 if (!enabled) { |
259 i = keyword_matches.erase(i); | 203 i = keyword_matches.erase(i); |
260 continue; | 204 continue; |
261 } | 205 } |
262 } | 206 } |
263 | |
264 // Prune any substituting keywords if there is no substitution. | |
265 if (TemplateURL::SupportsReplacement(template_url) && | |
266 remaining_input.empty() && !input.allow_exact_keyword_match()) { | |
267 i = keyword_matches.erase(i); | |
268 continue; | |
269 } | |
270 | |
271 ++i; | 207 ++i; |
272 } | 208 } |
273 if (keyword_matches.empty()) | 209 if (keyword_matches.empty()) |
274 return; | 210 return; |
275 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); | 211 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); |
276 | 212 |
277 // Limit to one exact or three inexact matches, and mark them up for display | 213 // Limit to one exact or three inexact matches, and mark them up for display |
278 // in the autocomplete popup. | 214 // in the autocomplete popup. |
279 // Any exact match is going to be the highest quality match, and thus at the | 215 // Any exact match is going to be the highest quality match, and thus at the |
280 // front of our vector. | 216 // front of our vector. |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
351 return false; | 287 return false; |
352 | 288 |
353 string16 trimmed_input; | 289 string16 trimmed_input; |
354 TrimWhitespace(input.text(), TRIM_TRAILING, &trimmed_input); | 290 TrimWhitespace(input.text(), TRIM_TRAILING, &trimmed_input); |
355 *keyword = TemplateURLService::CleanUserInputKeyword( | 291 *keyword = TemplateURLService::CleanUserInputKeyword( |
356 SplitKeywordFromInput(trimmed_input, true, remaining_input)); | 292 SplitKeywordFromInput(trimmed_input, true, remaining_input)); |
357 return !keyword->empty(); | 293 return !keyword->empty(); |
358 } | 294 } |
359 | 295 |
360 // static | 296 // static |
| 297 string16 KeywordProvider::SplitKeywordFromInput( |
| 298 const string16& input, |
| 299 bool trim_leading_whitespace, |
| 300 string16* remaining_input) { |
| 301 // Find end of first token. The AutocompleteController has trimmed leading |
| 302 // whitespace, so we need not skip over that. |
| 303 const size_t first_white(input.find_first_of(kWhitespaceUTF16)); |
| 304 DCHECK_NE(0U, first_white); |
| 305 if (first_white == string16::npos) |
| 306 return input; // Only one token provided. |
| 307 |
| 308 // Set |remaining_input| to everything after the first token. |
| 309 DCHECK(remaining_input != NULL); |
| 310 const size_t remaining_start = trim_leading_whitespace ? |
| 311 input.find_first_not_of(kWhitespaceUTF16, first_white) : first_white + 1; |
| 312 |
| 313 if (remaining_start < input.length()) |
| 314 remaining_input->assign(input.begin() + remaining_start, input.end()); |
| 315 |
| 316 // Return first token as keyword. |
| 317 return input.substr(0, first_white); |
| 318 } |
| 319 |
| 320 // static |
361 void KeywordProvider::FillInURLAndContents( | 321 void KeywordProvider::FillInURLAndContents( |
362 Profile* profile, | 322 Profile* profile, |
363 const string16& remaining_input, | 323 const string16& remaining_input, |
364 const TemplateURL* element, | 324 const TemplateURL* element, |
365 AutocompleteMatch* match) { | 325 AutocompleteMatch* match) { |
366 DCHECK(!element->short_name().empty()); | 326 DCHECK(!element->short_name().empty()); |
367 DCHECK(element->url()); | 327 DCHECK(element->url()); |
368 DCHECK(element->url()->IsValid()); | 328 DCHECK(element->url()->IsValid()); |
369 int message_id = element->IsExtensionKeyword() ? | 329 int message_id = element->IsExtensionKeyword() ? |
370 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; | 330 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
448 const bool keyword_complete = (prefix_length == keyword.length()); | 408 const bool keyword_complete = (prefix_length == keyword.length()); |
449 if (relevance < 0) { | 409 if (relevance < 0) { |
450 relevance = | 410 relevance = |
451 CalculateRelevance(input.type(), keyword_complete, | 411 CalculateRelevance(input.type(), keyword_complete, |
452 // When the user wants keyword matches to take | 412 // When the user wants keyword matches to take |
453 // preference, score them highly regardless of | 413 // preference, score them highly regardless of |
454 // whether the input provides query text. | 414 // whether the input provides query text. |
455 supports_replacement, input.prefer_keyword(), | 415 supports_replacement, input.prefer_keyword(), |
456 input.allow_exact_keyword_match()); | 416 input.allow_exact_keyword_match()); |
457 } | 417 } |
458 AutocompleteMatch match(this, relevance, false, | 418 AutocompleteMatch result(this, relevance, false, |
459 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : | 419 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : |
460 AutocompleteMatch::HISTORY_KEYWORD); | 420 AutocompleteMatch::HISTORY_KEYWORD); |
461 match.fill_into_edit.assign(keyword); | 421 result.fill_into_edit.assign(keyword); |
462 if (!remaining_input.empty() || !keyword_complete || supports_replacement) | 422 if (!remaining_input.empty() || !keyword_complete || supports_replacement) |
463 match.fill_into_edit.push_back(L' '); | 423 result.fill_into_edit.push_back(L' '); |
464 match.fill_into_edit.append(remaining_input); | 424 result.fill_into_edit.append(remaining_input); |
465 // If we wanted to set |result.inline_autocomplete_offset| correctly, we'd | 425 // If we wanted to set |result.inline_autocomplete_offset| correctly, we'd |
466 // need CleanUserInputKeyword() to return the amount of adjustment it's made | 426 // need CleanUserInputKeyword() to return the amount of adjustment it's made |
467 // to the user's input. Because right now inexact keyword matches can't score | 427 // to the user's input. Because right now inexact keyword matches can't score |
468 // more highly than a "what you typed" match from one of the other providers, | 428 // more highly than a "what you typed" match from one of the other providers, |
469 // we just don't bother to do this, and leave inline autocompletion off. | 429 // we just don't bother to do this, and leave inline autocompletion off. |
470 match.inline_autocomplete_offset = string16::npos; | 430 result.inline_autocomplete_offset = string16::npos; |
471 | 431 |
472 // Create destination URL and popup entry content by substituting user input | 432 // Create destination URL and popup entry content by substituting user input |
473 // into keyword templates. | 433 // into keyword templates. |
474 FillInURLAndContents(profile_, remaining_input, element, &match); | 434 FillInURLAndContents(profile_, remaining_input, element, &result); |
475 | 435 |
476 if (supports_replacement) | 436 if (supports_replacement) |
477 match.template_url = element; | 437 result.template_url = element; |
478 match.keyword = keyword; | 438 result.transition = content::PAGE_TRANSITION_KEYWORD; |
479 match.transition = content::PAGE_TRANSITION_KEYWORD; | |
480 | 439 |
481 return match; | 440 return result; |
482 } | 441 } |
483 | 442 |
484 void KeywordProvider::Observe(int type, | 443 void KeywordProvider::Observe(int type, |
485 const content::NotificationSource& source, | 444 const content::NotificationSource& source, |
486 const content::NotificationDetails& details) { | 445 const content::NotificationDetails& details) { |
487 TemplateURLService* model = GetTemplateURLService(); | 446 TemplateURLService* model = |
| 447 profile_ ? TemplateURLServiceFactory::GetForProfile(profile_) : model_; |
488 const AutocompleteInput& input = extension_suggest_last_input_; | 448 const AutocompleteInput& input = extension_suggest_last_input_; |
489 | 449 |
490 switch (type) { | 450 switch (type) { |
491 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED: | 451 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED: |
492 // Input has been accepted, so we're done with this input session. Ensure | 452 // Input has been accepted, so we're done with this input session. Ensure |
493 // we don't send the OnInputCancelled event, or handle any more stray | 453 // we don't send the OnInputCancelled event, or handle any more stray |
494 // suggestions_ready events. | 454 // suggestions_ready events. |
495 current_keyword_extension_id_.clear(); | 455 current_keyword_extension_id_.clear(); |
496 current_input_id_ = 0; | 456 current_input_id_ = 0; |
497 return; | 457 return; |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
554 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); | 514 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); |
555 return; | 515 return; |
556 } | 516 } |
557 | 517 |
558 default: | 518 default: |
559 NOTREACHED(); | 519 NOTREACHED(); |
560 return; | 520 return; |
561 } | 521 } |
562 } | 522 } |
563 | 523 |
564 TemplateURLService* KeywordProvider::GetTemplateURLService() const { | |
565 TemplateURLService* service = profile_ ? | |
566 TemplateURLServiceFactory::GetForProfile(profile_) : model_; | |
567 // Make sure the model is loaded. This is cheap and quickly bails out if | |
568 // the model is already loaded. | |
569 DCHECK(service); | |
570 service->Load(); | |
571 return service; | |
572 } | |
573 | |
574 void KeywordProvider::EnterExtensionKeywordMode( | 524 void KeywordProvider::EnterExtensionKeywordMode( |
575 const std::string& extension_id) { | 525 const std::string& extension_id) { |
576 DCHECK(current_keyword_extension_id_.empty()); | 526 DCHECK(current_keyword_extension_id_.empty()); |
577 current_keyword_extension_id_ = extension_id; | 527 current_keyword_extension_id_ = extension_id; |
578 | 528 |
579 ExtensionOmniboxEventRouter::OnInputStarted( | 529 ExtensionOmniboxEventRouter::OnInputStarted( |
580 profile_, current_keyword_extension_id_); | 530 profile_, current_keyword_extension_id_); |
581 } | 531 } |
582 | 532 |
583 void KeywordProvider::MaybeEndExtensionKeywordMode() { | 533 void KeywordProvider::MaybeEndExtensionKeywordMode() { |
584 if (!current_keyword_extension_id_.empty()) { | 534 if (!current_keyword_extension_id_.empty()) { |
585 ExtensionOmniboxEventRouter::OnInputCancelled( | 535 ExtensionOmniboxEventRouter::OnInputCancelled( |
586 profile_, current_keyword_extension_id_); | 536 profile_, current_keyword_extension_id_); |
587 | 537 |
588 current_keyword_extension_id_.clear(); | 538 current_keyword_extension_id_.clear(); |
589 } | 539 } |
590 } | 540 } |
OLD | NEW |