OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Support for client code that needs to interact with the requests, responses | |
7 * and notifications that are part of the analysis server's wire protocol. | |
8 */ | |
9 library analysis_server.plugin.protocol.protocol; | |
10 | |
11 import 'dart:collection'; | |
12 import 'dart:convert' hide JsonDecoder; | |
13 | |
14 import 'package:analysis_server/src/protocol/protocol_internal.dart'; | |
15 | |
16 part 'generated_protocol.dart'; | |
17 | |
18 /** | |
19 * A [RequestHandler] that supports [startup] and [shutdown] methods. | |
20 * | |
21 * Clients may not extend, implement or mix-in this class. | |
22 */ | |
23 abstract class DomainHandler implements RequestHandler { | |
24 /** | |
25 * Perform any operations associated with the shutdown of the domain. It is | |
26 * not guaranteed that this method will be called. If it is, it will be | |
27 * called after the last [Request] has been made. | |
28 */ | |
29 void shutdown() {} | |
30 | |
31 /** | |
32 * Perform any operations associated with the startup of the domain. This | |
33 * will be called before the first [Request]. | |
34 */ | |
35 void startup() {} | |
36 } | |
37 | |
38 /** | |
39 * An interface for enumerated types in the protocol. | |
40 * | |
41 * Clients may not extend, implement or mix-in this class. | |
42 */ | |
43 abstract class Enum { | |
44 /** | |
45 * The name of the enumerated value. This should match the name of the | |
46 * static getter which provides access to this enumerated value. | |
47 */ | |
48 String get name; | |
49 } | |
50 | |
51 /** | |
52 * A notification from the server about an event that occurred. | |
53 * | |
54 * Clients may not extend, implement or mix-in this class. | |
55 */ | |
56 class Notification { | |
57 /** | |
58 * The name of the JSON attribute containing the name of the event that | |
59 * triggered the notification. | |
60 */ | |
61 static const String EVENT = 'event'; | |
62 | |
63 /** | |
64 * The name of the JSON attribute containing the result values. | |
65 */ | |
66 static const String PARAMS = 'params'; | |
67 | |
68 /** | |
69 * The name of the event that triggered the notification. | |
70 */ | |
71 final String event; | |
72 | |
73 /** | |
74 * A table mapping the names of notification parameters to their values, or | |
75 * `null` if there are no notification parameters. | |
76 */ | |
77 Map<String, Object> _params; | |
78 | |
79 /** | |
80 * Initialize a newly created [Notification] to have the given [event] name. | |
81 * If [_params] is provided, it will be used as the params; otherwise no | |
82 * params will be used. | |
83 */ | |
84 Notification(this.event, [this._params]); | |
85 | |
86 /** | |
87 * Initialize a newly created instance based on the given JSON data. | |
88 */ | |
89 factory Notification.fromJson(Map json) { | |
90 return new Notification(json[Notification.EVENT], | |
91 json[Notification.PARAMS] as Map<String, Object>); | |
92 } | |
93 | |
94 /** | |
95 * Return a table representing the structure of the Json object that will be | |
96 * sent to the client to represent this response. | |
97 */ | |
98 Map<String, Object> toJson() { | |
99 Map<String, Object> jsonObject = {}; | |
100 jsonObject[EVENT] = event; | |
101 if (_params != null) { | |
102 jsonObject[PARAMS] = _params; | |
103 } | |
104 return jsonObject; | |
105 } | |
106 } | |
107 | |
108 /** | |
109 * A request that was received from the client. | |
110 * | |
111 * Clients may not extend, implement or mix-in this class. | |
112 */ | |
113 class Request { | |
114 /** | |
115 * The name of the JSON attribute containing the id of the request. | |
116 */ | |
117 static const String ID = 'id'; | |
118 | |
119 /** | |
120 * The name of the JSON attribute containing the name of the request. | |
121 */ | |
122 static const String METHOD = 'method'; | |
123 | |
124 /** | |
125 * The name of the JSON attribute containing the request parameters. | |
126 */ | |
127 static const String PARAMS = 'params'; | |
128 | |
129 /** | |
130 * The name of the optional JSON attribute indicating the time (milliseconds | |
131 * since epoch) at which the client made the request. | |
132 */ | |
133 static const String CLIENT_REQUEST_TIME = 'clientRequestTime'; | |
134 | |
135 /** | |
136 * The unique identifier used to identify this request. | |
137 */ | |
138 final String id; | |
139 | |
140 /** | |
141 * The method being requested. | |
142 */ | |
143 final String method; | |
144 | |
145 /** | |
146 * A table mapping the names of request parameters to their values. | |
147 */ | |
148 final Map<String, Object> _params; | |
149 | |
150 /** | |
151 * The time (milliseconds since epoch) at which the client made the request | |
152 * or `null` if this information is not provided by the client. | |
153 */ | |
154 final int clientRequestTime; | |
155 | |
156 /** | |
157 * Initialize a newly created [Request] to have the given [id] and [method] | |
158 * name. If [params] is supplied, it is used as the "params" map for the | |
159 * request. Otherwise an empty "params" map is allocated. | |
160 */ | |
161 Request(this.id, this.method, | |
162 [Map<String, Object> params, this.clientRequestTime]) | |
163 : _params = params ?? new HashMap<String, Object>(); | |
164 | |
165 /** | |
166 * Return a request parsed from the given json, or `null` if the [data] is | |
167 * not a valid json representation of a request. The [data] is expected to | |
168 * have the following format: | |
169 * | |
170 * { | |
171 * 'clientRequestTime': millisecondsSinceEpoch | |
172 * 'id': String, | |
173 * 'method': methodName, | |
174 * 'params': { | |
175 * paramter_name: value | |
176 * } | |
177 * } | |
178 * | |
179 * where both the parameters and clientRequestTime are optional. | |
180 * | |
181 * The parameters can contain any number of name/value pairs. The | |
182 * clientRequestTime must be an int representing the time at which the client | |
183 * issued the request (milliseconds since epoch). | |
184 */ | |
185 factory Request.fromJson(Map<String, dynamic> result) { | |
186 var id = result[Request.ID]; | |
187 var method = result[Request.METHOD]; | |
188 if (id is! String || method is! String) { | |
189 return null; | |
190 } | |
191 var time = result[Request.CLIENT_REQUEST_TIME]; | |
192 if (time != null && time is! int) { | |
193 return null; | |
194 } | |
195 var params = result[Request.PARAMS]; | |
196 if (params is Map || params == null) { | |
197 return new Request(id, method, params as Map<String, Object>, time); | |
198 } else { | |
199 return null; | |
200 } | |
201 } | |
202 | |
203 /** | |
204 * Return a request parsed from the given [data], or `null` if the [data] is | |
205 * not a valid json representation of a request. The [data] is expected to | |
206 * have the following format: | |
207 * | |
208 * { | |
209 * 'clientRequestTime': millisecondsSinceEpoch | |
210 * 'id': String, | |
211 * 'method': methodName, | |
212 * 'params': { | |
213 * paramter_name: value | |
214 * } | |
215 * } | |
216 * | |
217 * where both the parameters and clientRequestTime are optional. | |
218 * | |
219 * The parameters can contain any number of name/value pairs. The | |
220 * clientRequestTime must be an int representing the time at which the client | |
221 * issued the request (milliseconds since epoch). | |
222 */ | |
223 factory Request.fromString(String data) { | |
224 try { | |
225 var result = JSON.decode(data); | |
226 if (result is Map) { | |
227 return new Request.fromJson(result as Map<String, dynamic>); | |
228 } | |
229 return null; | |
230 } catch (exception) { | |
231 return null; | |
232 } | |
233 } | |
234 | |
235 /** | |
236 * Return a table representing the structure of the Json object that will be | |
237 * sent to the client to represent this response. | |
238 */ | |
239 Map<String, Object> toJson() { | |
240 Map<String, Object> jsonObject = new HashMap<String, Object>(); | |
241 jsonObject[ID] = id; | |
242 jsonObject[METHOD] = method; | |
243 if (_params.isNotEmpty) { | |
244 jsonObject[PARAMS] = _params; | |
245 } | |
246 if (clientRequestTime != null) { | |
247 jsonObject[CLIENT_REQUEST_TIME] = clientRequestTime; | |
248 } | |
249 return jsonObject; | |
250 } | |
251 } | |
252 | |
253 /** | |
254 * An exception that occurred during the handling of a request that requires | |
255 * that an error be returned to the client. | |
256 * | |
257 * Clients may not extend, implement or mix-in this class. | |
258 */ | |
259 class RequestFailure implements Exception { | |
260 /** | |
261 * The response to be returned as a result of the failure. | |
262 */ | |
263 final Response response; | |
264 | |
265 /** | |
266 * Initialize a newly created exception to return the given reponse. | |
267 */ | |
268 RequestFailure(this.response); | |
269 } | |
270 | |
271 /** | |
272 * An object that can handle requests and produce responses for them. | |
273 * | |
274 * Clients may not extend, implement or mix-in this class. | |
275 */ | |
276 abstract class RequestHandler { | |
277 /** | |
278 * Attempt to handle the given [request]. If the request is not recognized by | |
279 * this handler, return `null` so that other handlers will be given a chance | |
280 * to handle it. Otherwise, return the response that should be passed back to | |
281 * the client. | |
282 */ | |
283 Response handleRequest(Request request); | |
284 } | |
285 | |
286 /** | |
287 * A response to a request. | |
288 * | |
289 * Clients may not extend, implement or mix-in this class. | |
290 */ | |
291 class Response { | |
292 /** | |
293 * The [Response] instance that is returned when a real [Response] cannot | |
294 * be provided at the moment. | |
295 */ | |
296 static final Response DELAYED_RESPONSE = new Response('DELAYED_RESPONSE'); | |
297 | |
298 /** | |
299 * The name of the JSON attribute containing the id of the request for which | |
300 * this is a response. | |
301 */ | |
302 static const String ID = 'id'; | |
303 | |
304 /** | |
305 * The name of the JSON attribute containing the error message. | |
306 */ | |
307 static const String ERROR = 'error'; | |
308 | |
309 /** | |
310 * The name of the JSON attribute containing the result values. | |
311 */ | |
312 static const String RESULT = 'result'; | |
313 | |
314 /** | |
315 * The unique identifier used to identify the request that this response is | |
316 * associated with. | |
317 */ | |
318 final String id; | |
319 | |
320 /** | |
321 * The error that was caused by attempting to handle the request, or `null` if | |
322 * there was no error. | |
323 */ | |
324 final RequestError error; | |
325 | |
326 /** | |
327 * A table mapping the names of result fields to their values. Should be | |
328 * `null` if there is no result to send. | |
329 */ | |
330 Map<String, Object> _result; | |
331 | |
332 /** | |
333 * Initialize a newly created instance to represent a response to a request | |
334 * with the given [id]. If [_result] is provided, it will be used as the | |
335 * result; otherwise an empty result will be used. If an [error] is provided | |
336 * then the response will represent an error condition. | |
337 */ | |
338 Response(this.id, {Map<String, Object> result, this.error}) | |
339 : _result = result; | |
340 | |
341 /** | |
342 * Create and return the `DEBUG_PORT_COULD_NOT_BE_OPENED` error response. | |
343 */ | |
344 Response.debugPortCouldNotBeOpened(Request request, dynamic error) | |
345 : this(request.id, | |
346 error: new RequestError( | |
347 RequestErrorCode.DEBUG_PORT_COULD_NOT_BE_OPENED, '$error')); | |
348 | |
349 /** | |
350 * Initialize a newly created instance to represent the FILE_NOT_ANALYZED | |
351 * error condition. | |
352 */ | |
353 Response.fileNotAnalyzed(Request request, String file) | |
354 : this(request.id, | |
355 error: new RequestError(RequestErrorCode.FILE_NOT_ANALYZED, | |
356 'File is not analyzed: $file.')); | |
357 | |
358 /** | |
359 * Initialize a newly created instance to represent the FORMAT_INVALID_FILE | |
360 * error condition. | |
361 */ | |
362 Response.formatInvalidFile(Request request) | |
363 : this(request.id, | |
364 error: new RequestError(RequestErrorCode.FORMAT_INVALID_FILE, | |
365 'Error during `edit.format`: invalid file.')); | |
366 | |
367 /** | |
368 * Initialize a newly created instance to represent the FORMAT_WITH_ERROR | |
369 * error condition. | |
370 */ | |
371 Response.formatWithErrors(Request request) | |
372 : this(request.id, | |
373 error: new RequestError(RequestErrorCode.FORMAT_WITH_ERRORS, | |
374 'Error during `edit.format`: source contains syntax errors.')); | |
375 | |
376 /** | |
377 * Initialize a newly created instance based on the given JSON data. | |
378 */ | |
379 factory Response.fromJson(Map json) { | |
380 try { | |
381 Object id = json[Response.ID]; | |
382 if (id is! String) { | |
383 return null; | |
384 } | |
385 Object error = json[Response.ERROR]; | |
386 RequestError decodedError; | |
387 if (error is Map) { | |
388 decodedError = new RequestError.fromJson( | |
389 new ResponseDecoder(null), '.error', error); | |
390 } | |
391 Object result = json[Response.RESULT]; | |
392 Map<String, Object> decodedResult; | |
393 if (result is Map) { | |
394 decodedResult = result as Map<String, Object>; | |
395 } | |
396 return new Response(id, error: decodedError, result: decodedResult); | |
397 } catch (exception) { | |
398 return null; | |
399 } | |
400 } | |
401 | |
402 /** | |
403 * Initialize a newly created instance to represent the | |
404 * GET_ERRORS_INVALID_FILE error condition. | |
405 */ | |
406 Response.getErrorsInvalidFile(Request request) | |
407 : this(request.id, | |
408 error: new RequestError(RequestErrorCode.GET_ERRORS_INVALID_FILE, | |
409 'Error during `analysis.getErrors`: invalid file.')); | |
410 | |
411 /** | |
412 * Initialize a newly created instance to represent the | |
413 * GET_NAVIGATION_INVALID_FILE error condition. | |
414 */ | |
415 Response.getNavigationInvalidFile(Request request) | |
416 : this(request.id, | |
417 error: new RequestError( | |
418 RequestErrorCode.GET_NAVIGATION_INVALID_FILE, | |
419 'Error during `analysis.getNavigation`: invalid file.')); | |
420 | |
421 /** | |
422 * Initialize a newly created instance to represent the | |
423 * GET_REACHABLE_SOURCES_INVALID_FILE error condition. | |
424 */ | |
425 Response.getReachableSourcesInvalidFile(Request request) | |
426 : this(request.id, | |
427 error: new RequestError( | |
428 RequestErrorCode.GET_REACHABLE_SOURCES_INVALID_FILE, | |
429 'Error during `analysis.getReachableSources`: invalid file.')); | |
430 | |
431 /** | |
432 * Initialize a newly created instance to represent an error condition caused | |
433 * by an analysis.reanalyze [request] that specifies an analysis root that is | |
434 * not in the current list of analysis roots. | |
435 */ | |
436 Response.invalidAnalysisRoot(Request request, String rootPath) | |
437 : this(request.id, | |
438 error: new RequestError(RequestErrorCode.INVALID_ANALYSIS_ROOT, | |
439 "Invalid analysis root: $rootPath")); | |
440 | |
441 /** | |
442 * Initialize a newly created instance to represent an error condition caused | |
443 * by a [request] that specifies an execution context whose context root does | |
444 * not exist. | |
445 */ | |
446 Response.invalidExecutionContext(Request request, String contextId) | |
447 : this(request.id, | |
448 error: new RequestError(RequestErrorCode.INVALID_EXECUTION_CONTEXT, | |
449 "Invalid execution context: $contextId")); | |
450 | |
451 /** | |
452 * Initialize a newly created instance to represent the | |
453 * INVALID_FILE_PATH_FORMAT error condition. | |
454 */ | |
455 Response.invalidFilePathFormat(Request request, path) | |
456 : this(request.id, | |
457 error: new RequestError(RequestErrorCode.INVALID_FILE_PATH_FORMAT, | |
458 'Invalid file path format: $path')); | |
459 | |
460 /** | |
461 * Initialize a newly created instance to represent an error condition caused | |
462 * by a [request] that had invalid parameter. [path] is the path to the | |
463 * invalid parameter, in Javascript notation (e.g. "foo.bar" means that the | |
464 * parameter "foo" contained a key "bar" whose value was the wrong type). | |
465 * [expectation] is a description of the type of data that was expected. | |
466 */ | |
467 Response.invalidParameter(Request request, String path, String expectation) | |
468 : this(request.id, | |
469 error: new RequestError(RequestErrorCode.INVALID_PARAMETER, | |
470 "Invalid parameter '$path'. $expectation.")); | |
471 | |
472 /** | |
473 * Initialize a newly created instance to represent an error condition caused | |
474 * by a malformed request. | |
475 */ | |
476 Response.invalidRequestFormat() | |
477 : this('', | |
478 error: new RequestError( | |
479 RequestErrorCode.INVALID_REQUEST, 'Invalid request')); | |
480 | |
481 /** | |
482 * Initialize a newly created instance to represent an error condition caused | |
483 * by a request that requires an index, but indexing is disabled. | |
484 */ | |
485 Response.noIndexGenerated(Request request) | |
486 : this(request.id, | |
487 error: new RequestError( | |
488 RequestErrorCode.NO_INDEX_GENERATED, 'Indexing is disabled')); | |
489 | |
490 /** | |
491 * Initialize a newly created instance to represent the | |
492 * ORGANIZE_DIRECTIVES_ERROR error condition. | |
493 */ | |
494 Response.organizeDirectivesError(Request request, String message) | |
495 : this(request.id, | |
496 error: new RequestError( | |
497 RequestErrorCode.ORGANIZE_DIRECTIVES_ERROR, message)); | |
498 | |
499 /** | |
500 * Initialize a newly created instance to represent the | |
501 * REFACTORING_REQUEST_CANCELLED error condition. | |
502 */ | |
503 Response.refactoringRequestCancelled(Request request) | |
504 : this(request.id, | |
505 error: new RequestError( | |
506 RequestErrorCode.REFACTORING_REQUEST_CANCELLED, | |
507 'The `edit.getRefactoring` request was cancelled.')); | |
508 | |
509 /** | |
510 * Initialize a newly created instance to represent the SERVER_ERROR error | |
511 * condition. | |
512 */ | |
513 factory Response.serverError(Request request, exception, stackTrace) { | |
514 RequestError error = | |
515 new RequestError(RequestErrorCode.SERVER_ERROR, exception.toString()); | |
516 if (stackTrace != null) { | |
517 error.stackTrace = stackTrace.toString(); | |
518 } | |
519 return new Response(request.id, error: error); | |
520 } | |
521 | |
522 /** | |
523 * Initialize a newly created instance to represent the | |
524 * SORT_MEMBERS_INVALID_FILE error condition. | |
525 */ | |
526 Response.sortMembersInvalidFile(Request request) | |
527 : this(request.id, | |
528 error: new RequestError(RequestErrorCode.SORT_MEMBERS_INVALID_FILE, | |
529 'Error during `edit.sortMembers`: invalid file.')); | |
530 | |
531 /** | |
532 * Initialize a newly created instance to represent the | |
533 * SORT_MEMBERS_PARSE_ERRORS error condition. | |
534 */ | |
535 Response.sortMembersParseErrors(Request request, int numErrors) | |
536 : this(request.id, | |
537 error: new RequestError(RequestErrorCode.SORT_MEMBERS_PARSE_ERRORS, | |
538 'Error during `edit.sortMembers`: file has $numErrors scan/parse
errors.')); | |
539 | |
540 /** | |
541 * Initialize a newly created instance to represent an error condition caused | |
542 * by a `analysis.setPriorityFiles` [request] that includes one or more files | |
543 * that are not being analyzed. | |
544 */ | |
545 Response.unanalyzedPriorityFiles(String requestId, String fileNames) | |
546 : this(requestId, | |
547 error: new RequestError(RequestErrorCode.UNANALYZED_PRIORITY_FILES, | |
548 "Unanalyzed files cannot be a priority: '$fileNames'")); | |
549 | |
550 /** | |
551 * Initialize a newly created instance to represent an error condition caused | |
552 * by a [request] that cannot be handled by any known handlers. | |
553 */ | |
554 Response.unknownRequest(Request request) | |
555 : this(request.id, | |
556 error: new RequestError( | |
557 RequestErrorCode.UNKNOWN_REQUEST, 'Unknown request')); | |
558 | |
559 /** | |
560 * Initialize a newly created instance to represent an error condition caused | |
561 * by a [request] referencing a source that does not exist. | |
562 */ | |
563 Response.unknownSource(Request request) | |
564 : this(request.id, | |
565 error: new RequestError( | |
566 RequestErrorCode.UNKNOWN_SOURCE, 'Unknown source')); | |
567 | |
568 /** | |
569 * Initialize a newly created instance to represent an error condition caused | |
570 * by a [request] for a service that is not supported. | |
571 */ | |
572 Response.unsupportedFeature(String requestId, String message) | |
573 : this(requestId, | |
574 error: new RequestError( | |
575 RequestErrorCode.UNSUPPORTED_FEATURE, message)); | |
576 | |
577 /** | |
578 * Return a table mapping the names of result fields to their values. Should | |
579 * be `null` if there is no result to send. | |
580 */ | |
581 Map<String, Object> get result => _result; | |
582 | |
583 /** | |
584 * Return a table representing the structure of the Json object that will be | |
585 * sent to the client to represent this response. | |
586 */ | |
587 Map<String, Object> toJson() { | |
588 Map<String, Object> jsonObject = new HashMap<String, Object>(); | |
589 jsonObject[ID] = id; | |
590 if (error != null) { | |
591 jsonObject[ERROR] = error.toJson(); | |
592 } | |
593 if (_result != null) { | |
594 jsonObject[RESULT] = _result; | |
595 } | |
596 return jsonObject; | |
597 } | |
598 } | |
OLD | NEW |