
io.jsync.http.impl.DefaultHttpServerResponse Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsync.io Show documentation
Show all versions of jsync.io Show documentation
jsync.io is a non-blocking, event-driven networking framework for Java
/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.jsync.http.impl;
import io.jsync.AsyncResult;
import io.jsync.Handler;
import io.jsync.MultiMap;
import io.jsync.buffer.Buffer;
import io.jsync.file.impl.PathAdjuster;
import io.jsync.http.HttpServerResponse;
import io.jsync.impl.AsyncInternal;
import io.jsync.impl.DefaultFutureResult;
import io.jsync.utils.MimeUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.*;
import java.io.File;
/**
* @author Tim Fox
*/
public class DefaultHttpServerResponse implements HttpServerResponse {
private static final Buffer NOT_FOUND = new Buffer("Resource not found");
private static final Buffer FORBIDDEN = new Buffer("Forbidden");
private final AsyncInternal async;
private final ServerConnection conn;
private final HttpResponse response;
private final HttpVersion version;
private final boolean keepAlive;
private boolean headWritten;
private boolean written;
private Handler drainHandler;
private Handler exceptionHandler;
private Handler closeHandler;
private boolean chunked;
private boolean closed;
private ChannelFuture channelFuture;
private MultiMap headers;
private LastHttpContent trailing;
private MultiMap trailers;
private String statusMessage;
DefaultHttpServerResponse(final AsyncInternal async, ServerConnection conn, HttpRequest request) {
this.async = async;
this.conn = conn;
this.version = request.getProtocolVersion();
this.response = new DefaultHttpResponse(version, HttpResponseStatus.OK, false);
this.keepAlive = version == HttpVersion.HTTP_1_1 ||
(version == HttpVersion.HTTP_1_0 && request.headers().contains(io.jsync.http.HttpHeaders.CONNECTION, io.jsync.http.HttpHeaders.KEEP_ALIVE, true));
}
@Override
public MultiMap headers() {
if (headers == null) {
headers = new HttpHeadersAdapter(response.headers());
}
return headers;
}
@Override
public MultiMap trailers() {
if (trailers == null) {
if (trailing == null) {
trailing = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, false);
}
trailers = new HttpHeadersAdapter(trailing.trailingHeaders());
}
return trailers;
}
@Override
public int getStatusCode() {
return response.getStatus().code();
}
@Override
public HttpServerResponse setStatusCode(int statusCode) {
HttpResponseStatus status = statusMessage != null ? new HttpResponseStatus(statusCode, statusMessage) : HttpResponseStatus.valueOf(statusCode);
this.response.setStatus(status);
return this;
}
@Override
public HttpServerResponse setContentLength(int contentLength) {
checkWritten();
headers().set(HttpHeaders.Names.CONTENT_LENGTH, Integer.toString(contentLength));
return this;
}
@Override
public HttpServerResponse setContentType(String contentType) {
checkWritten();
headers().set(HttpHeaders.Names.CONTENT_TYPE, contentType);
return this;
}
@Override
public String getStatusMessage() {
return response.getStatus().reasonPhrase();
}
@Override
public HttpServerResponse setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
this.response.setStatus(new HttpResponseStatus(response.getStatus().code(), statusMessage));
return this;
}
@Override
public boolean isChunked() {
return chunked;
}
@Override
public DefaultHttpServerResponse setChunked(boolean chunked) {
checkWritten();
// HTTP 1.0 does not support chunking so we ignore this if HTTP 1.0
if (version != HttpVersion.HTTP_1_0) {
this.chunked = chunked;
}
return this;
}
@Override
public DefaultHttpServerResponse putHeader(String key, String value) {
checkWritten();
headers().set(key, value);
return this;
}
@Override
public DefaultHttpServerResponse putHeader(String key, Iterable values) {
checkWritten();
headers().set(key, values);
return this;
}
@Override
public DefaultHttpServerResponse putTrailer(String key, String value) {
checkWritten();
trailers().set(key, value);
return this;
}
@Override
public DefaultHttpServerResponse putTrailer(String key, Iterable values) {
checkWritten();
trailers().set(key, values);
return this;
}
@Override
public HttpServerResponse putHeader(CharSequence name, CharSequence value) {
checkWritten();
headers().set(name, value);
return this;
}
@Override
public HttpServerResponse putHeader(CharSequence name, Iterable values) {
checkWritten();
headers().set(name, values);
return this;
}
@Override
public HttpServerResponse putTrailer(CharSequence name, CharSequence value) {
checkWritten();
trailers().set(name, value);
return this;
}
@Override
public HttpServerResponse putTrailer(CharSequence name, Iterable value) {
checkWritten();
trailers().set(name, value);
return this;
}
@Override
public HttpServerResponse setWriteQueueMaxSize(int size) {
checkWritten();
conn.doSetWriteQueueMaxSize(size);
return this;
}
@Override
public boolean writeQueueFull() {
checkWritten();
return conn.doWriteQueueFull();
}
@Override
public HttpServerResponse drainHandler(Handler handler) {
checkWritten();
this.drainHandler = handler;
conn.handleInterestedOpsChanged(); //If the channel is already drained, we want to call it immediately
return this;
}
@Override
public HttpServerResponse exceptionHandler(Handler handler) {
checkWritten();
this.exceptionHandler = handler;
return this;
}
@Override
public HttpServerResponse closeHandler(Handler handler) {
checkWritten();
this.closeHandler = handler;
return this;
}
@Override
public DefaultHttpServerResponse write(Buffer chunk) {
ByteBuf buf = chunk.getByteBuf();
return write(buf, null);
}
@Override
public DefaultHttpServerResponse write(String chunk, String enc) {
return write(new Buffer(chunk, enc).getByteBuf(), null);
}
@Override
public DefaultHttpServerResponse write(String chunk) {
return write(new Buffer(chunk).getByteBuf(), null);
}
@Override
public void end(String chunk) {
end(new Buffer(chunk));
}
@Override
public void end(String chunk, String enc) {
end(new Buffer(chunk, enc));
}
@Override
public void end(Buffer chunk) {
if (!chunked && !contentLengthSet()) {
headers().set(io.jsync.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(chunk.length()));
}
ByteBuf buf = chunk.getByteBuf();
end0(buf);
}
@Override
public void close() {
if (!closed) {
if (headWritten) {
closeConnAfterWrite();
} else {
conn.close();
}
closed = true;
}
}
@Override
public void end() {
end0(Unpooled.EMPTY_BUFFER);
}
private void end0(ByteBuf data) {
checkWritten();
if (!headWritten) {
// if the head was not written yet we can write out everything in on go
// which is more cheap.
prepareHeaders();
FullHttpResponse resp;
if (trailing != null) {
resp = new AssembledFullHttpResponse(response, data, trailing.trailingHeaders(), trailing.getDecoderResult());
} else {
resp = new AssembledFullHttpResponse(response, data);
}
channelFuture = conn.write(resp);
headWritten = true;
} else {
if (!data.isReadable()) {
if (trailing == null) {
channelFuture = conn.write(LastHttpContent.EMPTY_LAST_CONTENT);
} else {
channelFuture = conn.write(trailing);
}
} else {
LastHttpContent content;
if (trailing != null) {
content = new AssembledLastHttpContent(data, trailing.trailingHeaders(), trailing.getDecoderResult());
} else {
content = new DefaultLastHttpContent(data, false);
}
channelFuture = conn.write(content);
}
}
if (!keepAlive) {
closeConnAfterWrite();
}
written = true;
conn.responseComplete();
}
@Override
public DefaultHttpServerResponse sendFile(String filename) {
return sendFile(filename, (String) null);
}
@Override
public DefaultHttpServerResponse sendFile(String filename, String notFoundResource) {
doSendFile(filename, notFoundResource, null);
return this;
}
@Override
public HttpServerResponse sendFile(String filename, Handler> resultHandler) {
return sendFile(filename, null, resultHandler);
}
@Override
public HttpServerResponse sendFile(String filename, String notFoundFile, Handler> resultHandler) {
doSendFile(filename, notFoundFile, resultHandler);
return this;
}
private void doSendFile(String filename, String notFoundResource, final Handler> resultHandler) {
if (headWritten) {
throw new IllegalStateException("Head already written");
}
checkWritten();
File file = new File(PathAdjuster.adjust(async, filename));
if (!file.exists()) {
if (notFoundResource != null) {
setStatusCode(HttpResponseStatus.NOT_FOUND.code());
sendFile(notFoundResource, (String) null, resultHandler);
} else {
sendNotFound();
}
} else if (file.isDirectory()) {
// send over a 403 Forbidden
sendForbidden();
} else {
if (!contentLengthSet()) {
putHeader(io.jsync.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));
}
if (!contentTypeSet()) {
int li = filename.lastIndexOf('.');
if (li != -1 && li != filename.length() - 1) {
String ext = filename.substring(li + 1, filename.length());
String contentType = MimeUtils.getMimeTypeForExtension(ext);
if (contentType != null) {
putHeader(io.jsync.http.HttpHeaders.CONTENT_TYPE, contentType);
}
}
}
prepareHeaders();
conn.queueForWrite(response);
conn.sendFile(file);
// write an empty last content to let the http encoder know the response is complete
channelFuture = conn.write(LastHttpContent.EMPTY_LAST_CONTENT);
headWritten = written = true;
if (resultHandler != null) {
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
final AsyncResult res;
if (future.isSuccess()) {
res = new DefaultFutureResult<>((Void) null);
} else {
res = new DefaultFutureResult<>(future.cause());
}
async.runOnContext(new Handler() {
@Override
public void handle(Void v) {
resultHandler.handle(res);
}
});
}
});
}
if (!keepAlive) {
closeConnAfterWrite();
}
conn.responseComplete();
}
}
private boolean contentLengthSet() {
if (headers == null) {
return false;
}
return response.headers().contains(io.jsync.http.HttpHeaders.CONTENT_LENGTH);
}
private boolean contentTypeSet() {
if (headers == null) {
return false;
}
return response.headers().contains(io.jsync.http.HttpHeaders.CONTENT_TYPE);
}
private void closeConnAfterWrite() {
if (channelFuture != null) {
channelFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
conn.close();
}
});
}
}
private void sendForbidden() {
setStatusCode(HttpResponseStatus.FORBIDDEN.code());
putHeader(io.jsync.http.HttpHeaders.CONTENT_TYPE, io.jsync.http.HttpHeaders.TEXT_HTML);
end(FORBIDDEN);
}
private void sendNotFound() {
setStatusCode(HttpResponseStatus.NOT_FOUND.code());
putHeader(io.jsync.http.HttpHeaders.CONTENT_TYPE, io.jsync.http.HttpHeaders.TEXT_HTML);
end(NOT_FOUND);
}
void handleDrained() {
if (drainHandler != null) {
drainHandler.handle(null);
}
}
void handleException(Throwable t) {
if (exceptionHandler != null) {
exceptionHandler.handle(t);
}
}
void handleClosed() {
if (closeHandler != null) {
closeHandler.handle(null);
}
}
private void checkWritten() {
if (written) {
throw new IllegalStateException("Response has already been written");
}
}
private void prepareHeaders() {
if (version == HttpVersion.HTTP_1_0 && keepAlive) {
response.headers().set(io.jsync.http.HttpHeaders.CONNECTION, io.jsync.http.HttpHeaders.KEEP_ALIVE);
}
if (chunked) {
response.headers().set(io.jsync.http.HttpHeaders.TRANSFER_ENCODING, io.jsync.http.HttpHeaders.CHUNKED);
} else if (version != HttpVersion.HTTP_1_0 && !contentLengthSet()) {
response.headers().set(io.jsync.http.HttpHeaders.CONTENT_LENGTH, "0");
}
}
private DefaultHttpServerResponse write(ByteBuf chunk, final Handler> doneHandler) {
checkWritten();
if (!headWritten && version != HttpVersion.HTTP_1_0 && !chunked && !contentLengthSet()) {
throw new IllegalStateException("You must set the Content-Length header to be the total size of the message "
+ "body BEFORE sending any data if you are not using HTTP chunked encoding.");
}
if (!headWritten) {
prepareHeaders();
channelFuture = conn.write(new AssembledHttpResponse(response, chunk));
headWritten = true;
} else {
channelFuture = conn.write(new DefaultHttpContent(chunk));
}
conn.addFuture(doneHandler, channelFuture);
return this;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy