/**
* Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019)
*
* contact.vitam@culture.gouv.fr
*
* This software is a computer program whose purpose is to implement a digital archiving back-office system managing
* high volumetry securely and efficiently.
*
* This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free
* software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as
* circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license,
* users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the
* successive licensors have only limited liability.
*
* In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or
* developing or reproducing the software by the user in light of its specific status of free software, that may mean
* that it is complicated to manipulate, and that also therefore means that it is reserved for developers and
* experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the
* software's suitability as regards their requirements in conditions enabling the security of their systems and/or data
* to be ensured and, more generally, to use and operate it in the same conditions as regards security.
*
* The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you
* accept its terms.
*/
package fr.gouv.vitam.common.client;
import java.util.Iterator;
import java.util.List;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Strings;
import fr.gouv.vitam.common.GlobalDataRest;
import fr.gouv.vitam.common.ParametersChecker;
import fr.gouv.vitam.common.exception.InvalidParseOperationException;
import fr.gouv.vitam.common.exception.VitamClientInternalException;
import fr.gouv.vitam.common.json.JsonHandler;
import fr.gouv.vitam.common.logging.VitamLogger;
import fr.gouv.vitam.common.logging.VitamLoggerFactory;
import fr.gouv.vitam.common.model.RequestResponseOK;
import fr.gouv.vitam.common.model.VitamAutoCloseable;
/**
* Utility to help with Http based Cursor that implements real Database Cursor on server side
*/
public class VitamRequestIterator<T> implements VitamAutoCloseable, Iterator<T> {
private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(VitamRequestIterator.class);
private final MockOrRestClient client;
private final JsonNode request;
private final String method;
private final String path;
private final MultivaluedHashMap<String, Object> headers;
private final Class<T> responseType;
private String xCursorId = null;
private boolean first = true;
private boolean closed = false;
private RequestResponseOK<T> objectResponse = null;
private Iterator<T> iterator = null;
/**
* Constructor</br>
* </br>
* Note: if of type AbstractMockClient or derived, request will be the returned unique result.
*
* @param client the client to use
* @param method the method to use
* @param path the path to use
* @param responseType the type of the response to be returned
* @param headers the headers to use, could be null
* @param request the request to use, could be null
* @throws IllegalArgumentException if one of mandatory arguments is null or empty
*/
// TODO P1 Add later on capability to handle maxNbPart in order to control the rate
public VitamRequestIterator(MockOrRestClient client, String method, String path,
Class<T> responseType, MultivaluedHashMap<String, Object> headers,
JsonNode request) {
ParametersChecker.checkParameter("Arguments method and path could not be null", method, path);
ParametersChecker.checkParameter("Argument client could not be null", client);
this.client = client;
this.method = method;
this.path = path;
if (headers != null) {
this.headers = headers;
} else {
this.headers = new MultivaluedHashMap<>();
}
this.request = request;
this.responseType = responseType;
}
@Override
public void close() {
if (closed) {
return;
}
// Callback to close the cursor
closed = true;
if (xCursorId == null) {
return;
}
if (client instanceof AbstractMockClient) {
return;
}
Response response = null;
try {
headers.add(GlobalDataRest.X_CURSOR, false);
headers.add(GlobalDataRest.X_CURSOR_ID, xCursorId);
response =
((AbstractCommonClient) client).performRequest(method, path, headers, MediaType.APPLICATION_JSON_TYPE);
} catch (final VitamClientInternalException e) {
throw new BadRequestException(e);
} finally {
client.consumeAnyEntityAndClose(response);
}
}
private boolean handleFirst(Response response) {
// TODO P1 Ignore for the moment X-Cursor-Timeout
xCursorId = (String) response.getHeaders().getFirst(GlobalDataRest.X_CURSOR_ID);
if (xCursorId == null && !closed) {
throw new BadRequestException("No Cursor returned");
} else {
headers.add(GlobalDataRest.X_CURSOR_ID, xCursorId);
}
try {
objectResponse = JsonHandler.getFromString(response.readEntity(String.class), RequestResponseOK.class, responseType);
} catch (InvalidParseOperationException e) {
LOGGER.error("Invalid response, json parsing fail", e);
return false;
}
iterator = objectResponse.getResults().iterator();
if (!iterator.hasNext()) {
objectResponse = null;
iterator = null;
return false;
}
return true;
}
private boolean handleNext(Response response) {
// TODO P1 Ignore for the moment X-Cursor-Timeout
try {
objectResponse = JsonHandler.getFromString(response.readEntity(String.class), RequestResponseOK.class, responseType);
} catch (InvalidParseOperationException e) {
LOGGER.error("Invalid response, json parsing fail", e);
return false;
}
iterator = objectResponse.getResults().iterator();
if (!iterator.hasNext()) {
objectResponse = null;
iterator = null;
return false;
}
return true;
}
/**
* @return true if there is a next element
* @throws BadRequestException (RuntimeException) if the request is in error
*/
@Override
public boolean hasNext() {
// next not called after a previous hasNext
if (objectResponse != null) {
return true;
}
if (closed) {
return false;
}
if (client instanceof AbstractMockClient) {
return true;
}
// First call must initialize the cursor-id
if (first) {
first = false;
Response response = null;
try {
headers.add(GlobalDataRest.X_CURSOR, true);
response = ((AbstractCommonClient) client).performRequest(method, path, headers, request,
MediaType.APPLICATION_JSON_TYPE,
MediaType.APPLICATION_JSON_TYPE);
switch (Response.Status.fromStatusCode(response.getStatus())) {
case NOT_FOUND:
closed = true;
return false;
case OK:
// Unique no Cursor
closed = true;
return handleFirst(response);
case PARTIAL_CONTENT:
// Multiple with Cursor
return handleFirst(response);
default:
closed = true;
throw new BadRequestException(Response.Status.PRECONDITION_FAILED.getReasonPhrase());
}
} catch (final VitamClientInternalException e) {
throw new BadRequestException(e);
} finally {
client.consumeAnyEntityAndClose(response);
}
} else {
Response response = null;
try {
response = ((AbstractCommonClient) client).performRequest(method, path, headers,
MediaType.APPLICATION_JSON_TYPE);
switch (Response.Status.fromStatusCode(response.getStatus())) {
case NOT_FOUND:
closed = true;
return false;
case OK:
// End of cursor
closed = true;
return handleNext(response);
case PARTIAL_CONTENT:
return handleNext(response);
default:
closed = true;
LOGGER.error(Response.Status.PRECONDITION_FAILED.getReasonPhrase());
throw new BadRequestException(Response.Status.PRECONDITION_FAILED.getReasonPhrase());
}
} catch (final VitamClientInternalException e) {
throw new BadRequestException(e);
} finally {
client.consumeAnyEntityAndClose(response);
}
}
}
@Override
public T next() {
if (client instanceof AbstractMockClient) {
closed = true;
if (request == null) {
getClass().getTypeParameters().getClass();
return (T) new Object();
}
}
T result = null;
if (objectResponse != null) {
result = iterator.next();
if (!iterator.hasNext()) {
objectResponse = null;
iterator = null;
}
}
return result;
}
private static boolean checkHeadersConformity(HttpHeaders headers) {
if (headers != null) {
boolean xcursor;
final MultivaluedMap<String, String> map = headers.getRequestHeaders();
if (!map.containsKey(GlobalDataRest.X_CURSOR)) {
throw new IllegalStateException(GlobalDataRest.X_CURSOR + " should be always defined");
}
if (map.get(GlobalDataRest.X_CURSOR).isEmpty()) {
throw new IllegalStateException(GlobalDataRest.X_CURSOR + " should be defined");
}
xcursor = Boolean.parseBoolean(map.getFirst(GlobalDataRest.X_CURSOR));
if (!xcursor && Strings.isNullOrEmpty(getCursorId(headers))) {
throw new IllegalStateException(GlobalDataRest.X_CURSOR + " should be true when " +
GlobalDataRest.X_CURSOR_ID + " is not set");
}
return xcursor;
}
throw new IllegalStateException("Headers is null");
}
/**
* Helper for server side to check if this is a end of cursor
*
* @param headers
* @return True if the cursor is to be ended on Server side
* @throws IllegalStateException if the headers are not consistent
*/
public static boolean isEndOfCursor(HttpHeaders headers) {
return !checkHeadersConformity(headers);
}
/**
* Helper for server side to check if this is a ending of cursor
*
* @param xcursor
* @param xcursorId
* @return True if the cursor is to be ended on Server side
*/
public static boolean isEndOfCursor(boolean xcursor, String xcursorId) {
return !xcursor && !Strings.isNullOrEmpty(xcursorId);
}
/**
* Helper for server side to check if this is a creation of cursor
*
* @param headers
* @return True if the cursor is to be created on Server side
* @throws IllegalStateException if the headers are not consistent
*/
public static boolean isNewCursor(HttpHeaders headers) {
final boolean xcursor = checkHeadersConformity(headers);
if (!xcursor) {
return false;
}
final List<String> cidlist = headers.getRequestHeader(GlobalDataRest.X_CURSOR_ID);
if (cidlist == null) {
return xcursor;
}
return isNewCursor(xcursor, cidlist.get(0));
}
/**
* Helper for server side to check if this is a creation of cursor
*
* @param xcursor
* @param xcursorId
* @return True if the cursor is to be created on Server side
*/
public static boolean isNewCursor(boolean xcursor, String xcursorId) {
return xcursor && Strings.isNullOrEmpty(xcursorId);
}
/**
* Helper for server side to get the cursor Id
*
* @param headers
* @return the X-Cursor-ID content
*/
public static String getCursorId(HttpHeaders headers) {
final List<String> cidlist = headers.getRequestHeader(GlobalDataRest.X_CURSOR_ID);
if (cidlist == null) {
return "";
}
return cidlist.get(0);
}
/**
* Helper for server and client to set the needed headers
*
* @param builder the current ResponseBuilder
* @param active True for create or continue, False for inactive (X-Cursor)
* @param xcursorId may be null, else contains the current X-Cursor-Id
* @return the ResponseBuilder with the new required header
*/
public static ResponseBuilder setHeaders(ResponseBuilder builder, boolean active, String xcursorId) {
builder.header(GlobalDataRest.X_CURSOR, active);
if (xcursorId != null) {
builder.header(GlobalDataRest.X_CURSOR_ID, xcursorId);
}
return builder;
}
}