All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.deps.io.netty.handler.codec.http.HttpContentEncoder Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package com.couchbase.client.deps.io.netty.handler.codec.http;

import com.couchbase.client.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.deps.io.netty.buffer.ByteBufHolder;
import com.couchbase.client.deps.io.netty.channel.ChannelHandlerContext;
import com.couchbase.client.deps.io.netty.channel.embedded.EmbeddedChannel;
import com.couchbase.client.deps.io.netty.handler.codec.MessageToMessageCodec;
import com.couchbase.client.deps.io.netty.handler.codec.http.HttpHeaders.Names;
import com.couchbase.client.deps.io.netty.handler.codec.http.HttpHeaders.Values;
import com.couchbase.client.deps.io.netty.util.ReferenceCountUtil;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;

/**
 * Encodes the content of the outbound {@link HttpResponse} and {@link HttpContent}.
 * The original content is replaced with the new content encoded by the
 * {@link EmbeddedChannel}, which is created by {@link #beginEncode(HttpResponse, String)}.
 * Once encoding is finished, the value of the 'Content-Encoding' header
 * is set to the target content encoding, as returned by
 * {@link #beginEncode(HttpResponse, String)}.
 * Also, the 'Content-Length' header is updated to the length of the
 * encoded content.  If there is no supported or allowed encoding in the
 * corresponding {@link HttpRequest}'s {@code "Accept-Encoding"} header,
 * {@link #beginEncode(HttpResponse, String)} should return {@code null} so that
 * no encoding occurs (i.e. pass-through).
 * 

* Please note that this is an abstract class. You have to extend this class * and implement {@link #beginEncode(HttpResponse, String)} properly to make * this class functional. For example, refer to the source code of * {@link HttpContentCompressor}. *

* This handler must be placed after {@link HttpObjectEncoder} in the pipeline * so that this handler can intercept HTTP responses before {@link HttpObjectEncoder} * converts them into {@link ByteBuf}s. */ public abstract class HttpContentEncoder extends MessageToMessageCodec { private enum State { PASS_THROUGH, AWAIT_HEADERS, AWAIT_CONTENT } private final Queue acceptEncodingQueue = new ArrayDeque(); private String acceptEncoding; private EmbeddedChannel encoder; private State state = State.AWAIT_HEADERS; @Override public boolean acceptOutboundMessage(Object msg) throws Exception { return msg instanceof HttpContent || msg instanceof HttpResponse; } @Override protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List out) throws Exception { String acceptedEncoding = msg.headers().get(HttpHeaders.Names.ACCEPT_ENCODING); if (acceptedEncoding == null) { acceptedEncoding = HttpHeaders.Values.IDENTITY; } acceptEncodingQueue.add(acceptedEncoding); out.add(ReferenceCountUtil.retain(msg)); } @Override protected void encode(ChannelHandlerContext ctx, HttpObject msg, List out) throws Exception { final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent; switch (state) { case AWAIT_HEADERS: { ensureHeaders(msg); assert encoder == null; final HttpResponse res = (HttpResponse) msg; /* * per rfc2616 4.3 Message Body * All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a * message-body. All other responses do include a message-body, although it MAY be of zero length. */ if (isPassthru(res)) { if (isFull) { out.add(ReferenceCountUtil.retain(res)); } else { out.add(res); // Pass through all following contents. state = State.PASS_THROUGH; } break; } // Get the list of encodings accepted by the peer. acceptEncoding = acceptEncodingQueue.poll(); if (acceptEncoding == null) { throw new IllegalStateException("cannot send more responses than requests"); } if (isFull) { // Pass through the full response with empty content and continue waiting for the the next resp. if (!((ByteBufHolder) res).content().isReadable()) { out.add(ReferenceCountUtil.retain(res)); break; } } // Prepare to encode the content. final Result result = beginEncode(res, acceptEncoding); // If unable to encode, pass through. if (result == null) { if (isFull) { out.add(ReferenceCountUtil.retain(res)); } else { out.add(res); // Pass through all following contents. state = State.PASS_THROUGH; } break; } encoder = result.contentEncoder(); // Encode the content and remove or replace the existing headers // so that the message looks like a decoded message. res.headers().set(Names.CONTENT_ENCODING, result.targetContentEncoding()); // Make the response chunked to simplify content transformation. res.headers().remove(Names.CONTENT_LENGTH); res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED); // Output the rewritten response. if (isFull) { // Convert full message into unfull one. HttpResponse newRes = new DefaultHttpResponse(res.getProtocolVersion(), res.getStatus()); newRes.headers().set(res.headers()); out.add(newRes); // Fall through to encode the content of the full response. } else { out.add(res); state = State.AWAIT_CONTENT; if (!(msg instanceof HttpContent)) { // only break out the switch statement if we have not content to process // See https://github.com/netty/netty/issues/2006 break; } // Fall through to encode the content } } case AWAIT_CONTENT: { ensureContent(msg); if (encodeContent((HttpContent) msg, out)) { state = State.AWAIT_HEADERS; } break; } case PASS_THROUGH: { ensureContent(msg); out.add(ReferenceCountUtil.retain(msg)); // Passed through all following contents of the current response. if (msg instanceof LastHttpContent) { state = State.AWAIT_HEADERS; } break; } } } private static boolean isPassthru(HttpResponse res) { final int code = res.getStatus().code(); return code < 200 || code == 204 || code == 304; } private static void ensureHeaders(HttpObject msg) { if (!(msg instanceof HttpResponse)) { throw new IllegalStateException( "unexpected message type: " + msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')'); } } private static void ensureContent(HttpObject msg) { if (!(msg instanceof HttpContent)) { throw new IllegalStateException( "unexpected message type: " + msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')'); } } private boolean encodeContent(HttpContent c, List out) { ByteBuf content = c.content(); encode(content, out); if (c instanceof LastHttpContent) { finishEncode(out); LastHttpContent last = (LastHttpContent) c; // Generate an additional chunk if the decoder produced // the last product on closure, HttpHeaders headers = last.trailingHeaders(); if (headers.isEmpty()) { out.add(LastHttpContent.EMPTY_LAST_CONTENT); } else { out.add(new ComposedLastHttpContent(headers)); } return true; } return false; } /** * Prepare to encode the HTTP message content. * * @param headers * the headers * @param acceptEncoding * the value of the {@code "Accept-Encoding"} header * * @return the result of preparation, which is composed of the determined * target content encoding and a new {@link EmbeddedChannel} that * encodes the content into the target content encoding. * {@code null} if {@code acceptEncoding} is unsupported or rejected * and thus the content should be handled as-is (i.e. no encoding). */ protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception; @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { cleanup(); super.handlerRemoved(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { cleanup(); super.channelInactive(ctx); } private void cleanup() { if (encoder != null) { // Clean-up the previous encoder if not cleaned up correctly. if (encoder.finish()) { for (;;) { ByteBuf buf = (ByteBuf) encoder.readOutbound(); if (buf == null) { break; } // Release the buffer // https://github.com/netty/netty/issues/1524 buf.release(); } } encoder = null; } } private void encode(ByteBuf in, List out) { // call retain here as it will call release after its written to the channel encoder.writeOutbound(in.retain()); fetchEncoderOutput(out); } private void finishEncode(List out) { if (encoder.finish()) { fetchEncoderOutput(out); } encoder = null; } private void fetchEncoderOutput(List out) { for (;;) { ByteBuf buf = (ByteBuf) encoder.readOutbound(); if (buf == null) { break; } if (!buf.isReadable()) { buf.release(); continue; } out.add(new DefaultHttpContent(buf)); } } public static final class Result { private final String targetContentEncoding; private final EmbeddedChannel contentEncoder; public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) { if (targetContentEncoding == null) { throw new NullPointerException("targetContentEncoding"); } if (contentEncoder == null) { throw new NullPointerException("contentEncoder"); } this.targetContentEncoding = targetContentEncoding; this.contentEncoder = contentEncoder; } public String targetContentEncoding() { return targetContentEncoding; } public EmbeddedChannel contentEncoder() { return contentEncoder; } } }