Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library http_server; | 5 library http_server; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:io'; | 8 import 'dart:io'; |
| 9 import 'dart:isolate'; | 9 import 'dart:isolate'; |
| 10 import 'dart:uri'; | 10 import 'dart:uri'; |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 30 /// In case a path does not refer to a file but rather to a directory, a | 30 /// In case a path does not refer to a file but rather to a directory, a |
| 31 /// directory listing will be displayed. | 31 /// directory listing will be displayed. |
| 32 | 32 |
| 33 const PREFIX_BUILDDIR = 'root_build'; | 33 const PREFIX_BUILDDIR = 'root_build'; |
| 34 const PREFIX_DARTDIR = 'root_dart'; | 34 const PREFIX_DARTDIR = 'root_dart'; |
| 35 | 35 |
| 36 // TODO(kustermann,ricow): We could change this to the following scheme: | 36 // TODO(kustermann,ricow): We could change this to the following scheme: |
| 37 // http://host:port/root_packages/X -> $BuildDir/packages/X | 37 // http://host:port/root_packages/X -> $BuildDir/packages/X |
| 38 // Issue: 8368 | 38 // Issue: 8368 |
| 39 | 39 |
| 40 | |
| 41 main() { | 40 main() { |
| 42 /** Convenience method for local testing. */ | 41 /** Convenience method for local testing. */ |
| 43 var parser = new ArgParser(); | 42 var parser = new ArgParser(); |
| 44 parser.addOption('port', abbr: 'p', | 43 parser.addOption('port', abbr: 'p', |
| 45 help: 'The main server port we wish to respond to requests.', | 44 help: 'The main server port we wish to respond to requests.', |
| 46 defaultsTo: '0'); | 45 defaultsTo: '0'); |
| 47 parser.addOption('crossOriginPort', abbr: 'c', | 46 parser.addOption('crossOriginPort', abbr: 'c', |
| 48 help: 'A different port that accepts request from the main server port.', | 47 help: 'A different port that accepts request from the main server port.', |
| 49 defaultsTo: '0'); | 48 defaultsTo: '0'); |
| 50 parser.addOption('mode', abbr: 'm', help: 'Testing mode.', | |
| 51 defaultsTo: 'release'); | |
| 52 parser.addOption('arch', abbr: 'a', help: 'Testing architecture.', | |
| 53 defaultsTo: 'ia32'); | |
| 54 parser.addFlag('help', abbr: 'h', negatable: false, | 49 parser.addFlag('help', abbr: 'h', negatable: false, |
| 55 help: 'Print this usage information.'); | 50 help: 'Print this usage information.'); |
| 56 parser.addOption('package-root', help: 'The package root to use.'); | 51 parser.addOption('build-directory', help: 'The package root to use.'); |
| 57 parser.addOption('network', help: 'The network interface to use.', | 52 parser.addOption('network', help: 'The network interface to use.', |
| 58 defaultsTo: '127.0.0.1'); | 53 defaultsTo: '127.0.0.1'); |
| 59 var args = parser.parse(new Options().arguments); | 54 var args = parser.parse(new Options().arguments); |
| 60 if (args['help']) { | 55 if (args['help']) { |
| 61 print(parser.getUsage()); | 56 print(parser.getUsage()); |
| 62 } else { | 57 } else { |
| 63 // Pretend we're running test.dart so that TestUtils doesn't get confused | 58 // Pretend we're running test.dart so that TestUtils doesn't get confused |
| 64 // about the "current directory." This is only used if we're trying to run | 59 // about the "current directory." This is only used if we're trying to run |
| 65 // this file independently for local testing. | 60 // this file independently for local testing. |
| 66 TestUtils.testScriptPath = new Path(new Options().script) | 61 TestUtils.testScriptPath = new Path(new Options().script) |
| 67 .directoryPath | 62 .directoryPath |
| 68 .join(new Path('../../test.dart')) | 63 .join(new Path('../../test.dart')) |
| 69 .canonicalize() | 64 .canonicalize() |
| 70 .toNativePath(); | 65 .toNativePath(); |
| 71 // Note: args['package-root'] is always the build directory. We have the | 66 var servers = new TestingServers(new Path(args['build-directory'])); |
| 72 // implicit assumption that it contains the 'packages' subdirectory. | 67 var port = int.parse(args['port']); |
| 73 // TODO: We should probably rename 'package-root' to 'build-directory'. | 68 var crossOriginPort = int.parse(args['crossOriginPort']); |
| 74 TestingServerRunner._packageRootDir = new Path(args['package-root']); | 69 servers.startServers(args['network'], |
| 75 TestingServerRunner._buildDirectory = new Path(args['package-root']); | 70 port: port, |
| 76 var network = args['network']; | 71 crossOriginPort: crossOriginPort); |
| 77 TestingServerRunner.startHttpServer(network, | 72 DebugLogger.info('Server listening on port ${servers.port}'); |
| 78 port: int.parse(args['port'])); | 73 DebugLogger.info('Server listening on port ${servers.crossOriginPort}'); |
| 79 print('Server listening on port ' | |
| 80 '${TestingServerRunner.serverList[0].port}.'); | |
| 81 TestingServerRunner.startHttpServer(network, | |
| 82 allowedPort: TestingServerRunner.serverList[0].port, port: | |
| 83 int.parse(args['crossOriginPort'])); | |
| 84 print( | |
| 85 'Server listening on port ${TestingServerRunner.serverList[1].port}.'); | |
| 86 } | 74 } |
| 87 } | 75 } |
| 76 | |
| 88 /** | 77 /** |
| 89 * Runs a set of servers that are initialized specifically for the needs of our | 78 * Runs a set of servers that are initialized specifically for the needs of our |
| 90 * test framework, such as dealing with package-root. | 79 * test framework, such as dealing with package-root. |
| 91 */ | 80 */ |
| 92 class TestingServerRunner { | 81 class TestingServers { |
| 93 static List serverList = []; | 82 List _serverList = []; |
| 94 static Path _packageRootDir = null; | 83 Path _buildDirectory = null; |
| 95 static Path _buildDirectory = null; | |
| 96 | 84 |
| 97 // Added as a getter so that the function will be called again each time the | 85 TestingServers(Path buildDirectory) { |
| 98 // default request handler closure is executed. | 86 _buildDirectory = TestUtils.absolutePath(buildDirectory); |
| 99 static Path get packageRootDir => _packageRootDir; | |
| 100 static Path get buildDirectory => _buildDirectory; | |
| 101 | |
| 102 static setPackageRootDir(Map configuration) { | |
| 103 _packageRootDir = TestUtils.absolutePath( | |
| 104 new Path(TestUtils.buildDir(configuration))); | |
| 105 } | 87 } |
| 106 | 88 |
| 107 static setBuildDir(Map configuration) { | 89 int get port => _serverList[0].port; |
| 108 _buildDirectory = TestUtils.absolutePath( | 90 int get crossOriginPort => _serverList[1].port; |
| 109 new Path(TestUtils.buildDir(configuration))); | 91 |
| 92 /** | |
| 93 * [startHttpServer] will start two Http servers. | |
| 94 * The first server listens on [port] and sets | |
| 95 * "Access-Control-Allow-Origin: *" | |
| 96 * The second server listens on [crossOriginPort] and sets | |
| 97 * "Access-Control-Allow-Origin: client:port1 | |
| 98 * "Access-Control-Allow-Credentials: true" | |
| 99 */ | |
| 100 void startServers(String host, {int port: 0, int crossOriginPort: 0}) { | |
| 101 _startHttpServer(host, port: port); | |
| 102 _startHttpServer(host, | |
| 103 port: crossOriginPort, | |
| 104 allowedPort:_serverList[0].port); | |
| 110 } | 105 } |
| 111 | 106 |
| 112 static startHttpServer(String host, {int allowedPort:-1, int port: 0}) { | 107 String httpServerCommandline() { |
|
Emily Fortuna
2013/02/22 17:45:30
:-)
| |
| 108 var dart = TestUtils.dartTestExecutable.toNativePath(); | |
| 109 var dartDir = TestUtils.dartDir(); | |
| 110 var script = dartDir.join(new Path("tools/testing/dart/http_server.dart")); | |
| 111 var buildDirectory = _buildDirectory.toNativePath(); | |
| 112 | |
| 113 return '$dart $script -p $port -c $crossOriginPort ' | |
| 114 '--build-directory=$buildDirectory'; | |
| 115 } | |
| 116 | |
| 117 void stopServers() { | |
| 118 for (var server in _serverList) { | |
| 119 server.close(); | |
| 120 } | |
| 121 } | |
| 122 | |
| 123 void _startHttpServer(String host, {int port: 0, int allowedPort: -1}) { | |
| 113 var httpServer = new HttpServer(); | 124 var httpServer = new HttpServer(); |
| 114 httpServer.onError = (e) { | 125 httpServer.onError = (e) { |
| 115 DebugLogger.error('HttpServer: an error occured: $e'); | 126 DebugLogger.error('HttpServer: an error occured: $e'); |
| 116 }; | 127 }; |
| 117 httpServer.defaultRequestHandler = (request, response) { | 128 httpServer.defaultRequestHandler = (request, response) { |
| 118 handleFileOrDirectoryRequest(request, response, allowedPort); | 129 _handleFileOrDirectoryRequest(request, response, allowedPort); |
| 119 }; | 130 }; |
| 120 httpServer.addRequestHandler( | 131 httpServer.addRequestHandler( |
| 121 (req) => req.path == "/echo", handleEchoRequest); | 132 (req) => req.path == "/echo", _handleEchoRequest); |
| 122 | 133 |
| 123 httpServer.listen(host, port); | 134 httpServer.listen(host, port); |
| 124 serverList.add(httpServer); | 135 _serverList.add(httpServer); |
| 125 } | 136 } |
| 126 | 137 |
| 127 | 138 void _handleFileOrDirectoryRequest(HttpRequest request, |
| 128 static void handleFileOrDirectoryRequest(HttpRequest request, | 139 HttpResponse response, |
| 129 HttpResponse response, | 140 int allowedPort) { |
| 130 int allowedPort) { | 141 var path = _getFilePathFromRequestPath(request.path); |
| 131 var path = getFilePathFromRequestPath(request.path); | |
| 132 if (path != null) { | 142 if (path != null) { |
| 133 var file = new File.fromPath(path); | 143 var file = new File.fromPath(path); |
| 134 file.exists().then((exists) { | 144 file.exists().then((exists) { |
| 135 if (exists) { | 145 if (exists) { |
| 136 sendFileContent(request, response, allowedPort, path, file); | 146 _sendFileContent(request, response, allowedPort, path, file); |
| 137 } else { | 147 } else { |
| 138 var directory = new Directory.fromPath(path); | 148 var directory = new Directory.fromPath(path); |
| 139 directory.exists().then((exists) { | 149 directory.exists().then((exists) { |
| 140 if (exists) { | 150 if (exists) { |
| 141 listDirectory(directory).then((entries) { | 151 _listDirectory(directory).then((entries) { |
| 142 sendDirectoryListing(entries, request, response); | 152 _sendDirectoryListing(entries, request, response); |
| 143 }); | 153 }); |
| 144 } else { | 154 } else { |
| 145 sendNotFound(request, response); | 155 _sendNotFound(request, response); |
| 146 } | 156 } |
| 147 }); | 157 }); |
| 148 } | 158 } |
| 149 }); | 159 }); |
| 150 } else { | 160 } else { |
| 151 if (request.path == '/') { | 161 if (request.path == '/') { |
| 152 var entries = [new _Entry('root_dart', 'root_dart/'), | 162 var entries = [new _Entry('root_dart', 'root_dart/'), |
| 153 new _Entry('root_build', 'root_build/'), | 163 new _Entry('root_build', 'root_build/'), |
| 154 new _Entry('echo', 'echo')]; | 164 new _Entry('echo', 'echo')]; |
| 155 sendDirectoryListing(entries, request, response); | 165 _sendDirectoryListing(entries, request, response); |
| 156 } else { | 166 } else { |
| 157 sendNotFound(request, response); | 167 _sendNotFound(request, response); |
| 158 } | 168 } |
| 159 } | 169 } |
| 160 } | 170 } |
| 161 | 171 |
| 162 static void handleEchoRequest(HttpRequest request, HttpResponse response) { | 172 void _handleEchoRequest(HttpRequest request, HttpResponse response) { |
| 163 response.headers.set("Access-Control-Allow-Origin", "*"); | 173 response.headers.set("Access-Control-Allow-Origin", "*"); |
| 164 request.inputStream.pipe(response.outputStream); | 174 request.inputStream.pipe(response.outputStream); |
| 165 } | 175 } |
| 166 | 176 |
| 167 static Path getFilePathFromRequestPath(String urlRequestPath) { | 177 Path _getFilePathFromRequestPath(String urlRequestPath) { |
| 168 // Go to the top of the file to see an explanation of the URL path scheme. | 178 // Go to the top of the file to see an explanation of the URL path scheme. |
| 169 var requestPath = new Path(urlRequestPath.substring(1)).canonicalize(); | 179 var requestPath = new Path(urlRequestPath.substring(1)).canonicalize(); |
| 170 var pathSegments = requestPath.segments(); | 180 var pathSegments = requestPath.segments(); |
| 171 if (pathSegments.length > 0) { | 181 if (pathSegments.length > 0) { |
| 172 var basePath; | 182 var basePath; |
| 173 var relativePath; | 183 var relativePath; |
| 174 if (pathSegments[0] == PREFIX_BUILDDIR) { | 184 if (pathSegments[0] == PREFIX_BUILDDIR) { |
| 175 basePath = _buildDirectory; | 185 basePath = _buildDirectory; |
| 176 relativePath = new Path( | 186 relativePath = new Path( |
| 177 pathSegments.getRange(1, pathSegments.length - 1).join('/')); | 187 pathSegments.getRange(1, pathSegments.length - 1).join('/')); |
| 178 } else if (pathSegments[0] == PREFIX_DARTDIR) { | 188 } else if (pathSegments[0] == PREFIX_DARTDIR) { |
| 179 basePath = TestUtils.dartDir(); | 189 basePath = TestUtils.dartDir(); |
| 180 relativePath = new Path( | 190 relativePath = new Path( |
| 181 pathSegments.getRange(1, pathSegments.length - 1).join('/')); | 191 pathSegments.getRange(1, pathSegments.length - 1).join('/')); |
| 182 } | 192 } |
| 183 var packagesDirName = 'packages'; | 193 var packagesDirName = 'packages'; |
| 184 var packagesIndex = pathSegments.indexOf(packagesDirName); | 194 var packagesIndex = pathSegments.indexOf(packagesDirName); |
| 185 if (packagesIndex != -1) { | 195 if (packagesIndex != -1) { |
| 186 var start = packagesIndex + 1; | 196 var start = packagesIndex + 1; |
| 187 var length = pathSegments.length - start; | 197 var length = pathSegments.length - start; |
| 188 basePath = _packageRootDir.append(packagesDirName); | 198 basePath = _buildDirectory.append(packagesDirName); |
| 189 relativePath = new Path( | 199 relativePath = new Path( |
| 190 pathSegments.getRange(start, length).join('/')); | 200 pathSegments.getRange(start, length).join('/')); |
| 191 } | 201 } |
| 192 if (basePath != null && relativePath != null) { | 202 if (basePath != null && relativePath != null) { |
| 193 return basePath.join(relativePath); | 203 return basePath.join(relativePath); |
| 194 } | 204 } |
| 195 } | 205 } |
| 196 return null; | 206 return null; |
| 197 } | 207 } |
| 198 | 208 |
| 199 static Future<List<_Entry>> listDirectory(Directory directory) { | 209 Future<List<_Entry>> _listDirectory(Directory directory) { |
| 200 var completer = new Completer(); | 210 var completer = new Completer(); |
| 201 var entries = []; | 211 var entries = []; |
| 202 | 212 |
| 203 directory.list() | 213 directory.list() |
| 204 ..onFile = (filepath) { | 214 ..onFile = (filepath) { |
| 205 var filename = new Path(filepath).filename; | 215 var filename = new Path(filepath).filename; |
| 206 entries.add(new _Entry(filename, filename)); | 216 entries.add(new _Entry(filename, filename)); |
| 207 } | 217 } |
| 208 ..onDir = (dirpath) { | 218 ..onDir = (dirpath) { |
| 209 var filename = new Path(dirpath).filename; | 219 var filename = new Path(dirpath).filename; |
| 210 entries.add(new _Entry(filename, '$filename/')); | 220 entries.add(new _Entry(filename, '$filename/')); |
| 211 } | 221 } |
| 212 ..onDone = (_) { | 222 ..onDone = (_) { |
| 213 completer.complete(entries); | 223 completer.complete(entries); |
| 214 }; | 224 }; |
| 215 return completer.future; | 225 return completer.future; |
| 216 } | 226 } |
| 217 | 227 |
| 218 /** | 228 void _sendDirectoryListing(List<_Entry> entries, |
| 219 * Sends a simple listing of all the files and sub-directories within | 229 HttpRequest request, |
| 220 * directory. | 230 HttpResponse response) { |
| 221 * | |
| 222 * This is intended to make it easier to browse tests when manually running | |
|
Emily Fortuna
2013/02/22 17:45:30
why delete the documentation here? The functionali
kustermann
2013/02/22 18:16:38
Well it did change a little bit (but not in this C
Emily Fortuna
2013/02/22 18:42:51
Err on the side of keeping the documentation that
kustermann
2013/02/25 11:51:22
IMHO, the people that are unaware of what the http
| |
| 223 * tests against this test server. | |
| 224 */ | |
| 225 static void sendDirectoryListing(entries, | |
| 226 HttpRequest request, | |
| 227 HttpResponse response) { | |
| 228 response.headers.set('Content-Type', 'text/html'); | 231 response.headers.set('Content-Type', 'text/html'); |
| 229 var header = '''<!DOCTYPE html> | 232 var header = '''<!DOCTYPE html> |
| 230 <html> | 233 <html> |
| 231 <head> | 234 <head> |
| 232 <title>${request.path}</title> | 235 <title>${request.path}</title> |
| 233 </head> | 236 </head> |
| 234 <body> | 237 <body> |
| 235 <code> | 238 <code> |
| 236 <div>${request.path}</div> | 239 <div>${request.path}</div> |
| 237 <hr/> | 240 <hr/> |
| 238 <ul>'''; | 241 <ul>'''; |
| 239 var footer = ''' | 242 var footer = ''' |
| 240 </ul> | 243 </ul> |
| 241 </code> | 244 </code> |
| 242 </body> | 245 </body> |
| 243 </html>'''; | 246 </html>'''; |
| 244 | 247 |
| 245 | 248 |
| 246 entries.sort(); | 249 entries.sort(); |
| 247 response.outputStream.writeString(header); | 250 response.outputStream.writeString(header); |
| 248 for (var entry in entries) { | 251 for (var entry in entries) { |
| 249 response.outputStream.writeString( | 252 response.outputStream.writeString( |
| 250 '<li><a href="${new Path(request.path).append(entry.name)}">' | 253 '<li><a href="${new Path(request.path).append(entry.name)}">' |
| 251 '${entry.displayName}</a></li>'); | 254 '${entry.displayName}</a></li>'); |
| 252 } | 255 } |
| 253 response.outputStream.writeString(footer); | 256 response.outputStream.writeString(footer); |
| 254 response.outputStream.close(); | 257 response.outputStream.close(); |
| 255 } | 258 } |
| 256 | 259 |
| 257 static void sendFileContent(HttpRequest request, | 260 void _sendFileContent(HttpRequest request, |
| 258 HttpResponse response, | 261 HttpResponse response, |
| 259 int allowedPort, | 262 int allowedPort, |
| 260 Path path, | 263 Path path, |
| 261 File file) { | 264 File file) { |
| 262 if (allowedPort != -1) { | 265 if (allowedPort != -1) { |
| 263 var origin = new Uri(request.headers.value('Origin')); | 266 var origin = new Uri(request.headers.value('Origin')); |
| 264 // Allow loading from http://*:$allowedPort in browsers. | 267 // Allow loading from http://*:$allowedPort in browsers. |
| 265 var allowedOrigin = | 268 var allowedOrigin = |
| 266 '${origin.scheme}://${origin.domain}:${allowedPort}'; | 269 '${origin.scheme}://${origin.domain}:${allowedPort}'; |
| 267 response.headers.set("Access-Control-Allow-Origin", allowedOrigin); | 270 response.headers.set("Access-Control-Allow-Origin", allowedOrigin); |
| 268 response.headers.set('Access-Control-Allow-Credentials', 'true'); | 271 response.headers.set('Access-Control-Allow-Credentials', 'true'); |
| 269 } else { | 272 } else { |
| 270 // No allowedPort specified. Allow from anywhere (but cross-origin | 273 // No allowedPort specified. Allow from anywhere (but cross-origin |
| 271 // requests *with credentials* will fail because you can't use "*"). | 274 // requests *with credentials* will fail because you can't use "*"). |
| 272 response.headers.set("Access-Control-Allow-Origin", "*"); | 275 response.headers.set("Access-Control-Allow-Origin", "*"); |
| 273 } | 276 } |
| 274 if (path.filename.endsWith('.html')) { | 277 if (path.filename.endsWith('.html')) { |
| 275 response.headers.set('Content-Type', 'text/html'); | 278 response.headers.set('Content-Type', 'text/html'); |
| 276 } else if (path.filename.endsWith('.js')) { | 279 } else if (path.filename.endsWith('.js')) { |
| 277 response.headers.set('Content-Type', 'application/javascript'); | 280 response.headers.set('Content-Type', 'application/javascript'); |
| 278 } else if (path.filename.endsWith('.dart')) { | 281 } else if (path.filename.endsWith('.dart')) { |
| 279 response.headers.set('Content-Type', 'application/dart'); | 282 response.headers.set('Content-Type', 'application/dart'); |
| 280 } | 283 } |
| 281 file.openInputStream().pipe(response.outputStream); | 284 file.openInputStream().pipe(response.outputStream); |
| 282 } | 285 } |
| 283 | 286 |
| 284 static void sendNotFound(HttpRequest request, HttpResponse response) { | 287 void _sendNotFound(HttpRequest request, HttpResponse response) { |
| 285 // NOTE: Since some tests deliberately try to access non-existent files. | 288 // NOTE: Since some tests deliberately try to access non-existent files. |
| 286 // We might want to remove this warning (otherwise it will show | 289 // We might want to remove this warning (otherwise it will show |
| 287 // up in the debug.log every time). | 290 // up in the debug.log every time). |
| 288 DebugLogger.warning('HttpServer: could not find file for request path: ' | 291 DebugLogger.warning('HttpServer: could not find file for request path: ' |
| 289 '"${request.path}"'); | 292 '"${request.path}"'); |
| 290 response.statusCode = HttpStatus.NOT_FOUND; | 293 response.statusCode = HttpStatus.NOT_FOUND; |
| 291 try { | 294 try { |
| 292 response.outputStream.close(); | 295 response.outputStream.close(); |
| 293 } catch (e) { | 296 } catch (e) { |
| 294 if (e is StreamException) { | 297 if (e is StreamException) { |
| 295 DebugLogger.warning('HttpServer: error while closing the response ' | 298 DebugLogger.warning('HttpServer: error while closing the response ' |
| 296 'stream: $e'); | 299 'stream: $e'); |
| 297 } else { | 300 } else { |
| 298 throw e; | 301 throw e; |
| 299 } | 302 } |
| 300 } | 303 } |
| 301 } | 304 } |
| 302 | |
| 303 static terminateHttpServers() { | |
| 304 for (var server in serverList) server.close(); | |
| 305 } | |
| 306 } | 305 } |
| 307 | 306 |
| 308 // Helper class for displaying directory listings. | 307 // Helper class for displaying directory listings. |
| 309 class _Entry { | 308 class _Entry { |
| 310 final String name; | 309 final String name; |
| 311 final String displayName; | 310 final String displayName; |
| 312 | 311 |
| 313 _Entry(this.name, this.displayName); | 312 _Entry(this.name, this.displayName); |
| 314 | 313 |
| 315 int compareTo(_Entry other) { | 314 int compareTo(_Entry other) { |
| 316 return name.compareTo(other.name); | 315 return name.compareTo(other.name); |
| 317 } | 316 } |
| 318 } | 317 } |
| OLD | NEW |