Skip to content
Snippets Groups Projects

Resolve "Amélioration d'un message d'erreur non explicite quand une valeur n'est pas fournie alors que OA_required est à true"

Files
27
package fr.inra.oresing.client;
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.file.AccumulatorPathVisitor;
import org.apache.commons.io.file.Counters;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.entity.mime.FileBody;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* Cette classe n’est pas utile en soi, elle a vocation à être portée en Groovy pour
* devenir le script qui est inclus dans le kit de téléversement.
*/
// à ajouter en tête du fichier Groovy
//
//@Grab(group = "commons-io", module = "commons-io", version = "2.8.0")
//@Grab(group = "org.apache.httpcomponents.client5", module = "httpclient5", version = "5.2.1")
public class Client {
public static void main(String[] args) throws IOException {
new Client().run();
}
public void run() throws IOException {
ClientConfiguration clientConfiguration = readConfiguration();
String applicationName = clientConfiguration.applicationName();
URI instanceUrl = clientConfiguration.instanceUrl();
String login;
String password;
boolean interactive = true;
Scanner scanner = new Scanner(System.in);
if (interactive) {
System.out.println("Veuillez saisir les informations de connexion à " + instanceUrl);
System.out.print("identifiant : ");
login = scanner.nextLine();
System.out.print("mot de passe : ");
password = scanner.nextLine();
} else {
login = "poussin";
password = "xxxx";
}
CookieStore cookieStore = new BasicCookieStore();
UriFactory uriFactory = new UriFactory(instanceUrl, applicationName);
try (CloseableHttpClient httpclient = HttpClients.custom()
.setDefaultCookieStore(cookieStore)
.build()) {
ClassicHttpRequest loginRequest = ClassicRequestBuilder.post()
.setUri(uriFactory.forLogin())
.addParameter("login", login)
.addParameter("password", password)
.build();
httpclient.execute(loginRequest, response -> {
switch (response.getCode()) {
case HttpURLConnection.HTTP_OK -> {
EntityUtils.consume(response.getEntity());
switch (cookieStore.getCookies().size()) {
case 0 -> fail("authentification échouée : pas de cookie d’authentification retourné");
case 1 -> {
if (cookieStore.getCookies().get(0).getName().equals("si-ore-jwt")) {
log("authentification OK");
} else {
fail("authentification échouée : pas de cookie d’authentification retourné");
}
}
default ->
fail(cookieStore.getCookies().size() + " cookies retournés à l’authentification");
}
}
case HttpURLConnection.HTTP_UNAUTHORIZED ->
fail("authentification échouée : identifiant ou mot de passe faux ?");
default ->
fail("%d en code de retour HTTP inattendu à l’authentification".formatted(response.getCode()));
}
return null;
});
ClassicHttpRequest getApplicationReferenceTypesRequest = ClassicRequestBuilder
.get(uriFactory.forApplicationReferenceTypes(applicationName))
.build();
List<String> refTypes = httpclient.execute(getApplicationReferenceTypesRequest, response ->
switch (response.getCode()) {
case HttpURLConnection.HTTP_OK ->
parseJsonInResponseBody(
response,
new TypeReference<List<String>>() {
}
);
case HttpURLConnection.HTTP_UNAUTHORIZED ->
throw new IllegalStateException("pas l’autorisation pour l’application " + applicationName);
default ->
throw new IllegalStateException("%d en code de retour HTTP inattendu".formatted(response.getCode()));
}
);
log("référentiels à importer :" + System.lineSeparator()
+ String.join(System.lineSeparator(), refTypes));
ClassicHttpRequest getApplicationDataTypesRequest = ClassicRequestBuilder
.get(uriFactory.forApplicationDataTypes(applicationName))
.build();
List<String> dataTypes = httpclient.execute(getApplicationDataTypesRequest, response ->
switch (response.getCode()) {
case HttpURLConnection.HTTP_OK ->
parseJsonInResponseBody(
response,
new TypeReference<List<String>>() {}
);
case HttpURLConnection.HTTP_UNAUTHORIZED ->
throw new IllegalStateException("pas l’autorisation pour l’application " + applicationName);
default ->
throw new IllegalStateException("%d en code de retour HTTP inattendu".formatted(response.getCode()));
});
log("données expérimentales à importer :" + System.lineSeparator()
+ String.join(System.lineSeparator(), dataTypes));
List<Command> commands = newCommands(refTypes, dataTypes);
if (interactive) {
String plan = commands.stream()
.map(Command::getDescription)
.collect(Collectors.joining(System.lineSeparator()));
log("Plan :");
log(plan);
System.out.print("est-ce que le plan convient ? [O/n]");
String planIsOkString = scanner.nextLine();
boolean planIsOk = Set.of("o", "oui", "").contains(planIsOkString.toLowerCase());
if (!planIsOk) {
fail("Abandon");
}
}
for (Command command : commands) {
log("va traiter " + command.getDescription());
ClassicHttpRequest request = command.getRequest(uriFactory);
httpclient.execute(request, command);
log("a traité " + command.getDescription());
}
} catch (IOException e) {
fail("Erreur réseau HTTP : " + e.getMessage());
}
}
private ClientConfiguration readConfiguration() throws IOException {
File configurationFile = new File("openAdom-client-configuration.json");
ClientConfiguration clientConfiguration = new ObjectMapper()
.readValue(
configurationFile,
ClientConfiguration.class
);
return clientConfiguration;
}
private List<Command> newCommands(List<String> refTypes, List<String> dataTypes) {
List<Command> referenceCommands = refTypes.stream()
.flatMap(refType -> getReferenceCommands(refType).stream())
.toList();
List<Command> dataCommands = dataTypes.stream()
.flatMap(dataType -> getUploadDataCommands(dataType).stream())
.toList();
List<Command> commands = new LinkedList<>();
commands.addAll(referenceCommands);
commands.addAll(dataCommands);
return commands;
}
private List<Command> getUploadDataCommands(String dataType) {
Path dataDirectoryForDataType = Path.of(dataType);
List<Command> commands;
if (dataDirectoryForDataType.toFile().exists()) {
if (dataDirectoryForDataType.toFile().isDirectory()) {
SortedSet<Path> csvFilePaths = findCsvFilePathsInDirectory(dataDirectoryForDataType);
commands = csvFilePaths.stream()
.map(Path::toFile)
.map(dataFile -> newUploadDataCommand(dataType, dataFile))
.toList();
} else {
logError("le répertoire " + dataDirectoryForDataType + " est un fichier mais il devrait être un dossier. On l’ignore.");
commands = Collections.emptyList();
}
} else {
log("le répertoire " + dataDirectoryForDataType + " n’existe pas. Pas de données à importer pour " + dataType);
commands = Collections.emptyList();
}
return commands;
}
private Command newUploadDataCommand(String dataType, File dataFile) {
return new Command() {
@Override
public String getDescription() {
return "Téléversement de %s pour alimenter le types de données %s".formatted(dataFile, dataType);
}
@Override
public ClassicHttpRequest getRequest(UriFactory uriFactory) {
HttpPost httpPost = new HttpPost(uriFactory.forUploadingData(dataType));
FileBody dataFileBody = new FileBody(dataFile);
HttpEntity reqEntity = MultipartEntityBuilder.create()
.addPart("file", dataFileBody)
.build();
httpPost.setEntity(reqEntity);
return httpPost;
}
@Override
public Void handleResponse(ClassicHttpResponse response) {
switch (response.getCode()) {
case HttpURLConnection.HTTP_CREATED -> {
log("import de %s terminé".formatted(dataFile));
}
case HttpURLConnection.HTTP_BAD_REQUEST -> {
List<CsvRowValidationCheckResult> csvRowValidationCheckResults =
parseJsonInResponseBody(
response,
new TypeReference<List<CsvRowValidationCheckResult>>() {}
);
logError(csvRowValidationCheckResults.toString());
}
default -> fail(
"%d en code de retour HTTP inattendu à l’import des données %s avec le fichier %s"
.formatted(response.getCode(), dataFile, dataFile)
);
}
return null;
}
};
}
private List<Command> getReferenceCommands(String refType) {
File refFile = new File(new File("references"), refType + ".csv");
List<Command> commands;
if (refFile.exists()) {
Set<Path> csvFilePathsInDirectory;
if (refFile.isFile()) {
csvFilePathsInDirectory = Collections.singleton(refFile.toPath());
} else if (refFile.isDirectory()) {
csvFilePathsInDirectory = findCsvFilePathsInDirectory(refFile.toPath());
} else {
throw new IllegalStateException("ne comprend pas de quel type est " + refFile);
}
commands = csvFilePathsInDirectory.stream()
.map(path -> newUploadReferenceCommand(refType, path.toFile()))
.collect(Collectors.toList());
} else {
logError("le fichier %s n’existe pas, on ignore l’import du référentiel %s".formatted(refFile, refType));
commands = Collections.emptyList();
}
return commands;
}
private Command newUploadReferenceCommand(String refType, File refFile) {
return new Command() {
@Override
public String getDescription() {
return "Téléversement de %s pour alimenter le référentiel %s".formatted(refFile, refType);
}
@Override
public ClassicHttpRequest getRequest(UriFactory uriFactory) {
HttpPost httpPost = new HttpPost(uriFactory.forUploadingReference(refType));
FileBody refFileBody = new FileBody(refFile);
HttpEntity reqEntity = MultipartEntityBuilder.create()
.addPart("file", refFileBody)
.build();
httpPost.setEntity(reqEntity);
return httpPost;
}
@Override
public Void handleResponse(ClassicHttpResponse response) {
switch (response.getCode()) {
case HttpURLConnection.HTTP_CREATED -> {
String message = "import de " + refFile + " terminé";
log(message);
}
case HttpURLConnection.HTTP_BAD_REQUEST -> {
List<CsvRowValidationCheckResult> csvRowValidationCheckResults =
parseJsonInResponseBody(
response,
new TypeReference<List<CsvRowValidationCheckResult>>() {}
);
logError(csvRowValidationCheckResults.toString());
}
default -> fail(
"%d en code de retour HTTP inattendu à l’import du référentiel %s avec le fichier %s"
.formatted(response.getCode(), refType, refFile)
);
}
return null;
}
};
}
private SortedSet<Path> findCsvFilePathsInDirectory(Path directory) {
try {
AccumulatorPathVisitor accumulatorPathVisitor = new AccumulatorPathVisitor(Counters.longPathCounters());
Files.walkFileTree(directory, accumulatorPathVisitor);
SortedSet<Path> csvFilePathsInDirectory = accumulatorPathVisitor.getFileList().stream()
.filter(path -> path.getFileName().toString().endsWith(".csv"))
.collect(Collectors.toCollection(TreeSet::new));
return Collections.unmodifiableSortedSet(csvFilePathsInDirectory);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void fail(String message) {
logError(message);
System.exit(1);
}
private static void logError(String string) {
System.err.println(string);
}
private static void log(String message) {
System.out.println(message);
}
private <T> T parseJsonInResponseBody(ClassicHttpResponse response, TypeReference<T> valueTypeRef) {
try (InputStream inputStream = response.getEntity().getContent()) {
T parsingResult = new ObjectMapper()
.readValue(
inputStream,
valueTypeRef
);
return parsingResult;
} catch (DatabindException e) {
throw new RuntimeException(e);
} catch (StreamReadException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
record CsvRowValidationCheckResult(
int lineNumber,
Client.ValidationCheckResult validationCheckResult
) {
}
record ValidationCheckResult(
ValidationLevel level,
ValidationMessage message,
Map<String, Object> messageParams,
String target
) {
}
enum ValidationLevel {
SUCCESS, WARN, ERROR;
}
enum ValidationMessage {
unexpectedHeaderColumn,
headerColumnPatternNotMatching,
unexpectedTokenCount,
invalidHeaders,
duplicatedHeaders,
emptyHeader
}
/**
* Les routes disponibles sur le serveur.
*/
record UriFactory(URI instanceUrl, String applicationName) {
private URI newUri(String endpoint) {
try {
return new URI("%s/api/v1/%s".formatted(instanceUrl, endpoint));
} catch (URISyntaxException e) {
throw new RuntimeException("ne devrait pas arriver", e);
}
}
public URI forLogin() {
return newUri("login");
}
public URI forUploadingReference(String refType) {
String endpoint = "applications/%s/references/%s".formatted(applicationName, refType);
return newUri(endpoint);
}
public URI forUploadingData(String dataType) {
String endpoint = "applications/%s/data/%s".formatted(applicationName, dataType);
return newUri(endpoint);
}
public URI forApplicationReferenceTypes(String applicationName) {
String endpoint = "applications/%s/references".formatted(applicationName);
return newUri(endpoint);
}
public URI forApplicationDataTypes(String applicationName) {
String endpoint = "applications/%s/data".formatted(applicationName);
return newUri(endpoint);
}
}
/**
* Une étape du téléversement soit un fichier à téléverser, une requête HTTP et comment traiter la réponse.
*/
interface Command extends HttpClientResponseHandler<Void> {
String getDescription();
ClassicHttpRequest getRequest(UriFactory uriFactory);
}
/**
* Le contenu du fichier de configuration du client.
*
* @param instanceUrl l’adresse du serveur au format "http://hote:port"
* @param applicationName le nom de l’application
*/
private record ClientConfiguration(URI instanceUrl, String applicationName) {
}
}
Loading