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

Side by Side Diff: headless/lib/browser/devtools_api/client_api_generator.py

Issue 2902583002: Add some closureised JS bindings for DevTools for use by headless embedder (Closed)
Patch Set: Don't run the test on windows because js_binary doesn't work Created 3 years, 6 months 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 import argparse 5 import argparse
6 import collections 6 import collections
7 import os.path 7 import os.path
8 import re 8 import re
9 import sys 9 import sys
10 try: 10 try:
(...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after
113 if not '.' in json['$ref']: 113 if not '.' in json['$ref']:
114 json['$ref'] = domain_name + '.' + json['$ref'] 114 json['$ref'] = domain_name + '.' + json['$ref']
115 115
116 for domain in json_api['domains']: 116 for domain in json_api['domains']:
117 PatchFullQualifiedRefsInDomain(domain, domain['domain']) 117 PatchFullQualifiedRefsInDomain(domain, domain['domain'])
118 118
119 119
120 def CreateUserTypeDefinition(domain, type): 120 def CreateUserTypeDefinition(domain, type):
121 namespace = CamelCaseToHackerStyle(domain['domain']) 121 namespace = CamelCaseToHackerStyle(domain['domain'])
122 return { 122 return {
123 'js_type': '!goog.DevTools.%s.%s' % (domain['domain'], type['id']),
Sami 2017/05/24 09:16:21 Is the goog namespace the one we should be using?
alex clarke (OOO till 29th) 2017/05/24 11:38:14 Well we are google so goog seems appropriate?
Sami 2017/05/25 17:53:35 Let's use the chrome namespace since this is more
alex clarke (OOO till 29th) 2017/05/26 11:37:02 As discussed offline we had to use chromium instea
123 'return_type': 'std::unique_ptr<headless::%s::%s>' % ( 124 'return_type': 'std::unique_ptr<headless::%s::%s>' % (
124 namespace, type['id']), 125 namespace, type['id']),
125 'pass_type': 'std::unique_ptr<headless::%s::%s>' % ( 126 'pass_type': 'std::unique_ptr<headless::%s::%s>' % (
126 namespace, type['id']), 127 namespace, type['id']),
127 'to_raw_type': '*%s', 128 'to_raw_type': '*%s',
128 'to_raw_return_type': '%s.get()', 129 'to_raw_return_type': '%s.get()',
129 'to_pass_type': 'std::move(%s)', 130 'to_pass_type': 'std::move(%s)',
130 'type': 'std::unique_ptr<headless::%s::%s>' % (namespace, type['id']), 131 'type': 'std::unique_ptr<headless::%s::%s>' % (namespace, type['id']),
131 'raw_type': 'headless::%s::%s' % (namespace, type['id']), 132 'raw_type': 'headless::%s::%s' % (namespace, type['id']),
132 'raw_pass_type': 'headless::%s::%s*' % (namespace, type['id']), 133 'raw_pass_type': 'headless::%s::%s*' % (namespace, type['id']),
133 'raw_return_type': 'const headless::%s::%s*' % (namespace, type['id']), 134 'raw_return_type': 'const headless::%s::%s*' % (namespace, type['id']),
134 } 135 }
135 136
136 137
137 def CreateEnumTypeDefinition(domain_name, type): 138 def CreateEnumTypeDefinition(domain_name, type):
138 namespace = CamelCaseToHackerStyle(domain_name) 139 namespace = CamelCaseToHackerStyle(domain_name)
139 return { 140 return {
141 'js_type': '!goog.DevTools.%s.%s' % (domain_name, type['id']),
140 'return_type': 'headless::%s::%s' % (namespace, type['id']), 142 'return_type': 'headless::%s::%s' % (namespace, type['id']),
141 'pass_type': 'headless::%s::%s' % (namespace, type['id']), 143 'pass_type': 'headless::%s::%s' % (namespace, type['id']),
142 'to_raw_type': '%s', 144 'to_raw_type': '%s',
143 'to_raw_return_type': '%s', 145 'to_raw_return_type': '%s',
144 'to_pass_type': '%s', 146 'to_pass_type': '%s',
145 'type': 'headless::%s::%s' % (namespace, type['id']), 147 'type': 'headless::%s::%s' % (namespace, type['id']),
146 'raw_type': 'headless::%s::%s' % (namespace, type['id']), 148 'raw_type': 'headless::%s::%s' % (namespace, type['id']),
147 'raw_pass_type': 'headless::%s::%s' % (namespace, type['id']), 149 'raw_pass_type': 'headless::%s::%s' % (namespace, type['id']),
148 'raw_return_type': 'headless::%s::%s' % (namespace, type['id']), 150 'raw_return_type': 'headless::%s::%s' % (namespace, type['id']),
149 } 151 }
150 152
151 153
152 def CreateObjectTypeDefinition(): 154 def CreateObjectTypeDefinition():
153 return { 155 return {
156 'js_type': '%s',
154 'return_type': 'std::unique_ptr<base::DictionaryValue>', 157 'return_type': 'std::unique_ptr<base::DictionaryValue>',
155 'pass_type': 'std::unique_ptr<base::DictionaryValue>', 158 'pass_type': 'std::unique_ptr<base::DictionaryValue>',
156 'to_raw_type': '*%s', 159 'to_raw_type': '*%s',
157 'to_raw_return_type': '%s.get()', 160 'to_raw_return_type': '%s.get()',
158 'to_pass_type': 'std::move(%s)', 161 'to_pass_type': 'std::move(%s)',
159 'type': 'std::unique_ptr<base::DictionaryValue>', 162 'type': 'std::unique_ptr<base::DictionaryValue>',
160 'raw_type': 'base::DictionaryValue', 163 'raw_type': 'base::DictionaryValue',
161 'raw_pass_type': 'base::DictionaryValue*', 164 'raw_pass_type': 'base::DictionaryValue*',
162 'raw_return_type': 'const base::DictionaryValue*', 165 'raw_return_type': 'const base::DictionaryValue*',
163 } 166 }
164 167
165 168
166 def WrapObjectTypeDefinition(type): 169 def WrapObjectTypeDefinition(type):
167 id = type.get('id', 'base::Value') 170 id = type.get('id', 'base::Value')
168 return { 171 return {
172 'js_type': '!Object',
169 'return_type': 'std::unique_ptr<%s>' % id, 173 'return_type': 'std::unique_ptr<%s>' % id,
170 'pass_type': 'std::unique_ptr<%s>' % id, 174 'pass_type': 'std::unique_ptr<%s>' % id,
171 'to_raw_type': '*%s', 175 'to_raw_type': '*%s',
172 'to_raw_return_type': '%s.get()', 176 'to_raw_return_type': '%s.get()',
173 'to_pass_type': 'std::move(%s)', 177 'to_pass_type': 'std::move(%s)',
174 'type': 'std::unique_ptr<%s>' % id, 178 'type': 'std::unique_ptr<%s>' % id,
175 'raw_type': id, 179 'raw_type': id,
176 'raw_pass_type': '%s*' % id, 180 'raw_pass_type': '%s*' % id,
177 'raw_return_type': 'const %s*' % id, 181 'raw_return_type': 'const %s*' % id,
178 } 182 }
179 183
180 184
181 def CreateAnyTypeDefinition(): 185 def CreateAnyTypeDefinition():
182 return { 186 return {
187 'js_type': '*',
183 'return_type': 'std::unique_ptr<base::Value>', 188 'return_type': 'std::unique_ptr<base::Value>',
184 'pass_type': 'std::unique_ptr<base::Value>', 189 'pass_type': 'std::unique_ptr<base::Value>',
185 'to_raw_type': '*%s', 190 'to_raw_type': '*%s',
186 'to_raw_return_type': '%s.get()', 191 'to_raw_return_type': '%s.get()',
187 'to_pass_type': 'std::move(%s)', 192 'to_pass_type': 'std::move(%s)',
188 'type': 'std::unique_ptr<base::Value>', 193 'type': 'std::unique_ptr<base::Value>',
189 'raw_type': 'base::Value', 194 'raw_type': 'base::Value',
190 'raw_pass_type': 'base::Value*', 195 'raw_pass_type': 'base::Value*',
191 'raw_return_type': 'const base::Value*', 196 'raw_return_type': 'const base::Value*',
192 } 197 }
193 198
194 199
195 def CreateStringTypeDefinition(): 200 def CreateStringTypeDefinition():
196 return { 201 return {
202 'js_type': 'string',
197 'return_type': 'std::string', 203 'return_type': 'std::string',
198 'pass_type': 'const std::string&', 204 'pass_type': 'const std::string&',
199 'to_pass_type': '%s', 205 'to_pass_type': '%s',
200 'to_raw_type': '%s', 206 'to_raw_type': '%s',
201 'to_raw_return_type': '%s', 207 'to_raw_return_type': '%s',
202 'type': 'std::string', 208 'type': 'std::string',
203 'raw_type': 'std::string', 209 'raw_type': 'std::string',
204 'raw_pass_type': 'const std::string&', 210 'raw_pass_type': 'const std::string&',
205 'raw_return_type': 'std::string', 211 'raw_return_type': 'std::string',
206 } 212 }
207 213
208 214
209 def CreatePrimitiveTypeDefinition(type): 215 def CreatePrimitiveTypeDefinition(type):
210 typedefs = { 216 typedefs = {
211 'number': 'double', 217 'number': 'double',
212 'integer': 'int', 218 'integer': 'int',
213 'boolean': 'bool', 219 'boolean': 'bool',
214 } 220 }
221 js_typedefs = {
222 'number': 'number',
223 'integer': 'number',
224 'boolean': 'boolean',
225 }
215 return { 226 return {
227 'js_type': js_typedefs[type],
216 'return_type': typedefs[type], 228 'return_type': typedefs[type],
217 'pass_type': typedefs[type], 229 'pass_type': typedefs[type],
218 'to_pass_type': '%s', 230 'to_pass_type': '%s',
219 'to_raw_type': '%s', 231 'to_raw_type': '%s',
220 'to_raw_return_type': '%s', 232 'to_raw_return_type': '%s',
221 'type': typedefs[type], 233 'type': typedefs[type],
222 'raw_type': typedefs[type], 234 'raw_type': typedefs[type],
223 'raw_pass_type': typedefs[type], 235 'raw_pass_type': typedefs[type],
224 'raw_return_type': typedefs[type], 236 'raw_return_type': typedefs[type],
225 } 237 }
226 238
227 239
228 type_definitions = {} 240 type_definitions = {}
229 type_definitions['number'] = CreatePrimitiveTypeDefinition('number') 241 type_definitions['number'] = CreatePrimitiveTypeDefinition('number')
230 type_definitions['integer'] = CreatePrimitiveTypeDefinition('integer') 242 type_definitions['integer'] = CreatePrimitiveTypeDefinition('integer')
231 type_definitions['boolean'] = CreatePrimitiveTypeDefinition('boolean') 243 type_definitions['boolean'] = CreatePrimitiveTypeDefinition('boolean')
232 type_definitions['string'] = CreateStringTypeDefinition() 244 type_definitions['string'] = CreateStringTypeDefinition()
233 type_definitions['object'] = CreateObjectTypeDefinition() 245 type_definitions['object'] = CreateObjectTypeDefinition()
234 type_definitions['any'] = CreateAnyTypeDefinition() 246 type_definitions['any'] = CreateAnyTypeDefinition()
235 247
236 248
237 def WrapArrayDefinition(type): 249 def WrapArrayDefinition(type):
238 return { 250 return {
251 'js_type': '!Array.<%s>' % type['js_type'],
239 'return_type': 'std::vector<%s>' % type['type'], 252 'return_type': 'std::vector<%s>' % type['type'],
240 'pass_type': 'std::vector<%s>' % type['type'], 253 'pass_type': 'std::vector<%s>' % type['type'],
241 'to_raw_type': '%s', 254 'to_raw_type': '%s',
242 'to_raw_return_type': '&%s', 255 'to_raw_return_type': '&%s',
243 'to_pass_type': 'std::move(%s)', 256 'to_pass_type': 'std::move(%s)',
244 'type': 'std::vector<%s>' % type['type'], 257 'type': 'std::vector<%s>' % type['type'],
245 'raw_type': 'std::vector<%s>' % type['type'], 258 'raw_type': 'std::vector<%s>' % type['type'],
246 'raw_pass_type': 'std::vector<%s>*' % type['type'], 259 'raw_pass_type': 'std::vector<%s>*' % type['type'],
247 'raw_return_type': 'const std::vector<%s>*' % type['type'], 260 'raw_return_type': 'const std::vector<%s>*' % type['type'],
248 } 261 }
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
302 315
303 316
304 def SynthesizeEnumType(domain, owner, type): 317 def SynthesizeEnumType(domain, owner, type):
305 type['id'] = ToTitleCase(owner) + ToTitleCase(type['name']) 318 type['id'] = ToTitleCase(owner) + ToTitleCase(type['name'])
306 type_definitions[domain['domain'] + '.' + type['id']] = ( 319 type_definitions[domain['domain'] + '.' + type['id']] = (
307 CreateEnumTypeDefinition(domain['domain'], type)) 320 CreateEnumTypeDefinition(domain['domain'], type))
308 type['$ref'] = domain['domain'] + '.' + type['id'] 321 type['$ref'] = domain['domain'] + '.' + type['id']
309 domain['types'].append(type) 322 domain['types'].append(type)
310 323
311 324
325 def SynthesizeJsConstructorArgs(properties):
Sami 2017/05/24 09:16:21 Please add some coverage in update client_api_gene
alex clarke (OOO till 29th) 2017/05/24 11:38:14 We don't need this anymore, removing.
326 args = []
327 for prop in properties:
328 if 'optional' in prop and prop['optional']:
329 continue
330 args.append(prop['name'])
331 for prop in properties:
332 if not 'optional' in prop or not prop['optional']:
333 continue
334 args.append('opt_' + prop['name'])
335 return ', '.join(args)
336
337
312 def SynthesizeCommandTypes(json_api): 338 def SynthesizeCommandTypes(json_api):
313 """Generate types for command parameters, return values and enum 339 """Generate types for command parameters, return values and enum
314 properties. 340 properties.
315 """ 341 """
316 for domain in json_api['domains']: 342 for domain in json_api['domains']:
317 if not 'types' in domain: 343 if not 'types' in domain:
318 domain['types'] = [] 344 domain['types'] = []
319 for type in domain['types']: 345 for type in domain['types']:
320 if type['type'] == 'object': 346 if type['type'] == 'object':
321 for property in type.get('properties', []): 347 for property in type.get('properties', []):
322 if 'enum' in property and not '$ref' in property: 348 if 'enum' in property and not '$ref' in property:
323 SynthesizeEnumType(domain, type['id'], property) 349 SynthesizeEnumType(domain, type['id'], property)
324 350
325 for command in domain.get('commands', []): 351 for command in domain.get('commands', []):
326 if 'parameters' in command: 352 if 'parameters' in command:
327 for parameter in command['parameters']: 353 for parameter in command['parameters']:
328 if 'enum' in parameter and not '$ref' in parameter: 354 if 'enum' in parameter and not '$ref' in parameter:
329 SynthesizeEnumType(domain, command['name'], parameter) 355 SynthesizeEnumType(domain, command['name'], parameter)
330 parameters_type = { 356 parameters_type = {
331 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Params', 357 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Params',
332 'type': 'object', 358 'type': 'object',
333 'description': 'Parameters for the %s command.' % ToTitleCase( 359 'description': 'Parameters for the %s command.' % ToTitleCase(
334 SanitizeLiteral(command['name'])), 360 SanitizeLiteral(command['name'])),
335 'properties': command['parameters'] 361 'properties': command['parameters'],
362 'js_constructor_args': SynthesizeJsConstructorArgs(
363 command['parameters'])
336 } 364 }
337 domain['types'].append(parameters_type) 365 domain['types'].append(parameters_type)
338 if 'returns' in command: 366 if 'returns' in command:
339 for parameter in command['returns']: 367 for parameter in command['returns']:
340 if 'enum' in parameter and not '$ref' in parameter: 368 if 'enum' in parameter and not '$ref' in parameter:
341 SynthesizeEnumType(domain, command['name'], parameter) 369 SynthesizeEnumType(domain, command['name'], parameter)
342 result_type = { 370 result_type = {
343 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Result', 371 'id': ToTitleCase(SanitizeLiteral(command['name'])) + 'Result',
344 'type': 'object', 372 'type': 'object',
345 'description': 'Result for the %s command.' % ToTitleCase( 373 'description': 'Result for the %s command.' % ToTitleCase(
346 SanitizeLiteral(command['name'])), 374 SanitizeLiteral(command['name'])),
347 'properties': command['returns'] 375 'properties': command['returns'],
376 'js_constructor_args': SynthesizeJsConstructorArgs(
377 command['returns'])
348 } 378 }
349 domain['types'].append(result_type) 379 domain['types'].append(result_type)
350 380
351 381
352 def SynthesizeEventTypes(json_api): 382 def SynthesizeEventTypes(json_api):
353 """Generate types for events and their properties. 383 """Generate types for events and their properties.
354 384
355 Note that parameter objects are also created for events without parameters to 385 Note that parameter objects are also created for events without parameters to
356 make it easier to introduce parameters later. 386 make it easier to introduce parameters later.
357 """ 387 """
358 for domain in json_api['domains']: 388 for domain in json_api['domains']:
359 if not 'types' in domain: 389 if not 'types' in domain:
360 domain['types'] = [] 390 domain['types'] = []
361 for event in domain.get('events', []): 391 for event in domain.get('events', []):
362 for parameter in event.get('parameters', []): 392 for parameter in event.get('parameters', []):
363 if 'enum' in parameter and not '$ref' in parameter: 393 if 'enum' in parameter and not '$ref' in parameter:
364 SynthesizeEnumType(domain, event['name'], parameter) 394 SynthesizeEnumType(domain, event['name'], parameter)
365 event_type = { 395 event_type = {
366 'id': ToTitleCase(event['name']) + 'Params', 396 'id': ToTitleCase(event['name']) + 'Params',
367 'type': 'object', 397 'type': 'object',
368 'description': 'Parameters for the %s event.' % ToTitleCase( 398 'description': 'Parameters for the %s event.' % ToTitleCase(
369 event['name']), 399 event['name']),
370 'properties': event.get('parameters', []) 400 'properties': event.get('parameters', []),
401 'js_constructor_args': SynthesizeJsConstructorArgs(
402 event.get('parameters', []))
371 } 403 }
372 domain['types'].append(event_type) 404 domain['types'].append(event_type)
373 405
374 406
375 def InitializeDomainDependencies(json_api): 407 def InitializeDomainDependencies(json_api):
376 """For each domain create list of domains given domain depends on, 408 """For each domain create list of domains given domain depends on,
377 including itself.""" 409 including itself."""
378 410
379 direct_deps = collections.defaultdict(set) 411 direct_deps = collections.defaultdict(set)
380 412
381 def GetDomainDepsFromRefs(domain_name, json): 413 def GetDomainDepsFromRefs(domain_name, json):
382 if isinstance(json, list): 414 if isinstance(json, list):
383 for value in json: 415 for value in json:
384 GetDomainDepsFromRefs(domain_name, value) 416 GetDomainDepsFromRefs(domain_name, value)
385 return 417 return
386 418
387 if not isinstance(json, dict): 419 if not isinstance(json, dict):
388 return 420 return
389 for value in json.itervalues(): 421 for value in json.itervalues():
390 GetDomainDepsFromRefs(domain_name, value) 422 GetDomainDepsFromRefs(domain_name, value)
391 423
392 if '$ref' in json: 424 if '$ref' in json:
393 if '.' in json['$ref']: 425 if '.' in json['$ref']:
394 dep = json['$ref'].split('.')[0] 426 dep = json['$ref'].split('.')[0]
395 direct_deps[domain_name].add(dep) 427 direct_deps[domain_name].add(dep)
396 428
397 for domain in json_api['domains']: 429 for domain in json_api['domains']:
398 direct_deps[domain['domain']] = set(domain.get('dependencies', [])) 430 deps = domain.get('dependencies', [])
431 js_dependencies = deps
Sami 2017/05/24 09:16:21 |js_dependencies| unused?
alex clarke (OOO till 29th) 2017/05/24 11:38:14 Done.
432 direct_deps[domain['domain']] = set(deps)
399 GetDomainDepsFromRefs(domain['domain'], domain) 433 GetDomainDepsFromRefs(domain['domain'], domain)
400 434
401 def TraverseDependencies(domain, deps): 435 def TraverseDependencies(domain, deps):
402 if domain in deps: 436 if domain in deps:
403 return 437 return
404 deps.add(domain) 438 deps.add(domain)
405 439
406 for dep in direct_deps[domain]: 440 for dep in direct_deps[domain]:
407 TraverseDependencies(dep, deps) 441 TraverseDependencies(dep, deps)
408 442
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
461 output_file = '%s/%s.%s' % (output_dirname, 495 output_file = '%s/%s.%s' % (output_dirname,
462 domain_name_to_file_name_func(domain_name), 496 domain_name_to_file_name_func(domain_name),
463 file_type) 497 file_type)
464 with open(output_file, 'w') as f: 498 with open(output_file, 'w') as f:
465 f.write(template.render(template_context)) 499 f.write(template.render(template_context))
466 500
467 501
468 def GenerateDomains(jinja_env, output_dirname, json_api): 502 def GenerateDomains(jinja_env, output_dirname, json_api):
469 GeneratePerDomain( 503 GeneratePerDomain(
470 jinja_env, os.path.join(output_dirname, 'devtools', 'domains'), json_api, 504 jinja_env, os.path.join(output_dirname, 'devtools', 'domains'), json_api,
471 'domain', ['cc', 'h'], 505 'domain', ['cc', 'h', 'js'],
472 lambda domain_name: domain_name) 506 lambda domain_name: domain_name)
473 507
474 508
475 def GenerateTypes(jinja_env, output_dirname, json_api): 509 def GenerateTypes(jinja_env, output_dirname, json_api):
476 # Generate forward declarations for types. 510 # Generate forward declarations for types.
477 GeneratePerDomain( 511 GeneratePerDomain(
478 jinja_env, os.path.join(output_dirname, 'devtools', 'internal'), 512 jinja_env, os.path.join(output_dirname, 'devtools', 'internal'),
479 json_api, 'domain_types_forward_declarations', ['h'], 513 json_api, 'domain_types_forward_declarations', ['h'],
480 lambda domain_name: 'types_forward_declarations_%s' % (domain_name, )) 514 lambda domain_name: 'types_forward_declarations_%s' % (domain_name, ))
481 # Generate types on per-domain basis. 515 # Generate types on per-domain basis.
(...skipping 17 matching lines...) Expand all
499 InitializeDomainDependencies(json_api) 533 InitializeDomainDependencies(json_api)
500 PatchExperimentalCommandsAndEvents(json_api) 534 PatchExperimentalCommandsAndEvents(json_api)
501 EnsureCommandsHaveParametersAndReturnTypes(json_api) 535 EnsureCommandsHaveParametersAndReturnTypes(json_api)
502 SynthesizeCommandTypes(json_api) 536 SynthesizeCommandTypes(json_api)
503 SynthesizeEventTypes(json_api) 537 SynthesizeEventTypes(json_api)
504 PatchFullQualifiedRefs(json_api) 538 PatchFullQualifiedRefs(json_api)
505 CreateTypeDefinitions(json_api) 539 CreateTypeDefinitions(json_api)
506 GenerateDomains(jinja_env, output_dirname, json_api) 540 GenerateDomains(jinja_env, output_dirname, json_api)
507 GenerateTypes(jinja_env, output_dirname, json_api) 541 GenerateTypes(jinja_env, output_dirname, json_api)
508 GenerateTypeConversions(jinja_env, output_dirname, json_api) 542 GenerateTypeConversions(jinja_env, output_dirname, json_api)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698