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}