DataModelRestMethodHandler.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.core.JsonProcessingException;
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.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.multipart.FileUpload;
import org.waarp.common.database.DbPreparedStatement;
import org.waarp.common.database.data.AbstractDbData;
import org.waarp.common.database.data.AbstractDbData.UpdatedInfo;
import org.waarp.common.database.exception.WaarpDatabaseException;
import org.waarp.common.database.exception.WaarpDatabaseNoConnectionException;
import org.waarp.common.database.exception.WaarpDatabaseSqlException;
import org.waarp.common.json.JsonHandler;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.utility.WaarpStringUtils;
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.HttpNotFoundRequestException;
import org.waarp.gateway.kernel.rest.HttpRestHandler.METHOD;
import java.nio.charset.UnsupportedCharsetException;
/**
* Generic Rest Model handler for Data model (CRUD access to a database table)
*/
public abstract class DataModelRestMethodHandler<E extends AbstractDbData>
extends RestMethodHandler {
public enum COMMAND_TYPE {
MULTIGET, GET, UPDATE, CREATE, DELETE, OPTIONS
}
/**
* Internal Logger
*/
private static final WaarpLogger logger =
WaarpLoggerFactory.getLogger(DataModelRestMethodHandler.class);
protected DataModelRestMethodHandler(final String name,
final RestConfiguration config,
final METHOD... method) {
super(name, name, true, config, METHOD.OPTIONS);
setMethods(method);
}
protected abstract void checkAuthorization(HttpRestHandler handler,
RestArgument arguments,
RestArgument result, METHOD method)
throws HttpForbiddenRequestException;
/**
* allowed: GET iff name or name/id, PUT iff name/id, POST iff name (no id),
* DELETE iff name/id and allowed
*/
@Override
public final void checkHandlerSessionCorrectness(
final HttpRestHandler handler, final RestArgument arguments,
final RestArgument result) throws HttpForbiddenRequestException {
final METHOD method = arguments.getMethod();
if (!isMethodIncluded(method)) {
logger.warn("NotAllowed: " + method + ':' + arguments.getUri() + ':' +
arguments.getUriArgs());
throw new HttpForbiddenRequestException("Unallowed Method: " + method);
}
checkAuthorization(handler, arguments, result, method);
final boolean hasOneExtraPathAsId = arguments.getSubUriSize() == 1;
final boolean hasNoExtraPath = arguments.getSubUriSize() == 0;
if (hasOneExtraPathAsId) {
arguments.addIdToUriArgs();
}
switch (method) {
case DELETE:
case PUT:
if (hasOneExtraPathAsId) {
return;
}
break;
case GET:
case OPTIONS:
return;
case POST:
if (hasNoExtraPath) {
return;
}
break;
default:
break;
}
logger.warn("NotAllowed: " + method + ':' + hasNoExtraPath + ':' +
hasOneExtraPathAsId + ':' + arguments.getUri() + ':' +
arguments.getUriArgs());
throw new HttpForbiddenRequestException(
"Unallowed Method and arguments combinaison");
}
@Override
public final void getFileUpload(final HttpRestHandler handler,
final FileUpload data,
final RestArgument arguments,
final RestArgument result)
throws HttpIncorrectRequestException {
throw new HttpIncorrectRequestException("File Upload not allowed");
}
@Override
public final Object getBody(final HttpRestHandler handler, final ByteBuf body,
final RestArgument arguments,
final RestArgument result)
throws HttpIncorrectRequestException {
// get the Json equivalent of the Body
ObjectNode node = null;
try {
final String json = body.toString(WaarpStringUtils.UTF8);
node = JsonHandler.getFromStringExc(json);
} catch (final UnsupportedCharsetException e) {
logger.warn("Error" + " : {}", e.getMessage());
throw new HttpIncorrectRequestException(e);
} catch (final JsonProcessingException e) {
result.setDetail("ERROR: JSON body cannot be parsed");
}
if (node != null) {
arguments.getBody().setAll(node);
}
return node;
}
@Override
public final void endParsingRequest(final HttpRestHandler handler,
final RestArgument arguments,
final RestArgument result,
final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
final METHOD method = arguments.getMethod();
switch (method) {
case DELETE:
delete(handler, arguments, result, body);
return;
case GET:
final boolean hasNoExtraPath = arguments.getSubUriSize() == 0;
if (hasNoExtraPath) {
getAll(handler, arguments, result, body);
} else {
getOne(handler, arguments, result, body);
}
return;
case OPTIONS:
optionsCommand(handler, arguments, result);
return;
case POST:
post(handler, arguments, result, body);
return;
case PUT:
put(handler, arguments, result, body);
return;
default:
break;
}
throw new HttpIncorrectRequestException("Incorrect request: " + method);
}
/**
* For Read or Update, should include a select() from the database. Shall
* not
* be used for Create. JSON_ID
* should be checked for the primary id.
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @return the Object E according to URI and other arguments
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected abstract E getItem(HttpRestHandler handler, RestArgument arguments,
RestArgument result, Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException;
/**
* To be used only in create mode. No insert should be done into the
* database.
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @return a new Object E according to URI and other arguments
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
*/
protected abstract E createItem(HttpRestHandler handler,
RestArgument arguments, RestArgument result,
Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException;
/**
* For getAll access
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @return the associated preparedStatement
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
*/
protected abstract DbPreparedStatement getPreparedStatement(
HttpRestHandler handler, RestArgument arguments, RestArgument result,
Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException;
/**
* @param statement
*
* @return the Object E according to statement (using next) or null if no
* more
* item
*
* @throws HttpIncorrectRequestException
* @throws HttpNotFoundRequestException
*/
protected abstract E getItemPreparedStatement(DbPreparedStatement statement)
throws HttpIncorrectRequestException, HttpNotFoundRequestException;
/**
* @return the primary property name used in the uri for Get,Put,Delete for
* unique access
*/
public abstract String getPrimaryPropertyName();
protected final void setOk(final HttpRestHandler handler,
final RestArgument result) {
handler.setStatus(HttpResponseStatus.OK);
result.setResult(HttpResponseStatus.OK);
}
/**
* Get all items, according to a possible filter
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected final void getAll(final HttpRestHandler handler,
final RestArgument arguments,
final RestArgument result, final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
final long limit = arguments.getLimitFromUri();
final DbPreparedStatement statement =
getPreparedStatement(handler, arguments, result, body);
try {
result.addFilter((ObjectNode) body);
int count = 0;
try {
statement.executeQuery();
} catch (final WaarpDatabaseNoConnectionException e) {
throw new HttpIncorrectRequestException(e);
} catch (final WaarpDatabaseSqlException e) {
throw new HttpNotFoundRequestException(e);
}
try {
for (; count < limit && statement.getNext(); count++) {
final E item = getItemPreparedStatement(statement);
if (item != null) {
result.addResult(item.getJson());
}
}
} catch (final WaarpDatabaseNoConnectionException e) {
throw new HttpIncorrectRequestException(e);
} catch (final WaarpDatabaseSqlException e) {
throw new HttpNotFoundRequestException(e);
}
result.addCountLimit(count, limit);
result.setCommand(COMMAND_TYPE.MULTIGET);
setOk(handler, result);
} finally {
statement.realClose();
}
}
/**
* Get one item according to id
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected final void getOne(final HttpRestHandler handler,
final RestArgument arguments,
final RestArgument result, final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
final E item = getItem(handler, arguments, result, body);
result.addAnswer(item.getJson());
result.setCommand(COMMAND_TYPE.GET);
setOk(handler, result);
}
/**
* Update one item according to id
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected void put(final HttpRestHandler handler,
final RestArgument arguments, final RestArgument result,
final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
final E item = getItem(handler, arguments, result, body);
try {
item.setFromJson(arguments.getBody(), true);
} catch (final WaarpDatabaseSqlException e) {
throw new HttpIncorrectRequestException(
"Issue while using Json formatting", e);
}
item.changeUpdatedInfo(UpdatedInfo.TOSUBMIT);
try {
item.update();
} catch (final WaarpDatabaseException e) {
throw new HttpIncorrectRequestException(
"Issue while updating to database", e);
}
result.addAnswer(item.getJson());
result.setCommand(COMMAND_TYPE.UPDATE);
setOk(handler, result);
}
/**
* Create one item
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
*/
protected final void post(final HttpRestHandler handler,
final RestArgument arguments,
final RestArgument result, final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException {
final E item = createItem(handler, arguments, result, body);
item.changeUpdatedInfo(UpdatedInfo.TOSUBMIT);
try {
item.insert();
} catch (final WaarpDatabaseException e) {
// Revert to update
try {
item.update();
} catch (final WaarpDatabaseException ex) {
throw new HttpIncorrectRequestException(
"Issue while inserting to database", e);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Save {}", item.getJson());
}
result.addAnswer(item.getJson());
result.setCommand(COMMAND_TYPE.CREATE);
setOk(handler, result);
}
/**
* delete one item
*
* @param handler
* @param arguments
* @param result
* @param body
*
* @throws HttpIncorrectRequestException
* @throws HttpInvalidAuthenticationException
* @throws HttpNotFoundRequestException
*/
protected final void delete(final HttpRestHandler handler,
final RestArgument arguments,
final RestArgument result, final Object body)
throws HttpIncorrectRequestException, HttpInvalidAuthenticationException,
HttpNotFoundRequestException {
final E item = getItem(handler, arguments, result, body);
try {
item.delete();
} catch (final WaarpDatabaseException e) {
throw new HttpIncorrectRequestException(
"Issue while deleting from database", e);
}
result.addAnswer(item.getJson());
result.setCommand(COMMAND_TYPE.DELETE);
setOk(handler, result);
}
@Override
public final ChannelFuture sendResponse(final HttpRestHandler handler,
final ChannelHandlerContext ctx,
final RestArgument arguments,
final RestArgument result,
final Object body,
final HttpResponseStatus status) {
final String answer = result.toString();
final ByteBuf buffer =
Unpooled.wrappedBuffer(answer.getBytes(WaarpStringUtils.UTF8));
final HttpResponse response = handler.getResponse(buffer);
if (status == HttpResponseStatus.UNAUTHORIZED) {
return ctx.writeAndFlush(response);
}
response.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
response.headers().add(HttpHeaderNames.REFERER, handler.getRequest().uri());
logger.debug("Will write: {}", body);
final ChannelFuture future = ctx.writeAndFlush(response);
if (handler.isWillClose()) {
SysErrLogger.FAKE_LOGGER.syserr(
"Will close session in DataModelRestMethodHandler");
return future;
}
return null;
}
}