1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "base/json/json_reader.h" 6 #include "base/json/json_writer.h" 7 #include "base/memory/scoped_ptr.h" 8 #include "base/strings/string_number_conversions.h" 9 #include "base/strings/string_util.h" 10 #include "base/strings/stringprintf.h" 11 #include "base/strings/utf_string_conversions.h" 12 #include "base/values.h" 13 #include "content/browser/geolocation/fake_access_token_store.h" 14 #include "content/browser/geolocation/location_arbitrator_impl.h" 15 #include "content/browser/geolocation/network_location_provider.h" 16 #include "net/url_request/test_url_fetcher_factory.h" 17 #include "net/url_request/url_request_status.h" 18 #include "testing/gtest/include/gtest/gtest.h" 19 20 namespace content { 21 22 // Constants used in multiple tests. 23 const char kTestServerUrl[] = "https://www.geolocation.test/service"; 24 const char kAccessTokenString[] = "accessToken"; 25 26 // Using #define so we can easily paste this into various other strings. 27 #define REFERENCE_ACCESS_TOKEN "2:k7j3G6LaL6u_lafw:4iXOeOpTh1glSXe" 28 29 // Stops the specified (nested) message loop when the listener is called back. 30 class MessageLoopQuitListener { 31 public: 32 MessageLoopQuitListener() 33 : client_message_loop_(base::MessageLoop::current()), 34 updated_provider_(NULL) { 35 CHECK(client_message_loop_); 36 } 37 38 void LocationUpdateAvailable(const LocationProvider* provider, 39 const Geoposition& position) { 40 EXPECT_EQ(client_message_loop_, base::MessageLoop::current()); 41 updated_provider_ = provider; 42 client_message_loop_->Quit(); 43 } 44 45 base::MessageLoop* client_message_loop_; 46 const LocationProvider* updated_provider_; 47 }; 48 49 // A mock implementation of WifiDataProviderImplBase for testing. Adapted from 50 // http://gears.googlecode.com/svn/trunk/gears/geolocation/geolocation_test.cc 51 class MockWifiDataProviderImpl : public WifiDataProviderImplBase { 52 public: 53 // Factory method for use with WifiDataProvider::SetFactory. 54 static WifiDataProviderImplBase* GetInstance() { 55 CHECK(instance_); 56 return instance_; 57 } 58 59 static MockWifiDataProviderImpl* CreateInstance() { 60 CHECK(!instance_); 61 instance_ = new MockWifiDataProviderImpl; 62 return instance_; 63 } 64 65 MockWifiDataProviderImpl() 66 : start_calls_(0), 67 stop_calls_(0), 68 got_data_(true) { 69 } 70 71 // WifiDataProviderImplBase implementation. 72 virtual void StartDataProvider() OVERRIDE { 73 ++start_calls_; 74 } 75 76 virtual void StopDataProvider() OVERRIDE { 77 ++stop_calls_; 78 } 79 80 virtual bool GetData(WifiData* data_out) OVERRIDE { 81 CHECK(data_out); 82 *data_out = data_; 83 return got_data_; 84 } 85 86 void SetData(const WifiData& new_data) { 87 got_data_ = true; 88 const bool differs = data_.DiffersSignificantly(new_data); 89 data_ = new_data; 90 if (differs) 91 this->RunCallbacks(); 92 } 93 94 void set_got_data(bool got_data) { got_data_ = got_data; } 95 int start_calls_; 96 int stop_calls_; 97 98 private: 99 virtual ~MockWifiDataProviderImpl() { 100 CHECK(this == instance_); 101 instance_ = NULL; 102 } 103 104 static MockWifiDataProviderImpl* instance_; 105 106 WifiData data_; 107 bool got_data_; 108 109 DISALLOW_COPY_AND_ASSIGN(MockWifiDataProviderImpl); 110 }; 111 112 MockWifiDataProviderImpl* MockWifiDataProviderImpl::instance_ = NULL; 113 114 // Main test fixture 115 class GeolocationNetworkProviderTest : public testing::Test { 116 public: 117 virtual void SetUp() { 118 test_server_url_ = GURL(kTestServerUrl); 119 access_token_store_ = new FakeAccessTokenStore; 120 wifi_data_provider_ = 121 MockWifiDataProviderImpl::CreateInstance(); 122 } 123 124 virtual void TearDown() { 125 WifiDataProvider::ResetFactory(); 126 } 127 128 LocationProvider* CreateProvider(bool set_permission_granted) { 129 LocationProvider* provider = NewNetworkLocationProvider( 130 access_token_store_.get(), 131 NULL, // No URLContextGetter needed, as using test urlfecther factory. 132 test_server_url_, 133 access_token_store_->access_token_set_[test_server_url_]); 134 if (set_permission_granted) 135 provider->OnPermissionGranted(); 136 return provider; 137 } 138 139 protected: 140 GeolocationNetworkProviderTest() { 141 // TODO(joth): Really these should be in SetUp, not here, but they take no 142 // effect on Mac OS Release builds if done there. I kid not. Figure out why. 143 WifiDataProvider::SetFactory(MockWifiDataProviderImpl::GetInstance); 144 } 145 146 // Returns the current url fetcher (if any) and advances the id ready for the 147 // next test step. 148 net::TestURLFetcher* get_url_fetcher_and_advance_id() { 149 net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID( 150 NetworkLocationRequest::url_fetcher_id_for_tests); 151 if (fetcher) 152 ++NetworkLocationRequest::url_fetcher_id_for_tests; 153 return fetcher; 154 } 155 156 static int IndexToChannel(int index) { return index + 4; } 157 158 // Creates wifi data containing the specified number of access points, with 159 // some differentiating charactistics in each. 160 static WifiData CreateReferenceWifiScanData(int ap_count) { 161 WifiData data; 162 for (int i = 0; i < ap_count; ++i) { 163 AccessPointData ap; 164 ap.mac_address = 165 base::ASCIIToUTF16(base::StringPrintf("%02d-34-56-78-54-32", i)); 166 ap.radio_signal_strength = ap_count - i; 167 ap.channel = IndexToChannel(i); 168 ap.signal_to_noise = i + 42; 169 ap.ssid = base::ASCIIToUTF16("Some nice+network|name\\"); 170 data.access_point_data.insert(ap); 171 } 172 return data; 173 } 174 175 static void CreateReferenceWifiScanDataJson( 176 int ap_count, int start_index, base::ListValue* wifi_access_point_list) { 177 std::vector<std::string> wifi_data; 178 for (int i = 0; i < ap_count; ++i) { 179 base::DictionaryValue* ap = new base::DictionaryValue(); 180 ap->SetString("macAddress", base::StringPrintf("%02d-34-56-78-54-32", i)); 181 ap->SetInteger("signalStrength", start_index + ap_count - i); 182 ap->SetInteger("age", 0); 183 ap->SetInteger("channel", IndexToChannel(i)); 184 ap->SetInteger("signalToNoiseRatio", i + 42); 185 wifi_access_point_list->Append(ap); 186 } 187 } 188 189 static Geoposition CreateReferencePosition(int id) { 190 Geoposition pos; 191 pos.latitude = id; 192 pos.longitude = -(id + 1); 193 pos.altitude = 2 * id; 194 pos.timestamp = base::Time::Now(); 195 return pos; 196 } 197 198 static std::string PrettyJson(const base::Value& value) { 199 std::string pretty; 200 base::JSONWriter::WriteWithOptions( 201 &value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &pretty); 202 return pretty; 203 } 204 205 static testing::AssertionResult JsonGetList( 206 const std::string& field, 207 const base::DictionaryValue& dict, 208 const base::ListValue** output_list) { 209 if (!dict.GetList(field, output_list)) 210 return testing::AssertionFailure() << "Dictionary " << PrettyJson(dict) 211 << " is missing list field " << field; 212 return testing::AssertionSuccess(); 213 } 214 215 static testing::AssertionResult JsonFieldEquals( 216 const std::string& field, 217 const base::DictionaryValue& expected, 218 const base::DictionaryValue& actual) { 219 const base::Value* expected_value; 220 const base::Value* actual_value; 221 if (!expected.Get(field, &expected_value)) 222 return testing::AssertionFailure() 223 << "Expected dictionary " << PrettyJson(expected) 224 << " is missing field " << field; 225 if (!expected.Get(field, &actual_value)) 226 return testing::AssertionFailure() 227 << "Actual dictionary " << PrettyJson(actual) 228 << " is missing field " << field; 229 if (!expected_value->Equals(actual_value)) 230 return testing::AssertionFailure() 231 << "Field " << field << " mismatch: " << PrettyJson(*expected_value) 232 << " != " << PrettyJson(*actual_value); 233 return testing::AssertionSuccess(); 234 } 235 236 static GURL UrlWithoutQuery(const GURL& url) { 237 url::Replacements<char> replacements; 238 replacements.ClearQuery(); 239 return url.ReplaceComponents(replacements); 240 } 241 242 testing::AssertionResult IsTestServerUrl(const GURL& request_url) { 243 const GURL a(UrlWithoutQuery(test_server_url_)); 244 const GURL b(UrlWithoutQuery(request_url)); 245 if (a == b) 246 return testing::AssertionSuccess(); 247 return testing::AssertionFailure() << a << " != " << b; 248 } 249 250 void CheckRequestIsValid(const net::TestURLFetcher& request, 251 int expected_routers, 252 int expected_wifi_aps, 253 int wifi_start_index, 254 const std::string& expected_access_token) { 255 const GURL& request_url = request.GetOriginalURL(); 256 257 EXPECT_TRUE(IsTestServerUrl(request_url)); 258 259 // Check to see that the api key is being appended for the default 260 // network provider url. 261 bool is_default_url = UrlWithoutQuery(request_url) == 262 UrlWithoutQuery(LocationArbitratorImpl::DefaultNetworkProviderURL()); 263 EXPECT_EQ(is_default_url, !request_url.query().empty()); 264 265 const std::string& upload_data = request.upload_data(); 266 ASSERT_FALSE(upload_data.empty()); 267 std::string json_parse_error_msg; 268 scoped_ptr<base::Value> parsed_json( 269 base::JSONReader::ReadAndReturnError( 270 upload_data, 271 base::JSON_PARSE_RFC, 272 NULL, 273 &json_parse_error_msg)); 274 EXPECT_TRUE(json_parse_error_msg.empty()); 275 ASSERT_TRUE(parsed_json.get() != NULL); 276 277 const base::DictionaryValue* request_json; 278 ASSERT_TRUE(parsed_json->GetAsDictionary(&request_json)); 279 280 if (!is_default_url) { 281 if (expected_access_token.empty()) 282 ASSERT_FALSE(request_json->HasKey(kAccessTokenString)); 283 else { 284 std::string access_token; 285 EXPECT_TRUE(request_json->GetString(kAccessTokenString, &access_token)); 286 EXPECT_EQ(expected_access_token, access_token); 287 } 288 } 289 290 if (expected_wifi_aps) { 291 base::ListValue expected_wifi_aps_json; 292 CreateReferenceWifiScanDataJson( 293 expected_wifi_aps, 294 wifi_start_index, 295 &expected_wifi_aps_json); 296 EXPECT_EQ(size_t(expected_wifi_aps), expected_wifi_aps_json.GetSize()); 297 298 const base::ListValue* wifi_aps_json; 299 ASSERT_TRUE(JsonGetList("wifiAccessPoints", *request_json, 300 &wifi_aps_json)); 301 for (size_t i = 0; i < expected_wifi_aps_json.GetSize(); ++i ) { 302 const base::DictionaryValue* expected_json; 303 ASSERT_TRUE(expected_wifi_aps_json.GetDictionary(i, &expected_json)); 304 const base::DictionaryValue* actual_json; 305 ASSERT_TRUE(wifi_aps_json->GetDictionary(i, &actual_json)); 306 ASSERT_TRUE(JsonFieldEquals("macAddress", *expected_json, 307 *actual_json)); 308 ASSERT_TRUE(JsonFieldEquals("signalStrength", *expected_json, 309 *actual_json)); 310 ASSERT_TRUE(JsonFieldEquals("channel", *expected_json, *actual_json)); 311 ASSERT_TRUE(JsonFieldEquals("signalToNoiseRatio", *expected_json, 312 *actual_json)); 313 } 314 } else { 315 ASSERT_FALSE(request_json->HasKey("wifiAccessPoints")); 316 } 317 EXPECT_TRUE(request_url.is_valid()); 318 } 319 320 GURL test_server_url_; 321 base::MessageLoop main_message_loop_; 322 scoped_refptr<FakeAccessTokenStore> access_token_store_; 323 net::TestURLFetcherFactory url_fetcher_factory_; 324 scoped_refptr<MockWifiDataProviderImpl> wifi_data_provider_; 325 }; 326 327 TEST_F(GeolocationNetworkProviderTest, CreateDestroy) { 328 // Test fixture members were SetUp correctly. 329 EXPECT_EQ(&main_message_loop_, base::MessageLoop::current()); 330 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 331 EXPECT_TRUE(NULL != provider.get()); 332 provider.reset(); 333 SUCCEED(); 334 } 335 336 TEST_F(GeolocationNetworkProviderTest, StartProvider) { 337 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 338 EXPECT_TRUE(provider->StartProvider(false)); 339 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 340 ASSERT_TRUE(fetcher != NULL); 341 CheckRequestIsValid(*fetcher, 0, 0, 0, std::string()); 342 } 343 344 TEST_F(GeolocationNetworkProviderTest, StartProviderDefaultUrl) { 345 test_server_url_ = LocationArbitratorImpl::DefaultNetworkProviderURL(); 346 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 347 EXPECT_TRUE(provider->StartProvider(false)); 348 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 349 ASSERT_TRUE(fetcher != NULL); 350 CheckRequestIsValid(*fetcher, 0, 0, 0, std::string()); 351 } 352 353 TEST_F(GeolocationNetworkProviderTest, StartProviderLongRequest) { 354 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 355 EXPECT_TRUE(provider->StartProvider(false)); 356 const int kFirstScanAps = 20; 357 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kFirstScanAps)); 358 main_message_loop_.RunUntilIdle(); 359 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 360 ASSERT_TRUE(fetcher != NULL); 361 // The request url should have been shortened to less than 2048 characters 362 // in length by not including access points with the lowest signal strength 363 // in the request. 364 EXPECT_LT(fetcher->GetOriginalURL().spec().size(), size_t(2048)); 365 CheckRequestIsValid(*fetcher, 0, 16, 4, std::string()); 366 } 367 368 TEST_F(GeolocationNetworkProviderTest, MultipleWifiScansComplete) { 369 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 370 EXPECT_TRUE(provider->StartProvider(false)); 371 372 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 373 ASSERT_TRUE(fetcher != NULL); 374 EXPECT_TRUE(IsTestServerUrl(fetcher->GetOriginalURL())); 375 376 // Complete the network request with bad position fix. 377 const char* kNoFixNetworkResponse = 378 "{" 379 " \"status\": \"ZERO_RESULTS\"" 380 "}"; 381 fetcher->set_url(test_server_url_); 382 fetcher->set_status(net::URLRequestStatus()); 383 fetcher->set_response_code(200); // OK 384 fetcher->SetResponseString(kNoFixNetworkResponse); 385 fetcher->delegate()->OnURLFetchComplete(fetcher); 386 387 Geoposition position; 388 provider->GetPosition(&position); 389 EXPECT_FALSE(position.Validate()); 390 391 // Now wifi data arrives -- SetData will notify listeners. 392 const int kFirstScanAps = 6; 393 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kFirstScanAps)); 394 main_message_loop_.RunUntilIdle(); 395 fetcher = get_url_fetcher_and_advance_id(); 396 ASSERT_TRUE(fetcher != NULL); 397 // The request should have the wifi data. 398 CheckRequestIsValid(*fetcher, 0, kFirstScanAps, 0, std::string()); 399 400 // Send a reply with good position fix. 401 const char* kReferenceNetworkResponse = 402 "{" 403 " \"accessToken\": \"" REFERENCE_ACCESS_TOKEN "\"," 404 " \"accuracy\": 1200.4," 405 " \"location\": {" 406 " \"lat\": 51.0," 407 " \"lng\": -0.1" 408 " }" 409 "}"; 410 fetcher->set_url(test_server_url_); 411 fetcher->set_status(net::URLRequestStatus()); 412 fetcher->set_response_code(200); // OK 413 fetcher->SetResponseString(kReferenceNetworkResponse); 414 fetcher->delegate()->OnURLFetchComplete(fetcher); 415 416 provider->GetPosition(&position); 417 EXPECT_EQ(51.0, position.latitude); 418 EXPECT_EQ(-0.1, position.longitude); 419 EXPECT_EQ(1200.4, position.accuracy); 420 EXPECT_FALSE(position.timestamp.is_null()); 421 EXPECT_TRUE(position.Validate()); 422 423 // Token should be in the store. 424 EXPECT_EQ(base::UTF8ToUTF16(REFERENCE_ACCESS_TOKEN), 425 access_token_store_->access_token_set_[test_server_url_]); 426 427 // Wifi updated again, with one less AP. This is 'close enough' to the 428 // previous scan, so no new request made. 429 const int kSecondScanAps = kFirstScanAps - 1; 430 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kSecondScanAps)); 431 main_message_loop_.RunUntilIdle(); 432 fetcher = get_url_fetcher_and_advance_id(); 433 EXPECT_FALSE(fetcher); 434 435 provider->GetPosition(&position); 436 EXPECT_EQ(51.0, position.latitude); 437 EXPECT_EQ(-0.1, position.longitude); 438 EXPECT_TRUE(position.Validate()); 439 440 // Now a third scan with more than twice the original amount -> new request. 441 const int kThirdScanAps = kFirstScanAps * 2 + 1; 442 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kThirdScanAps)); 443 main_message_loop_.RunUntilIdle(); 444 fetcher = get_url_fetcher_and_advance_id(); 445 EXPECT_TRUE(fetcher); 446 CheckRequestIsValid(*fetcher, 0, kThirdScanAps, 0, REFERENCE_ACCESS_TOKEN); 447 // ...reply with a network error. 448 449 fetcher->set_url(test_server_url_); 450 fetcher->set_status(net::URLRequestStatus(net::URLRequestStatus::FAILED, -1)); 451 fetcher->set_response_code(200); // should be ignored 452 fetcher->SetResponseString(std::string()); 453 fetcher->delegate()->OnURLFetchComplete(fetcher); 454 455 // Error means we now no longer have a fix. 456 provider->GetPosition(&position); 457 EXPECT_FALSE(position.Validate()); 458 459 // Wifi scan returns to original set: should be serviced from cache. 460 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kFirstScanAps)); 461 main_message_loop_.RunUntilIdle(); 462 EXPECT_FALSE(get_url_fetcher_and_advance_id()); // No new request created. 463 464 provider->GetPosition(&position); 465 EXPECT_EQ(51.0, position.latitude); 466 EXPECT_EQ(-0.1, position.longitude); 467 EXPECT_TRUE(position.Validate()); 468 } 469 470 TEST_F(GeolocationNetworkProviderTest, NoRequestOnStartupUntilWifiData) { 471 MessageLoopQuitListener listener; 472 wifi_data_provider_->set_got_data(false); 473 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 474 EXPECT_TRUE(provider->StartProvider(false)); 475 476 provider->SetUpdateCallback( 477 base::Bind(&MessageLoopQuitListener::LocationUpdateAvailable, 478 base::Unretained(&listener))); 479 480 main_message_loop_.RunUntilIdle(); 481 EXPECT_FALSE(get_url_fetcher_and_advance_id()) 482 << "Network request should not be created right away on startup when " 483 "wifi data has not yet arrived"; 484 485 wifi_data_provider_->SetData(CreateReferenceWifiScanData(1)); 486 main_message_loop_.RunUntilIdle(); 487 EXPECT_TRUE(get_url_fetcher_and_advance_id()); 488 } 489 490 TEST_F(GeolocationNetworkProviderTest, NewDataReplacesExistingNetworkRequest) { 491 // Send initial request with empty data 492 scoped_ptr<LocationProvider> provider(CreateProvider(true)); 493 EXPECT_TRUE(provider->StartProvider(false)); 494 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 495 EXPECT_TRUE(fetcher); 496 497 // Now wifi data arrives; new request should be sent. 498 wifi_data_provider_->SetData(CreateReferenceWifiScanData(4)); 499 main_message_loop_.RunUntilIdle(); 500 fetcher = get_url_fetcher_and_advance_id(); 501 EXPECT_TRUE(fetcher); 502 } 503 504 TEST_F(GeolocationNetworkProviderTest, NetworkRequestDeferredForPermission) { 505 scoped_ptr<LocationProvider> provider(CreateProvider(false)); 506 EXPECT_TRUE(provider->StartProvider(false)); 507 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 508 EXPECT_FALSE(fetcher); 509 provider->OnPermissionGranted(); 510 511 fetcher = get_url_fetcher_and_advance_id(); 512 ASSERT_TRUE(fetcher != NULL); 513 514 EXPECT_TRUE(IsTestServerUrl(fetcher->GetOriginalURL())); 515 } 516 517 TEST_F(GeolocationNetworkProviderTest, 518 NetworkRequestWithWifiDataDeferredForPermission) { 519 access_token_store_->access_token_set_[test_server_url_] = 520 base::UTF8ToUTF16(REFERENCE_ACCESS_TOKEN); 521 scoped_ptr<LocationProvider> provider(CreateProvider(false)); 522 EXPECT_TRUE(provider->StartProvider(false)); 523 net::TestURLFetcher* fetcher = get_url_fetcher_and_advance_id(); 524 EXPECT_FALSE(fetcher); 525 526 static const int kScanCount = 4; 527 wifi_data_provider_->SetData(CreateReferenceWifiScanData(kScanCount)); 528 main_message_loop_.RunUntilIdle(); 529 530 fetcher = get_url_fetcher_and_advance_id(); 531 EXPECT_FALSE(fetcher); 532 533 provider->OnPermissionGranted(); 534 535 fetcher = get_url_fetcher_and_advance_id(); 536 ASSERT_TRUE(fetcher != NULL); 537 538 CheckRequestIsValid(*fetcher, 0, kScanCount, 0, REFERENCE_ACCESS_TOKEN); 539 } 540 541 TEST_F(GeolocationNetworkProviderTest, NetworkPositionCache) { 542 NetworkLocationProvider::PositionCache cache; 543 544 const int kCacheSize = NetworkLocationProvider::PositionCache::kMaximumSize; 545 for (int i = 1; i < kCacheSize * 2 + 1; ++i) { 546 Geoposition pos = CreateReferencePosition(i); 547 bool ret = cache.CachePosition(CreateReferenceWifiScanData(i), pos); 548 EXPECT_TRUE(ret) << i; 549 const Geoposition* item = 550 cache.FindPosition(CreateReferenceWifiScanData(i)); 551 ASSERT_TRUE(item) << i; 552 EXPECT_EQ(pos.latitude, item->latitude) << i; 553 EXPECT_EQ(pos.longitude, item->longitude) << i; 554 if (i <= kCacheSize) { 555 // Nothing should have spilled yet; check oldest item is still there. 556 EXPECT_TRUE(cache.FindPosition(CreateReferenceWifiScanData(1))); 557 } else { 558 const int evicted = i - kCacheSize; 559 EXPECT_FALSE(cache.FindPosition(CreateReferenceWifiScanData(evicted))); 560 EXPECT_TRUE(cache.FindPosition(CreateReferenceWifiScanData(evicted + 1))); 561 } 562 } 563 } 564 565 } // namespace content 566