001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2019–2020 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.jersey.netty; 018 019import java.lang.reflect.Type; 020 021import java.net.URI; 022 023import java.util.Arrays; 024import java.util.List; 025import java.util.Objects; 026 027import java.util.function.Supplier; 028 029import java.util.logging.Logger; 030 031import javax.ws.rs.core.Configuration; 032import javax.ws.rs.core.SecurityContext; 033 034import io.netty.buffer.ByteBuf; 035import io.netty.buffer.ByteBufAllocator; 036import io.netty.buffer.ByteBufHolder; 037 038import io.netty.channel.ChannelConfig; // for javadoc only 039import io.netty.channel.ChannelHandlerContext; 040import io.netty.channel.ChannelInboundHandlerAdapter; // for javadoc only 041import io.netty.channel.ChannelPipeline; // for javadoc only 042 043import io.netty.handler.codec.MessageToMessageDecoder; 044 045import org.glassfish.jersey.internal.PropertiesDelegate; 046 047import org.glassfish.jersey.internal.util.collection.Ref; 048 049import org.glassfish.jersey.server.ContainerRequest; 050 051import org.glassfish.jersey.server.internal.ContainerUtils; 052 053/** 054 * A {@link MessageToMessageDecoder} that decodes messages of a 055 * specific type into {@link ContainerRequest}s. 056 * 057 * <p>Instances of this class are normally followed in a {@link 058 * ChannelPipeline} by instances of the {@link 059 * AbstractContainerRequestHandlingResponseWriter} class.</p> 060 * 061 * @param <T> the common supertype of messages that can be decoded 062 * 063 * @param <H> the type of {@linkplain #isHeaders(Object) "headers" messages} 064 * 065 * @param <D> the type of {@linkplain #isData(Object) "data" messages} 066 * 067 * @author <a href="https://about.me/lairdnelson" 068 * target="_parent">Laird Nelson</a> 069 * 070 * @see #decode(ChannelHandlerContext, Object, List) 071 * 072 * @see MessageToMessageDecoder 073 * 074 * @see ContainerRequest 075 * 076 * @see AbstractContainerRequestHandlingResponseWriter 077 */ 078public abstract class AbstractContainerRequestDecoder<T, H extends T, D extends T> extends MessageToMessageDecoder<T> { 079 080 081 /* 082 * Static fields. 083 */ 084 085 086 private static final String cn = AbstractContainerRequestDecoder.class.getName(); 087 088 private static final Logger logger = Logger.getLogger(cn); 089 090 private static final Type channelHandlerContextRefType = ChannelHandlerContextReferencingFactory.genericRefType.getType(); 091 092 093 /* 094 * Instance fields. 095 */ 096 097 098 private final Class<H> headersClass; 099 100 private final Type headersClassRefType; 101 102 private final Class<D> dataClass; 103 104 private final URI baseUri; 105 106 private final Supplier<? extends Configuration> configurationSupplier; 107 108 private TerminableByteBufInputStream terminableByteBufInputStream; 109 110 private ContainerRequest containerRequestUnderConstruction; 111 112 113 /* 114 * Constructors. 115 */ 116 117 118 /** 119 * Creates a new {@link AbstractContainerRequestDecoder} implementation. 120 * 121 * @param baseUri the base {@link URI} against which relative 122 * request URIs will be resolved; may be {@code null} in which case 123 * the return value of {@link URI#create(String) URI.create("/")} 124 * will be used instead 125 * 126 * @param headersClass the type representing a "headers" message; 127 * must not be {@code null} 128 * 129 * @param dataClass the type representing a "data" message; must not 130 * be {@code null} 131 * 132 * @exception NullPointerException if either {@code headersClass} or 133 * {@code dataClass} is {@code null} 134 * 135 * @see #AbstractContainerRequestDecoder(URI, Configuration, Class, 136 * Class) 137 * 138 * @deprecated Please use the {@link 139 * #AbstractContainerRequestDecoder(URI, Configuration, Class, 140 * Class)} constructor instead. 141 */ 142 @Deprecated 143 protected AbstractContainerRequestDecoder(final URI baseUri, 144 final Class<H> headersClass, 145 final Class<D> dataClass) { 146 this(baseUri, AbstractContainerRequestDecoder::returnNull, headersClass, dataClass); 147 } 148 149 /** 150 * Creates a new {@link AbstractContainerRequestDecoder} implementation. 151 * 152 * @param baseUri the base {@link URI} against which relative 153 * request URIs will be resolved; may be {@code null} in which case 154 * the return value of {@link URI#create(String) URI.create("/")} 155 * will be used instead 156 * 157 * @param configuration a {@link Configuration} describing how the 158 * container is configured; may be {@code null} 159 * 160 * @param headersClass the type representing a "headers" message; 161 * must not be {@code null} 162 * 163 * @param dataClass the type representing a "data" message; must not 164 * be {@code null} 165 * 166 * @exception NullPointerException if either {@code headersClass} or 167 * {@code dataClass} is {@code null} 168 */ 169 protected AbstractContainerRequestDecoder(final URI baseUri, 170 final Configuration configuration, 171 final Class<H> headersClass, 172 final Class<D> dataClass) { 173 this(baseUri, configuration == null ? AbstractContainerRequestDecoder::returnNull : new ImmutableSupplier<>(configuration), headersClass, dataClass); 174 } 175 176 /** 177 * Creates a new {@link AbstractContainerRequestDecoder} implementation. 178 * 179 * @param baseUri the base {@link URI} against which relative 180 * request URIs will be resolved; may be {@code null} in which case 181 * the return value of {@link URI#create(String) URI.create("/")} 182 * will be used instead 183 * 184 * @param configurationSupplier a {@link Supplier} of {@link 185 * Configuration} instances describing how the container is 186 * configured; may be {@code null} 187 * 188 * @param headersClass the type representing a "headers" message; 189 * must not be {@code null} 190 * 191 * @param dataClass the type representing a "data" message; must not 192 * be {@code null} 193 * 194 * @exception NullPointerException if either {@code headersClass} or 195 * {@code dataClass} is {@code null} 196 */ 197 protected AbstractContainerRequestDecoder(final URI baseUri, 198 final Supplier<? extends Configuration> configurationSupplier, 199 final Class<H> headersClass, 200 final Class<D> dataClass) { 201 super(); 202 this.baseUri = baseUri == null ? URI.create("/") : baseUri; 203 if (configurationSupplier == null) { 204 this.configurationSupplier = AbstractContainerRequestDecoder::returnNull; 205 } else { 206 this.configurationSupplier = configurationSupplier; 207 } 208 this.headersClass = Objects.requireNonNull(headersClass); 209 this.headersClassRefType = new ParameterizedType(Ref.class, headersClass); 210 this.dataClass = Objects.requireNonNull(dataClass); 211 } 212 213 214 /* 215 * Instance methods. 216 */ 217 218 219 /** 220 * Overrides the {@link 221 * ChannelInboundHandlerAdapter#channelReadComplete(ChannelHandlerContext)} 222 * method to {@linkplain ChannelHandlerContext#read() request a 223 * read} when necessary, taking {@linkplain 224 * ChannelConfig#isAutoRead() the auto-read status of the associated 225 * <code>Channel</code>} into account. 226 * 227 * @param channelHandlerContext the {@link ChannelHandlerContext} in 228 * effect; must not be {@code null} 229 * 230 * @exception NullPointerException if {@code channelHandlerContext} 231 * is {@code null} 232 * 233 * @see ChannelConfig#isAutoRead() 234 * 235 * @see 236 * ChannelInboundHandlerAdapter#channelReadComplete(ChannelHandlerContext) 237 */ 238 @Override 239 public void channelReadComplete(final ChannelHandlerContext channelHandlerContext) 240 throws Exception { 241 super.channelReadComplete(channelHandlerContext); 242 if (this.containerRequestUnderConstruction != null && 243 !channelHandlerContext.channel().config().isAutoRead()) { 244 channelHandlerContext.read(); 245 } 246 } 247 248 /** 249 * Returns {@code true} if the supplied {@code message} is an 250 * instance of either the {@linkplain 251 * #AbstractContainerRequestDecoder(URI, Class, Class) headers type 252 * or data type supplied at construction time}, and {@code false} in 253 * all other cases. 254 * 255 * @param message the message to interrogate; may be {@code null} in 256 * which case {@code false} will be returned 257 * 258 * @return {@code true} if the supplied {@code message} is an 259 * instance of either the {@linkplain 260 * #AbstractContainerRequestDecoder(URI, Class, Class) headers type 261 * or data type supplied at construction time}; {@code false} in all 262 * other cases 263 * 264 * @see #AbstractContainerRequestDecoder(URI, Class, Class) 265 */ 266 @Override 267 public boolean acceptInboundMessage(final Object message) { 268 return this.headersClass.isInstance(message) || this.dataClass.isInstance(message); 269 } 270 271 /** 272 * Returns {@code true} if the supplied message represents a 273 * "headers" message (as distinguished from a "data" message). 274 * 275 * @param message the message to interrogate; will not be {@code 276 * null} 277 * 278 * @return {@code true} if the supplied message represents a 279 * "headers" message; {@code false} otherwise 280 * 281 * @see #isData(Object) 282 */ 283 protected boolean isHeaders(final T message) { 284 return this.headersClass.isInstance(message); 285 } 286 287 /** 288 * Extracts and returns a {@link String} representing a request URI 289 * from the supplied message, which is guaranteed to be a 290 * {@linkplain #isHeaders(Object) "headers" message}. 291 * 292 * <p>Implementations of this method may return {@code null} but 293 * normally will not.</p> 294 * 295 * @param message the message to interrogate; will not be {@code 296 * null} 297 * 298 * @return a {@link String} representing a request URI from the 299 * supplied message, or {@code null} 300 */ 301 protected abstract String getRequestUriString(final H message); 302 303 /** 304 * Extracts and returns a {@link String} representing a request 305 * method from the supplied message, which is guaranteed to be a 306 * {@linkplain #isHeaders(Object) "headers" message}. 307 * 308 * <p>Implementations of this method may return {@code null} but 309 * normally will not.</p> 310 * 311 * @param message the message to interrogate; will not be {@code 312 * null} 313 * 314 * @return a {@link String} representing a request method from the 315 * supplied message, or {@code null} 316 */ 317 protected abstract String getMethod(final H message); 318 319 /** 320 * Creates and returns a {@link SecurityContext} appropriate for the 321 * supplied message, which is guaranteed to be a {@linkplain 322 * #isHeaders(Object) "headers" message}. 323 * 324 * <p>Implementations of this method must not return {@code 325 * null}.</p> 326 * 327 * @param message the {@linkplain #isHeaders(Object) "headers" 328 * message} for which a new {@link SecurityContext} is to be 329 * returned; will not be {@code null} 330 * 331 * @return a new, non-{@code null} {@link SecurityContext} 332 */ 333 protected SecurityContext createSecurityContext(final H message) { 334 return new SecurityContextAdapter(); 335 } 336 337 /** 338 * Creates and returns a {@link PropertiesDelegate} appropriate for the 339 * supplied message, which is guaranteed to be a {@linkplain 340 * #isHeaders(Object) "headers" message}. 341 * 342 * <p>Implementations of this method must not return {@code 343 * null}.</p> 344 * 345 * @param message the {@linkplain #isHeaders(Object) "headers" 346 * message} for which a new {@link PropertiesDelegate} is to be 347 * returned; will not be {@code null} 348 * 349 * @return a new, non-{@code null} {@link PropertiesDelegate} 350 */ 351 protected PropertiesDelegate createPropertiesDelegate(final H message) { 352 return new MapBackedPropertiesDelegate(); 353 } 354 355 /** 356 * Installs the supplied {@code message} into the supplied {@link 357 * ContainerRequest} in some way. 358 * 359 * <p>This implementation calls {@link 360 * ContainerRequest#setProperty(String, Object)} with the 361 * {@linkplain Class#getName() fully-qualified class name} of the 362 * headers class supplied at construction time as the key, and the 363 * supplied {@code message} as the value.</p> 364 * 365 * @param channelHandlerContext the {@link ChannelHandlerContext} in 366 * effect; will not be {@code null}; supplied for convenience; 367 * overrides may (and often do) ignore this parameter 368 * 369 * @param message the message to install; will not be {@code null} 370 * and is guaranteed to be a {@linkplain #isHeaders(Object) 371 * "headers" message} 372 * 373 * @param containerRequest the just-constructed {@link 374 * ContainerRequest} into which to install the supplied {@code 375 * message}; will not be {@code null} 376 */ 377 protected void installMessage(final ChannelHandlerContext channelHandlerContext, 378 final H message, 379 final ContainerRequest containerRequest) { 380 containerRequest.setProperty(ChannelHandlerContext.class.getName(), channelHandlerContext); 381 containerRequest.setProperty(this.headersClass.getName(), message); 382 } 383 384 /** 385 * Returns {@code true} if the supplied message represents a 386 * "data" message (as distinguished from a "headers" message). 387 * 388 * @param message the message to interrogate; will not be {@code 389 * null} 390 * 391 * @return {@code true} if the supplied message represents a 392 * "data" message; {@code false} otherwise 393 * 394 * @see #isHeaders(Object) 395 */ 396 protected boolean isData(final T message) { 397 return this.dataClass.isInstance(message); 398 } 399 400 /** 401 * Extracts any content from the supplied {@linkplain 402 * #isData(Object) "data" message} as a {@link ByteBuf}, or returns 403 * {@code null} if there is no such content. 404 * 405 * @param message the {@linkplain #isData(Object) "data" message} 406 * from which a {@link ByteBuf} is to be extracted; will not be 407 * {@code null} 408 * 409 * @return a {@link ByteBuf} representing the message's content, or 410 * {@code null} 411 */ 412 protected ByteBuf getContent(final D message) { 413 final ByteBuf returnValue; 414 if (message instanceof ByteBuf) { 415 returnValue = (ByteBuf)message; 416 } else if (message instanceof ByteBufHolder) { 417 returnValue = ((ByteBufHolder)message).content(); 418 } else { 419 returnValue = null; 420 } 421 return returnValue; 422 } 423 424 /** 425 * Returns {@code true} if there will be no further message 426 * components in an overall larger message after the supplied one. 427 * 428 * @param message the message component to interrogate; will not be {@code 429 * null}; may be either a {@linkplain #isHeaders(Object) "headers"} 430 * or {@linkplain #isData(Object) "data"} message component 431 * 432 * @return {@code true} if and only if there will be no further 433 * message components to come 434 */ 435 protected abstract boolean isLast(final T message); 436 437 /** 438 * Decodes the supplied {@code message} into a {@link 439 * ContainerRequest} and adds it to the supplied {@code out} {@link 440 * List}. 441 * 442 * @param channelHandlerContext the {@link ChannelHandlerContext} in 443 * effect; must not be {@code null} 444 * 445 * @param message the message to decode; must not be {@code null} 446 * and must be {@linkplain #acceptInboundMessage(Object) acceptable} 447 * 448 * @param out a {@link List} of {@link Object}s that result from 449 * decoding the supplied {@code message}; must not be {@code null} 450 * and must be mutable 451 * 452 * @exception NullPointerException if {@code channelHandlerContext} 453 * or {@code out} is {@code null} 454 * 455 * @exception IllegalArgumentException if {@code message} is {@code 456 * null} or otherwise unacceptable 457 * 458 * @exception IllegalStateException if there is an internal problem 459 * with state management 460 */ 461 @Override 462 protected final void decode(final ChannelHandlerContext channelHandlerContext, 463 final T message, 464 final List<Object> out) { 465 if (isHeaders(message)) { 466 if (this.containerRequestUnderConstruction == null) { 467 if (this.terminableByteBufInputStream == null) { 468 final URI requestUri; 469 final H headersMessage = this.headersClass.cast(message); 470 final String requestUriString = this.getRequestUriString(headersMessage); 471 if (requestUriString == null) { 472 requestUri = this.baseUri; 473 } else if (requestUriString.startsWith("/") && requestUriString.length() > 1) { 474 requestUri = this.baseUri.resolve(ContainerUtils.encodeUnsafeCharacters(requestUriString.substring(1))); 475 } else { 476 requestUri = this.baseUri.resolve(ContainerUtils.encodeUnsafeCharacters(requestUriString)); 477 } 478 final String method = this.getMethod(headersMessage); 479 final SecurityContext securityContext = this.createSecurityContext(headersMessage); 480 final PropertiesDelegate propertiesDelegate = this.createPropertiesDelegate(headersMessage); 481 final ContainerRequest containerRequest = 482 new ContainerRequest(this.baseUri, 483 requestUri, 484 method, 485 securityContext == null ? new SecurityContextAdapter() : securityContext, 486 propertiesDelegate == null ? new MapBackedPropertiesDelegate() : propertiesDelegate, 487 this.configurationSupplier.get()); 488 this.installMessage(channelHandlerContext, headersMessage, containerRequest); 489 containerRequest.setRequestScopedInitializer(injectionManager -> { 490 // See JerseyChannelInitializer, where the factories of 491 // factories that produce references of the things we're 492 // interested in are installed. Here, in request scope 493 // itself, we set the actual target of those references. 494 // This is apparently the proper way to do this sort of 495 // thing in Jersey (!) and examples of this pattern show 496 // up throughout its codebase. With jaw somewhat agape, 497 // we follow suit. 498 final Ref<ChannelHandlerContext> channelHandlerContextRef = injectionManager.getInstance(channelHandlerContextRefType); 499 if (channelHandlerContextRef != null) { 500 channelHandlerContextRef.set(channelHandlerContext); 501 } 502 final Ref<H> headersRef = injectionManager.getInstance(this.headersClassRefType); 503 if (headersRef != null) { 504 headersRef.set(headersMessage); 505 } 506 }); 507 if (this.isLast(message)) { 508 out.add(containerRequest); 509 } else { 510 this.containerRequestUnderConstruction = containerRequest; 511 } 512 } else { 513 throw new IllegalStateException("this.terminableByteBufInputStream != null: " + this.terminableByteBufInputStream); 514 } 515 } else { 516 throw new IllegalStateException("this.containerRequestUnderConstruction != null: " + this.containerRequestUnderConstruction); 517 } 518 } else if (this.isData(message)) { 519 final D dataMessage = this.dataClass.cast(message); 520 final ByteBuf content = this.getContent(dataMessage); 521 if (content == null || content.readableBytes() <= 0) { 522 if (this.isLast(message)) { 523 if (this.terminableByteBufInputStream == null) { 524 if (this.containerRequestUnderConstruction == null) { 525 // Do nothing; this is a final, zero-content message and 526 // we already dealt with the previous headers message 527 // component. 528 } else { 529 out.add(this.containerRequestUnderConstruction); 530 this.containerRequestUnderConstruction = null; 531 } 532 } else if (this.containerRequestUnderConstruction == null) { 533 throw new IllegalStateException("this.containerRequestUnderConstruction == null && this.terminableByteBufInputStream != null: " + this.terminableByteBufInputStream); 534 } else { 535 out.add(this.containerRequestUnderConstruction); 536 this.containerRequestUnderConstruction = null; 537 this.terminableByteBufInputStream.terminate(); 538 this.terminableByteBufInputStream = null; 539 } 540 } else if (this.containerRequestUnderConstruction == null) { 541 throw new IllegalStateException("this.containerRequestUnderConstruction == null"); 542 } else { 543 // We got an empty chunk in the middle of the stream. 544 // Ignore it. Note that 545 // #channelReadComplete(ChannelHandlerContext) will take 546 // care of auto-read-or-not situations. 547 } 548 } else if (this.containerRequestUnderConstruction == null) { 549 throw new IllegalStateException("this.containerRequestUnderConstruction == null"); 550 } else if (this.isLast(message)) { 551 final TerminableByteBufInputStream terminableByteBufInputStream; 552 if (this.terminableByteBufInputStream == null) { 553 final TerminableByteBufInputStream newlyCreatedTerminableByteBufInputStream = this.createTerminableByteBufInputStream(channelHandlerContext.alloc()); 554 this.containerRequestUnderConstruction.setEntityStream(newlyCreatedTerminableByteBufInputStream); 555 out.add(this.containerRequestUnderConstruction); 556 terminableByteBufInputStream = newlyCreatedTerminableByteBufInputStream; 557 } else { 558 terminableByteBufInputStream = this.terminableByteBufInputStream; 559 this.terminableByteBufInputStream = null; 560 } 561 assert this.terminableByteBufInputStream == null; 562 assert terminableByteBufInputStream != null; 563 content.retain(); // see https://github.com/microbean/microbean-jersey-netty/issues/12 564 terminableByteBufInputStream.addByteBuf(content); 565 terminableByteBufInputStream.terminate(); 566 this.containerRequestUnderConstruction = null; 567 } else { 568 if (this.terminableByteBufInputStream == null) { 569 final TerminableByteBufInputStream newlyCreatedTerminableByteBufInputStream = this.createTerminableByteBufInputStream(channelHandlerContext.alloc()); 570 this.terminableByteBufInputStream = newlyCreatedTerminableByteBufInputStream; 571 this.containerRequestUnderConstruction.setEntityStream(newlyCreatedTerminableByteBufInputStream); 572 out.add(this.containerRequestUnderConstruction); 573 } 574 content.retain(); // see https://github.com/microbean/microbean-jersey-netty/issues/12 575 this.terminableByteBufInputStream.addByteBuf(content); 576 } 577 } else { 578 throw new IllegalArgumentException("Unexpected message: " + message); 579 } 580 } 581 582 /** 583 * Creates and returns a new {@link TerminableByteBufInputStream}. 584 * 585 * @param byteBufAllocator a {@link ByteBufAllocator} that may be 586 * used or ignored; will not be {@code null} 587 * 588 * @return a new, non-{@code null} {@link 589 * TerminableByteBufInputStream} 590 */ 591 protected TerminableByteBufInputStream createTerminableByteBufInputStream(final ByteBufAllocator byteBufAllocator) { 592 return new TerminableByteBufInputStream(byteBufAllocator); 593 } 594 595 596 /* 597 * Static methods. 598 */ 599 600 601 /** 602 * Returns {@code null} when invoked. 603 * 604 * <p>This method is used only via method reference and only in 605 * pathological cases.</p> 606 * 607 * @return {@code null} in all cases 608 */ 609 private static final Configuration returnNull() { 610 return null; 611 } 612 613 614 /* 615 * Inner and nested classes. 616 */ 617 618 619 private static final class ParameterizedType implements java.lang.reflect.ParameterizedType { 620 621 private final Type rawType; 622 623 private final Type[] actualTypeArguments; 624 625 private ParameterizedType(final Type rawType, final Type... actualTypeArguments) { 626 super(); 627 this.rawType = rawType; 628 this.actualTypeArguments = actualTypeArguments; 629 } 630 631 @Override 632 public final Type[] getActualTypeArguments() { 633 return this.actualTypeArguments; 634 } 635 636 @Override 637 public final Type getRawType() { 638 return this.rawType; 639 } 640 641 @Override 642 public final Type getOwnerType() { 643 return null; 644 } 645 646 @Override 647 public int hashCode() { 648 final Object rawType = this.getRawType(); 649 final int actualTypeArgumentsHashCode = Arrays.hashCode(this.getActualTypeArguments()); 650 return rawType == null ? actualTypeArgumentsHashCode : actualTypeArgumentsHashCode ^ rawType.hashCode(); 651 } 652 653 @Override 654 public final boolean equals(final Object other) { 655 if (other == this) { 656 return true; 657 } else if (other instanceof java.lang.reflect.ParameterizedType) { 658 final java.lang.reflect.ParameterizedType her = (java.lang.reflect.ParameterizedType)other; 659 660 final Object rawType = this.getRawType(); 661 if (rawType == null) { 662 if (her.getRawType() != null) { 663 return false; 664 } 665 } else if (!rawType.equals(her.getRawType())) { 666 return false; 667 } 668 669 final Object[] actualTypeArguments = this.getActualTypeArguments(); 670 if (actualTypeArguments == null) { 671 if (her.getActualTypeArguments() != null) { 672 return false; 673 } 674 } else if (!Arrays.equals(actualTypeArguments, her.getActualTypeArguments())) { 675 return false; 676 } 677 678 return true; 679 } else { 680 return false; 681 } 682 } 683 684 } 685 686}