001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019 package org.apache.hadoop.fs.http.server; 020 021 import org.apache.hadoop.classification.InterfaceAudience; 022 import org.apache.hadoop.conf.Configuration; 023 import org.apache.hadoop.fs.FileSystem; 024 import org.apache.hadoop.fs.http.client.HttpFSFileSystem; 025 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.OperationParam; 026 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.AccessTimeParam; 027 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.BlockSizeParam; 028 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.DataParam; 029 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.RecursiveParam; 030 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.DoAsParam; 031 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.FilterParam; 032 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.GroupParam; 033 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.LenParam; 034 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.ModifiedTimeParam; 035 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.OffsetParam; 036 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.OverwriteParam; 037 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.OwnerParam; 038 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.PermissionParam; 039 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.ReplicationParam; 040 import org.apache.hadoop.fs.http.server.HttpFSParametersProvider.DestinationParam; 041 import org.apache.hadoop.lib.service.FileSystemAccess; 042 import org.apache.hadoop.lib.service.FileSystemAccessException; 043 import org.apache.hadoop.lib.service.Groups; 044 import org.apache.hadoop.lib.service.Instrumentation; 045 import org.apache.hadoop.lib.service.ProxyUser; 046 import org.apache.hadoop.lib.servlet.FileSystemReleaseFilter; 047 import org.apache.hadoop.lib.servlet.HostnameFilter; 048 import org.apache.hadoop.lib.wsrs.InputStreamEntity; 049 import org.apache.hadoop.lib.wsrs.Parameters; 050 import org.apache.hadoop.security.authentication.server.AuthenticationToken; 051 import org.json.simple.JSONObject; 052 import org.slf4j.Logger; 053 import org.slf4j.LoggerFactory; 054 import org.slf4j.MDC; 055 056 import javax.ws.rs.Consumes; 057 import javax.ws.rs.DELETE; 058 import javax.ws.rs.GET; 059 import javax.ws.rs.POST; 060 import javax.ws.rs.PUT; 061 import javax.ws.rs.Path; 062 import javax.ws.rs.PathParam; 063 import javax.ws.rs.Produces; 064 import javax.ws.rs.QueryParam; 065 import javax.ws.rs.core.Context; 066 import javax.ws.rs.core.MediaType; 067 import javax.ws.rs.core.Response; 068 import javax.ws.rs.core.UriBuilder; 069 import javax.ws.rs.core.UriInfo; 070 import java.io.IOException; 071 import java.io.InputStream; 072 import java.net.URI; 073 import java.security.AccessControlException; 074 import java.security.Principal; 075 import java.text.MessageFormat; 076 import java.util.List; 077 import java.util.Map; 078 079 /** 080 * Main class of HttpFSServer server. 081 * <p/> 082 * The <code>HttpFSServer</code> class uses Jersey JAX-RS to binds HTTP requests to the 083 * different operations. 084 */ 085 @Path(HttpFSFileSystem.SERVICE_VERSION) 086 @InterfaceAudience.Private 087 public class HttpFSServer { 088 private static Logger AUDIT_LOG = LoggerFactory.getLogger("httpfsaudit"); 089 090 /** 091 * Resolves the effective user that will be used to request a FileSystemAccess filesystem. 092 * <p/> 093 * If the doAs-user is NULL or the same as the user, it returns the user. 094 * <p/> 095 * Otherwise it uses proxyuser rules (see {@link ProxyUser} to determine if the 096 * current user can impersonate the doAs-user. 097 * <p/> 098 * If the current user cannot impersonate the doAs-user an 099 * <code>AccessControlException</code> will be thrown. 100 * 101 * @param user principal for whom the filesystem instance is. 102 * @param doAs do-as user, if any. 103 * 104 * @return the effective user. 105 * 106 * @throws IOException thrown if an IO error occurrs. 107 * @throws AccessControlException thrown if the current user cannot impersonate 108 * the doAs-user. 109 */ 110 private String getEffectiveUser(Principal user, String doAs) throws IOException { 111 String effectiveUser = user.getName(); 112 if (doAs != null && !doAs.equals(user.getName())) { 113 ProxyUser proxyUser = HttpFSServerWebApp.get().get(ProxyUser.class); 114 String proxyUserName; 115 if (user instanceof AuthenticationToken) { 116 proxyUserName = ((AuthenticationToken)user).getUserName(); 117 } else { 118 proxyUserName = user.getName(); 119 } 120 proxyUser.validate(proxyUserName, HostnameFilter.get(), doAs); 121 effectiveUser = doAs; 122 AUDIT_LOG.info("Proxy user [{}] DoAs user [{}]", proxyUserName, doAs); 123 } 124 return effectiveUser; 125 } 126 127 /** 128 * Executes a {@link FileSystemAccess.FileSystemExecutor} using a filesystem for the effective 129 * user. 130 * 131 * @param user principal making the request. 132 * @param doAs do-as user, if any. 133 * @param executor FileSystemExecutor to execute. 134 * 135 * @return FileSystemExecutor response 136 * 137 * @throws IOException thrown if an IO error occurrs. 138 * @throws FileSystemAccessException thrown if a FileSystemAccess releated error occurred. Thrown 139 * exceptions are handled by {@link HttpFSExceptionProvider}. 140 */ 141 private <T> T fsExecute(Principal user, String doAs, FileSystemAccess.FileSystemExecutor<T> executor) 142 throws IOException, FileSystemAccessException { 143 String hadoopUser = getEffectiveUser(user, doAs); 144 FileSystemAccess fsAccess = HttpFSServerWebApp.get().get(FileSystemAccess.class); 145 Configuration conf = HttpFSServerWebApp.get().get(FileSystemAccess.class).getFileSystemConfiguration(); 146 return fsAccess.execute(hadoopUser, conf, executor); 147 } 148 149 /** 150 * Returns a filesystem instance. The fileystem instance is wired for release at the completion of 151 * the current Servlet request via the {@link FileSystemReleaseFilter}. 152 * <p/> 153 * If a do-as user is specified, the current user must be a valid proxyuser, otherwise an 154 * <code>AccessControlException</code> will be thrown. 155 * 156 * @param user principal for whom the filesystem instance is. 157 * @param doAs do-as user, if any. 158 * 159 * @return a filesystem for the specified user or do-as user. 160 * 161 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 162 * handled by {@link HttpFSExceptionProvider}. 163 * @throws FileSystemAccessException thrown if a FileSystemAccess releated error occurred. Thrown 164 * exceptions are handled by {@link HttpFSExceptionProvider}. 165 */ 166 private FileSystem createFileSystem(Principal user, String doAs) throws IOException, FileSystemAccessException { 167 String hadoopUser = getEffectiveUser(user, doAs); 168 FileSystemAccess fsAccess = HttpFSServerWebApp.get().get(FileSystemAccess.class); 169 Configuration conf = HttpFSServerWebApp.get().get(FileSystemAccess.class).getFileSystemConfiguration(); 170 FileSystem fs = fsAccess.createFileSystem(hadoopUser, conf); 171 FileSystemReleaseFilter.setFileSystem(fs); 172 return fs; 173 } 174 175 private void enforceRootPath(HttpFSFileSystem.Operation op, String path) { 176 if (!path.equals("/")) { 177 throw new UnsupportedOperationException( 178 MessageFormat.format("Operation [{0}], invalid path [{1}], must be '/'", 179 op, path)); 180 } 181 } 182 183 /** 184 * Special binding for '/' as it is not handled by the wildcard binding. 185 * 186 * @param user the principal of the user making the request. 187 * @param op the HttpFS operation of the request. 188 * @param params the HttpFS parameters of the request. 189 * 190 * @return the request response. 191 * 192 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 193 * handled by {@link HttpFSExceptionProvider}. 194 * @throws FileSystemAccessException thrown if a FileSystemAccess releated 195 * error occurred. Thrown exceptions are handled by 196 * {@link HttpFSExceptionProvider}. 197 */ 198 @GET 199 @Path("/") 200 @Produces(MediaType.APPLICATION_JSON) 201 public Response getRoot(@Context Principal user, 202 @QueryParam(OperationParam.NAME) OperationParam op, 203 @Context Parameters params) 204 throws IOException, FileSystemAccessException { 205 return get(user, "", op, params); 206 } 207 208 private String makeAbsolute(String path) { 209 return "/" + ((path != null) ? path : ""); 210 } 211 212 /** 213 * Binding to handle GET requests, supported operations are 214 * 215 * @param user the principal of the user making the request. 216 * @param path the path for operation. 217 * @param op the HttpFS operation of the request. 218 * @param params the HttpFS parameters of the request. 219 * 220 * @return the request response. 221 * 222 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 223 * handled by {@link HttpFSExceptionProvider}. 224 * @throws FileSystemAccessException thrown if a FileSystemAccess releated 225 * error occurred. Thrown exceptions are handled by 226 * {@link HttpFSExceptionProvider}. 227 */ 228 @GET 229 @Path("{path:.*}") 230 @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON}) 231 public Response get(@Context Principal user, 232 @PathParam("path") String path, 233 @QueryParam(OperationParam.NAME) OperationParam op, 234 @Context Parameters params) 235 throws IOException, FileSystemAccessException { 236 Response response; 237 path = makeAbsolute(path); 238 MDC.put(HttpFSFileSystem.OP_PARAM, op.value().name()); 239 String doAs = params.get(DoAsParam.NAME, DoAsParam.class); 240 switch (op.value()) { 241 case OPEN: { 242 //Invoking the command directly using an unmanaged FileSystem that is 243 // released by the FileSystemReleaseFilter 244 FSOperations.FSOpen command = new FSOperations.FSOpen(path); 245 FileSystem fs = createFileSystem(user, doAs); 246 InputStream is = command.execute(fs); 247 Long offset = params.get(OffsetParam.NAME, OffsetParam.class); 248 Long len = params.get(LenParam.NAME, LenParam.class); 249 AUDIT_LOG.info("[{}] offset [{}] len [{}]", 250 new Object[]{path, offset, len}); 251 InputStreamEntity entity = new InputStreamEntity(is, offset, len); 252 response = 253 Response.ok(entity).type(MediaType.APPLICATION_OCTET_STREAM).build(); 254 break; 255 } 256 case GETFILESTATUS: { 257 FSOperations.FSFileStatus command = 258 new FSOperations.FSFileStatus(path); 259 Map json = fsExecute(user, doAs, command); 260 AUDIT_LOG.info("[{}]", path); 261 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 262 break; 263 } 264 case LISTSTATUS: { 265 String filter = params.get(FilterParam.NAME, FilterParam.class); 266 FSOperations.FSListStatus command = new FSOperations.FSListStatus( 267 path, filter); 268 Map json = fsExecute(user, doAs, command); 269 AUDIT_LOG.info("[{}] filter [{}]", path, 270 (filter != null) ? filter : "-"); 271 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 272 break; 273 } 274 case GETHOMEDIRECTORY: { 275 enforceRootPath(op.value(), path); 276 FSOperations.FSHomeDir command = new FSOperations.FSHomeDir(); 277 JSONObject json = fsExecute(user, doAs, command); 278 AUDIT_LOG.info(""); 279 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 280 break; 281 } 282 case INSTRUMENTATION: { 283 enforceRootPath(op.value(), path); 284 Groups groups = HttpFSServerWebApp.get().get(Groups.class); 285 List<String> userGroups = groups.getGroups(user.getName()); 286 if (!userGroups.contains(HttpFSServerWebApp.get().getAdminGroup())) { 287 throw new AccessControlException( 288 "User not in HttpFSServer admin group"); 289 } 290 Instrumentation instrumentation = 291 HttpFSServerWebApp.get().get(Instrumentation.class); 292 Map snapshot = instrumentation.getSnapshot(); 293 response = Response.ok(snapshot).build(); 294 break; 295 } 296 case GETCONTENTSUMMARY: { 297 FSOperations.FSContentSummary command = 298 new FSOperations.FSContentSummary(path); 299 Map json = fsExecute(user, doAs, command); 300 AUDIT_LOG.info("[{}]", path); 301 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 302 break; 303 } 304 case GETFILECHECKSUM: { 305 FSOperations.FSFileChecksum command = 306 new FSOperations.FSFileChecksum(path); 307 Map json = fsExecute(user, doAs, command); 308 AUDIT_LOG.info("[{}]", path); 309 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 310 break; 311 } 312 case GETFILEBLOCKLOCATIONS: { 313 response = Response.status(Response.Status.BAD_REQUEST).build(); 314 break; 315 } 316 default: { 317 throw new IOException( 318 MessageFormat.format("Invalid HTTP GET operation [{0}]", 319 op.value())); 320 } 321 } 322 return response; 323 } 324 325 326 /** 327 * Binding to handle DELETE requests. 328 * 329 * @param user the principal of the user making the request. 330 * @param path the path for operation. 331 * @param op the HttpFS operation of the request. 332 * @param params the HttpFS parameters of the request. 333 * 334 * @return the request response. 335 * 336 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 337 * handled by {@link HttpFSExceptionProvider}. 338 * @throws FileSystemAccessException thrown if a FileSystemAccess releated 339 * error occurred. Thrown exceptions are handled by 340 * {@link HttpFSExceptionProvider}. 341 */ 342 @DELETE 343 @Path("{path:.*}") 344 @Produces(MediaType.APPLICATION_JSON) 345 public Response delete(@Context Principal user, 346 @PathParam("path") String path, 347 @QueryParam(OperationParam.NAME) OperationParam op, 348 @Context Parameters params) 349 throws IOException, FileSystemAccessException { 350 Response response; 351 path = makeAbsolute(path); 352 MDC.put(HttpFSFileSystem.OP_PARAM, op.value().name()); 353 String doAs = params.get(DoAsParam.NAME, DoAsParam.class); 354 switch (op.value()) { 355 case DELETE: { 356 Boolean recursive = 357 params.get(RecursiveParam.NAME, RecursiveParam.class); 358 AUDIT_LOG.info("[{}] recursive [{}]", path, recursive); 359 FSOperations.FSDelete command = 360 new FSOperations.FSDelete(path, recursive); 361 JSONObject json = fsExecute(user, doAs, command); 362 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 363 break; 364 } 365 default: { 366 throw new IOException( 367 MessageFormat.format("Invalid HTTP DELETE operation [{0}]", 368 op.value())); 369 } 370 } 371 return response; 372 } 373 374 /** 375 * Binding to handle POST requests. 376 * 377 * @param is the inputstream for the request payload. 378 * @param user the principal of the user making the request. 379 * @param uriInfo the of the request. 380 * @param path the path for operation. 381 * @param op the HttpFS operation of the request. 382 * @param params the HttpFS parameters of the request. 383 * 384 * @return the request response. 385 * 386 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 387 * handled by {@link HttpFSExceptionProvider}. 388 * @throws FileSystemAccessException thrown if a FileSystemAccess releated 389 * error occurred. Thrown exceptions are handled by 390 * {@link HttpFSExceptionProvider}. 391 */ 392 @POST 393 @Path("{path:.*}") 394 @Consumes({"*/*"}) 395 @Produces({MediaType.APPLICATION_JSON}) 396 public Response post(InputStream is, 397 @Context Principal user, 398 @Context UriInfo uriInfo, 399 @PathParam("path") String path, 400 @QueryParam(OperationParam.NAME) OperationParam op, 401 @Context Parameters params) 402 throws IOException, FileSystemAccessException { 403 Response response; 404 path = makeAbsolute(path); 405 MDC.put(HttpFSFileSystem.OP_PARAM, op.value().name()); 406 String doAs = params.get(DoAsParam.NAME, DoAsParam.class); 407 switch (op.value()) { 408 case APPEND: { 409 Boolean hasData = params.get(DataParam.NAME, DataParam.class); 410 if (!hasData) { 411 response = Response.temporaryRedirect( 412 createUploadRedirectionURL(uriInfo, 413 HttpFSFileSystem.Operation.APPEND)).build(); 414 } else { 415 FSOperations.FSAppend command = 416 new FSOperations.FSAppend(is, path); 417 fsExecute(user, doAs, command); 418 AUDIT_LOG.info("[{}]", path); 419 response = Response.ok().type(MediaType.APPLICATION_JSON).build(); 420 } 421 break; 422 } 423 default: { 424 throw new IOException( 425 MessageFormat.format("Invalid HTTP POST operation [{0}]", 426 op.value())); 427 } 428 } 429 return response; 430 } 431 432 /** 433 * Creates the URL for an upload operation (create or append). 434 * 435 * @param uriInfo uri info of the request. 436 * @param uploadOperation operation for the upload URL. 437 * 438 * @return the URI for uploading data. 439 */ 440 protected URI createUploadRedirectionURL(UriInfo uriInfo, Enum<?> uploadOperation) { 441 UriBuilder uriBuilder = uriInfo.getRequestUriBuilder(); 442 uriBuilder = uriBuilder.replaceQueryParam(OperationParam.NAME, uploadOperation). 443 queryParam(DataParam.NAME, Boolean.TRUE); 444 return uriBuilder.build(null); 445 } 446 447 448 /** 449 * Binding to handle PUT requests. 450 * 451 * @param is the inputstream for the request payload. 452 * @param user the principal of the user making the request. 453 * @param uriInfo the of the request. 454 * @param path the path for operation. 455 * @param op the HttpFS operation of the request. 456 * @param params the HttpFS parameters of the request. 457 * 458 * @return the request response. 459 * 460 * @throws IOException thrown if an IO error occurred. Thrown exceptions are 461 * handled by {@link HttpFSExceptionProvider}. 462 * @throws FileSystemAccessException thrown if a FileSystemAccess releated 463 * error occurred. Thrown exceptions are handled by 464 * {@link HttpFSExceptionProvider}. 465 */ 466 @PUT 467 @Path("{path:.*}") 468 @Consumes({"*/*"}) 469 @Produces({MediaType.APPLICATION_JSON}) 470 public Response put(InputStream is, 471 @Context Principal user, 472 @Context UriInfo uriInfo, 473 @PathParam("path") String path, 474 @QueryParam(OperationParam.NAME) OperationParam op, 475 @Context Parameters params) 476 throws IOException, FileSystemAccessException { 477 Response response; 478 path = makeAbsolute(path); 479 MDC.put(HttpFSFileSystem.OP_PARAM, op.value().name()); 480 String doAs = params.get(DoAsParam.NAME, DoAsParam.class); 481 switch (op.value()) { 482 case CREATE: { 483 Boolean hasData = params.get(DataParam.NAME, DataParam.class); 484 if (!hasData) { 485 response = Response.temporaryRedirect( 486 createUploadRedirectionURL(uriInfo, 487 HttpFSFileSystem.Operation.CREATE)).build(); 488 } else { 489 Short permission = params.get(PermissionParam.NAME, 490 PermissionParam.class); 491 Boolean override = params.get(OverwriteParam.NAME, 492 OverwriteParam.class); 493 Short replication = params.get(ReplicationParam.NAME, 494 ReplicationParam.class); 495 Long blockSize = params.get(BlockSizeParam.NAME, 496 BlockSizeParam.class); 497 FSOperations.FSCreate command = 498 new FSOperations.FSCreate(is, path, permission, override, 499 replication, blockSize); 500 fsExecute(user, doAs, command); 501 AUDIT_LOG.info( 502 "[{}] permission [{}] override [{}] replication [{}] blockSize [{}]", 503 new Object[]{path, permission, override, replication, blockSize}); 504 response = Response.status(Response.Status.CREATED).build(); 505 } 506 break; 507 } 508 case MKDIRS: { 509 Short permission = params.get(PermissionParam.NAME, 510 PermissionParam.class); 511 FSOperations.FSMkdirs command = 512 new FSOperations.FSMkdirs(path, permission); 513 JSONObject json = fsExecute(user, doAs, command); 514 AUDIT_LOG.info("[{}] permission [{}]", path, permission); 515 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 516 break; 517 } 518 case RENAME: { 519 String toPath = params.get(DestinationParam.NAME, DestinationParam.class); 520 FSOperations.FSRename command = 521 new FSOperations.FSRename(path, toPath); 522 JSONObject json = fsExecute(user, doAs, command); 523 AUDIT_LOG.info("[{}] to [{}]", path, toPath); 524 response = Response.ok(json).type(MediaType.APPLICATION_JSON).build(); 525 break; 526 } 527 case SETOWNER: { 528 String owner = params.get(OwnerParam.NAME, OwnerParam.class); 529 String group = params.get(GroupParam.NAME, GroupParam.class); 530 FSOperations.FSSetOwner command = 531 new FSOperations.FSSetOwner(path, owner, group); 532 fsExecute(user, doAs, command); 533 AUDIT_LOG.info("[{}] to (O/G)[{}]", path, owner + ":" + group); 534 response = Response.ok().build(); 535 break; 536 } 537 case SETPERMISSION: { 538 Short permission = params.get(PermissionParam.NAME, 539 PermissionParam.class); 540 FSOperations.FSSetPermission command = 541 new FSOperations.FSSetPermission(path, permission); 542 fsExecute(user, doAs, command); 543 AUDIT_LOG.info("[{}] to [{}]", path, permission); 544 response = Response.ok().build(); 545 break; 546 } 547 case SETREPLICATION: { 548 Short replication = params.get(ReplicationParam.NAME, 549 ReplicationParam.class); 550 FSOperations.FSSetReplication command = 551 new FSOperations.FSSetReplication(path, replication); 552 JSONObject json = fsExecute(user, doAs, command); 553 AUDIT_LOG.info("[{}] to [{}]", path, replication); 554 response = Response.ok(json).build(); 555 break; 556 } 557 case SETTIMES: { 558 Long modifiedTime = params.get(ModifiedTimeParam.NAME, 559 ModifiedTimeParam.class); 560 Long accessTime = params.get(AccessTimeParam.NAME, 561 AccessTimeParam.class); 562 FSOperations.FSSetTimes command = 563 new FSOperations.FSSetTimes(path, modifiedTime, accessTime); 564 fsExecute(user, doAs, command); 565 AUDIT_LOG.info("[{}] to (M/A)[{}]", path, 566 modifiedTime + ":" + accessTime); 567 response = Response.ok().build(); 568 break; 569 } 570 default: { 571 throw new IOException( 572 MessageFormat.format("Invalid HTTP PUT operation [{0}]", 573 op.value())); 574 } 575 } 576 return response; 577 } 578 579 }