View Javadoc
1   /*
2    * Copyright 2021 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.compression;
17  
18  import com.aayushatharva.brotli4j.encoder.BrotliEncoderChannel;
19  import com.aayushatharva.brotli4j.encoder.Encoder;
20  import io.netty.buffer.ByteBuf;
21  import io.netty.buffer.Unpooled;
22  import io.netty.channel.Channel;
23  import io.netty.channel.ChannelFuture;
24  import io.netty.channel.ChannelHandler;
25  import io.netty.channel.ChannelHandlerContext;
26  import io.netty.channel.ChannelPromise;
27  import io.netty.handler.codec.MessageToByteEncoder;
28  import io.netty.util.AttributeKey;
29  import io.netty.util.ReferenceCountUtil;
30  import io.netty.util.internal.ObjectUtil;
31  
32  import java.io.IOException;
33  import java.nio.ByteBuffer;
34  import java.nio.channels.ClosedChannelException;
35  import java.nio.channels.WritableByteChannel;
36  
37  /**
38   * Compress a {@link ByteBuf} with the Brotli compression.
39   * <p>
40   * See <a href="https://github.com/google/brotli">brotli</a>.
41   */
42  @ChannelHandler.Sharable
43  public final class BrotliEncoder extends MessageToByteEncoder<ByteBuf> {
44  
45      private static final AttributeKey<Writer> ATTR = AttributeKey.valueOf("BrotliEncoderWriter");
46  
47      private final Encoder.Parameters parameters;
48      private final boolean isSharable;
49      private Writer writer;
50  
51      /**
52       * Create a new {@link BrotliEncoder} Instance with {@link BrotliOptions#DEFAULT}
53       * and {@link #isSharable()} set to {@code true}
54       */
55      public BrotliEncoder() {
56          this(BrotliOptions.DEFAULT);
57      }
58  
59      /**
60       * Create a new {@link BrotliEncoder} Instance
61       *
62       * @param brotliOptions {@link BrotliOptions} to use and
63       *                      {@link #isSharable()} set to {@code true}
64       */
65      public BrotliEncoder(BrotliOptions brotliOptions) {
66          this(brotliOptions.parameters());
67      }
68  
69      /**
70       * Create a new {@link BrotliEncoder} Instance
71       * and {@link #isSharable()} set to {@code true}
72       *
73       * @param parameters {@link Encoder.Parameters} to use
74       */
75      public BrotliEncoder(Encoder.Parameters parameters) {
76          this(parameters, true);
77      }
78  
79      /**
80       * <p>
81       * Create a new {@link BrotliEncoder} Instance and specify
82       * whether this instance will be shared with multiple pipelines or not.
83       * </p>
84       *
85       * If {@link #isSharable()} is true then on {@link #handlerAdded(ChannelHandlerContext)} call,
86       * a new {@link Writer} will create, and it will be mapped using {@link Channel#attr(AttributeKey)}
87       * so {@link BrotliEncoder} can be shared with multiple pipelines. This works fine but there on every
88       * {@link #encode(ChannelHandlerContext, ByteBuf, ByteBuf)} call, we have to get the {@link Writer} associated
89       * with the appropriate channel. And this will add a overhead. So it is recommended to set {@link #isSharable()}
90       * to {@code false} and create new {@link BrotliEncoder} instance for every pipeline.
91       *
92       * @param parameters {@link Encoder.Parameters} to use
93       * @param isSharable Set to {@code true} if this instance is shared else set to {@code false}
94       */
95      public BrotliEncoder(Encoder.Parameters parameters, boolean isSharable) {
96          super(ByteBuf.class);
97          this.parameters = ObjectUtil.checkNotNull(parameters, "Parameters");
98          this.isSharable = isSharable;
99      }
100 
101     @Override
102     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
103         Writer writer = new Writer(parameters, ctx);
104         if (isSharable) {
105             ctx.channel().attr(ATTR).set(writer);
106         } else {
107             this.writer = writer;
108         }
109         super.handlerAdded(ctx);
110     }
111 
112     @Override
113     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
114         finish(ctx);
115         super.handlerRemoved(ctx);
116     }
117 
118     @Override
119     protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
120         // NO-OP
121     }
122 
123     @Override
124     protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
125         if (!msg.isReadable()) {
126             return Unpooled.EMPTY_BUFFER;
127         }
128 
129         Writer writer;
130         if (isSharable) {
131             writer = ctx.channel().attr(ATTR).get();
132         } else {
133             writer = this.writer;
134         }
135 
136         // If Writer is 'null' then Writer is not open.
137         if (writer == null) {
138             return Unpooled.EMPTY_BUFFER;
139         } else {
140             writer.encode(msg, preferDirect);
141             return writer.writableBuffer;
142         }
143     }
144 
145     @Override
146     public boolean isSharable() {
147         return isSharable;
148     }
149 
150     /**
151      * Finish the encoding, close streams and write final {@link ByteBuf} to the channel.
152      *
153      * @param ctx {@link ChannelHandlerContext} which we want to close
154      * @throws IOException If an error occurred during closure
155      */
156     public void finish(ChannelHandlerContext ctx) throws IOException {
157         finishEncode(ctx);
158     }
159 
160     private ChannelFuture finishEncode(ChannelHandlerContext ctx) throws IOException {
161         Writer writer;
162 
163         if (isSharable) {
164             writer = ctx.channel().attr(ATTR).getAndSet(null);
165         } else {
166             writer = this.writer;
167         }
168 
169         if (writer != null) {
170             writer.close();
171             this.writer = null;
172             return writer.closeFuture;
173         }
174         return ctx.newSucceededFuture();
175     }
176 
177     @Override
178     public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) throws Exception {
179         ChannelFuture f = finishEncode(ctx);
180         EncoderUtil.closeAfterFinishEncode(ctx, f, promise);
181     }
182 
183     /**
184      * {@link Writer} is the implementation of {@link WritableByteChannel} which encodes
185      * Brotli data and stores it into {@link ByteBuf}.
186      */
187     private static final class Writer implements WritableByteChannel {
188 
189         private ByteBuf writableBuffer;
190         private final BrotliEncoderChannel brotliEncoderChannel;
191         private final ChannelHandlerContext ctx;
192         private final ChannelPromise closeFuture;
193         private boolean closeInitiated;
194         private boolean isClosed;
195 
196         private Writer(Encoder.Parameters parameters, ChannelHandlerContext ctx) throws IOException {
197             brotliEncoderChannel = new BrotliEncoderChannel(this, parameters);
198             this.ctx = ctx;
199             this.closeFuture = ctx.newPromise();
200         }
201 
202         private void encode(ByteBuf msg, boolean preferDirect) throws Exception {
203             try {
204                 allocate(preferDirect);
205 
206                 // Compress data and flush it into Buffer.
207                 //
208                 // As soon as we call flush, Encoder will be triggered to write encoded
209                 // data into WritableByteChannel.
210                 //
211                 // A race condition will not arise because one flush call to encoder will result
212                 // in only 1 call at `write(ByteBuffer)`.
213                 ByteBuffer nioBuffer = CompressionUtil.safeReadableNioBuffer(msg);
214                 int position = nioBuffer.position();
215                 brotliEncoderChannel.write(nioBuffer);
216                 msg.skipBytes(nioBuffer.position() - position);
217                 brotliEncoderChannel.flush();
218             } catch (Exception e) {
219                 ReferenceCountUtil.release(msg);
220                 throw e;
221             }
222         }
223 
224         private void allocate(boolean preferDirect) {
225             if (preferDirect) {
226                 writableBuffer = ctx.alloc().ioBuffer();
227             } else {
228                 writableBuffer = ctx.alloc().buffer();
229             }
230         }
231 
232         @Override
233         public int write(ByteBuffer src) throws IOException {
234             if (!isOpen()) {
235                 throw new ClosedChannelException();
236             }
237 
238             return writableBuffer.writeBytes(src).readableBytes();
239         }
240 
241         @Override
242         public boolean isOpen() {
243             return !isClosed;
244         }
245 
246         @Override
247         public void close() {
248             if (closeInitiated) {
249                 return;
250             }
251             closeInitiated = true;
252             ctx.executor().execute(new Runnable() {
253                 @Override
254                 public void run() {
255                     try {
256                         finish(closeFuture);
257                     } catch (IOException ex) {
258                         closeFuture.setFailure(new IllegalStateException("Failed to finish encoding", ex));
259                     }
260                 }
261             });
262         }
263 
264         public void finish(final ChannelPromise promise) throws IOException {
265             if (!isClosed) {
266                 // Allocate a buffer and write last pending data.
267                 allocate(true);
268 
269                 try {
270                     brotliEncoderChannel.close();
271                     isClosed = true;
272                 } catch (Exception ex) {
273                     promise.setFailure(ex);
274 
275                     // Since we have already allocated Buffer for close operation,
276                     // we will release that buffer to prevent memory leak.
277                     ReferenceCountUtil.release(writableBuffer);
278                     return;
279                 }
280 
281                 ctx.writeAndFlush(writableBuffer, promise);
282             }
283         }
284     }
285 }