Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(663)

Side by Side Diff: components/ntp_snippets/remote/ntp_snippets_fetcher.h

Issue 2578173002: NTP: Extract JSON requests from Fetcher. (Closed)
Patch Set: Updating comments. Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 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 #ifndef COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_ 5 #ifndef COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_
6 #define COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_ 6 #define COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_
7 7
8 #include <memory> 8 #include <memory>
9 #include <queue> 9 #include <queue>
10 #include <set>
11 #include <string> 10 #include <string>
12 #include <utility> 11 #include <utility>
13 #include <vector> 12 #include <vector>
14 13
15 #include "base/callback.h" 14 #include "base/callback.h"
16 #include "base/memory/weak_ptr.h" 15 #include "base/memory/weak_ptr.h"
17 #include "base/optional.h" 16 #include "base/optional.h"
18 #include "base/time/tick_clock.h" 17 #include "base/time/tick_clock.h"
19 #include "base/time/time.h"
20 #include "components/ntp_snippets/category.h" 18 #include "components/ntp_snippets/category.h"
21 #include "components/ntp_snippets/category_info.h" 19 #include "components/ntp_snippets/category_info.h"
22 #include "components/ntp_snippets/remote/ntp_snippet.h" 20 #include "components/ntp_snippets/remote/ntp_snippet.h"
21 #include "components/ntp_snippets/remote/ntp_snippets_json_request.h"
22 #include "components/ntp_snippets/remote/ntp_snippets_request_params.h"
23 #include "components/ntp_snippets/remote/request_throttler.h" 23 #include "components/ntp_snippets/remote/request_throttler.h"
24 #include "components/ntp_snippets/status.h" 24 #include "components/ntp_snippets/status.h"
25 #include "components/translate/core/browser/language_model.h" 25 #include "components/translate/core/browser/language_model.h"
26 #include "google_apis/gaia/oauth2_token_service.h"
27 #include "net/http/http_request_headers.h"
28 #include "net/url_request/url_request_context_getter.h" 26 #include "net/url_request/url_request_context_getter.h"
29 27
30 class PrefService; 28 class PrefService;
31 class SigninManagerBase; 29 class SigninManagerBase;
32 30
33 namespace base { 31 namespace base {
34 class Value; 32 class Value;
35 } // namespace base 33 } // namespace base
36 34
37 namespace ntp_snippets { 35 namespace ntp_snippets {
(...skipping 10 matching lines...) Expand all
48 // Provides the CategoryInfo data for article suggestions. If |title| is 46 // Provides the CategoryInfo data for article suggestions. If |title| is
49 // nullopt, then the default, hard-coded title will be used. 47 // nullopt, then the default, hard-coded title will be used.
50 CategoryInfo BuildArticleCategoryInfo( 48 CategoryInfo BuildArticleCategoryInfo(
51 const base::Optional<base::string16>& title); 49 const base::Optional<base::string16>& title);
52 50
53 // Provides the CategoryInfo data for other remote suggestions. 51 // Provides the CategoryInfo data for other remote suggestions.
54 CategoryInfo BuildRemoteCategoryInfo(const base::string16& title, 52 CategoryInfo BuildRemoteCategoryInfo(const base::string16& title,
55 bool allow_fetching_more_results); 53 bool allow_fetching_more_results);
56 54
57 // Fetches snippet data for the NTP from the server. 55 // Fetches snippet data for the NTP from the server.
56 // TODO(fhorschig): Untangle cyclic dependencies by introducing a
57 // NTPSnippetsFetcherInterface. (Would be good for mock implementations, too)
58 class NTPSnippetsFetcher : public OAuth2TokenService::Consumer, 58 class NTPSnippetsFetcher : public OAuth2TokenService::Consumer,
59 public OAuth2TokenService::Observer { 59 public OAuth2TokenService::Observer {
60 public: 60 public:
61 // Callbacks for JSON parsing, needed because the parsing is platform-
62 // dependent.
63 using SuccessCallback =
64 base::Callback<void(std::unique_ptr<base::Value> result)>;
65 using ErrorCallback = base::Callback<void(const std::string& error)>;
66 using ParseJSONCallback =
67 base::Callback<void(const std::string& raw_json_string,
68 const SuccessCallback& success_callback,
69 const ErrorCallback& error_callback)>;
70
71 struct FetchedCategory { 61 struct FetchedCategory {
72 Category category; 62 Category category;
73 CategoryInfo info; 63 CategoryInfo info;
74 NTPSnippet::PtrVector snippets; 64 NTPSnippet::PtrVector snippets;
75 65
76 FetchedCategory(Category c, CategoryInfo&& info); 66 FetchedCategory(Category c, CategoryInfo&& info);
77 FetchedCategory(FetchedCategory&&); // = default, in .cc 67 FetchedCategory(FetchedCategory&&); // = default, in .cc
78 ~FetchedCategory(); // = default, in .cc 68 ~FetchedCategory(); // = default, in .cc
79 FetchedCategory& operator=(FetchedCategory&&); // = default, in .cc 69 FetchedCategory& operator=(FetchedCategory&&); // = default, in .cc
80 }; 70 };
81 using FetchedCategoriesVector = std::vector<FetchedCategory>; 71 using FetchedCategoriesVector = std::vector<FetchedCategory>;
82 using OptionalFetchedCategories = base::Optional<FetchedCategoriesVector>; 72 using OptionalFetchedCategories = base::Optional<FetchedCategoriesVector>;
83 73
84 // Enumeration listing all possible outcomes for fetch attempts. Used for UMA
85 // histograms, so do not change existing values. Insert new values at the end,
86 // and update the histogram definition.
87 enum class FetchResult {
88 SUCCESS,
89 DEPRECATED_EMPTY_HOSTS,
90 URL_REQUEST_STATUS_ERROR,
91 HTTP_ERROR,
92 JSON_PARSE_ERROR,
93 INVALID_SNIPPET_CONTENT_ERROR,
94 OAUTH_TOKEN_ERROR,
95 INTERACTIVE_QUOTA_ERROR,
96 NON_INTERACTIVE_QUOTA_ERROR,
97 RESULT_MAX
98 };
99
100 // |snippets| contains parsed snippets if a fetch succeeded. If problems 74 // |snippets| contains parsed snippets if a fetch succeeded. If problems
101 // occur, |snippets| contains no value (no actual vector in base::Optional). 75 // occur, |snippets| contains no value (no actual vector in base::Optional).
102 // Error details can be retrieved using last_status(). 76 // Error details can be retrieved using last_status().
103 using SnippetsAvailableCallback = 77 using SnippetsAvailableCallback =
104 base::OnceCallback<void(Status status, 78 base::OnceCallback<void(Status status,
105 OptionalFetchedCategories fetched_categories)>; 79 OptionalFetchedCategories fetched_categories)>;
106 80
107 // Enumeration listing all possible variants of dealing with personalization.
108 enum class Personalization { kPersonal, kNonPersonal, kBoth };
109
110 // Contains all the parameters for one fetch.
111 struct Params {
112 Params();
113 Params(const Params&);
114 ~Params();
115
116 // BCP 47 language code specifying the user's UI language.
117 std::string language_code;
118
119 // A set of suggestion IDs that should not be returned again.
120 std::set<std::string> excluded_ids;
121
122 // Maximum number of snippets to fetch.
123 int count_to_fetch = 0;
124
125 // Whether this is an interactive request, i.e. triggered by an explicit
126 // user action. Typically, non-interactive requests are subject to a daily
127 // quota.
128 bool interactive_request = false;
129
130 // If set, only return results for this category.
131 base::Optional<Category> exclusive_category;
132 };
133
134 NTPSnippetsFetcher( 81 NTPSnippetsFetcher(
135 SigninManagerBase* signin_manager, 82 SigninManagerBase* signin_manager,
136 OAuth2TokenService* token_service, 83 OAuth2TokenService* token_service,
137 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter, 84 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter,
138 PrefService* pref_service, 85 PrefService* pref_service,
139 translate::LanguageModel* language_model, 86 translate::LanguageModel* language_model,
140 const ParseJSONCallback& parse_json_callback, 87 const ParseJSONCallback& parse_json_callback,
141 const std::string& api_key, 88 const std::string& api_key,
142 const UserClassifier* user_classifier); 89 const UserClassifier* user_classifier);
143 ~NTPSnippetsFetcher() override; 90 ~NTPSnippetsFetcher() override;
144 91
145 // Initiates a fetch from the server. When done (successfully or not), calls 92 // Initiates a fetch from the server. When done (successfully or not), calls
146 // the subscriber of SetCallback(). 93 // the callback.
147 // 94 //
148 // If an ongoing fetch exists, it will be silently abandoned and a new one 95 // If an ongoing fetch exists, both fetches won't influence each other (i.e.
149 // started, without triggering an additional callback (i.e. the callback will 96 // every callback will be called exactly once).
150 // only be called once). 97 void FetchSnippets(const NTPSnippetsRequestParams& params,
151 void FetchSnippets(const Params& params, SnippetsAvailableCallback callback); 98 SnippetsAvailableCallback callback);
99
100 std::string PersonalizationModeString() const;
152 101
153 // Debug string representing the status/result of the last fetch attempt. 102 // Debug string representing the status/result of the last fetch attempt.
154 const std::string& last_status() const { return last_status_; } 103 const std::string& last_status() const { return last_status_; }
155 104
156 // Returns the last JSON fetched from the server. 105 // Returns the last JSON fetched from the server.
157 const std::string& last_json() const { 106 const std::string& last_json() const {
158 return last_fetch_json_; 107 return last_fetch_json_;
159 } 108 }
160 109
161 // Returns the personalization setting of the fetcher. 110 // Returns the personalization setting of the fetcher as used in tests.
111 // TODO(fhorschig): Reconsider these tests and remove this getter.
162 Personalization personalization() const { return personalization_; } 112 Personalization personalization() const { return personalization_; }
163 113
164 // Returns the URL endpoint used by the fetcher. 114 // Returns the URL endpoint used by the fetcher.
165 const GURL& fetch_url() const { return fetch_url_; } 115 const GURL& fetch_url() const { return fetch_url_; }
166 116
167 // Overrides internal clock for testing purposes. 117 // Overrides internal clock for testing purposes.
168 void SetTickClockForTesting(std::unique_ptr<base::TickClock> tick_clock) { 118 void SetTickClockForTesting(std::unique_ptr<base::TickClock> tick_clock) {
169 tick_clock_ = std::move(tick_clock); 119 tick_clock_ = std::move(tick_clock);
170 } 120 }
171 121
172 private: 122 private:
173 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 123 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
174 BuildRequestAuthenticated); 124 BuildRequestAuthenticated);
175 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 125 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
176 BuildRequestUnauthenticated); 126 BuildRequestUnauthenticated);
177 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 127 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
178 BuildRequestExcludedIds); 128 BuildRequestExcludedIds);
179 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 129 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
180 BuildRequestNoUserClass); 130 BuildRequestNoUserClass);
181 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 131 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
182 BuildRequestWithTwoLanguages); 132 BuildRequestWithTwoLanguages);
183 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 133 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
184 BuildRequestWithUILanguageOnly); 134 BuildRequestWithUILanguageOnly);
185 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest, 135 FRIEND_TEST_ALL_PREFIXES(ChromeReaderSnippetsFetcherTest,
186 BuildRequestWithOtherLanguageOnly); 136 BuildRequestWithOtherLanguageOnly);
187 friend class ChromeReaderSnippetsFetcherTest; 137 friend class ChromeReaderSnippetsFetcherTest;
188 138
189 enum FetchAPI { 139 void FetchSnippetsNonAuthenticated(
190 CHROME_READER_API, 140 internal::NTPSnippetsJsonRequest::Builder builder,
191 CHROME_CONTENT_SUGGESTIONS_API, 141 SnippetsAvailableCallback callback);
192 }; 142 void FetchSnippetsAuthenticated(
193 143 internal::NTPSnippetsJsonRequest::Builder builder,
194 class JsonRequest; 144 SnippetsAvailableCallback callback,
195 145 const std::string& account_id,
196 // A class that builds authenticated and non-authenticated JsonRequests. 146 const std::string& oauth_access_token);
197 // This class is only in the header for testing. 147 void StartRequest(internal::NTPSnippetsJsonRequest::Builder builder,
198 // TODO(fhorschig): Move into separate file with snippets::internal namespace. 148 SnippetsAvailableCallback callback);
199 class RequestBuilder {
200 public:
201 RequestBuilder();
202 RequestBuilder(RequestBuilder&&);
203 ~RequestBuilder();
204
205 // Builds a Request object that contains all data to fetch new snippets.
206 std::unique_ptr<JsonRequest> Build() const;
207
208 RequestBuilder& SetAuthentication(const std::string& account_id,
209 const std::string& auth_header);
210 RequestBuilder& SetCreationTime(base::TimeTicks creation_time);
211 RequestBuilder& SetFetchAPI(FetchAPI fetch_api);
212 // The language_model borrowed from the fetcher needs to stay alive until
213 // the request body is built.
214 RequestBuilder& SetLanguageModel(
215 const translate::LanguageModel* language_model);
216 RequestBuilder& SetParams(const Params& params);
217 RequestBuilder& SetParseJsonCallback(ParseJSONCallback callback);
218 RequestBuilder& SetPersonalization(Personalization personalization);
219 // The tick_clock borrowed from the fetcher will be injected into the
220 // request. It will be used at build time and after the fetch returned.
221 // It has to be alive until the request is destroyed.
222 RequestBuilder& SetTickClock(base::TickClock* tick_clock);
223 RequestBuilder& SetUrl(const GURL& url);
224 RequestBuilder& SetUrlRequestContextGetter(
225 const scoped_refptr<net::URLRequestContextGetter>& context_getter);
226 RequestBuilder& SetUserClassifier(const UserClassifier& user_classifier);
227
228 // These preview methods allow to inspect the Request without exposing it
229 // publicly.
230 // TODO(fhorschig): Remove these when moving the RequestBuilder to
231 // snippets::internal and trigger the request to intercept the request.
232 std::string PreviewRequestBodyForTesting() { return BuildBody(); }
233 std::string PreviewRequestHeadersForTesting() { return BuildHeaders(); }
234 RequestBuilder& SetUserClassForTesting(const std::string& user_class) {
235 user_class_ = user_class;
236 return *this;
237 }
238
239 private:
240 std::string BuildHeaders() const;
241 std::string BuildBody() const;
242 std::unique_ptr<net::URLFetcher> BuildURLFetcher(
243 net::URLFetcherDelegate* request,
244 const std::string& headers,
245 const std::string& body) const;
246
247 bool ReturnOnlyPersonalizedResults() const {
248 return !obfuscated_gaia_id_.empty() &&
249 personalization_ == NTPSnippetsFetcher::Personalization::kPersonal;
250 }
251
252 void PrepareLanguages(
253 translate::LanguageModel::LanguageInfo* ui_language,
254 translate::LanguageModel::LanguageInfo* other_top_language) const;
255
256 // Only required, if the request needs to be sent.
257 std::string auth_header_;
258 base::TickClock* tick_clock_;
259 FetchAPI fetch_api_;
260 Params params_;
261 ParseJSONCallback parse_json_callback_;
262 Personalization personalization_;
263 GURL url_;
264 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
265
266 // Optional properties.
267 std::string obfuscated_gaia_id_;
268 std::string user_class_;
269 const translate::LanguageModel* language_model_;
270
271 DISALLOW_COPY_AND_ASSIGN(RequestBuilder);
272 };
273
274 void FetchSnippetsNonAuthenticated(RequestBuilder builder,
275 SnippetsAvailableCallback callback);
276 void FetchSnippetsAuthenticated(RequestBuilder builder,
277 SnippetsAvailableCallback callback,
278 const std::string& account_id,
279 const std::string& oauth_access_token);
280 void StartRequest(RequestBuilder builder, SnippetsAvailableCallback callback);
281 149
282 void StartTokenRequest(); 150 void StartTokenRequest();
283 151
284 // OAuth2TokenService::Consumer overrides: 152 // OAuth2TokenService::Consumer overrides:
285 void OnGetTokenSuccess(const OAuth2TokenService::Request* request, 153 void OnGetTokenSuccess(const OAuth2TokenService::Request* request,
286 const std::string& access_token, 154 const std::string& access_token,
287 const base::Time& expiration_time) override; 155 const base::Time& expiration_time) override;
288 void OnGetTokenFailure(const OAuth2TokenService::Request* request, 156 void OnGetTokenFailure(const OAuth2TokenService::Request* request,
289 const GoogleServiceAuthError& error) override; 157 const GoogleServiceAuthError& error) override;
290 158
291 // OAuth2TokenService::Observer overrides: 159 // OAuth2TokenService::Observer overrides:
292 void OnRefreshTokenAvailable(const std::string& account_id) override; 160 void OnRefreshTokenAvailable(const std::string& account_id) override;
293 161
294 void JsonRequestDone(std::unique_ptr<JsonRequest> request, 162 void JsonRequestDone(
295 SnippetsAvailableCallback callback, 163 std::unique_ptr<internal::NTPSnippetsJsonRequest> request,
296 std::unique_ptr<base::Value> result, 164 SnippetsAvailableCallback callback,
297 FetchResult status_code, 165 std::unique_ptr<base::Value> result,
298 const std::string& error_details); 166 internal::FetchResult status_code,
167 const std::string& error_details);
299 void FetchFinished(OptionalFetchedCategories categories, 168 void FetchFinished(OptionalFetchedCategories categories,
300 SnippetsAvailableCallback callback, 169 SnippetsAvailableCallback callback,
301 FetchResult status_code, 170 internal::FetchResult status_code,
302 const std::string& error_details); 171 const std::string& error_details);
303 172
304 bool JsonToSnippets(const base::Value& parsed, 173 bool JsonToSnippets(const base::Value& parsed,
305 NTPSnippetsFetcher::FetchedCategoriesVector* categories); 174 NTPSnippetsFetcher::FetchedCategoriesVector* categories);
306 175
307 bool DemandQuotaForRequest(bool interactive_request); 176 bool DemandQuotaForRequest(bool interactive_request);
308 177
309 // Does the fetcher use authentication to get personalized results? 178 // Does the fetcher use authentication to get personalized results?
310 bool NeedsAuthentication() const; 179 bool NeedsAuthentication() const;
311 180
312 // Authentication for signed-in users. 181 // Authentication for signed-in users.
313 SigninManagerBase* signin_manager_; 182 SigninManagerBase* signin_manager_;
314 OAuth2TokenService* token_service_; 183 OAuth2TokenService* token_service_;
315 std::unique_ptr<OAuth2TokenService::Request> oauth_request_; 184 std::unique_ptr<OAuth2TokenService::Request> oauth_request_;
316 bool waiting_for_refresh_token_ = false; 185 bool waiting_for_refresh_token_ = false;
317 186
318 // When a token request gets canceled, we want to retry once. 187 // When a token request gets canceled, we want to retry once.
319 bool oauth_token_retried_ = false; 188 bool oauth_token_retried_ = false;
320 189
321 // Holds the URL request context. 190 // Holds the URL request context.
322 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; 191 scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
323 192
324 // Stores requests that wait for an access token. 193 // Stores requests that wait for an access token.
325 std::queue<std::pair<RequestBuilder, SnippetsAvailableCallback>> 194 std::queue<std::pair<internal::NTPSnippetsJsonRequest::Builder,
195 SnippetsAvailableCallback>>
326 pending_requests_; 196 pending_requests_;
327 197
328 // Weak reference, not owned. 198 // Weak reference, not owned.
329 translate::LanguageModel* const language_model_; 199 translate::LanguageModel* const language_model_;
330 200
331 const ParseJSONCallback parse_json_callback_; 201 const ParseJSONCallback parse_json_callback_;
332 202
333 // API endpoint for fetching snippets. 203 // API endpoint for fetching snippets.
334 const GURL fetch_url_; 204 const GURL fetch_url_;
335 // Which API to use 205 // Which API to use
336 const FetchAPI fetch_api_; 206 const internal::FetchAPI fetch_api_;
337 207
338 // API key to use for non-authenticated requests. 208 // API key to use for non-authenticated requests.
339 const std::string api_key_; 209 const std::string api_key_;
340 210
341 // The variant of the fetching to use, loaded from variation parameters. 211 // The variant of the fetching to use, loaded from variation parameters.
342 Personalization personalization_; 212 Personalization personalization_;
343 213
344 // Allow for an injectable tick clock for testing. 214 // Allow for an injectable tick clock for testing.
345 std::unique_ptr<base::TickClock> tick_clock_; 215 std::unique_ptr<base::TickClock> tick_clock_;
346 216
347 // Classifier that tells us how active the user is. Not owned. 217 // Classifier that tells us how active the user is. Not owned.
348 const UserClassifier* user_classifier_; 218 const UserClassifier* user_classifier_;
349 219
350 // Request throttlers for limiting requests for different classes of users. 220 // Request throttlers for limiting requests for different classes of users.
351 RequestThrottler request_throttler_rare_ntp_user_; 221 RequestThrottler request_throttler_rare_ntp_user_;
352 RequestThrottler request_throttler_active_ntp_user_; 222 RequestThrottler request_throttler_active_ntp_user_;
353 RequestThrottler request_throttler_active_suggestions_consumer_; 223 RequestThrottler request_throttler_active_suggestions_consumer_;
354 224
355 // Info on the last finished fetch. 225 // Info on the last finished fetch.
356 std::string last_status_; 226 std::string last_status_;
357 std::string last_fetch_json_; 227 std::string last_fetch_json_;
358 228
359 base::WeakPtrFactory<NTPSnippetsFetcher> weak_ptr_factory_; 229 base::WeakPtrFactory<NTPSnippetsFetcher> weak_ptr_factory_;
360 230
361 DISALLOW_COPY_AND_ASSIGN(NTPSnippetsFetcher); 231 DISALLOW_COPY_AND_ASSIGN(NTPSnippetsFetcher);
362 }; 232 };
363 } // namespace ntp_snippets 233 } // namespace ntp_snippets
364 234
365 #endif // COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_ 235 #endif // COMPONENTS_NTP_SNIPPETS_REMOTE_NTP_SNIPPETS_FETCHER_H_
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698