ratpack.server.internal.DefaultResponseTransmitter Maven / Gradle / Ivy
/*
* Copyright 2014 the original author or authors.
*
* Licensed 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 ratpack.server.internal;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioStream;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ratpack.event.internal.DefaultEventController;
import ratpack.exec.ExecControl;
import ratpack.file.internal.ActivationBackedMimeTypes;
import ratpack.file.internal.ResponseTransmitter;
import ratpack.file.internal.ShouldCompressPredicate;
import ratpack.func.Pair;
import ratpack.handling.Context;
import ratpack.handling.RequestOutcome;
import ratpack.handling.internal.DefaultRequestOutcome;
import ratpack.http.Request;
import ratpack.http.SentResponse;
import ratpack.http.internal.*;
import ratpack.server.CompressionConfig;
import ratpack.util.internal.InternalRatpackError;
import java.io.FileInputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicBoolean;
public class DefaultResponseTransmitter implements ResponseTransmitter {
private final static Logger LOGGER = LoggerFactory.getLogger(DefaultResponseTransmitter.class);
private static final Runnable NOOP_RUNNABLE = () -> {
};
private final AtomicBoolean transmitted;
private final ExecControl execControl;
private final Channel channel;
private final FullHttpRequest nettyRequest;
private final Request ratpackRequest;
private final HttpHeaders responseHeaders;
private final DefaultEventController requestOutcomeEventController;
private final boolean isKeepAlive;
private final boolean isSsl;
private long stopTime;
private Runnable onWritabilityChanged = NOOP_RUNNABLE;
public DefaultResponseTransmitter(AtomicBoolean transmitted, ExecControl execControl, Channel channel, FullHttpRequest nettyRequest, Request ratpackRequest, HttpHeaders responseHeaders, DefaultEventController requestOutcomeEventController) {
this.transmitted = transmitted;
this.execControl = execControl;
this.channel = channel;
this.nettyRequest = nettyRequest.retain();
this.ratpackRequest = ratpackRequest;
this.responseHeaders = responseHeaders;
this.requestOutcomeEventController = requestOutcomeEventController;
this.isKeepAlive = HttpHeaderUtil.isKeepAlive(nettyRequest);
this.isSsl = channel.pipeline().get(SslHandler.class) != null;
}
private ChannelFuture pre(HttpResponseStatus responseStatus) {
if (transmitted.compareAndSet(false, true)) {
stopTime = System.nanoTime();
HttpResponse headersResponse = new CustomHttpResponse(responseStatus, responseHeaders);
nettyRequest.release();
if (isKeepAlive) {
headersResponse.headers().set(HttpHeaderConstants.CONNECTION, HttpHeaderConstants.KEEP_ALIVE);
}
if (channel.isOpen()) {
return channel.writeAndFlush(headersResponse).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
return null;
}
} else {
LOGGER.warn("attempt at double transmission for: " + ratpackRequest.getRawUri(), new InternalRatpackError(""));
return null;
}
}
@Override
public void transmit(final Context context, final HttpResponseStatus responseStatus, final ByteBuf body) {
responseHeaders.set(HttpHeaderConstants.CONTENT_LENGTH, body.readableBytes());
transmit(context, responseStatus, new DefaultHttpContent(body));
}
private void transmit(final Context context, final HttpResponseStatus responseStatus, final Object body) {
ChannelFuture channelFuture = pre(responseStatus);
if (channelFuture == null) {
return;
}
channelFuture.addListener(future -> {
if (channel.isOpen()) {
channel.write(body);
post(responseStatus);
}
});
}
@Override
public void transmit(final Context context, final HttpResponseStatus responseStatus, final BasicFileAttributes basicFileAttributes, final Path file) {
String contentType = responseHeaders.get(HttpHeaderConstants.CONTENT_TYPE);
final long size = basicFileAttributes.size();
Pair fileDetails = Pair.of(size, contentType);
CompressionConfig compressionConfig = context.get(CompressionConfig.class);
final boolean compressionEnabled = compressionConfig.isCompressResponses();
final Predicate super Pair> shouldCompress;
if (compressionEnabled) {
ImmutableSet blacklist = compressionConfig.getMimeTypeBlackList();
shouldCompress = new ShouldCompressPredicate(
compressionConfig.getMinSize(),
compressionConfig.getMimeTypeWhiteList(),
blacklist.isEmpty() ? ActivationBackedMimeTypes.getDefaultExcludedMimeTypes() : blacklist
);
} else {
shouldCompress = Predicates.alwaysFalse();
}
final boolean compressThis = compressionEnabled && (contentType != null && shouldCompress.apply(fileDetails));
if (!compressThis) {
// Signal to the compressor not to compress this
responseHeaders.set(HttpHeaderConstants.CONTENT_ENCODING, HttpHeaderConstants.IDENTITY);
}
responseHeaders.set(HttpHeaderConstants.CONTENT_LENGTH, size);
if (!isSsl && !compressThis && file.getFileSystem().equals(FileSystems.getDefault())) {
execControl.blocking(() -> new FileInputStream(file.toFile()).getChannel()).then(fileChannel -> {
FileRegion defaultFileRegion = new DefaultFileRegion(fileChannel, 0, size);
transmit(context, responseStatus, defaultFileRegion);
});
} else {
execControl.blocking(() -> Files.newByteChannel(file)).then(fileChannel ->
transmit(context, responseStatus, new HttpChunkedInput(new ChunkedNioStream(fileChannel)))
);
}
}
@Override
public Subscriber transmitter(final Context context, final HttpResponseStatus responseStatus) {
CompressionConfig compressionConfig = context.get(CompressionConfig.class);
if (!compressionConfig.isCompressResponses()) {
// Signal to the compressor not to compress this
responseHeaders.set(HttpHeaderConstants.CONTENT_ENCODING, HttpHeaderConstants.IDENTITY);
}
return new Subscriber() {
private Subscription subscription;
private final AtomicBoolean done = new AtomicBoolean();
private final ChannelFutureListener cancelOnFailure = future -> {
if (!done.get()) {
if (!future.isSuccess()) {
cancel();
}
}
};
private void cancel() {
if (done.compareAndSet(false, true)) {
subscription.cancel();
post(responseStatus);
}
}
@Override
public void onSubscribe(Subscription s) {
if (this.subscription != null) {
s.cancel();
return;
}
this.subscription = s;
onWritabilityChanged = () -> {
if (channel.isWritable() && !done.get()) {
subscription.request(1);
}
};
ChannelFuture channelFuture = pre(responseStatus);
if (channelFuture == null) {
s.cancel();
notifyListeners(responseStatus, channel.close());
} else {
channelFuture.addListener(cancelOnFailure);
if (channel.isWritable()) {
subscription.request(1);
}
}
}
@Override
public void onNext(ByteBuf o) {
if (channel.isOpen()) {
channel.writeAndFlush(new DefaultHttpContent(o)).addListener(cancelOnFailure);
if (channel.isWritable()) {
subscription.request(1);
}
}
}
@Override
public void onError(Throwable t) {
LOGGER.warn("Exception thrown transmitting stream", t);
if (done.compareAndSet(false, true)) {
post(responseStatus);
}
}
@Override
public void onComplete() {
if (done.compareAndSet(false, true)) {
post(responseStatus);
}
}
};
}
private void post(HttpResponseStatus responseStatus) {
if (channel.isOpen()) {
ChannelFuture lastContentFuture = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!isKeepAlive) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
notifyListeners(responseStatus, lastContentFuture);
} else {
notifyListeners(responseStatus, channel.newSucceededFuture());
}
}
private void notifyListeners(final HttpResponseStatus responseStatus, ChannelFuture future) {
if (requestOutcomeEventController.isHasListeners()) {
future.addListener(ignore -> {
SentResponse sentResponse = new DefaultSentResponse(new NettyHeadersBackedHeaders(responseHeaders), new DefaultStatus(responseStatus));
RequestOutcome requestOutcome = new DefaultRequestOutcome(ratpackRequest, sentResponse, stopTime);
requestOutcomeEventController.fire(requestOutcome);
});
}
}
public void writabilityChanged() {
onWritabilityChanged.run();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy