001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.reactive.function; 018 019import java.util.List; 020import java.util.stream.Collectors; 021 022import org.reactivestreams.Publisher; 023import reactor.core.publisher.Mono; 024 025import org.springframework.core.ParameterizedTypeReference; 026import org.springframework.core.ReactiveAdapter; 027import org.springframework.core.ReactiveAdapterRegistry; 028import org.springframework.core.ResolvableType; 029import org.springframework.core.io.Resource; 030import org.springframework.core.io.buffer.DataBuffer; 031import org.springframework.http.HttpEntity; 032import org.springframework.http.MediaType; 033import org.springframework.http.ReactiveHttpOutputMessage; 034import org.springframework.http.client.MultipartBodyBuilder; 035import org.springframework.http.client.reactive.ClientHttpRequest; 036import org.springframework.http.codec.HttpMessageWriter; 037import org.springframework.http.codec.ServerSentEvent; 038import org.springframework.http.server.reactive.ServerHttpResponse; 039import org.springframework.lang.Nullable; 040import org.springframework.util.Assert; 041import org.springframework.util.LinkedMultiValueMap; 042import org.springframework.util.MultiValueMap; 043 044/** 045 * Static factory methods for {@link BodyInserter} implementations. 046 * 047 * @author Arjen Poutsma 048 * @author Rossen Stoyanchev 049 * @author Sebastien Deleuze 050 * @since 5.0 051 */ 052public abstract class BodyInserters { 053 054 private static final ResolvableType RESOURCE_TYPE = ResolvableType.forClass(Resource.class); 055 056 private static final ResolvableType SSE_TYPE = ResolvableType.forClass(ServerSentEvent.class); 057 058 private static final ResolvableType FORM_DATA_TYPE = 059 ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); 060 061 private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics( 062 MultiValueMap.class, String.class, Object.class); 063 064 private static final BodyInserter<Void, ReactiveHttpOutputMessage> EMPTY_INSERTER = 065 (response, context) -> response.setComplete(); 066 067 private static final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); 068 069 070 /** 071 * Inserter that does not write. 072 * @return the inserter 073 */ 074 @SuppressWarnings("unchecked") 075 public static <T> BodyInserter<T, ReactiveHttpOutputMessage> empty() { 076 return (BodyInserter<T, ReactiveHttpOutputMessage>) EMPTY_INSERTER; 077 } 078 079 /** 080 * Inserter to write the given value. 081 * <p>Alternatively, consider using the {@code bodyValue(Object)} shortcuts on 082 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 083 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 084 * @param body the value to write 085 * @param <T> the type of the body 086 * @return the inserter to write a single value 087 * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an 088 * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, 089 * for which {@link #fromPublisher(Publisher, Class)} or 090 * {@link #fromProducer(Object, Class)} should be used. 091 * @see #fromPublisher(Publisher, Class) 092 * @see #fromProducer(Object, Class) 093 */ 094 public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromValue(T body) { 095 Assert.notNull(body, "'body' must not be null"); 096 Assert.isNull(registry.getAdapter(body.getClass()), "'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type"); 097 return (message, context) -> 098 writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null); 099 } 100 101 /** 102 * Inserter to write the given object. 103 * <p>Alternatively, consider using the {@code bodyValue(Object)} shortcuts on 104 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 105 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 106 * @param body the body to write to the response 107 * @param <T> the type of the body 108 * @return the inserter to write a single object 109 * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an 110 * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, 111 * for which {@link #fromPublisher(Publisher, Class)} or 112 * {@link #fromProducer(Object, Class)} should be used. 113 * @see #fromPublisher(Publisher, Class) 114 * @see #fromProducer(Object, Class) 115 * @deprecated As of Spring Framework 5.2, in favor of {@link #fromValue(Object)} 116 */ 117 @Deprecated 118 public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromObject(T body) { 119 return fromValue(body); 120 } 121 122 /** 123 * Inserter to write the given producer of value(s) which must be a {@link Publisher} 124 * or another producer adaptable to a {@code Publisher} via 125 * {@link ReactiveAdapterRegistry}. 126 * <p>Alternatively, consider using the {@code body} shortcuts on 127 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 128 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 129 * @param <T> the type of the body 130 * @param producer the source of body value(s). 131 * @param elementClass the class of values to be produced 132 * @return the inserter to write a producer 133 * @since 5.2 134 */ 135 public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromProducer(T producer, Class<?> elementClass) { 136 Assert.notNull(producer, "'producer' must not be null"); 137 Assert.notNull(elementClass, "'elementClass' must not be null"); 138 ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); 139 Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); 140 return (message, context) -> 141 writeWithMessageWriters(message, context, producer, ResolvableType.forClass(elementClass), adapter); 142 } 143 144 /** 145 * Inserter to write the given producer of value(s) which must be a {@link Publisher} 146 * or another producer adaptable to a {@code Publisher} via 147 * {@link ReactiveAdapterRegistry}. 148 * <p>Alternatively, consider using the {@code body} shortcuts on 149 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 150 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 151 * @param <T> the type of the body 152 * @param producer the source of body value(s). 153 * @param elementTypeRef the type of values to be produced 154 * @return the inserter to write a producer 155 * @since 5.2 156 */ 157 public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromProducer( 158 T producer, ParameterizedTypeReference<?> elementTypeRef) { 159 160 Assert.notNull(producer, "'producer' must not be null"); 161 Assert.notNull(elementTypeRef, "'elementTypeRef' must not be null"); 162 ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); 163 Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); 164 return (message, context) -> 165 writeWithMessageWriters(message, context, producer, ResolvableType.forType(elementTypeRef), adapter); 166 } 167 168 /** 169 * Inserter to write the given {@link Publisher}. 170 * <p>Alternatively, consider using the {@code body} shortcuts on 171 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 172 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 173 * @param publisher the publisher to write with 174 * @param elementClass the class of elements in the publisher 175 * @param <T> the type of the elements contained in the publisher 176 * @param <P> the {@code Publisher} type 177 * @return the inserter to write a {@code Publisher} 178 */ 179 public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMessage> fromPublisher( 180 P publisher, Class<T> elementClass) { 181 182 Assert.notNull(publisher, "'publisher' must not be null"); 183 Assert.notNull(elementClass, "'elementClass' must not be null"); 184 return (message, context) -> 185 writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass), null); 186 } 187 188 /** 189 * Inserter to write the given {@link Publisher}. 190 * <p>Alternatively, consider using the {@code body} shortcuts on 191 * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and 192 * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. 193 * @param publisher the publisher to write with 194 * @param elementTypeRef the type of elements contained in the publisher 195 * @param <T> the type of the elements contained in the publisher 196 * @param <P> the {@code Publisher} type 197 * @return the inserter to write a {@code Publisher} 198 */ 199 public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMessage> fromPublisher( 200 P publisher, ParameterizedTypeReference<T> elementTypeRef) { 201 202 Assert.notNull(publisher, "'publisher' must not be null"); 203 Assert.notNull(elementTypeRef, "'elementTypeRef' must not be null"); 204 return (message, context) -> 205 writeWithMessageWriters(message, context, publisher, ResolvableType.forType(elementTypeRef.getType()), null); 206 } 207 208 /** 209 * Inserter to write the given {@code Resource}. 210 * <p>If the resource can be resolved to a {@linkplain Resource#getFile() file}, it will 211 * be copied using <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy</a>. 212 * @param resource the resource to write to the output message 213 * @param <T> the type of the {@code Resource} 214 * @return the inserter to write a {@code Publisher} 215 */ 216 public static <T extends Resource> BodyInserter<T, ReactiveHttpOutputMessage> fromResource(T resource) { 217 Assert.notNull(resource, "'resource' must not be null"); 218 return (outputMessage, context) -> { 219 ResolvableType elementType = RESOURCE_TYPE; 220 HttpMessageWriter<Resource> writer = findWriter(context, elementType, null); 221 MediaType contentType = outputMessage.getHeaders().getContentType(); 222 return write(Mono.just(resource), elementType, contentType, outputMessage, context, writer); 223 }; 224 } 225 226 /** 227 * Inserter to write the given {@code ServerSentEvent} publisher. 228 * <p>Alternatively, you can provide event data objects via 229 * {@link #fromPublisher(Publisher, Class)} or {@link #fromProducer(Object, Class)}, 230 * and set the "Content-Type" to {@link MediaType#TEXT_EVENT_STREAM text/event-stream}. 231 * @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body 232 * @param <T> the type of the data elements in the {@link ServerSentEvent} 233 * @return the inserter to write a {@code ServerSentEvent} publisher 234 * @see <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events W3C recommendation</a> 235 */ 236 // Parameterized for server-side use 237 public static <T, S extends Publisher<ServerSentEvent<T>>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents( 238 S eventsPublisher) { 239 240 Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null"); 241 return (serverResponse, context) -> { 242 ResolvableType elementType = SSE_TYPE; 243 MediaType mediaType = MediaType.TEXT_EVENT_STREAM; 244 HttpMessageWriter<ServerSentEvent<T>> writer = findWriter(context, elementType, mediaType); 245 return write(eventsPublisher, elementType, mediaType, serverResponse, context, writer); 246 }; 247 } 248 249 /** 250 * Return a {@link FormInserter} to write the given {@code MultiValueMap} 251 * as URL-encoded form data. The returned inserter allows for additional 252 * entries to be added via {@link FormInserter#with(String, Object)}. 253 * <p>Note that you can also use the {@code bodyValue(Object)} method in the 254 * request builders of both the {@code WebClient} and {@code WebTestClient}. 255 * In that case the setting of the request content type is also not required, 256 * just be sure the map contains String values only or otherwise it would be 257 * interpreted as a multipart request. 258 * @param formData the form data to write to the output message 259 * @return the inserter that allows adding more form data 260 */ 261 public static FormInserter<String> fromFormData(MultiValueMap<String, String> formData) { 262 return new DefaultFormInserter().with(formData); 263 } 264 265 /** 266 * Return a {@link FormInserter} to write the given key-value pair as 267 * URL-encoded form data. The returned inserter allows for additional 268 * entries to be added via {@link FormInserter#with(String, Object)}. 269 * @param name the key to add to the form 270 * @param value the value to add to the form 271 * @return the inserter that allows adding more form data 272 */ 273 public static FormInserter<String> fromFormData(String name, String value) { 274 Assert.notNull(name, "'name' must not be null"); 275 Assert.notNull(value, "'value' must not be null"); 276 return new DefaultFormInserter().with(name, value); 277 } 278 279 /** 280 * Return a {@link MultipartInserter} to write the given 281 * {@code MultiValueMap} as multipart data. Values in the map can be an 282 * Object or an {@link HttpEntity}. 283 * <p>Note that you can also build the multipart data externally with 284 * {@link MultipartBodyBuilder}, and pass the resulting map directly to the 285 * {@code bodyValue(Object)} shortcut method in {@code WebClient}. 286 * @param multipartData the form data to write to the output message 287 * @return the inserter that allows adding more parts 288 * @see MultipartBodyBuilder 289 */ 290 public static MultipartInserter fromMultipartData(MultiValueMap<String, ?> multipartData) { 291 Assert.notNull(multipartData, "'multipartData' must not be null"); 292 return new DefaultMultipartInserter().withInternal(multipartData); 293 } 294 295 /** 296 * Return a {@link MultipartInserter} to write the given parts, 297 * as multipart data. Values in the map can be an Object or an 298 * {@link HttpEntity}. 299 * <p>Note that you can also build the multipart data externally with 300 * {@link MultipartBodyBuilder}, and pass the resulting map directly to the 301 * {@code bodyValue(Object)} shortcut method in {@code WebClient}. 302 * @param name the part name 303 * @param value the part value, an Object or {@code HttpEntity} 304 * @return the inserter that allows adding more parts 305 */ 306 public static MultipartInserter fromMultipartData(String name, Object value) { 307 Assert.notNull(name, "'name' must not be null"); 308 Assert.notNull(value, "'value' must not be null"); 309 return new DefaultMultipartInserter().with(name, value); 310 } 311 312 /** 313 * Return a {@link MultipartInserter} to write the given asynchronous parts, 314 * as multipart data. 315 * <p>Note that you can also build the multipart data externally with 316 * {@link MultipartBodyBuilder}, and pass the resulting map directly to the 317 * {@code bodyValue(Object)} shortcut method in {@code WebClient}. 318 * @param name the part name 319 * @param publisher the publisher that forms the part value 320 * @param elementClass the class contained in the {@code publisher} 321 * @return the inserter that allows adding more parts 322 */ 323 public static <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData( 324 String name, P publisher, Class<T> elementClass) { 325 326 return new DefaultMultipartInserter().withPublisher(name, publisher, elementClass); 327 } 328 329 /** 330 * Variant of {@link #fromMultipartAsyncData(String, Publisher, Class)} that 331 * accepts a {@link ParameterizedTypeReference} for the element type, which 332 * allows specifying generic type information. 333 * <p>Note that you can also build the multipart data externally with 334 * {@link MultipartBodyBuilder}, and pass the resulting map directly to the 335 * {@code bodyValue(Object)} shortcut method in {@code WebClient}. 336 * @param name the part name 337 * @param publisher the publisher that forms the part value 338 * @param typeReference the type contained in the {@code publisher} 339 * @return the inserter that allows adding more parts 340 */ 341 public static <T, P extends Publisher<T>> MultipartInserter fromMultipartAsyncData( 342 String name, P publisher, ParameterizedTypeReference<T> typeReference) { 343 344 return new DefaultMultipartInserter().withPublisher(name, publisher, typeReference); 345 } 346 347 /** 348 * Inserter to write the given {@code Publisher<DataBuffer>} to the body. 349 * @param publisher the data buffer publisher to write 350 * @param <T> the type of the publisher 351 * @return the inserter to write directly to the body 352 * @see ReactiveHttpOutputMessage#writeWith(Publisher) 353 */ 354 public static <T extends Publisher<DataBuffer>> BodyInserter<T, ReactiveHttpOutputMessage> fromDataBuffers( 355 T publisher) { 356 357 Assert.notNull(publisher, "'publisher' must not be null"); 358 return (outputMessage, context) -> outputMessage.writeWith(publisher); 359 } 360 361 362 private static <M extends ReactiveHttpOutputMessage> Mono<Void> writeWithMessageWriters( 363 M outputMessage, BodyInserter.Context context, Object body, ResolvableType bodyType, @Nullable ReactiveAdapter adapter) { 364 365 Publisher<?> publisher; 366 if (body instanceof Publisher) { 367 publisher = (Publisher<?>) body; 368 } 369 else if (adapter != null) { 370 publisher = adapter.toPublisher(body); 371 } 372 else { 373 publisher = Mono.just(body); 374 } 375 MediaType mediaType = outputMessage.getHeaders().getContentType(); 376 return context.messageWriters().stream() 377 .filter(messageWriter -> messageWriter.canWrite(bodyType, mediaType)) 378 .findFirst() 379 .map(BodyInserters::cast) 380 .map(writer -> write(publisher, bodyType, mediaType, outputMessage, context, writer)) 381 .orElseGet(() -> Mono.error(unsupportedError(bodyType, context, mediaType))); 382 } 383 384 private static UnsupportedMediaTypeException unsupportedError(ResolvableType bodyType, 385 BodyInserter.Context context, @Nullable MediaType mediaType) { 386 387 List<MediaType> supportedMediaTypes = context.messageWriters().stream() 388 .flatMap(reader -> reader.getWritableMediaTypes().stream()) 389 .collect(Collectors.toList()); 390 391 return new UnsupportedMediaTypeException(mediaType, supportedMediaTypes, bodyType); 392 } 393 394 private static <T> Mono<Void> write(Publisher<? extends T> input, ResolvableType type, 395 @Nullable MediaType mediaType, ReactiveHttpOutputMessage message, 396 BodyInserter.Context context, HttpMessageWriter<T> writer) { 397 398 return context.serverRequest() 399 .map(request -> { 400 ServerHttpResponse response = (ServerHttpResponse) message; 401 return writer.write(input, type, type, mediaType, request, response, context.hints()); 402 }) 403 .orElseGet(() -> writer.write(input, type, mediaType, message, context.hints())); 404 } 405 406 private static <T> HttpMessageWriter<T> findWriter( 407 BodyInserter.Context context, ResolvableType elementType, @Nullable MediaType mediaType) { 408 409 return context.messageWriters().stream() 410 .filter(messageWriter -> messageWriter.canWrite(elementType, mediaType)) 411 .findFirst() 412 .map(BodyInserters::<T>cast) 413 .orElseThrow(() -> new IllegalStateException( 414 "No HttpMessageWriter for \"" + mediaType + "\" and \"" + elementType + "\"")); 415 } 416 417 @SuppressWarnings("unchecked") 418 private static <T> HttpMessageWriter<T> cast(HttpMessageWriter<?> messageWriter) { 419 return (HttpMessageWriter<T>) messageWriter; 420 } 421 422 423 /** 424 * Extension of {@link BodyInserter} that allows for adding form data or 425 * multipart form data. 426 * 427 * @param <T> the value type 428 */ 429 public interface FormInserter<T> extends BodyInserter<MultiValueMap<String, T>, ClientHttpRequest> { 430 431 // FormInserter is parameterized to ClientHttpRequest (for client-side use only) 432 433 /** 434 * Adds the specified key-value pair to the form. 435 * @param key the key to be added 436 * @param value the value to be added 437 * @return this inserter for adding more parts 438 */ 439 FormInserter<T> with(String key, T value); 440 441 /** 442 * Adds the specified values to the form. 443 * @param values the values to be added 444 * @return this inserter for adding more parts 445 */ 446 FormInserter<T> with(MultiValueMap<String, T> values); 447 448 } 449 450 451 /** 452 * Extension of {@link FormInserter} that allows for adding asynchronous parts. 453 */ 454 public interface MultipartInserter extends FormInserter<Object> { 455 456 /** 457 * Add an asynchronous part with {@link Publisher}-based content. 458 * @param name the name of the part to add 459 * @param publisher the part contents 460 * @param elementClass the type of elements contained in the publisher 461 * @return this inserter for adding more parts 462 */ 463 <T, P extends Publisher<T>> MultipartInserter withPublisher(String name, P publisher, 464 Class<T> elementClass); 465 466 /** 467 * Variant of {@link #withPublisher(String, Publisher, Class)} that accepts a 468 * {@link ParameterizedTypeReference} for the element type, which allows 469 * specifying generic type information. 470 * @param name the key to be added 471 * @param publisher the publisher to be added as value 472 * @param typeReference the type of elements contained in {@code publisher} 473 * @return this inserter for adding more parts 474 */ 475 <T, P extends Publisher<T>> MultipartInserter withPublisher(String name, P publisher, 476 ParameterizedTypeReference<T> typeReference); 477 478 } 479 480 481 private static class DefaultFormInserter implements FormInserter<String> { 482 483 private final MultiValueMap<String, String> data = new LinkedMultiValueMap<>(); 484 485 @Override 486 public FormInserter<String> with(String key, @Nullable String value) { 487 this.data.add(key, value); 488 return this; 489 } 490 491 @Override 492 public FormInserter<String> with(MultiValueMap<String, String> values) { 493 this.data.addAll(values); 494 return this; 495 } 496 497 @Override 498 public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) { 499 HttpMessageWriter<MultiValueMap<String, String>> messageWriter = 500 findWriter(context, FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED); 501 return messageWriter.write(Mono.just(this.data), FORM_DATA_TYPE, 502 MediaType.APPLICATION_FORM_URLENCODED, 503 outputMessage, context.hints()); 504 } 505 } 506 507 508 private static class DefaultMultipartInserter implements MultipartInserter { 509 510 private final MultipartBodyBuilder builder = new MultipartBodyBuilder(); 511 512 @Override 513 public MultipartInserter with(String key, Object value) { 514 this.builder.part(key, value); 515 return this; 516 } 517 518 @Override 519 public MultipartInserter with(MultiValueMap<String, Object> values) { 520 return withInternal(values); 521 } 522 523 @SuppressWarnings("unchecked") 524 private MultipartInserter withInternal(MultiValueMap<String, ?> values) { 525 values.forEach((key, valueList) -> { 526 for (Object value : valueList) { 527 this.builder.part(key, value); 528 } 529 }); 530 return this; 531 } 532 533 @Override 534 public <T, P extends Publisher<T>> MultipartInserter withPublisher( 535 String name, P publisher, Class<T> elementClass) { 536 537 this.builder.asyncPart(name, publisher, elementClass); 538 return this; 539 } 540 541 @Override 542 public <T, P extends Publisher<T>> MultipartInserter withPublisher( 543 String name, P publisher, ParameterizedTypeReference<T> typeReference) { 544 545 this.builder.asyncPart(name, publisher, typeReference); 546 return this; 547 } 548 549 @Override 550 public Mono<Void> insert(ClientHttpRequest outputMessage, Context context) { 551 HttpMessageWriter<MultiValueMap<String, HttpEntity<?>>> messageWriter = 552 findWriter(context, MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA); 553 MultiValueMap<String, HttpEntity<?>> body = this.builder.build(); 554 return messageWriter.write(Mono.just(body), MULTIPART_DATA_TYPE, 555 MediaType.MULTIPART_FORM_DATA, outputMessage, context.hints()); 556 } 557 } 558 559}