001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2019 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.io.OutputStream;
020
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024
025import java.util.function.Supplier;
026import java.util.function.UnaryOperator;
027
028import java.util.logging.Logger;
029
030import javax.ws.rs.HttpMethod;
031
032import javax.ws.rs.core.Response.StatusType;
033
034import io.netty.channel.ChannelHandlerContext;
035import io.netty.channel.ChannelPromise;
036
037import io.netty.handler.codec.http.DefaultFullHttpResponse;
038import io.netty.handler.codec.http.DefaultHttpResponse;
039import io.netty.handler.codec.http.HttpContent;
040import io.netty.handler.codec.http.HttpHeaders;
041import io.netty.handler.codec.http.HttpMessage;
042import io.netty.handler.codec.http.HttpRequest;
043import io.netty.handler.codec.http.HttpResponse; // for javadoc only
044import io.netty.handler.codec.http.HttpResponseStatus;
045import io.netty.handler.codec.http.HttpUtil;
046import io.netty.handler.codec.http.HttpVersion;
047
048import io.netty.util.concurrent.Future;
049import io.netty.util.concurrent.GenericFutureListener;
050
051import org.glassfish.jersey.server.ApplicationHandler;
052import org.glassfish.jersey.server.ContainerRequest;
053import org.glassfish.jersey.server.ContainerResponse;
054
055import org.microbean.jersey.netty.AbstractByteBufBackedChannelOutboundInvokingOutputStream.ByteBufCreator;
056
057/**
058 * An {@link AbstractContainerRequestHandlingResponseWriter}
059 * implemented in terms of {@link HttpRequest}s, {@link HttpResponse}s
060 * and {@link
061 * ByteBufBackedChannelOutboundInvokingHttpContentOutputStream}s.
062 *
063 * @author <a href="https://about.me/lairdnelson"
064 * target="_parent">Laird Nelson</a>
065 *
066 * @see AbstractContainerRequestHandlingResponseWriter
067 *
068 * @see ByteBufBackedChannelOutboundInvokingHttpContentOutputStream
069 *
070 * @see #writeStatusAndHeaders(long, ContainerResponse)
071 *
072 * @see #createOutputStream(long, ContainerResponse)
073 */
074public final class HttpContainerRequestHandlingResponseWriter extends AbstractContainerRequestHandlingResponseWriter<HttpContent> {
075
076
077  /*
078   * Static fields.
079   */
080
081
082  private static final String cn = HttpContainerRequestHandlingResponseWriter.class.getName();
083
084  private static final Logger logger = Logger.getLogger(cn);
085
086  private static final GenericFutureListener<? extends Future<? super Void>> listener = new LoggingWriteListener(logger);
087
088
089  /*
090   * Instance fields.
091   */
092
093
094  /**
095   * The {@link HttpVersion} currently in effect.
096   *
097   * <p>The only reason this field has to exist is so that the {@link
098   * #writeFailureMessage(Throwable)} method knows what HTTP version
099   * to use (1.0 or 1.1).</p>
100   *
101   * @see #writeFailureMessage(Throwable)
102   */
103  private HttpVersion httpVersion;
104
105
106  /*
107   * Constructors.
108   */
109
110
111  /**
112   * Creates a new {@link HttpContainerRequestHandlingResponseWriter}.
113   *
114   * @param applicationHandler an {@link ApplicationHandler}
115   * representing a <a
116   * href="https://jakarta.ee/specifications/restful-ws/"
117   * target="_parent">Jakarta RESTful Web Services application</a>
118   * whose {@link ApplicationHandler#handle(ContainerRequest)} method
119   * will serve as the bridge between Netty and Jersey; may be {@code
120   * null} somewhat pathologically but normally is not
121   *
122   * @see ApplicationHandler
123   *
124   * @see ApplicationHandler#handle(ContainerRequest)
125   */
126  public HttpContainerRequestHandlingResponseWriter(final ApplicationHandler applicationHandler) {
127    super(new ImmutableSupplier<>(applicationHandler));
128  }
129
130  /**
131   * Creates a new {@link HttpContainerRequestHandlingResponseWriter}.
132   *
133   * @param applicationHandler an {@link ApplicationHandler}
134   * representing a <a
135   * href="https://jakarta.ee/specifications/restful-ws/"
136   * target="_parent">Jakarta RESTful Web Services application</a>
137   * whose {@link ApplicationHandler#handle(ContainerRequest)} method
138   * will serve as the bridge between Netty and Jersey; may be {@code
139   * null} somewhat pathologically but normally is not
140   *
141   * @param flushThreshold the minimum number of bytes that an {@link
142   * OutputStream} returned by the {@link #createOutputStream(long,
143   * ContainerResponse)} method must write before an automatic
144   * {@linkplain OutputStream#flush() flush} may take place; if less
145   * than {@code 0} {@code 0} will be used instead; if {@link
146   * Integer#MAX_VALUE} then it is suggested that no automatic
147   * flushing will occur
148   *
149   * @param byteBufCreator a {@link ByteBufCreator} that will be used
150   * by the {@link #createOutputStream(long, ContainerResponse)}
151   * method; may be {@code null}
152   *
153   * @see ApplicationHandler
154   *
155   * @see ApplicationHandler#handle(ContainerRequest)
156   */
157  public HttpContainerRequestHandlingResponseWriter(final ApplicationHandler applicationHandler,
158                                                    final int flushThreshold,
159                                                    final ByteBufCreator byteBufCreator) {
160    super(new ImmutableSupplier<>(applicationHandler), flushThreshold, byteBufCreator);
161  }
162
163  /**
164   * Creates a new {@link HttpContainerRequestHandlingResponseWriter}.
165   *
166   * @param applicationHandlerSupplier a {@link Supplier} of an {@link
167   * ApplicationHandler} representing a <a
168   * href="https://jakarta.ee/specifications/restful-ws/"
169   * target="_parent">Jakarta RESTful Web Services application</a>
170   * whose {@link ApplicationHandler#handle(ContainerRequest)} method
171   * will serve as the bridge between Netty and Jersey; may be {@code
172   * null} somewhat pathologically but normally is not
173   *
174   * @see ApplicationHandler
175   *
176   * @see ApplicationHandler#handle(ContainerRequest)
177   */
178  public HttpContainerRequestHandlingResponseWriter(final Supplier<? extends ApplicationHandler> applicationHandlerSupplier) {
179    super(applicationHandlerSupplier);
180  }
181
182  /**
183   * Creates a new {@link HttpContainerRequestHandlingResponseWriter}.
184   *
185   * @param applicationHandlerSupplier a {@link Supplier} of an {@link
186   * ApplicationHandler} representing a <a
187   * href="https://jakarta.ee/specifications/restful-ws/"
188   * target="_parent">Jakarta RESTful Web Services application</a>
189   * whose {@link ApplicationHandler#handle(ContainerRequest)} method
190   * will serve as the bridge between Netty and Jersey; may be {@code
191   * null} somewhat pathologically but normally is not
192   *
193   * @param flushThreshold the minimum number of bytes that an {@link
194   * OutputStream} returned by the {@link #createOutputStream(long,
195   * ContainerResponse)} method must write before an automatic
196   * {@linkplain OutputStream#flush() flush} may take place; if less
197   * than {@code 0} {@code 0} will be used instead; if {@link
198   * Integer#MAX_VALUE} then it is suggested that no automatic
199   * flushing will occur
200   *
201   * @param byteBufCreator a {@link ByteBufCreator} that will be used
202   * by the {@link #createOutputStream(long, ContainerResponse)}
203   * method; may be {@code null}
204   *
205   * @see ApplicationHandler
206   *
207   * @see ApplicationHandler#handle(ContainerRequest)
208   */
209  public HttpContainerRequestHandlingResponseWriter(final Supplier<? extends ApplicationHandler> applicationHandlerSupplier,
210                                                    final int flushThreshold,
211                                                    final ByteBufCreator byteBufCreator) {
212    super(applicationHandlerSupplier, flushThreshold, byteBufCreator);
213  }
214
215
216  /*
217   * Instance methods.
218   */
219
220
221  /**
222   * Writes the status and headers portion of the response present in
223   * the supplied {@link ContainerResponse} and returns {@code true}
224   * if further output is forthcoming.
225   *
226   * <p>This implementation writes an instance of either {@link
227   * DefaultHttpResponse} or {@link DefaultFullHttpResponse}.</p>
228   *
229   * @param contentLength the content length as determined by the
230   * logic encapsulated by the {@link
231   * ApplicationHandler#handle(ContainerRequest)} method; a value less
232   * than zero indicates an unknown content length
233   *
234   * @param containerResponse the {@link ContainerResponse} containing
235   * status and headers information; must not be {@code null}
236   *
237   * @return {@code true} if the {@link #createOutputStream(long,
238   * ContainerResponse)} method should be invoked, <em>i.e.</em> if
239   * further output is forthcoming
240   *
241   * @exception NullPointerException if {@code containerResponse} is
242   * {@code null}
243   *
244   * @exception IllegalArgumentException if the supplied {@link
245   * ContainerResponse} returns {@code null} from its {@link
246   * ContainerResponse#getRequestContext()} method or if that {@link
247   * ContainerRequest} returns something that is not an {@link
248   * HttpRequest} from invoking {@link
249   * ContainerRequest#getProperty(String)
250   * getProperty("io.netty.handler.codec.http.HttpRequest")} on it
251   *
252   * @see ApplicationHandler#handle(ContainerRequest)
253   *
254   * @see #createOutputStream(long, ContainerResponse)
255   */
256  @Override
257  protected final boolean writeStatusAndHeaders(final long contentLength,
258                                                final ContainerResponse containerResponse) {
259    final ContainerRequest containerRequest = containerResponse.getRequestContext();
260    if (containerRequest == null) {
261      throw new IllegalArgumentException("containerResponse.getRequestContext() == null");
262    }
263
264    final HttpRequest httpRequest;
265    final Object httpRequestValue = containerRequest.getProperty(HttpRequest.class.getName());
266    if (!(httpRequestValue instanceof HttpRequest)) {
267      throw new IllegalArgumentException("containerResponse; !(containerResponse.getRequestContext().getProperty(\"" +
268                                         HttpRequest.class.getName() +
269                                         "\") instanceof HttpRequest): " + httpRequestValue);
270    } else {
271      httpRequest = (HttpRequest)httpRequestValue;
272    }
273
274    final HttpVersion httpVersion = httpRequest.protocolVersion();
275    this.httpVersion = httpVersion;
276
277    final HttpResponseStatus status;
278    final StatusType responseStatusType = containerResponse.getStatusInfo();
279    if (responseStatusType == null) {
280      status = HttpResponseStatus.valueOf(containerResponse.getStatus());
281    } else {
282      final String reasonPhrase = responseStatusType.getReasonPhrase();
283      if (reasonPhrase == null) {
284        status = HttpResponseStatus.valueOf(containerResponse.getStatus());
285      } else {
286        status = HttpResponseStatus.valueOf(containerResponse.getStatus(), reasonPhrase);
287      }
288    }
289
290    final HttpMessage httpResponse;
291    final boolean needsOutputStream;
292    if (contentLength < 0L) {
293      needsOutputStream = !HttpMethod.HEAD.equalsIgnoreCase(containerRequest.getMethod());
294      httpResponse = new DefaultHttpResponse(httpVersion, status);
295      copyHeaders(containerResponse.getStringHeaders(), httpResponse.headers());
296      HttpUtil.setTransferEncodingChunked(httpResponse, true);
297    } else if (contentLength == 0L) {
298      needsOutputStream = false;
299      httpResponse = new DefaultFullHttpResponse(httpVersion, status);
300      copyHeaders(containerResponse.getStringHeaders(), httpResponse.headers());
301      HttpUtil.setContentLength(httpResponse, 0L);
302    } else {
303      needsOutputStream = !HttpMethod.HEAD.equalsIgnoreCase(containerRequest.getMethod());
304      httpResponse = new DefaultHttpResponse(httpVersion, status);
305      copyHeaders(containerResponse.getStringHeaders(), httpResponse.headers());
306      HttpUtil.setContentLength(httpResponse, contentLength);
307    }
308    if (HttpUtil.isKeepAlive(httpRequest)) {
309      HttpUtil.setKeepAlive(httpResponse, true);
310    }
311
312    final ChannelHandlerContext channelHandlerContext = Objects.requireNonNull(this.getChannelHandlerContext());
313
314    final ChannelPromise channelPromise = channelHandlerContext.newPromise();
315    assert channelPromise != null;
316    channelPromise.addListener(listener);
317
318    // Remember that
319    // AbstractContainerRequestHandlingResponseWriter#channelReadComplete(ChannelHandlerContext)
320    // will call ChannelHandlerContext#flush() in all cases.
321    channelHandlerContext.write(httpResponse, channelPromise);
322
323    return needsOutputStream;
324  }
325
326  /**
327   * Creates and returns a new {@link
328   * ByteBufBackedChannelOutboundInvokingHttpContentOutputStream}.
329   *
330   * <p>This method never returns {@code null}.</p>
331   *
332   * @param contentLength the content length as determined by the
333   * logic encapsulated by the {@link
334   * ApplicationHandler#handle(ContainerRequest)} method; must not be
335   * {@code 0L}
336   *
337   * @param containerResponse a {@link ContainerResponse} for which an
338   * {@link OutputStream} is being created and returned; must not be
339   * {@code null}; ignored by this implementation
340   *
341   * @return a new {@link
342   * ByteBufBackedChannelOutboundInvokingHttpContentOutputStream}
343   *
344   * @exception NullPointerException if {@code containerResponse} is
345   * {@code null} or if {@link #getChannelHandlerContext()} returns
346   * {@code null}
347   *
348   * @exception IllegalArgumentException if {@code contentLength} is
349   * {@code 0L}
350   *
351   * @see ByteBufBackedChannelOutboundInvokingHttpContentOutputStream
352   */
353  @Override
354  protected final AbstractChannelOutboundInvokingOutputStream<? extends HttpContent> createOutputStream(final long contentLength,
355                                                                                                        final ContainerResponse containerResponse) {
356    if (contentLength == 0L) {
357      throw new IllegalArgumentException("contentLength == 0L");
358    }
359    return new ByteBufBackedChannelOutboundInvokingHttpContentOutputStream(this.getChannelHandlerContext(),
360                                                                           this.getFlushThreshold(),
361                                                                           false,
362                                                                           this.getByteBufCreator()) {
363      @Override
364      protected final ChannelPromise newPromise() {
365        final ChannelPromise returnValue = super.newPromise();
366        if (returnValue != null && !returnValue.isVoid()) {
367          returnValue.addListener(listener);
368        }
369        return returnValue;
370      }
371    };
372  }
373
374  /**
375   * Overrides {@link
376   * AbstractContainerRequestHandlingResponseWriter#commit()} to still
377   * effectively do nothing, but clean up some internal state.
378   *
379   * @see AbstractContainerRequestHandlingResponseWriter#commit()
380   */
381  @Override
382  public final void commit() {
383    this.httpVersion = null;
384    super.commit();
385  }
386
387  /**
388   * Writes an appropriate failure message using the return value of
389   * the {@link #getChannelHandlerContext()} method.
390   *
391   * @param failureCause the {@link Throwable} responsible for this
392   * method's invocation; may be {@code null} in pathological cases;
393   * ignored by this implementation
394   *
395   * @exception NullPointerException if {@link
396   * #getChannelHandlerContext()} returns {@code null}
397   */
398  @Override
399  protected final void writeFailureMessage(final Throwable failureCause) {
400    final HttpVersion httpVersion = this.httpVersion;
401    this.httpVersion = null;
402
403    final ChannelHandlerContext channelHandlerContext = Objects.requireNonNull(this.getChannelHandlerContext());
404
405    final ChannelPromise channelPromise = channelHandlerContext.newPromise();
406    assert channelPromise != null;
407    channelPromise.addListener(listener);
408
409    final HttpMessage failureMessage = new DefaultFullHttpResponse(httpVersion, HttpResponseStatus.INTERNAL_SERVER_ERROR);
410    HttpUtil.setContentLength(failureMessage, 0L);
411
412    channelHandlerContext.write(failureMessage, channelPromise);
413  }
414
415
416  /*
417   * Static methods.
418   */
419
420
421  private static final void copyHeaders(final Map<? extends String, ? extends List<String>> headersSource,
422                                        final HttpHeaders nettyHeaders) {
423    AbstractContainerRequestHandlingResponseWriter.copyHeaders(headersSource, UnaryOperator.identity(), nettyHeaders::add);
424  }
425
426}