@Override
 public void setPropertiesFromHttpHeaders(Multimap<String, String> headers) {
   boolean chunked =
       any(
           headers.entries(),
           new Predicate<Entry<String, String>>() {
             @Override
             public boolean apply(Entry<String, String> input) {
               return "Transfer-Encoding".equalsIgnoreCase(input.getKey())
                   && "chunked".equalsIgnoreCase(input.getValue());
             }
           });
   for (Entry<String, String> header : headers.entries()) {
     if (!chunked && CONTENT_LENGTH.equalsIgnoreCase(header.getKey())) {
       setContentLength(new Long(header.getValue()));
     } else if ("Content-MD5".equalsIgnoreCase(header.getKey())) {
       setContentMD5(CryptoStreams.base64(header.getValue()));
     } else if (CONTENT_TYPE.equalsIgnoreCase(header.getKey())) {
       setContentType(header.getValue());
     } else if ("Content-Disposition".equalsIgnoreCase(header.getKey())) {
       setContentDisposition(header.getValue());
     } else if ("Content-Encoding".equalsIgnoreCase(header.getKey())) {
       setContentEncoding(header.getValue());
     } else if ("Content-Language".equalsIgnoreCase(header.getKey())) {
       setContentLanguage(header.getValue());
     }
   }
 }
/**
 * A HTTP2 frame reader that will decompress data frames according to the {@code content-encoding}
 * header for each stream.
 */
public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader {
  private static final AsciiString CONTENT_ENCODING_LOWER_CASE = CONTENT_ENCODING.toLowerCase();
  private static final AsciiString CONTENT_LENGTH_LOWER_CASE = CONTENT_LENGTH.toLowerCase();
  private static final Http2ConnectionAdapter CLEAN_UP_LISTENER =
      new Http2ConnectionAdapter() {
        @Override
        public void streamRemoved(Http2Stream stream) {
          final EmbeddedChannel decoder = stream.decompressor();
          if (decoder != null) {
            cleanup(stream, decoder);
          }
        }
      };

  private final Http2Connection connection;
  private final boolean strict;

  /**
   * Create a new instance with non-strict deflate decoding. {@link
   * #DecompressorHttp2FrameReader(Http2Connection, boolean)}
   */
  public DecompressorHttp2FrameReader(Http2Connection connection) {
    this(connection, false);
  }

  /**
   * Create a new instance.
   *
   * @param strict
   *     <ul>
   *       <li>{@code true} to use use strict handling of deflate if used
   *       <li>{@code false} be more lenient with decompression
   *     </ul>
   */
  public DecompressorHttp2FrameReader(Http2Connection connection, boolean strict) {
    this.connection = connection;
    this.strict = strict;

    connection.addListener(CLEAN_UP_LISTENER);
  }

  /**
   * Returns a new {@link EmbeddedChannel} that decodes the HTTP2 message content encoded in the
   * specified {@code contentEncoding}.
   *
   * @param contentEncoding the value of the {@code content-encoding} header
   * @return a new {@link ByteToMessageDecoder} if the specified encoding is supported. {@code null}
   *     otherwise (alternatively, you can throw a {@link Http2Exception} to block unknown
   *     encoding).
   * @throws Http2Exception If the specified encoding is not not supported and warrants an exception
   */
  protected EmbeddedChannel newContentDecoder(CharSequence contentEncoding) throws Http2Exception {
    if (GZIP.equalsIgnoreCase(contentEncoding) || XGZIP.equalsIgnoreCase(contentEncoding)) {
      return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));
    }
    if (DEFLATE.equalsIgnoreCase(contentEncoding) || XDEFLATE.equalsIgnoreCase(contentEncoding)) {
      final ZlibWrapper wrapper = strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE;
      // To be strict, 'deflate' means ZLIB, but some servers were not implemented correctly.
      return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(wrapper));
    }
    // 'identity' or unsupported
    return null;
  }

  /**
   * Returns the expected content encoding of the decoded content. This getMethod returns {@code
   * "identity"} by default, which is the case for most decoders.
   *
   * @param contentEncoding the value of the {@code content-encoding} header
   * @return the expected content encoding of the new content.
   * @throws Http2Exception if the {@code contentEncoding} is not supported and warrants an
   *     exception
   */
  protected AsciiString getTargetContentEncoding(
      @SuppressWarnings("UnusedParameters") CharSequence contentEncoding) throws Http2Exception {
    return HttpHeaders.Values.IDENTITY;
  }

  /**
   * Checks if a new decoder object is needed for the stream identified by {@code streamId}. This
   * method will modify the {@code content-encoding} header contained in {@code builder}.
   *
   * @param streamId The identifier for the headers inside {@code builder}
   * @param builder Object representing headers which have been read
   * @param endOfStream Indicates if the stream has ended
   * @throws Http2Exception If the {@code content-encoding} is not supported
   */
  private void initDecoder(int streamId, Http2Headers headers, boolean endOfStream)
      throws Http2Exception {
    // Convert the names into a case-insensitive map.
    final Http2Stream stream = connection.stream(streamId);
    if (stream != null) {
      EmbeddedChannel decoder = stream.decompressor();
      if (decoder == null) {
        if (!endOfStream) {
          // Determine the content encoding.
          AsciiString contentEncoding = headers.get(CONTENT_ENCODING_LOWER_CASE);
          if (contentEncoding == null) {
            contentEncoding = IDENTITY;
          }
          decoder = newContentDecoder(contentEncoding);
          if (decoder != null) {
            stream.decompressor(decoder);
            // Decode the content and remove or replace the existing headers
            // so that the message looks like a decoded message.
            AsciiString targetContentEncoding = getTargetContentEncoding(contentEncoding);
            if (IDENTITY.equalsIgnoreCase(targetContentEncoding)) {
              headers.remove(CONTENT_ENCODING_LOWER_CASE);
            } else {
              headers.set(CONTENT_ENCODING_LOWER_CASE, targetContentEncoding);
            }
          }
        }
      } else if (endOfStream) {
        cleanup(stream, decoder);
      }
      if (decoder != null) {
        // The content length will be for the compressed data.  Since we will decompress the data
        // this content-length will not be correct.  Instead of queuing messages or delaying sending
        // header frames...just remove the content-length header
        headers.remove(CONTENT_LENGTH_LOWER_CASE);
      }
    }
  }

  /**
   * Release remaining content from the {@link EmbeddedChannel} and remove the decoder from the
   * {@link Http2Stream}.
   *
   * @param stream The stream for which {@code decoder} is the decompressor for
   * @param decoder The decompressor for {@code stream}
   */
  private static void cleanup(Http2Stream stream, EmbeddedChannel decoder) {
    if (decoder.finish()) {
      for (; ; ) {
        final ByteBuf buf = decoder.readInbound();
        if (buf == null) {
          break;
        }
        buf.release();
      }
    }
    stream.decompressor(null);
  }

  /**
   * Read the next decoded {@link ByteBuf} from the {@link EmbeddedChannel} or {@code null} if one
   * does not exist.
   *
   * @param decoder The channel to read from
   * @return The next decoded {@link ByteBuf} from the {@link EmbeddedChannel} or {@code null} if
   *     one does not exist
   */
  private static ByteBuf nextReadableBuf(EmbeddedChannel decoder) {
    for (; ; ) {
      final ByteBuf buf = decoder.readInbound();
      if (buf == null) {
        return null;
      }
      if (!buf.isReadable()) {
        buf.release();
        continue;
      }
      return buf;
    }
  }

  @Override
  protected void notifyListenerOnDataRead(
      ChannelHandlerContext ctx,
      int streamId,
      ByteBuf data,
      int padding,
      boolean endOfStream,
      Http2FrameListener listener)
      throws Http2Exception {
    final Http2Stream stream = connection.stream(streamId);
    final EmbeddedChannel decoder = stream == null ? null : stream.decompressor();
    if (decoder == null) {
      super.notifyListenerOnDataRead(ctx, streamId, data, padding, endOfStream, listener);
    } else {
      // call retain here as it will call release after its written to the channel
      decoder.writeInbound(data.retain());
      ByteBuf buf = nextReadableBuf(decoder);
      if (buf == null) {
        if (endOfStream) {
          super.notifyListenerOnDataRead(
              ctx, streamId, Unpooled.EMPTY_BUFFER, padding, true, listener);
        }
        // END_STREAM is not set and the data could not be decoded yet.
        // The assumption has to be there will be more data frames to complete the decode.
        // We don't have enough information here to know if this is an error.
      } else {
        for (; ; ) {
          final ByteBuf nextBuf = nextReadableBuf(decoder);
          if (nextBuf == null) {
            super.notifyListenerOnDataRead(ctx, streamId, buf, padding, endOfStream, listener);
            break;
          } else {
            super.notifyListenerOnDataRead(ctx, streamId, buf, padding, false, listener);
          }
          buf = nextBuf;
        }
      }

      if (endOfStream) {
        cleanup(stream, decoder);
      }
    }
  }

  @Override
  protected void notifyListenerOnHeadersRead(
      ChannelHandlerContext ctx,
      int streamId,
      Http2Headers headers,
      int streamDependency,
      short weight,
      boolean exclusive,
      int padding,
      boolean endOfStream,
      Http2FrameListener listener)
      throws Http2Exception {
    initDecoder(streamId, headers, endOfStream);
    super.notifyListenerOnHeadersRead(
        ctx,
        streamId,
        headers,
        streamDependency,
        weight,
        exclusive,
        padding,
        endOfStream,
        listener);
  }

  @Override
  protected void notifyListenerOnHeadersRead(
      ChannelHandlerContext ctx,
      int streamId,
      Http2Headers headers,
      int padding,
      boolean endOfStream,
      Http2FrameListener listener)
      throws Http2Exception {
    initDecoder(streamId, headers, endOfStream);
    super.notifyListenerOnHeadersRead(ctx, streamId, headers, padding, endOfStream, listener);
  }
}