HttpRestHandler.java
/*
* This file is part of Waarp Project (named also Waarp or GG).
*
* Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
* tags. See the COPYRIGHT.txt in the distribution for a full listing of
* individual contributors.
*
* All Waarp Project is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Waarp . If not, see <http://www.gnu.org/licenses/>.
*/
package org.waarp.gateway.kernel.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.DiskAttribute;
import io.netty.handler.codec.http.multipart.DiskFileUpload;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType;
import org.waarp.common.crypto.ssl.WaarpSslUtility;
import org.waarp.common.database.DbSession;
import org.waarp.common.exception.CryptoException;
import org.waarp.common.json.JsonHandler;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpNettyUtil;
import org.waarp.common.utility.WaarpStringUtils;
import org.waarp.gateway.kernel.database.DbConstantGateway;
import org.waarp.gateway.kernel.exception.HttpForbiddenRequestException;
import org.waarp.gateway.kernel.exception.HttpIncorrectRequestException;
import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
import org.waarp.gateway.kernel.exception.HttpMethodNotAllowedRequestException;
import org.waarp.gateway.kernel.exception.HttpNotFoundRequestException;
import java.io.File;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
/**
* Handler for HTTP Rest support
*/
public abstract class HttpRestHandler
extends SimpleChannelInboundHandler<HttpObject> {
/**
* Internal Logger
*/
private static final WaarpLogger logger =
WaarpLoggerFactory.getLogger(HttpRestHandler.class);
/*
* Note: Presence de BODY dans toutes les requetes/responses = Content-Length ou Transfer-Encoding HEAD:
* response pas de BODY
*
*/
public enum METHOD {
/**
* REST: Standard GET item
* <p>
* The GET method means retrieve whatever information (in the form of
* an
* entity) is identified by the
* Request-URI. If the Request-URI refers to a data-producing process,
* it is
* the produced data which shall be
* returned as the entity in the response and not the source text of
* the
* process, unless that text happens to
* be the output of the process.
*/
GET(HttpMethod.GET),
/**
* REST: Update existing item
* <p>
* The PUT method requests that the enclosed entity be stored under the
* supplied Request-URI.
*/
PUT(HttpMethod.PUT),
/**
* REST: Create a new item
* <p>
* The POST method is used to request that the origin server accept the
* entity enclosed in the request as a
* new subordinate of the resource identified by the Request-URI in the
* Request-Line.
*/
POST(HttpMethod.POST),
/**
* REST: Delete existing item
* <p>
* The DELETE method requests that the origin server delete the resource
* identified by the Request-URI.
*/
DELETE(HttpMethod.DELETE),
/**
* REST: what options are supported for the URI
* <p>
* The OPTIONS method represents a request for information about the
* communication options available on the
* request/response chain identified by the Request-URI. This method
* allows
* the client to determine the
* options and/or requirements associated with a resource, or the
* capabilities of a server, without implying a
* resource action or initiating a resource retrieval.
*/
OPTIONS(HttpMethod.OPTIONS),
/**
* REST: as GET but no BODY (existence ? metadata ?)
* <p>
* The HEAD method is identical to GET except that the server MUST NOT
* return a message-body in the response.
*/
HEAD(HttpMethod.HEAD),
/**
* REST: should not be used, use POST instead
* <p>
* The PATCH method requests that a set of changes described in the
* request
* entity be applied to the resource
* identified by the Request-URI.
*/
PATCH(HttpMethod.PATCH),
/**
* REST: unknown usage
* <p>
* The TRACE method is used to invoke a remote, application-layer
* loop-back
* of the request message.
*/
TRACE(HttpMethod.TRACE),
/**
* REST: unknown
* <p>
* This specification reserves the method name CONNECT for use with a
* proxy
* that can dynamically switch to
* being a tunnel
*/
CONNECT(HttpMethod.CONNECT);
public final HttpMethod method;
METHOD(final HttpMethod method) {
this.method = method;
}
}
public static final HttpDataFactory factory =
new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
// Disk if size exceed MINSIZE = 16K
// XXX FIXME TODO to setup outside !
public static String TempPath = "J:/GG/ARK/TMP";
public static ChannelGroup group;
/**
* Initialize the Disk support
*
* @param tempPath system temp directory
*
* @throws IOException
* @throws CryptoException
*/
public static void initialize(final String tempPath) {
TempPath = tempPath;
final File file = new File(tempPath);
file.mkdirs();//NOSONAR
DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
// on exit (in normal
// exit)
DiskFileUpload.baseDirectory = TempPath; // system temp
// directory
DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
// exit (in normal exit)
DiskAttribute.baseDirectory = TempPath; // system temp directory
}
public static final RestConfiguration defaultConfiguration =
new RestConfiguration();
public HashMap<String, RestMethodHandler> restHashMap;
public final RestConfiguration restConfiguration;
protected final RootOptionsRestMethodHandler rootHandler;
protected HttpPostRequestDecoder decoder;
protected HttpResponseStatus status = HttpResponseStatus.OK;
protected HttpRequest request;
protected RestMethodHandler handler;
protected DbSession dbSession;
private boolean willClose;
/**
* Arguments received
*/
protected RestArgument arguments;
/**
* The only structure that might be needed is: ARGS_COOKIE (subset)
*/
protected RestArgument response;
/**
* JSON decoded object
*/
protected Object jsonObject;
/**
* Cumulative chunks
*/
protected ByteBuf cumulativeBody;
protected HttpRestHandler(final RestConfiguration config) {
restConfiguration = config;
rootHandler = new RootOptionsRestMethodHandler(config);
}
protected static class HttpCleanChannelFutureListener
implements ChannelFutureListener {
protected final HttpRestHandler handler;
/**
* @param handler
*/
public HttpCleanChannelFutureListener(final HttpRestHandler handler) {
this.handler = handler;
}
@Override
public final void operationComplete(final ChannelFuture future) {
handler.clean();
}
}
@Override
public final void channelActive(final ChannelHandlerContext ctx)
throws Exception {
if (group != null) {
group.add(ctx.channel());
}
super.channelActive(ctx);
}
/**
* Clean method
* <p>
* Override if needed
*/
protected final void clean() {
if (arguments != null) {
arguments.clean();
arguments = null;
}
if (response != null) {
response.clean();
response = null;
}
if (decoder != null) {
decoder.cleanFiles();
decoder = null;
}
handler = null;
cumulativeBody = null;
jsonObject = null;
if (dbSession != null) {
dbSession.enUseConnectionNoDisconnect();
dbSession = null;
}
}
/**
* Called at the beginning of every new request
* <p>
* Override if needed
*/
protected final void initialize() {
// clean previous FileUpload if Any
clean();
status = HttpResponseStatus.OK;
request = null;
setWillClose(false);
arguments = new RestArgument(JsonHandler.createObjectNode());
response = new RestArgument(JsonHandler.createObjectNode());
}
/**
* @return the DbSession associated with the current request (might be Admin
* dbSession if none)
*/
public final DbSession getDbSession() {
return dbSession == null? DbConstantGateway.admin.getSession() : dbSession;
}
/**
* To be used for instance to check correctness of connection<br>
* Note that ARG_METHOD is only set from current request. It might be also
* set
* from URI or HEADER and
* therefore should be done in this method.
*
* @param ctx
*
* @throws HttpInvalidAuthenticationException
*/
protected abstract void checkConnection(ChannelHandlerContext ctx)
throws HttpInvalidAuthenticationException;
/**
* Method to set Cookies in httpResponse from response ObjectNode
*
* @param httpResponse
*/
protected final void setCookies(final FullHttpResponse httpResponse) {
if (response == null) {
return;
}
final ObjectNode cookieON = response.getCookieArgs();
if (!cookieON.isMissingNode()) {
final Iterator<Entry<String, JsonNode>> iter = cookieON.fields();
while (iter.hasNext()) {
final Entry<String, JsonNode> entry = iter.next();
httpResponse.headers().add(HttpHeaderNames.SET_COOKIE,
ServerCookieEncoder.LAX.encode(
entry.getKey(),
entry.getValue().asText()));
}
}
}
/**
* Could be overwritten if necessary
*
* @return RestMethodHandler associated with the current context
*
* @throws HttpIncorrectRequestException
* @throws HttpMethodNotAllowedRequestException
* @throws HttpForbiddenRequestException
*/
protected final RestMethodHandler getHandler()
throws HttpMethodNotAllowedRequestException,
HttpForbiddenRequestException {
final METHOD method = arguments.getMethod();
final String uri = arguments.getBaseUri();
boolean restFound = false;
RestMethodHandler handlerNew = restHashMap.get(uri);
if (handlerNew != null) {
handlerNew.checkHandlerSessionCorrectness(this, arguments, response);
if (handlerNew.isMethodIncluded(method)) {
restFound = true;
}
}
if (handlerNew == null && method == METHOD.OPTIONS) {
handlerNew = rootHandler;
// use Options default handler
restFound = true;
}
logger.debug("{} {} {}", method, uri, restFound);
if (!restFound) {
throw new HttpMethodNotAllowedRequestException(
"No Method found for that URI: " + uri);
}
return handlerNew;
}
@Override
protected void channelRead0(final ChannelHandlerContext ctx,
final HttpObject msg) {
logger.debug("Msg Received");
try {
if (msg instanceof HttpRequest) {
initialize();
request = (HttpRequest) msg;
arguments.setRequest(request);
final Iterator<Entry<CharSequence, CharSequence>> iterator =
request.headers().iteratorCharSequence();
arguments.setHeaderArgs(iterator);
arguments.setCookieArgs(request.headers().get(HttpHeaderNames.COOKIE));
logger.debug("DEBUG: {}", arguments);
checkConnection(ctx);
handler = getHandler();
if (arguments.getMethod() == METHOD.OPTIONS) {
response.setFromArgument(arguments);
handler.optionsCommand(this, arguments, response);
finalizeSend(ctx);
return;
}
if (request instanceof FullHttpRequest) {
if (handler.isBodyJsonDecoded()) {
final ByteBuf buffer = ((FullHttpRequest) request).content();
jsonObject = getBodyJsonArgs(buffer);
} else {
// decoder for 1 chunk
createDecoder();
// Not chunk version
readAllHttpData();
}
response.setFromArgument(arguments);
handler.endParsingRequest(this, arguments, response, jsonObject);
finalizeSend(ctx);
return;
}
// no body yet
if (!handler.isBodyJsonDecoded()) {
createDecoder();
}
} else {
// New chunk is received
if (handler != null) {
bodyChunk(ctx, (HttpContent) msg);
}
}
} catch (final HttpIncorrectRequestException e1) {
// real error => 400
if (handler != null) {
status =
handler.handleException(this, arguments, response, jsonObject, e1);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.BAD_REQUEST;
}
logger.warn("Error: {}", e1.getMessage(), e1);
if (response.getDetail().isEmpty()) {
response.setDetail(e1.getMessage());
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
} catch (final HttpMethodNotAllowedRequestException e1) {
if (handler != null) {
status =
handler.handleException(this, arguments, response, jsonObject, e1);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.METHOD_NOT_ALLOWED;
}
logger.warn("Error: {}", e1.getMessage());
if (response.getDetail().isEmpty()) {
response.setDetail(e1.getMessage());
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
} catch (final HttpForbiddenRequestException e1) {
if (handler != null) {
status =
handler.handleException(this, arguments, response, jsonObject, e1);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.FORBIDDEN;
}
logger.warn("Error: {}", e1.getMessage());
if (response.getDetail().isEmpty()) {
response.setDetail(e1.getMessage());
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
} catch (final HttpInvalidAuthenticationException e1) {
if (handler != null) {
status =
handler.handleException(this, arguments, response, jsonObject, e1);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.UNAUTHORIZED;
}
logger.warn("Error: {}", e1.getMessage());
if (response.getDetail().isEmpty()) {
response.setDetail(e1.getMessage());
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
} catch (final HttpNotFoundRequestException e1) {
if (handler != null) {
status =
handler.handleException(this, arguments, response, jsonObject, e1);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.NOT_FOUND;
}
logger.warn("Error: {}", e1.getMessage());
if (response.getDetail().isEmpty()) {
response.setDetail(e1.getMessage());
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
}
}
/**
* Create the decoder
*
* @throws HttpIncorrectRequestException
*/
protected final void createDecoder() throws HttpIncorrectRequestException {
final HttpMethod method = request.method();
if (!method.equals(HttpMethod.HEAD)) {
// in order decoder allows to parse
request.setMethod(HttpMethod.POST);
}
try {
decoder = new HttpPostRequestDecoder(factory, request);
} catch (final ErrorDataDecoderException e1) {
status = HttpResponseStatus.NOT_ACCEPTABLE;
throw new HttpIncorrectRequestException(e1);
} catch (final Exception e1) {
// GETDOWNLOAD Method: should not try to create a HttpPostRequestDecoder
// So OK but stop here
status = HttpResponseStatus.NOT_ACCEPTABLE;
throw new HttpIncorrectRequestException(e1);
}
}
/**
* Read all InterfaceHttpData from finished transfer
*
* @throws HttpIncorrectRequestException
*/
protected final void readAllHttpData() throws HttpIncorrectRequestException {
final List<InterfaceHttpData> datas;
try {
datas = decoder.getBodyHttpDatas();
} catch (final NotEnoughDataDecoderException e1) {
// Should not be!
logger.warn("decoder issue: {}", e1.getMessage());
status = HttpResponseStatus.NOT_ACCEPTABLE;
throw new HttpIncorrectRequestException(e1);
}
for (final InterfaceHttpData data : datas) {
readHttpData(data);
}
}
/**
* Read one Data
*
* @param data
*
* @throws HttpIncorrectRequestException
*/
protected final void readHttpData(final InterfaceHttpData data)
throws HttpIncorrectRequestException {
if (data.getHttpDataType() == HttpDataType.Attribute) {
final ObjectNode body = arguments.getBody();
try {
body.put(data.getName(), ((Attribute) data).getValue());
} catch (final IOException e) {
throw new HttpIncorrectRequestException("Bad reading", e);
}
} else if (data.getHttpDataType() == HttpDataType.FileUpload) {
final FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {
handler.getFileUpload(this, fileUpload, arguments, response);
} else {
logger.warn("File still pending but should not");
fileUpload.delete();
status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
throw new HttpIncorrectRequestException(
"File still pending but should not");
}
} else {
logger.warn("Unknown element: " + data);
}
}
/**
* To allow quick answer even if in very bad shape
*
* @param ctx
*/
protected final void forceClosing(final ChannelHandlerContext ctx) {
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
}
if (ctx.channel().isActive()) {
setWillClose(true);
final String answer =
"<html><body>Error " + status.reasonPhrase() + "</body></html>";
final FullHttpResponse httpResponse = getResponse(
Unpooled.wrappedBuffer(answer.getBytes(WaarpStringUtils.UTF8)));
httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html");
httpResponse.headers().set(HttpHeaderNames.REFERER, request.uri());
final ChannelFuture future = ctx.writeAndFlush(httpResponse);
logger.debug("Will close");
future.addListener(WaarpSslUtility.SSLCLOSE);
}
clean();
}
/**
* @param content
*
* @return the Http Response according to the status and the content if not
* null (setting the CONTENT_LENGTH)
*/
public final FullHttpResponse getResponse(final ByteBuf content) {
// Decide whether to close the connection or not.
if (request == null) {
final FullHttpResponse httpResponse;
if (content == null) {
httpResponse =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, status);
} else {
httpResponse =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, status, content);
httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,
content.array().length);
}
setCookies(httpResponse);
setWillClose(true);
return httpResponse;
}
boolean keepAlive = HttpUtil.isKeepAlive(request);
setWillClose(isWillClose() || status != HttpResponseStatus.OK ||
HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(
request.headers().get(HttpHeaderNames.CONNECTION)) ||
request.protocolVersion().equals(HttpVersion.HTTP_1_0) &&
!keepAlive);
if (isWillClose()) {
keepAlive = false;
}
// Build the response object.
final FullHttpResponse httpResponse;
if (content != null) {
httpResponse =
new DefaultFullHttpResponse(request.protocolVersion(), status,
content);
httpResponse.headers()
.set(HttpHeaderNames.CONTENT_LENGTH, content.array().length);
} else {
httpResponse =
new DefaultFullHttpResponse(request.protocolVersion(), status);
}
if (keepAlive) {
httpResponse.headers()
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
setCookies(httpResponse);
return httpResponse;
}
/**
* Method that get a chunk of data
*
* @param ctx
* @param chunk
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected final void bodyChunk(final ChannelHandlerContext ctx,
final HttpContent chunk)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
// New chunk is received: only for Post!
if (handler.isBodyJsonDecoded()) {
final ByteBuf buffer = chunk.content();
if (cumulativeBody != null) {
if (buffer.isReadable()) {
cumulativeBody = Unpooled.wrappedBuffer(cumulativeBody, buffer);
}
} else {
cumulativeBody = buffer.slice();
}
} else {
try {
decoder.offer(chunk);
} catch (final ErrorDataDecoderException e1) {
status = HttpResponseStatus.NOT_ACCEPTABLE;
throw new HttpIncorrectRequestException(e1);
}
// example of reading chunk by chunk (minimize memory usage due to
// Factory)
readHttpDataChunkByChunk();
}
// example of reading only if at the end
if (chunk instanceof LastHttpContent) {
if (handler.isBodyJsonDecoded()) {
jsonObject = getBodyJsonArgs(cumulativeBody);
WaarpNettyUtil.release(cumulativeBody);
cumulativeBody = null;
}
response.setFromArgument(arguments);
handler.endParsingRequest(this, arguments, response, jsonObject);
finalizeSend(ctx);
}
}
protected final void finalizeSend(final ChannelHandlerContext ctx) {
final ChannelFuture future;
if (arguments.getMethod() == METHOD.OPTIONS) {
future = handler.sendOptionsResponse(this, ctx, response, status);
} else {
future = handler.sendResponse(this, ctx, arguments, response, jsonObject,
status);
}
if (future != null) {
future.addListener(WaarpSslUtility.SSLCLOSE);
}
clean();
logger.debug("Cleaned");
}
/**
* Get Body args as JSON body
*
* @param data
*
* @throws HttpIncorrectRequestException
*/
protected final Object getBodyJsonArgs(final ByteBuf data)
throws HttpIncorrectRequestException {
if (data == null || data.readableBytes() == 0) {
return null;
}
return handler.getBody(this, data, arguments, response);
}
/**
* Read request by chunk and getting values from chunk to chunk
*
* @throws HttpIncorrectRequestException
*/
protected final void readHttpDataChunkByChunk()
throws HttpIncorrectRequestException {
try {
while (decoder.hasNext()) {
final InterfaceHttpData data = decoder.next();
if (data != null) {
// new value
readHttpData(data);
}
}
} catch (final EndOfDataDecoderException e1) {
// end
}
}
@Override
public final void exceptionCaught(final ChannelHandlerContext ctx,
final Throwable cause) {
if (ctx.channel().isActive()) {
if (cause != null && cause.getMessage() != null) {
logger.warn("Exception {}", cause.getMessage());
} else {
logger.warn("Exception Received", cause);
}
if (cause instanceof ClosedChannelException ||
cause instanceof IOException) {
return;
}
if (handler != null) {
status = handler.handleException(this, arguments, response, jsonObject,
(Exception) cause);
}
if (status == HttpResponseStatus.OK) {
status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
}
if (handler != null) {
finalizeSend(ctx);
} else {
forceClosing(ctx);
}
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx)
throws Exception {
super.channelInactive(ctx);
clean();
}
/**
* @return the status
*/
public final HttpResponseStatus getStatus() {
return status;
}
/**
* @param status the status to set
*/
public final void setStatus(final HttpResponseStatus status) {
this.status = status;
}
/**
* @return the request
*/
public final HttpRequest getRequest() {
return request;
}
/**
* @return the willClose
*/
public final boolean isWillClose() {
return willClose;
}
/**
* @param willClose the willClose to set
*/
public final void setWillClose(final boolean willClose) {
this.willClose = willClose;
}
}