| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 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 | 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 get.handler; | 5 library get.handler; |
| 6 | 6 |
| 7 import 'dart:convert'; | 7 import 'dart:convert'; |
| 8 import 'dart:io'; | 8 import 'dart:io'; |
| 9 import 'dart:math'; | 9 import 'dart:math'; |
| 10 | 10 |
| 11 import 'package:analysis_server/src/analysis_server.dart'; | 11 import 'package:analysis_server/src/analysis_server.dart'; |
| 12 import 'package:analysis_server/src/domain_completion.dart'; |
| 13 import 'package:analysis_server/src/services/completion/completion_manager.dart'
; |
| 12 import 'package:analysis_server/src/socket_server.dart'; | 14 import 'package:analysis_server/src/socket_server.dart'; |
| 13 import 'package:analyzer/file_system/file_system.dart'; | 15 import 'package:analyzer/file_system/file_system.dart'; |
| 14 import 'package:analyzer/src/generated/source.dart'; | |
| 15 import 'package:analyzer/src/generated/engine.dart'; | 16 import 'package:analyzer/src/generated/engine.dart'; |
| 16 import 'package:analyzer/src/generated/java_engine.dart'; | 17 import 'package:analyzer/src/generated/java_engine.dart'; |
| 18 import 'package:analyzer/src/generated/source.dart'; |
| 19 |
| 17 import 'analysis_server.dart'; | 20 import 'analysis_server.dart'; |
| 18 | 21 |
| 19 /** | 22 /** |
| 20 * Instances of the class [GetHandler] handle GET requests. | 23 * Instances of the class [GetHandler] handle GET requests. |
| 21 */ | 24 */ |
| 22 class GetHandler { | 25 class GetHandler { |
| 23 /** | 26 /** |
| 24 * The path used to request the status of the analysis server as a whole. | 27 * The path used to request the status of the analysis server as a whole. |
| 25 */ | 28 */ |
| 26 static const String STATUS_PATH = '/status'; | 29 static const String STATUS_PATH = '/status'; |
| 27 | 30 |
| 28 /** | 31 /** |
| 32 * The path used to request code completion information. |
| 33 */ |
| 34 static const String COMPLETION_PATH = '/completion'; |
| 35 |
| 36 /** |
| 29 * The path used to request the list of source files in a certain cache | 37 * The path used to request the list of source files in a certain cache |
| 30 * state. | 38 * state. |
| 31 */ | 39 */ |
| 32 static const String CACHE_STATE_PATH = '/cache_state'; | 40 static const String CACHE_STATE_PATH = '/cache_state'; |
| 33 | 41 |
| 34 /** | 42 /** |
| 35 * Query parameter used to represent the cache state to search for, when | 43 * Query parameter used to represent the cache state to search for, when |
| 36 * accessing [CACHE_STATE_PATH]. | 44 * accessing [CACHE_STATE_PATH]. |
| 37 */ | 45 */ |
| 38 static const String STATE_QUERY_PARAM = 'state'; | 46 static const String STATE_QUERY_PARAM = 'state'; |
| (...skipping 27 matching lines...) Expand all Loading... |
| 66 | 74 |
| 67 /** | 75 /** |
| 68 * Handle a GET request received by the HTTP server. | 76 * Handle a GET request received by the HTTP server. |
| 69 */ | 77 */ |
| 70 void handleGetRequest(HttpRequest request) { | 78 void handleGetRequest(HttpRequest request) { |
| 71 String path = request.uri.path; | 79 String path = request.uri.path; |
| 72 if (path == STATUS_PATH) { | 80 if (path == STATUS_PATH) { |
| 73 _returnServerStatus(request); | 81 _returnServerStatus(request); |
| 74 } else if (path == CACHE_STATE_PATH) { | 82 } else if (path == CACHE_STATE_PATH) { |
| 75 _returnCacheState(request); | 83 _returnCacheState(request); |
| 84 } else if (path == COMPLETION_PATH) { |
| 85 _returnCompletionInfo(request); |
| 76 } else { | 86 } else { |
| 77 _returnUnknownRequest(request); | 87 _returnUnknownRequest(request); |
| 78 } | 88 } |
| 79 } | 89 } |
| 80 | 90 |
| 81 /** | 91 /** |
| 82 * Create a link to [path] with query parameters [params], with inner HTML | 92 * Create a link to [path] with query parameters [params], with inner HTML |
| 83 * [innerHtml]. | 93 * [innerHtml]. |
| 84 */ | 94 */ |
| 85 String _makeLink(String path, Map<String, String> params, String innerHtml) { | 95 String _makeLink(String path, Map<String, String> params, String innerHtml) { |
| 86 Uri uri = new Uri(path: path, queryParameters: params); | 96 Uri uri = new Uri(path: path, queryParameters: params); |
| 87 return '<a href="${HTML_ESCAPE.convert(uri.toString())}">$innerHtml</a>'; | 97 return '<a href="${HTML_ESCAPE.convert(uri.toString())}">$innerHtml</a>'; |
| 88 } | 98 } |
| 89 | 99 |
| 90 /** | 100 /** |
| 91 * Return a response indicating the set of source files in a certain cache | 101 * Return a response indicating the set of source files in a certain cache |
| 92 * state. | 102 * state. |
| 93 */ | 103 */ |
| 94 void _returnCacheState(HttpRequest request) { | 104 void _returnCacheState(HttpRequest request) { |
| 95 // Figure out what CacheState is being searched for. | 105 // Figure out what CacheState is being searched for. |
| 96 String stateQueryParam = request.uri.queryParameters[STATE_QUERY_PARAM]; | 106 String stateQueryParam = request.uri.queryParameters[STATE_QUERY_PARAM]; |
| 97 if (stateQueryParam == null) { | 107 if (stateQueryParam == null) { |
| 98 return _returnFailure(request, | 108 return _returnFailure( |
| 109 request, |
| 99 'Query parameter $STATE_QUERY_PARAM required'); | 110 'Query parameter $STATE_QUERY_PARAM required'); |
| 100 } | 111 } |
| 101 CacheState stateFilter = null; | 112 CacheState stateFilter = null; |
| 102 for (CacheState value in CacheState.values) { | 113 for (CacheState value in CacheState.values) { |
| 103 if (value.toString() == stateQueryParam) { | 114 if (value.toString() == stateQueryParam) { |
| 104 stateFilter = value; | 115 stateFilter = value; |
| 105 } | 116 } |
| 106 } | 117 } |
| 107 if (stateFilter == null) { | 118 if (stateFilter == null) { |
| 108 return _returnFailure(request, | 119 return _returnFailure( |
| 120 request, |
| 109 'Query parameter $STATE_QUERY_PARAM is invalid'); | 121 'Query parameter $STATE_QUERY_PARAM is invalid'); |
| 110 } | 122 } |
| 111 | 123 |
| 112 // Figure out which context is being searched for. | 124 // Figure out which context is being searched for. |
| 113 String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; | 125 String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| 114 if (contextFilter == null) { | 126 if (contextFilter == null) { |
| 115 return _returnFailure(request, | 127 return _returnFailure( |
| 128 request, |
| 116 'Query parameter $CONTEXT_QUERY_PARAM required'); | 129 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| 117 } | 130 } |
| 118 | 131 |
| 119 // Figure out which descriptor is being searched for. | 132 // Figure out which descriptor is being searched for. |
| 120 String descriptorFilter = | 133 String descriptorFilter = |
| 121 request.uri.queryParameters[DESCRIPTOR_QUERY_PARAM]; | 134 request.uri.queryParameters[DESCRIPTOR_QUERY_PARAM]; |
| 122 if (descriptorFilter == null) { | 135 if (descriptorFilter == null) { |
| 123 return _returnFailure(request, | 136 return _returnFailure( |
| 137 request, |
| 124 'Query parameter $DESCRIPTOR_QUERY_PARAM required'); | 138 'Query parameter $DESCRIPTOR_QUERY_PARAM required'); |
| 125 } | 139 } |
| 126 | 140 |
| 127 AnalysisServer analysisServer = _server.analysisServer; | 141 AnalysisServer analysisServer = _server.analysisServer; |
| 128 if (analysisServer == null) { | 142 if (analysisServer == null) { |
| 129 return _returnFailure(request, 'Analysis server not running'); | 143 return _returnFailure(request, 'Analysis server not running'); |
| 130 } | 144 } |
| 131 HttpResponse response = request.response; | 145 HttpResponse response = request.response; |
| 132 response.statusCode = HttpStatus.OK; | 146 response.statusCode = HttpStatus.OK; |
| 133 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); | 147 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| 134 response.write('<html>'); | 148 response.write('<html>'); |
| 135 response.write('<head>'); | 149 response.write('<head>'); |
| 136 response.write('<title>Dart Analysis Server - Search result</title>'); | 150 response.write('<title>Dart Analysis Server - Search result</title>'); |
| 137 response.write('</head>'); | 151 response.write('</head>'); |
| 138 response.write('<body>'); | 152 response.write('<body>'); |
| 139 response.write('<h1>'); | 153 response.write('<h1>'); |
| 140 response.write('Files with state ${HTML_ESCAPE.convert(stateQueryParam)}'); | 154 response.write('Files with state ${HTML_ESCAPE.convert(stateQueryParam)}'); |
| 141 response.write(' for descriptor ${HTML_ESCAPE.convert(descriptorFilter)}'); | 155 response.write(' for descriptor ${HTML_ESCAPE.convert(descriptorFilter)}'); |
| 142 response.write(' in context ${HTML_ESCAPE.convert(contextFilter)}'); | 156 response.write(' in context ${HTML_ESCAPE.convert(contextFilter)}'); |
| 143 response.write('</h1>'); | 157 response.write('</h1>'); |
| 144 response.write('<ul>'); | 158 response.write('<ul>'); |
| 145 int count = 0; | 159 int count = 0; |
| 146 analysisServer.folderMap.forEach((Folder folder, | 160 analysisServer.folderMap.forEach( |
| 147 AnalysisContextImpl context) { | 161 (Folder folder, AnalysisContextImpl context) { |
| 148 if (folder.path != contextFilter) { | 162 if (folder.path != contextFilter) { |
| 149 return; | 163 return; |
| 150 } | 164 } |
| 151 context.visitCacheItems((Source source, SourceEntry dartEntry, | 165 context.visitCacheItems( |
| 152 DataDescriptor rowDesc, CacheState state) { | 166 (Source source, SourceEntry dartEntry, DataDescriptor rowDesc, CacheSt
ate state) |
| 167 { |
| 153 if (state != stateFilter || rowDesc.toString() != descriptorFilter) { | 168 if (state != stateFilter || rowDesc.toString() != descriptorFilter) { |
| 154 return; | 169 return; |
| 155 } | 170 } |
| 156 response.write('<li>${HTML_ESCAPE.convert(source.fullName)}</li>'); | 171 response.write('<li>${HTML_ESCAPE.convert(source.fullName)}</li>'); |
| 157 count++; | 172 count++; |
| 158 }); | 173 }); |
| 159 }); | 174 }); |
| 160 response.write('</ul>'); | 175 response.write('</ul>'); |
| 161 response.write('<p>$count files found</p>'); | 176 response.write('<p>$count files found</p>'); |
| 162 response.write('</body>'); | 177 response.write('</body>'); |
| 163 response.write('</html>'); | 178 response.write('</html>'); |
| 164 response.close(); | 179 response.close(); |
| 165 } | 180 } |
| 166 | 181 |
| 167 /** | 182 /** |
| 183 * Return a response displaying code completion information. |
| 184 */ |
| 185 void _returnCompletionInfo(HttpRequest request) { |
| 186 var refresh = request.requestedUri.queryParameters['refresh']; |
| 187 HttpResponse response = request.response; |
| 188 response.statusCode = HttpStatus.OK; |
| 189 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| 190 response.write('<html>'); |
| 191 response.write('<head>'); |
| 192 response.write('<title>Dart Analysis Server - Completion Stats</title>'); |
| 193 response.write('<style>'); |
| 194 response.write('td.right {text-align: right;}'); |
| 195 response.write('</style>'); |
| 196 if (refresh is String) { |
| 197 int seconds = int.parse(refresh, onError: (_) => 5); |
| 198 response.write('<meta http-equiv="refresh" content="$seconds">'); |
| 199 } |
| 200 response.write('</head>'); |
| 201 response.write('<body>'); |
| 202 _writeCompletionInfo(response); |
| 203 response.write('<form>'); |
| 204 response.write( |
| 205 '<input type="button" onClick="history.go(0)" value="Refresh">'); |
| 206 response.write('</form>'); |
| 207 response.write('<p>Append "?refresh=5" to refresh every 5 seconds</p>'); |
| 208 response.write('</body>'); |
| 209 response.write('</html>'); |
| 210 response.close(); |
| 211 } |
| 212 |
| 213 void _returnFailure(HttpRequest request, String message) { |
| 214 HttpResponse response = request.response; |
| 215 response.statusCode = HttpStatus.OK; |
| 216 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| 217 response.write('<html>'); |
| 218 response.write('<head>'); |
| 219 response.write('<title>Dart Analysis Server - Failure</title>'); |
| 220 response.write('</head>'); |
| 221 response.write('<body>'); |
| 222 response.write(HTML_ESCAPE.convert(message)); |
| 223 response.write('</body>'); |
| 224 response.write('</html>'); |
| 225 response.close(); |
| 226 } |
| 227 |
| 228 /** |
| 168 * Return a response indicating the status of the analysis server. | 229 * Return a response indicating the status of the analysis server. |
| 169 */ | 230 */ |
| 170 void _returnServerStatus(HttpRequest request) { | 231 void _returnServerStatus(HttpRequest request) { |
| 171 HttpResponse response = request.response; | 232 HttpResponse response = request.response; |
| 172 response.statusCode = HttpStatus.OK; | 233 response.statusCode = HttpStatus.OK; |
| 173 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); | 234 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| 174 response.write('<html>'); | 235 response.write('<html>'); |
| 175 response.write('<head>'); | 236 response.write('<head>'); |
| 176 response.write('<title>Dart Analysis Server - Status</title>'); | 237 response.write('<title>Dart Analysis Server - Status</title>'); |
| 177 response.write('<style>'); | 238 response.write('<style>'); |
| (...skipping 12 matching lines...) Expand all Loading... |
| 190 response.write('<p>Running (not analyzing)</p>'); | 251 response.write('<p>Running (not analyzing)</p>'); |
| 191 } | 252 } |
| 192 response.write('<h1>Analysis Contexts</h1>'); | 253 response.write('<h1>Analysis Contexts</h1>'); |
| 193 response.write('<h2>Summary</h2>'); | 254 response.write('<h2>Summary</h2>'); |
| 194 response.write('<table>'); | 255 response.write('<table>'); |
| 195 List headerRowText = ['Context']; | 256 List headerRowText = ['Context']; |
| 196 headerRowText.addAll(CacheState.values); | 257 headerRowText.addAll(CacheState.values); |
| 197 _writeRow(response, headerRowText, header: true); | 258 _writeRow(response, headerRowText, header: true); |
| 198 Map<Folder, AnalysisContext> folderMap = analysisServer.folderMap; | 259 Map<Folder, AnalysisContext> folderMap = analysisServer.folderMap; |
| 199 List<Folder> folders = folderMap.keys.toList(); | 260 List<Folder> folders = folderMap.keys.toList(); |
| 200 folders.sort((Folder first, Folder second) | 261 folders.sort( |
| 201 => first.shortName.compareTo(second.shortName)); | 262 (Folder first, Folder second) => first.shortName.compareTo(second.shor
tName)); |
| 202 folders.forEach((Folder folder) { | 263 folders.forEach((Folder folder) { |
| 203 AnalysisContextImpl context = folderMap[folder]; | 264 AnalysisContextImpl context = folderMap[folder]; |
| 204 String key = folder.shortName; | 265 String key = folder.shortName; |
| 205 AnalysisContextStatistics statistics = context.statistics; | 266 AnalysisContextStatistics statistics = context.statistics; |
| 206 Map<CacheState, int> totals = <CacheState, int>{}; | 267 Map<CacheState, int> totals = <CacheState, int>{}; |
| 207 for (CacheState state in CacheState.values) { | 268 for (CacheState state in CacheState.values) { |
| 208 totals[state] = 0; | 269 totals[state] = 0; |
| 209 } | 270 } |
| 210 statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { | 271 statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { |
| 211 for (CacheState state in CacheState.values) { | 272 for (CacheState state in CacheState.values) { |
| 212 totals[state] += row.getCount(state); | 273 totals[state] += row.getCount(state); |
| 213 } | 274 } |
| 214 }); | 275 }); |
| 215 List rowText = ['<a href="#context_${HTML_ESCAPE.convert(key)}">$key</a>
']; | 276 List rowText = [ |
| 277 '<a href="#context_${HTML_ESCAPE.convert(key)}">$key</a>']; |
| 216 for (CacheState state in CacheState.values) { | 278 for (CacheState state in CacheState.values) { |
| 217 rowText.add(totals[state]); | 279 rowText.add(totals[state]); |
| 218 } | 280 } |
| 219 _writeRow(response, rowText, classes: [null, "right"]); | 281 _writeRow(response, rowText, classes: [null, "right"]); |
| 220 }); | 282 }); |
| 221 response.write('</table>'); | 283 response.write('</table>'); |
| 222 folders.forEach((Folder folder) { | 284 folders.forEach((Folder folder) { |
| 223 AnalysisContextImpl context = folderMap[folder]; | 285 AnalysisContextImpl context = folderMap[folder]; |
| 224 String key = folder.shortName; | 286 String key = folder.shortName; |
| 225 response.write('<h2><a name="context_${HTML_ESCAPE.convert(key)}">Analys
is Context: $key</a></h2>'); | 287 response.write( |
| 288 '<h2><a name="context_${HTML_ESCAPE.convert(key)}">Analysis Context:
$key</a></h2>'); |
| 226 AnalysisContextStatistics statistics = context.statistics; | 289 AnalysisContextStatistics statistics = context.statistics; |
| 227 response.write('<table>'); | 290 response.write('<table>'); |
| 228 _writeRow(response, headerRowText, header: true); | 291 _writeRow(response, headerRowText, header: true); |
| 229 statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { | 292 statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { |
| 230 List rowText = [row.name]; | 293 List rowText = [row.name]; |
| 231 for (CacheState state in CacheState.values) { | 294 for (CacheState state in CacheState.values) { |
| 232 String text = row.getCount(state).toString(); | 295 String text = row.getCount(state).toString(); |
| 233 Map<String, String> params = <String, String>{ | 296 Map<String, String> params = <String, String>{ |
| 234 STATE_QUERY_PARAM: state.toString(), | 297 STATE_QUERY_PARAM: state.toString(), |
| 235 CONTEXT_QUERY_PARAM: folder.path, | 298 CONTEXT_QUERY_PARAM: folder.path, |
| (...skipping 22 matching lines...) Expand all Loading... |
| 258 response.close(); | 321 response.close(); |
| 259 } | 322 } |
| 260 | 323 |
| 261 /** | 324 /** |
| 262 * Return an error in response to an unrecognized request received by the HTTP | 325 * Return an error in response to an unrecognized request received by the HTTP |
| 263 * server. | 326 * server. |
| 264 */ | 327 */ |
| 265 void _returnUnknownRequest(HttpRequest request) { | 328 void _returnUnknownRequest(HttpRequest request) { |
| 266 HttpResponse response = request.response; | 329 HttpResponse response = request.response; |
| 267 response.statusCode = HttpStatus.NOT_FOUND; | 330 response.statusCode = HttpStatus.NOT_FOUND; |
| 268 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/plain"); | 331 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| 269 response.write('Not found'); | 332 response.write('<html>'); |
| 333 response.write('<head>'); |
| 334 response.write('<title>Dart Analysis Server - Page Not Found</title>'); |
| 335 response.write('</head>'); |
| 336 response.write('<body>'); |
| 337 response.write('<h1>Page Not Found</h1>'); |
| 338 response.write('<p>Try one of these links instead:</p>'); |
| 339 response.write('<ul>'); |
| 340 response.write('<li><a href="$STATUS_PATH">Server Status</a></li>'); |
| 341 response.write('<li><a href="$COMPLETION_PATH">Completion Stats</a></li>'); |
| 342 response.write('<ul>'); |
| 343 response.write('</body>'); |
| 344 response.write('</html>'); |
| 270 response.close(); | 345 response.close(); |
| 271 } | 346 } |
| 272 | 347 |
| 273 void _returnFailure(HttpRequest request, String message) { | 348 /** |
| 274 HttpResponse response = request.response; | 349 * Append code completion information. |
| 275 response.statusCode = HttpStatus.OK; | 350 */ |
| 276 response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); | 351 void _writeCompletionInfo(HttpResponse response) { |
| 277 response.write('<html>'); | 352 response.write('<h1>Code Completion</h1>'); |
| 278 response.write('<head>'); | 353 AnalysisServer analysisServer = _server.analysisServer; |
| 279 response.write('<title>Dart Analysis Server - Failure</title>'); | 354 if (analysisServer == null) { |
| 280 response.write('</head>'); | 355 response.write('<p>Not running</p>'); |
| 281 response.write('<body>'); | 356 return; |
| 282 response.write(HTML_ESCAPE.convert(message)); | 357 } |
| 283 response.write('</body>'); | 358 CompletionDomainHandler handler = analysisServer.handlers.firstWhere( |
| 284 response.write('</html>'); | 359 (h) => h is CompletionDomainHandler, |
| 285 response.close(); | 360 orElse: () => null); |
| 361 if (handler == null) { |
| 362 response.write('<p>No code completion</p>'); |
| 363 return; |
| 364 } |
| 365 CompletionPerformance performance = handler.performance; |
| 366 if (performance == null) { |
| 367 response.write('<p>No performance stats yet</p>'); |
| 368 return; |
| 369 } |
| 370 response.write('<h2>Performance</h2>'); |
| 371 response.write('<table>'); |
| 372 _writeRow(response, ['Elapsed', '', 'Operation'], header: true); |
| 373 performance.operations.forEach((OperationPerformance op) { |
| 374 String elapsed = op.elapsed != null ? op.elapsed.toString() : '???'; |
| 375 _writeRow(response, [elapsed, ' ', op.name]); |
| 376 }); |
| 377 response.write('</table>'); |
| 286 } | 378 } |
| 287 | 379 |
| 288 /** | 380 /** |
| 289 * Write a single row within a table to the given [response] object. The row | 381 * Write a single row within a table to the given [response] object. The row |
| 290 * will have one cell for each of the [columns], and will be a header row if | 382 * will have one cell for each of the [columns], and will be a header row if |
| 291 * [header] is `true`. | 383 * [header] is `true`. |
| 292 */ | 384 */ |
| 293 void _writeRow(HttpResponse response, List<Object> columns, | 385 void _writeRow(HttpResponse response, List<Object> columns, {bool header: |
| 294 {bool header : false, List<String> classes}) { | 386 false, List<String> classes}) { |
| 295 response.write('<tr>'); | 387 response.write('<tr>'); |
| 296 int count = columns.length; | 388 int count = columns.length; |
| 297 int maxClassIndex = classes == null ? 0 : classes.length - 1; | 389 int maxClassIndex = classes == null ? 0 : classes.length - 1; |
| 298 for (int i = 0; i < count; i++) { | 390 for (int i = 0; i < count; i++) { |
| 299 String classAttribute = ''; | 391 String classAttribute = ''; |
| 300 if (classes != null) { | 392 if (classes != null) { |
| 301 String className = classes[min(i, maxClassIndex)]; | 393 String className = classes[min(i, maxClassIndex)]; |
| 302 if (className != null) { | 394 if (className != null) { |
| 303 classAttribute = ' class="$className"'; | 395 classAttribute = ' class="$className"'; |
| 304 } | 396 } |
| 305 } | 397 } |
| 306 if (header) { | 398 if (header) { |
| 307 response.write('<th$classAttribute>'); | 399 response.write('<th$classAttribute>'); |
| 308 } else { | 400 } else { |
| 309 response.write('<td$classAttribute>'); | 401 response.write('<td$classAttribute>'); |
| 310 } | 402 } |
| 311 response.write(columns[i]); | 403 response.write(columns[i]); |
| 312 if (header) { | 404 if (header) { |
| 313 response.write('</th>'); | 405 response.write('</th>'); |
| 314 } else { | 406 } else { |
| 315 response.write('</td>'); | 407 response.write('</td>'); |
| 316 } | 408 } |
| 317 }; | 409 } |
| 410 ; |
| 318 response.write('</tr>'); | 411 response.write('</tr>'); |
| 319 } | 412 } |
| 320 } | 413 } |
| OLD | NEW |