diff --git a/build.gradle b/build.gradle index 94e7df8c6..86b611f63 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ tasks.withType(JavaCompile).configureEach { allprojects { group = 'fr.insee.eno' - version = '3.23.8' + version = '3.24.0' } subprojects { diff --git a/eno-ws/build.gradle b/eno-ws/build.gradle index fc5c2f813..85e25acf2 100644 --- a/eno-ws/build.gradle +++ b/eno-ws/build.gradle @@ -34,9 +34,12 @@ dependencies { // Lunatic implementation libs.lunatic.model // Spring Web + implementation 'org.springframework.boot:spring-boot-starter-web' + // Web Client + // since Spring MVC RestTemplate class is deprecated, the webflux dependency is added to import WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' - // Open API // https://springdoc.org/v2/#spring-webflux-support - implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0' + // Open API + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -44,7 +47,6 @@ dependencies { developmentOnly 'org.springframework.boot:spring-boot-devtools' // Tests testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'io.projectreactor:reactor-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/EnoWsApplication.java b/eno-ws/src/main/java/fr/insee/eno/ws/EnoWsApplication.java index 5cd3ded2c..8897966c4 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/EnoWsApplication.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/EnoWsApplication.java @@ -10,6 +10,7 @@ import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.util.UriComponentsBuilder; import java.net.InetSocketAddress; import java.net.ProxySelector; @@ -26,8 +27,12 @@ public static void main(String[] args) { } @Bean - public WebClient webClient(@Value("${eno.legacy.ws.url}") String baseUrl, - @Value("${proxy.host}") Optional proxyHost, + public UriComponentsBuilder uriBuilderWithBaseUrl(@Value("${eno.legacy.ws.url}") String baseUrl) { + return UriComponentsBuilder.fromHttpUrl(baseUrl); + } + + @Bean + public WebClient webClient(@Value("${proxy.host}") Optional proxyHost, @Value("${proxy.port}") Optional proxyPort, WebClient.Builder builder) { if (proxyHost.isPresent() && proxyPort.isPresent()) { @@ -38,8 +43,7 @@ public WebClient webClient(@Value("${eno.legacy.ws.url}") String baseUrl, } else { builder.clientConnector(new JdkClientHttpConnector()); } - return builder.baseUrl(baseUrl) - .build(); + return builder.build(); } @Configuration diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/PassThrough.java b/eno-ws/src/main/java/fr/insee/eno/ws/PassThrough.java deleted file mode 100644 index 216707711..000000000 --- a/eno-ws/src/main/java/fr/insee/eno/ws/PassThrough.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.insee.eno.ws; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.PooledDataBuffer; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -import java.time.Duration; -import java.util.Optional; - -import static org.springframework.web.reactive.function.BodyInserters.fromPublisher; - -/** Client to redirect requests send to Eno "java" web-service to the legacy Eno web-service. */ -@Component -public class PassThrough { - - private final WebClient webClient; - - private final Integer timeout; - - public PassThrough(WebClient webClient, @Value("${eno.webclient.timeout}") Optional timeout) { - this.webClient = webClient; - if(timeout.isEmpty()) { - throw new IllegalArgumentException("Timeout is not configured for webclient"); - } - this.timeout = timeout.get(); - } - - // TODO: better request headers managements - - public Mono passePlatGet(ServerHttpRequest serverRequest, ServerHttpResponse response) { - return response.writeWith(this.webClient.get() - .uri(builder -> builder.path(serverRequest.getURI().getPath()) - .queryParams(serverRequest.getQueryParams()) - .build()) - .headers(httpHeaders -> { - httpHeaders.clear(); - serverRequest.getHeaders().forEach((key, strings) -> { - if (!"Host".equals(key)) httpHeaders.put(key, strings); - }); - }) - .exchangeToFlux(r -> { - response.setStatusCode(r.statusCode()); - response.getHeaders().clear(); - r.headers().asHttpHeaders().forEach((key, strings) -> - response.getHeaders().put(key.replace(":", ""), strings)); - return r.bodyToFlux(DataBuffer.class); - }) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release) - .timeout(Duration.ofSeconds(timeout))); - } - - public Mono passePlatPost(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith( - this.webClient.post() - .uri(builder -> builder.path(request.getURI().getPath()) - .queryParams(request.getQueryParams()) - .build()) - .body(fromPublisher(request.getBody(), DataBuffer.class)) - .headers(httpHeaders -> { - httpHeaders.clear(); - request.getHeaders().forEach((key, strings) -> { - if (!"Host".equals(key)) httpHeaders.put(key, strings); - }); - }) - .exchangeToFlux(r -> { - response.setStatusCode(r.statusCode()); - response.getHeaders().clear(); - r.headers().asHttpHeaders().forEach((key, strings) -> - response.getHeaders().put(key.replace(":", ""), strings)); - return r.bodyToFlux(DataBuffer.class); - }) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release) - .timeout(Duration.ofSeconds(timeout))); - } - -} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationCustomController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationCustomController.java index 19586aba8..88bf21294 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationCustomController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationCustomController.java @@ -1,23 +1,28 @@ package fr.insee.eno.ws.controller; -import fr.insee.eno.ws.PassThrough; -import fr.insee.eno.ws.controller.utils.ReactiveControllerUtils; +import fr.insee.eno.core.exceptions.business.EnoParametersException; +import fr.insee.eno.legacy.parameters.OutFormat; +import fr.insee.eno.ws.controller.utils.EnoJavaControllerUtils; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; +import fr.insee.eno.ws.exception.DDIToLunaticException; +import fr.insee.eno.ws.exception.EnoControllerException; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.codec.multipart.Part; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; -import reactor.core.publisher.Mono; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URI; + +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.addMultipartToBody; +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.questionnaireFilename; @Tag(name = "Generation from DDI (custom parameters)") @Controller @@ -26,12 +31,12 @@ @SuppressWarnings("unused") public class GenerationCustomController { - private final PassThrough passePlat; - private final ReactiveControllerUtils controllerUtils; + private final EnoJavaControllerUtils javaControllerUtils; + private final EnoXmlControllerUtils xmlControllerUtils; - public GenerationCustomController(PassThrough passePlat, ReactiveControllerUtils controllerUtils) { - this.passePlat = passePlat; - this.controllerUtils = controllerUtils; + public GenerationCustomController(EnoJavaControllerUtils javaControllerUtils, EnoXmlControllerUtils xmlControllerUtils) { + this.javaControllerUtils = javaControllerUtils; + this.xmlControllerUtils = xmlControllerUtils; } @Operation( @@ -42,19 +47,12 @@ public GenerationCustomController(PassThrough passePlat, ReactiveControllerUtils "You can get a parameters file by using the endpoint `/parameters/java/{context}/LUNATIC/{mode}`") @PostMapping(value = "ddi-2-lunatic-json", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono> generateLunaticCustomParams( - @RequestPart(value="in") Mono ddiFile, - @RequestPart(value="params") Mono parametersFile, - @Parameter(name = "specificTreatment", schema = @Schema(type="string", format="binary")) - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment) { - /* - specificTreatment parameter is a part instead of a FilePart. This workaround is used to make swagger work - when empty value is checked for this input file on the endpoint. - When empty value is checked, swagger send no content-type nor filename for this multipart file. In this case, - Spring considers having a DefaultFormField object instead of FilePart and exceptions is thrown - There is no way at this moment to disable the allow empty value when filed is not required. - */ - return controllerUtils.ddiToLunaticJson(ddiFile, parametersFile, specificTreatment); + public ResponseEntity generateLunaticCustomParams( + @RequestPart(value="in") MultipartFile ddiFile, + @RequestPart(value="params") MultipartFile parametersFile, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment) + throws DDIToLunaticException, EnoControllerException, EnoParametersException, IOException { + return javaControllerUtils.ddiToLunaticJson(ddiFile, parametersFile, specificTreatment); } @Operation( @@ -66,13 +64,24 @@ public Mono> generateLunaticCustomParams( "You can get a parameters file by using the endpoint `/parameters/xml/BUSINESS/XFORMS`") @PostMapping(value = "ddi-2-xforms", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateXformsCustomParams( - @RequestPart(value="in") Mono in, - @RequestPart(value="params") Mono params, - @RequestPart(value="metadata") Mono metadata, - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment, - ServerHttpRequest request, ServerHttpResponse response) { - return passePlat.passePlatPost(request, response); + public ResponseEntity generateXformsCustomParams( + @RequestPart(value="in") MultipartFile in, + @RequestPart(value="params") MultipartFile params, + @RequestPart(value="metadata") MultipartFile metadata, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment) + throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + addMultipartToBody(multipartBodyBuilder, params, "params"); + if (metadata != null) + addMultipartToBody(multipartBodyBuilder, metadata, "metadata"); + if (specificTreatment != null) + addMultipartToBody(multipartBodyBuilder, specificTreatment, "specificTreatment"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("questionnaire/ddi-2-xforms").build().toUri(); + String outFilename = questionnaireFilename(OutFormat.XFORMS, false); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } @Operation( @@ -84,13 +93,24 @@ public Mono generateXformsCustomParams( "You can get a parameters file by using the endpoint `/parameters/xml/{context}/FO`") @PostMapping(value = "ddi-2-fo", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateFOCustomParams( - @RequestPart(value="in") Mono in, - @RequestPart(value="params") Mono params, - @RequestPart(value="metadata") Mono metadata, - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment, - ServerHttpRequest request, ServerHttpResponse response) { - return passePlat.passePlatPost(request, response); + public ResponseEntity generateFOCustomParams( + @RequestPart(value="in") MultipartFile in, + @RequestPart(value="params") MultipartFile params, + @RequestPart(value="metadata") MultipartFile metadata, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment) + throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + addMultipartToBody(multipartBodyBuilder, params, "params"); + if (metadata != null) + addMultipartToBody(multipartBodyBuilder, metadata, "metadata"); + if (specificTreatment != null) + addMultipartToBody(multipartBodyBuilder, specificTreatment, "specificTreatment"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("questionnaire/ddi-2-fo").build().toUri(); + String outFilename = questionnaireFilename(OutFormat.XFORMS, false); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationPoguesController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationPoguesController.java index fb232d5e5..b78bb1a98 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationPoguesController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationPoguesController.java @@ -1,18 +1,24 @@ package fr.insee.eno.ws.controller; -import fr.insee.eno.ws.PassThrough; +import fr.insee.eno.legacy.parameters.OutFormat; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; +import fr.insee.eno.ws.exception.EnoControllerException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; -import reactor.core.publisher.Mono; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; + +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.addMultipartToBody; +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.questionnaireFilename; @Tag(name = "Generation from Pogues") @Controller @@ -21,10 +27,10 @@ @SuppressWarnings("unused") public class GenerationPoguesController { - private final PassThrough passThrough; + private final EnoXmlControllerUtils xmlControllerUtils; - public GenerationPoguesController(PassThrough passThrough) { - this.passThrough = passThrough; + public GenerationPoguesController(EnoXmlControllerUtils xmlControllerUtils) { + this.xmlControllerUtils = xmlControllerUtils; } @Operation( @@ -33,10 +39,15 @@ public GenerationPoguesController(PassThrough passThrough) { "Generation of a DDI from a Pogues questionnaire (in the xml format).") @PostMapping(value="poguesxml-2-ddi", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateDDIQuestionnaire( - @RequestPart(value="in") Mono in, - ServerHttpRequest request, ServerHttpResponse response) { - return passThrough.passePlatPost(request, response); + public ResponseEntity generateDDIQuestionnaire( + @RequestPart(value="in") MultipartFile in) throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("questionnaire/poguesxml-2-ddi").build().toUri(); + String outFilename = questionnaireFilename(OutFormat.XFORMS, false); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationStandardController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationStandardController.java index 94dd7ad5e..e3e9809dd 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationStandardController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationStandardController.java @@ -4,26 +4,25 @@ import fr.insee.eno.core.parameter.Format; import fr.insee.eno.legacy.parameters.CaptureEnum; import fr.insee.eno.legacy.parameters.Context; -import fr.insee.eno.ws.PassThrough; -import fr.insee.eno.ws.controller.utils.ReactiveControllerUtils; -import fr.insee.eno.ws.exception.ContextException; -import fr.insee.eno.ws.exception.MetadataFileException; -import fr.insee.eno.ws.exception.ModeParameterException; -import fr.insee.eno.ws.exception.MultiModelException; +import fr.insee.eno.legacy.parameters.OutFormat; +import fr.insee.eno.ws.controller.utils.EnoJavaControllerUtils; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; +import fr.insee.eno.ws.exception.*; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.codec.multipart.Part; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Mono; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URI; + +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.addMultipartToBody; +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.questionnaireFilename; @Tag(name = "Generation from DDI (standard parameters)") @Controller @@ -32,12 +31,13 @@ @SuppressWarnings("unused") public class GenerationStandardController { - private final ReactiveControllerUtils controllerUtils; - private final PassThrough passThrough; + private final EnoJavaControllerUtils javaControllerUtils; + private final EnoXmlControllerUtils xmlControllerUtils; - public GenerationStandardController(ReactiveControllerUtils controllerUtils, PassThrough passThrough) { - this.controllerUtils = controllerUtils; - this.passThrough = passThrough; + public GenerationStandardController(EnoJavaControllerUtils javaControllerUtils, + EnoXmlControllerUtils xmlControllerUtils) { + this.javaControllerUtils = javaControllerUtils; + this.xmlControllerUtils = xmlControllerUtils; } @Operation( @@ -47,28 +47,21 @@ public GenerationStandardController(ReactiveControllerUtils controllerUtils, Pas "in function of context and mode. An optional specific treatment `json` file can be added.") @PostMapping(value = "{context}/lunatic-json/{mode}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono> generateLunatic( - @RequestPart(value="in") Mono ddiFile, - @Parameter(name = "specificTreatment", schema = @Schema(type="string", format="binary")) - @RequestPart(value="specificTreatment", required = false) Mono specificTreatment, + public ResponseEntity generateLunatic( + @RequestPart(value="in") MultipartFile ddiFile, + @RequestPart(value="specificTreatment", required = false) MultipartFile specificTreatment, @PathVariable EnoParameters.Context context, @PathVariable(name = "mode") EnoParameters.ModeParameter modeParameter, - @RequestParam(defaultValue = "false") boolean dsfr) { - /* - specificTreatment parameter is a part instead of a FilePart. This workaround is used to make swagger work - when empty value is checked for this input file on the endpoint. - When empty value is checked, swagger send no content-type nor filename for this multipart file. In this case, - Spring considers having a DefaultFormField object instead of FilePart and exceptions is thrown - There is no way at this moment to disable the allow empty value when filed is not required. - */ - + @RequestParam(defaultValue = "false") boolean dsfr) + throws ModeParameterException, DDIToLunaticException, EnoControllerException, IOException { + // if (EnoParameters.ModeParameter.PAPI.equals(modeParameter)) - return Mono.error(new ModeParameterException("Lunatic format is not compatible with the mode 'PAPER'.")); + throw new ModeParameterException("Lunatic format is not compatible with the mode 'PAPER'."); // EnoParameters enoParameters = EnoParameters.of(context, modeParameter, Format.LUNATIC); enoParameters.getLunaticParameters().setDsfr(dsfr); // - return controllerUtils.ddiToLunaticJson(ddiFile, enoParameters, specificTreatment); + return javaControllerUtils.ddiToLunaticJson(ddiFile, enoParameters, specificTreatment); } @Operation( @@ -81,25 +74,34 @@ public Mono> generateLunatic( "If the multi-model option is set to true, the output questionnaire(s) are put in a zip file.") @PostMapping(value = "{context}/xforms", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateXforms( - @RequestPart(value="in") Mono in, - @RequestPart(value="metadata", required = false) Mono metadata, - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment, + public ResponseEntity generateXforms( + @RequestPart(value="in") MultipartFile in, + @RequestPart(value="metadata", required = false) MultipartFile metadata, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment, @PathVariable Context context, - @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel, - ServerHttpRequest request, ServerHttpResponse response) { + @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel) + throws MetadataFileException, ContextException, MultiModelException, EnoControllerException { + // if (Context.HOUSEHOLD.equals(context)) - return Mono.error(new ContextException("Xforms format is not compatible with 'HOUSEHOLD' context.")); + throw new ContextException("Xforms format is not compatible with 'HOUSEHOLD' context."); if (Context.BUSINESS.equals(context) && (! multiModel)) - return Mono.error(new MultiModelException("Multi-model option must be 'true' in 'BUSINESS' context.")); - metadata.hasElement() - .flatMap(hasElementValue -> { - if (Context.BUSINESS.equals(context) && Boolean.FALSE.equals(hasElementValue)) - return Mono.error(new MetadataFileException( - "The metadata file is required in 'BUSINESS' context.")); - return null; - }); - return passThrough.passePlatPost(request, response); + throw new MultiModelException("Multi-model option must be 'true' in 'BUSINESS' context."); + if (Context.BUSINESS.equals(context) && (metadata == null)) + throw new MetadataFileException("The metadata file is required in 'BUSINESS' context."); + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + if (metadata != null) + addMultipartToBody(multipartBodyBuilder, metadata, "metadata"); + if (specificTreatment != null) + addMultipartToBody(multipartBodyBuilder, specificTreatment, "specificTreatment"); + // + URI uri = xmlControllerUtils.newUriBuilder() + .path("/questionnaire/{context}/xforms") + .queryParam("multi-model", multiModel) + .build(context); + String outFilename = questionnaireFilename(OutFormat.XFORMS, multiModel); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } @Operation( @@ -113,25 +115,36 @@ public Mono generateXforms( "If the multi-model option is set to true, the output questionnaire(s) are put in a zip file." ) @PostMapping(value = "{context}/fo", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateFO( - @RequestPart(value="in") Mono in, - @RequestPart(value="metadata", required = false) Mono metadata, - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment, + public ResponseEntity generateFO( + @RequestPart(value="in") MultipartFile in, + @RequestPart(value="metadata", required = false) MultipartFile metadata, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment, @RequestParam(value="Format-column", required=false) Integer nbColumn, @RequestParam(value="Capture", required=false) CaptureEnum capture, @PathVariable Context context, - @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel, - ServerHttpRequest request, ServerHttpResponse response) { + @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel) + throws MultiModelException, MetadataFileException, EnoControllerException { + // if (Context.BUSINESS.equals(context) && (! multiModel)) - return Mono.error(new MultiModelException("Multi-model option must be 'true' in 'BUSINESS' context.")); - metadata.hasElement() - .flatMap(hasElementValue -> { - if (Context.BUSINESS.equals(context) && Boolean.FALSE.equals(hasElementValue)) - return Mono.error(new MetadataFileException( - "The metadata file is required in 'BUSINESS' context.")); - return null; - }); - return passThrough.passePlatPost(request, response); + throw new MultiModelException("Multi-model option must be 'true' in 'BUSINESS' context."); + if (Context.BUSINESS.equals(context) && metadata != null) + throw new MetadataFileException("The metadata file is required in 'BUSINESS' context."); + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + if (metadata != null) + addMultipartToBody(multipartBodyBuilder, metadata, "metadata"); + if (specificTreatment != null) + addMultipartToBody(multipartBodyBuilder, specificTreatment, "specificTreatment"); + // + URI uri = xmlControllerUtils.newUriBuilder() + .path("/questionnaire/{context}/fo") + .queryParam("Format-column", nbColumn) + .queryParam("Capture", capture) + .queryParam("multi-model", multiModel) + .build(context); + String outFilename = questionnaireFilename(OutFormat.FO, multiModel); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } @Operation( @@ -141,11 +154,16 @@ public Mono generateFO( "context.") @PostMapping(value = "{context}/fodt", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generateFODT( - @RequestPart(value="in") Mono in, - @PathVariable Context context, - ServerHttpRequest request, ServerHttpResponse response) { - return passThrough.passePlatPost(request, response); + public ResponseEntity generateFODT( + @RequestPart(value="in") MultipartFile in, + @PathVariable Context context) throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("/questionnaire/{context}/fodt").build(context); + String outFilename = questionnaireFilename(OutFormat.FODT, false); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationWithMappingController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationWithMappingController.java index 9a6e0c031..eae89f0af 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationWithMappingController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/GenerationWithMappingController.java @@ -1,19 +1,23 @@ package fr.insee.eno.ws.controller; -import fr.insee.eno.ws.PassThrough; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; +import fr.insee.eno.ws.exception.EnoControllerException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; -import reactor.core.publisher.Mono; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; + +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.addMultipartToBody; @Tag(name = "Generation with custom mapping") @Controller @@ -22,10 +26,10 @@ @SuppressWarnings("unused") public class GenerationWithMappingController { - private final PassThrough passThrough; + private final EnoXmlControllerUtils xmlControllerUtils; - public GenerationWithMappingController(PassThrough passThrough) { - this.passThrough = passThrough; + public GenerationWithMappingController(EnoXmlControllerUtils xmlControllerUtils) { + this.xmlControllerUtils = xmlControllerUtils; } @Operation( @@ -37,15 +41,28 @@ public GenerationWithMappingController(PassThrough passThrough) { "If the multi-model option is set to true, the output questionnaire(s) are put in a zip file.") @PostMapping(value = "in-2-out", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono generate( - @RequestPart(value="in") Mono in, - @RequestPart(value="params") Mono params, - @RequestPart(value="metadata", required=false) Mono metadata, - @RequestPart(value="specificTreatment", required=false) Mono specificTreatment, - @RequestPart(value="mapping", required=false) Mono mapping, - @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel, - ServerHttpRequest request, ServerHttpResponse response) { - return passThrough.passePlatPost(request, response); + public ResponseEntity generate( + @RequestPart(value="in") MultipartFile in, + @RequestPart(value="params") MultipartFile params, + @RequestPart(value="metadata", required=false) MultipartFile metadata, + @RequestPart(value="specificTreatment", required=false) MultipartFile specificTreatment, + @RequestPart(value="mapping", required=false) MultipartFile mapping, + @RequestParam(value="multi-model", required=false, defaultValue="false") boolean multiModel) + throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + addMultipartToBody(multipartBodyBuilder, params, "params"); + if (metadata != null) + addMultipartToBody(multipartBodyBuilder, metadata, "metadata"); + if (specificTreatment != null) + addMultipartToBody(multipartBodyBuilder, specificTreatment, "specificTreatment"); + if (mapping != null) + addMultipartToBody(multipartBodyBuilder, mapping, "mapping"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("questionnaire/in-2-out").build().toUri(); + String outFilename = multiModel ? "questionnaire.zip" : "questionnaire.txt"; + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, outFilename); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersJavaController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersJavaController.java index 0e4517c79..481ce18da 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersJavaController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersJavaController.java @@ -1,16 +1,18 @@ package fr.insee.eno.ws.controller; -import fr.insee.eno.core.parameter.Format; +import com.fasterxml.jackson.core.JsonProcessingException; import fr.insee.eno.core.parameter.EnoParameters; +import fr.insee.eno.core.parameter.Format; import fr.insee.eno.ws.controller.utils.HeadersUtils; import fr.insee.eno.ws.service.ParameterService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Mono; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @Tag(name="Parameters (Eno Java)") @RestController() @@ -39,19 +41,15 @@ public ParametersJavaController(ParameterService parameterService) { description = "Returns a `json` parameters file with standard values, in function of context and mode, " + "for the concerned out format, to be used in _Eno Java_ services that require a parameters file.") @GetMapping(value = "{context}/{outFormat}/{mode}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public Mono> getJavaParameters( + public ResponseEntity getJavaParameters( @PathVariable EnoParameters.Context context, @PathVariable OutFormat outFormat, - @PathVariable(name = "mode") EnoParameters.ModeParameter modeParameter) { - // + @PathVariable(name = "mode") EnoParameters.ModeParameter modeParameter) throws JsonProcessingException { String parametersFileName = "eno-parameters-" + context + "-" + modeParameter + "-" + outFormat + ".json"; - // - return parameterService.defaultParams(context, toCoreFormat(outFormat), modeParameter) - .map(params -> ResponseEntity - .ok() - .cacheControl(CacheControl.noCache()) - .headers(HeadersUtils.with(parametersFileName)) - .body(params)); + String defaultParams = parameterService.defaultParams(context, toCoreFormat(outFormat), modeParameter); + return ResponseEntity.ok() + .headers(HeadersUtils.with(parametersFileName)) + .body(defaultParams); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersXmlController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersXmlController.java index fbe562128..3a79d34f1 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersXmlController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/ParametersXmlController.java @@ -2,20 +2,21 @@ import fr.insee.eno.legacy.parameters.Context; import fr.insee.eno.legacy.parameters.Mode; -import fr.insee.eno.ws.PassThrough; +import fr.insee.eno.legacy.parameters.OutFormat; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import reactor.core.publisher.Mono; + +import java.net.URI; @Tag(name="Parameters (Eno Xml)") @Controller @@ -24,11 +25,10 @@ public class ParametersXmlController { private static final Logger LOGGER = LoggerFactory.getLogger(ParametersXmlController.class); + private final EnoXmlControllerUtils xmlControllerUtils; - private final PassThrough passePlat; - - public ParametersXmlController(PassThrough passePlat) { - this.passePlat = passePlat; + public ParametersXmlController(EnoXmlControllerUtils xmlControllerUtils) { + this.xmlControllerUtils = xmlControllerUtils; } @Operation( @@ -36,8 +36,9 @@ public ParametersXmlController(PassThrough passePlat) { description= "Return the default parameters file for Eno Xml. This file cannot be used directly: " + "you have to fill the `Pipeline` section according to the desired transformation.") @GetMapping(value="all", produces=MediaType.APPLICATION_OCTET_STREAM_VALUE) - public Mono getAllXmlParameters(ServerHttpRequest request, ServerHttpResponse response) { - return passePlat.passePlatGet(request, response); + public ResponseEntity getAllXmlParameters() { + URI uri = xmlControllerUtils.newUriBuilder().path("parameters/xml/all").build().toUri(); + return xmlControllerUtils.sendGetRequest(uri, "eno-parameters-ALL.xml"); } @Operation( @@ -45,12 +46,22 @@ public Mono getAllXmlParameters(ServerHttpRequest request, ServerHttpRespo description = "Returns a `xml` parameters file with standard values, in function of context and mode, " + "for the concerned out format, to be used in _Eno Xml_ services that require a parameters file.") @GetMapping(value="{context}/{outFormat}", produces=MediaType.APPLICATION_OCTET_STREAM_VALUE) - public Mono getXmlParameters( + public ResponseEntity getXmlParameters( @PathVariable Context context, - @PathVariable fr.insee.eno.legacy.parameters.OutFormat outFormat, - @RequestParam(value="Mode",required=false) Mode mode, - ServerHttpRequest request, ServerHttpResponse response) { - return passePlat.passePlatGet(request, response); + @PathVariable OutFormat outFormat, + @RequestParam(value="Mode",required=false) Mode mode) { + URI uri = xmlControllerUtils.newUriBuilder() + .path("parameters/xml/{context}/{outFormat}") + .queryParam("Mode", mode) + .build(context, outFormat); + return xmlControllerUtils.sendGetRequest(uri, enoXmlParametersFilename(context, mode, outFormat)); + } + + private String enoXmlParametersFilename(Context context, Mode mode, OutFormat outFormat) { + String contextSuffix = "-" + context; + String modeSuffix = mode != null ? "-" + mode : ""; + String outFormatSuffix = "-" + outFormat; + return "eno-parameters" + contextSuffix + modeSuffix + outFormatSuffix + ".xml"; } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/UtilsController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/UtilsController.java index 0ae5a1846..9bffba076 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/UtilsController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/UtilsController.java @@ -1,17 +1,20 @@ package fr.insee.eno.ws.controller; import fr.insee.eno.core.utils.XpathToVtl; -import fr.insee.eno.ws.PassThrough; +import fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils; +import fr.insee.eno.ws.exception.EnoControllerException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.web.bind.annotation.*; -import reactor.core.publisher.Mono; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URI; + +import static fr.insee.eno.ws.controller.utils.EnoXmlControllerUtils.addMultipartToBody; @Tag(name = "Utils") @RestController @@ -20,21 +23,33 @@ @SuppressWarnings("unused") public class UtilsController { - private final PassThrough passThrough; + private final EnoXmlControllerUtils xmlControllerUtils; - public UtilsController(PassThrough passThrough) { - this.passThrough = passThrough; + public UtilsController(EnoXmlControllerUtils xmlControllerUtils) { + this.xmlControllerUtils = xmlControllerUtils; } + /** + * Converts a DDI 3.2 file to a DDI 3.3 file. + * @param in DDI 3.2 file. + * @return DDI file converted to DDI 3.3 version. + * @deprecated DDI 3.2 is no longer supported. + */ @Operation( summary = "Generation of DDI 3.3 from DDI 3.2.", - description = "Generation of a DDI in 3.3 version from the given DDI in 3.2 version.") + description = "Generation of a DDI in 3.3 version from the given DDI in 3.2 version. " + + "_Note: DDI 3.2 is no longer supported._") @PostMapping(value = "ddi32-2-ddi33", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public Mono convertDDI32ToDDI33( - @RequestPart(value="in") Mono in, - ServerHttpRequest request, ServerHttpResponse response) { - return passThrough.passePlatPost(request, response); + @Deprecated(since = "3.24.0") + public ResponseEntity convertDDI32ToDDI33( + @RequestPart(value="in") MultipartFile in) throws EnoControllerException { + // + MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder(); + addMultipartToBody(multipartBodyBuilder, in, "in"); + // + URI uri = xmlControllerUtils.newUriBuilder().path("utils/ddi32-2-ddi33").build().toUri(); + return xmlControllerUtils.sendPostRequest(uri, multipartBodyBuilder, "ddi33.xml"); } /** @@ -49,11 +64,11 @@ public Mono convertDDI32ToDDI33( "_Note: The usage of XPath in questionnaires is now deprecated._") @PostMapping(value = "xpath-2-vtl") @Deprecated(since = "3.18.1") - public Mono> convertXpathToVTL( + public ResponseEntity convertXpathToVTL( @RequestParam(value="xpath") String xpath) { String result = XpathToVtl.parseToVTL(xpath); log.info("Xpath expression given parsed to VTL: {}", result); - return Mono.just(ResponseEntity.ok().body(result)); + return ResponseEntity.ok().body(result); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/exception/EnoExceptionController.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/exception/EnoExceptionController.java index 65219c10d..2b6fcf891 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/exception/EnoExceptionController.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/exception/EnoExceptionController.java @@ -1,13 +1,13 @@ package fr.insee.eno.ws.controller.exception; import fr.insee.eno.core.exceptions.business.EnoParametersException; -import fr.insee.eno.ws.exception.ContextException; -import fr.insee.eno.ws.exception.MetadataFileException; -import fr.insee.eno.ws.exception.ModeParameterException; import fr.insee.eno.legacy.exception.EnoGenerationException; import fr.insee.eno.legacy.exception.EnoLegacyParametersException; import fr.insee.eno.treatments.exceptions.SpecificTreatmentsDeserializationException; import fr.insee.eno.treatments.exceptions.SpecificTreatmentsValidationException; +import fr.insee.eno.ws.exception.ContextException; +import fr.insee.eno.ws.exception.MetadataFileException; +import fr.insee.eno.ws.exception.ModeParameterException; import fr.insee.eno.ws.exception.MultiModelException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import java.io.IOException; @ControllerAdvice @Slf4j @@ -65,6 +66,11 @@ public ResponseEntity exception(SpecificTreatmentsValidationException ex return new ResponseEntity<>("Erreur durant la vérification du json de traitement spécifique : "+exception.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } + @ExceptionHandler(IOException.class) + public ResponseEntity exception(IOException ioException) { + return new ResponseEntity<>("I/O error: " + ioException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + @ExceptionHandler(value = Exception.class) public ResponseEntity exception(Exception exception) { log.error("Unhandled exception thrown in controller: ", exception); diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoJavaControllerUtils.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoJavaControllerUtils.java new file mode 100644 index 000000000..fb6c8b2da --- /dev/null +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoJavaControllerUtils.java @@ -0,0 +1,84 @@ +package fr.insee.eno.ws.controller.utils; + +import fr.insee.eno.core.exceptions.business.EnoParametersException; +import fr.insee.eno.core.parameter.EnoParameters; +import fr.insee.eno.treatments.LunaticPostProcessing; +import fr.insee.eno.ws.exception.DDIToLunaticException; +import fr.insee.eno.ws.exception.EnoControllerException; +import fr.insee.eno.ws.service.DDIToLunaticService; +import fr.insee.eno.ws.service.ParameterService; +import fr.insee.eno.ws.service.SpecificTreatmentsService; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Class to factorize code in Eno Java controllers' methods. + */ +@Component +public class EnoJavaControllerUtils { + + public static final String LUNATIC_JSON_FILE_NAME = "lunatic-form.json"; + + private final DDIToLunaticService ddiToLunaticService; + private final ParameterService parameterService; + private final SpecificTreatmentsService specificTreatmentsService; + + public EnoJavaControllerUtils(DDIToLunaticService ddiToLunaticService, + ParameterService parameterService, + SpecificTreatmentsService specificTreatmentsService) { + this.ddiToLunaticService = ddiToLunaticService; + this.parameterService = parameterService; + this.specificTreatmentsService = specificTreatmentsService; + } + + private EnoParameters readEnoJavaParametersFile(MultipartFile parametersFile) + throws EnoParametersException, IOException { + if (parametersFile == null || parametersFile.isEmpty()) + throw new EnoParametersException("Parameters file is missing."); + String fileName = parametersFile.getOriginalFilename(); + if (fileName == null) + throw new EnoParametersException("Parameters file names is null."); + if (! fileName.endsWith(".json")) + throw new EnoParametersException("Eno Java parameters file name must end with '.json'."); + return parameterService.parse(new ByteArrayInputStream(parametersFile.getBytes())); + } + + private LunaticPostProcessing createLunaticPostProcessing(MultipartFile specificTreatmentsFile) + throws IOException { + if (specificTreatmentsFile == null || specificTreatmentsFile.isEmpty()) + return null; + return specificTreatmentsService.generateFrom(new ByteArrayInputStream(specificTreatmentsFile.getBytes())); + } + + public ResponseEntity ddiToLunaticJson(MultipartFile ddiFile, MultipartFile parametersFile, + MultipartFile specificTreatmentsFile) + throws EnoParametersException, IOException, EnoControllerException, DDIToLunaticException { + EnoParameters enoParameters = readEnoJavaParametersFile(parametersFile); + LunaticPostProcessing lunaticPostProcessing = createLunaticPostProcessing(specificTreatmentsFile); + return ddiToLunaticJson(ddiFile, enoParameters, lunaticPostProcessing); + } + + public ResponseEntity ddiToLunaticJson(MultipartFile ddiFile, EnoParameters enoParameters, + MultipartFile specificTreatmentsFile) + throws IOException, EnoControllerException, DDIToLunaticException { + LunaticPostProcessing lunaticPostProcessing = createLunaticPostProcessing(specificTreatmentsFile); + return ddiToLunaticJson(ddiFile, enoParameters, lunaticPostProcessing); + } + + private ResponseEntity ddiToLunaticJson(MultipartFile ddiFile, EnoParameters enoParameters, + LunaticPostProcessing lunaticPostProcessing) + throws EnoControllerException, IOException, DDIToLunaticException { + if (ddiFile.isEmpty()) + throw new EnoControllerException("DDI file is missing."); + String lunaticJson = ddiToLunaticService.transformToJson( + new ByteArrayInputStream(ddiFile.getBytes()), enoParameters, lunaticPostProcessing); + return ResponseEntity.ok() + .headers(HeadersUtils.with(LUNATIC_JSON_FILE_NAME)) + .body(lunaticJson); + } + +} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoXmlControllerUtils.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoXmlControllerUtils.java new file mode 100644 index 000000000..a736ef1b5 --- /dev/null +++ b/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/EnoXmlControllerUtils.java @@ -0,0 +1,93 @@ +package fr.insee.eno.ws.controller.utils; + +import fr.insee.eno.legacy.parameters.OutFormat; +import fr.insee.eno.ws.exception.EnoControllerException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; +import java.net.URI; + +/** + * Class to factorize code in Eno Xml controllers' (which consist in redirection to the legacy Xml web-service) + * methods. + */ +@Component +public class EnoXmlControllerUtils { + + private final String baseUrl; + private final WebClient webClient; + public UriComponentsBuilder newUriBuilder() { + return UriComponentsBuilder.fromHttpUrl(baseUrl); + } + + public EnoXmlControllerUtils(@Value("${eno.legacy.ws.url}") String baseUrl, + WebClient webClient) { + // Base url is not passed to the web client instance since each controller can pass a new URI object. + this.baseUrl = baseUrl; + this.webClient = webClient; + } + + public ResponseEntity sendGetRequest(URI uri, String outFilename) { + String responseBody = webClient.get() + .uri(uri) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class)) + .block(); + return ResponseEntity.ok() + .headers(HeadersUtils.with(outFilename)) + .body(responseBody); + } + + public ResponseEntity sendPostRequest(URI uri, MultipartBodyBuilder multipartBodyBuilder, String outFilename) { + String result = webClient.post() + .uri(uri) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build())) + .exchangeToMono(clientResponse -> clientResponse.bodyToMono(String.class)) + .block(); + return ResponseEntity.ok() + .headers(HeadersUtils.with(outFilename)) + .body(result); + } + + public static void addMultipartToBody(MultipartBodyBuilder multipartBodyBuilder, MultipartFile multipartFile, + String partName) throws EnoControllerException { + try { + multipartBodyBuilder.part(partName, multipartFileToByteArray(multipartFile)); + } catch (IOException e) { + throw new EnoControllerException( + "Unable to access content of given file " + multipartFile.getOriginalFilename()); + } + } + + private static ByteArrayResource multipartFileToByteArray(MultipartFile multipartFile) throws IOException { + // Ugly but I didn't find anything better + return new ByteArrayResource(multipartFile.getBytes()) { + @Override + public String getFilename() { + return multipartFile.getOriginalFilename(); + } + }; + } + + public static String questionnaireFilename(OutFormat outFormat, boolean multiModel) { + if(multiModel) return "questionnaires.zip"; + return switch (outFormat){ + case FO -> "questionnaire.fo"; + case FODT -> "questionnaire.fodt"; + case DDI -> "ddi-questionnaire.xml"; + case XFORMS -> "questionnaire.xhtml"; + }; + } + +} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/ReactiveControllerUtils.java b/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/ReactiveControllerUtils.java deleted file mode 100644 index 0d30f2988..000000000 --- a/eno-ws/src/main/java/fr/insee/eno/ws/controller/utils/ReactiveControllerUtils.java +++ /dev/null @@ -1,101 +0,0 @@ -package fr.insee.eno.ws.controller.utils; - -import fr.insee.eno.core.exceptions.business.EnoParametersException; -import fr.insee.eno.core.parameter.EnoParameters; -import fr.insee.eno.treatments.LunaticPostProcessing; -import fr.insee.eno.ws.service.DDIToLunaticService; -import fr.insee.eno.ws.service.ParameterService; -import fr.insee.eno.ws.service.SpecificTreatmentsService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.CacheControl; -import org.springframework.http.ResponseEntity; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.codec.multipart.Part; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -import java.io.InputStream; -import java.io.SequenceInputStream; - -/** Class to factorize code in Eno Java controllers' methods. */ -@Component -@Slf4j -public class ReactiveControllerUtils { - - public static final String LUNATIC_JSON_FILE_NAME = "lunatic-form.json"; - - private final DDIToLunaticService ddiToLunaticService; - private final ParameterService parameterService; - private final SpecificTreatmentsService specificTreatmentsService; - - public ReactiveControllerUtils(DDIToLunaticService ddiToLunaticService, - ParameterService parameterService, - SpecificTreatmentsService specificTreatmentsService) { - this.ddiToLunaticService = ddiToLunaticService; - this.parameterService = parameterService; - this.specificTreatmentsService = specificTreatmentsService; - } - - // NB: code is quite well factored here, notice that most methods are private - - private Mono filePartToInputStream(FilePart filePart) { - return filePart.content() - .map(dataBuffer -> dataBuffer.asInputStream(true)) - .reduce(SequenceInputStream::new); - } - - private Mono readEnoJavaParametersFile(Mono parametersFile) { - return parametersFile - .flatMap(this::validateEnoJavaParametersFileName) - .flatMap(this::filePartToInputStream) - .flatMap(parameterService::parse); - } - - private Mono validateEnoJavaParametersFileName(FilePart filePart) { - if (! filePart.filename().endsWith(".json")) - return Mono.error(new EnoParametersException("Eno Java parameters file name must end with '.json'.")); - return Mono.just(filePart); - } - - private Mono createLunaticPostProcessing(Mono specificTreatment) { - return specificTreatment - .filter(FilePart.class::isInstance) - .map(FilePart.class::cast) - .flatMap(this::filePartToInputStream) - .flatMap(specificTreatmentsService::generateFrom) - .switchIfEmpty(Mono.just(new LunaticPostProcessing())); - /* - * This workaround (next filter) is used to make swagger works when empty value is checked for this input file on the endpoint - * - there is no way to disallow empty checkbox value at this moment on swagger (though openAPI support configuring this) - * - when empty value, spring boot considers the input as a DefaultFormField and not a file part, causing exceptions - * if trying to cast to file part :-/ - */ - } - - public Mono> ddiToLunaticJson(Mono ddiFile, Mono parametersFile, - Mono specificTreatmentsFile) { - Mono parametersMono = readEnoJavaParametersFile(parametersFile); - Mono postProcessingMono = createLunaticPostProcessing(specificTreatmentsFile); - return Mono.zip(parametersMono, postProcessingMono).flatMap(tuple -> - ddiToLunaticJson(ddiFile, tuple.getT1(), tuple.getT2())); - } - - public Mono> ddiToLunaticJson(Mono ddiFile, EnoParameters enoParameters, - Mono specificTreatmentsFile) { - return createLunaticPostProcessing(specificTreatmentsFile).flatMap(lunaticPostProcessing -> - ddiToLunaticJson(ddiFile, enoParameters, lunaticPostProcessing)); - } - - private Mono> ddiToLunaticJson(Mono ddiFile, EnoParameters enoParameters, - LunaticPostProcessing lunaticPostProcessing) { - return ddiFile - .flatMap(this::filePartToInputStream) - .flatMap(inputStream -> ddiToLunaticService.transformToJson(inputStream, enoParameters, lunaticPostProcessing)) - .map(result -> ResponseEntity - .ok() - .cacheControl(CacheControl.noCache()) - .headers(HeadersUtils.with(LUNATIC_JSON_FILE_NAME)) - .body(result)); - } - -} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/exception/DDIToLunaticException.java b/eno-ws/src/main/java/fr/insee/eno/ws/exception/DDIToLunaticException.java new file mode 100644 index 000000000..9cb04b971 --- /dev/null +++ b/eno-ws/src/main/java/fr/insee/eno/ws/exception/DDIToLunaticException.java @@ -0,0 +1,12 @@ +package fr.insee.eno.ws.exception; + +/** + * Generic error to be thrown when an exception occurs during the DDI to Lunatic transformation. + */ +public class DDIToLunaticException extends Exception { + + public DDIToLunaticException(Exception e) { + super(e); + } + +} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoControllerException.java b/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoControllerException.java new file mode 100644 index 000000000..d2ae56646 --- /dev/null +++ b/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoControllerException.java @@ -0,0 +1,9 @@ +package fr.insee.eno.ws.exception; + +public class EnoControllerException extends Exception { + + public EnoControllerException(String message) { + super(message); + } + +} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoRedirectionException.java b/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoRedirectionException.java new file mode 100644 index 000000000..45a0ae71a --- /dev/null +++ b/eno-ws/src/main/java/fr/insee/eno/ws/exception/EnoRedirectionException.java @@ -0,0 +1,9 @@ +package fr.insee.eno.ws.exception; + +public class EnoRedirectionException extends RuntimeException { + + public EnoRedirectionException(String message) { + super(message); + } + +} diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/service/DDIToLunaticService.java b/eno-ws/src/main/java/fr/insee/eno/ws/service/DDIToLunaticService.java index a8eaa6b1b..6c708ddf6 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/service/DDIToLunaticService.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/service/DDIToLunaticService.java @@ -4,11 +4,11 @@ import fr.insee.eno.core.parameter.EnoParameters; import fr.insee.eno.core.serialize.LunaticSerializer; import fr.insee.eno.treatments.LunaticPostProcessing; +import fr.insee.eno.ws.exception.DDIToLunaticException; import fr.insee.lunatic.model.flat.Questionnaire; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; import java.io.InputStream; @@ -21,26 +21,29 @@ public class DDIToLunaticService { @Value("${version.lunatic.model}") String lunaticModelVersion; - public Mono transformToJson(InputStream ddiInputStream, EnoParameters enoParameters, LunaticPostProcessing lunaticPostProcessings) { + public String transformToJson(InputStream ddiInputStream, EnoParameters enoParameters, LunaticPostProcessing lunaticPostProcessing) + throws DDIToLunaticException { try { Questionnaire lunaticQuestionnaire = DDIToLunatic.transform(ddiInputStream, enoParameters); lunaticQuestionnaire.setEnoCoreVersion(enoVersion); lunaticQuestionnaire.setLunaticModelVersion(lunaticModelVersion); - lunaticPostProcessings.apply(lunaticQuestionnaire); - return Mono.just(LunaticSerializer.serializeToJson(lunaticQuestionnaire)); + if (lunaticPostProcessing != null) + lunaticPostProcessing.apply(lunaticQuestionnaire); + return LunaticSerializer.serializeToJson(lunaticQuestionnaire); } catch (Exception e) { - return Mono.error(e); + throw new DDIToLunaticException(e); } } - public Mono transformToJson(InputStream ddiInputStream, EnoParameters enoParameters) { + public String transformToJson(InputStream ddiInputStream, EnoParameters enoParameters) + throws DDIToLunaticException { try { Questionnaire lunaticQuestionnaire = DDIToLunatic.transform(ddiInputStream, enoParameters); lunaticQuestionnaire.setEnoCoreVersion(enoVersion); lunaticQuestionnaire.setLunaticModelVersion(lunaticModelVersion); - return Mono.just(LunaticSerializer.serializeToJson(lunaticQuestionnaire)); + return LunaticSerializer.serializeToJson(lunaticQuestionnaire); } catch (Exception e) { - return Mono.error(e); + throw new DDIToLunaticException(e); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/service/ParameterService.java b/eno-ws/src/main/java/fr/insee/eno/ws/service/ParameterService.java index 29a5c5e6e..7087a4eaf 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/service/ParameterService.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/service/ParameterService.java @@ -1,11 +1,11 @@ package fr.insee.eno.ws.service; +import com.fasterxml.jackson.core.JsonProcessingException; import fr.insee.eno.core.exceptions.business.EnoParametersException; +import fr.insee.eno.core.parameter.EnoParameters; import fr.insee.eno.core.parameter.EnoParameters.ModeParameter; import fr.insee.eno.core.parameter.Format; -import fr.insee.eno.core.parameter.EnoParameters; import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; import java.io.IOException; import java.io.InputStream; @@ -13,20 +13,12 @@ @Service public class ParameterService { - public Mono parse(InputStream parametersInputStream) { - try { - return Mono.just(EnoParameters.parse(parametersInputStream)); - } catch (IOException | EnoParametersException e) { - return Mono.error(e); - } + public EnoParameters parse(InputStream parametersInputStream) throws EnoParametersException, IOException { + return EnoParameters.parse(parametersInputStream); } - public Mono defaultParams(EnoParameters.Context context, Format format, ModeParameter modeParameter) { - try { - return Mono.just(EnoParameters.serialize(EnoParameters.of(context, modeParameter, format))); - } catch (IOException ioException) { - return Mono.error(ioException); - } + public String defaultParams(EnoParameters.Context context, Format format, ModeParameter modeParameter) throws JsonProcessingException { + return EnoParameters.serialize(EnoParameters.of(context, modeParameter, format)); } } diff --git a/eno-ws/src/main/java/fr/insee/eno/ws/service/SpecificTreatmentsService.java b/eno-ws/src/main/java/fr/insee/eno/ws/service/SpecificTreatmentsService.java index 1b2bff8f8..b3b72e877 100644 --- a/eno-ws/src/main/java/fr/insee/eno/ws/service/SpecificTreatmentsService.java +++ b/eno-ws/src/main/java/fr/insee/eno/ws/service/SpecificTreatmentsService.java @@ -7,10 +7,7 @@ import fr.insee.eno.treatments.dto.EnoSuggesterType; import fr.insee.eno.treatments.dto.Regroupement; import fr.insee.eno.treatments.dto.SpecificTreatments; -import fr.insee.eno.treatments.exceptions.SpecificTreatmentsDeserializationException; -import fr.insee.eno.treatments.exceptions.SpecificTreatmentsValidationException; import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; import java.io.InputStream; import java.util.List; @@ -18,27 +15,21 @@ @Service public class SpecificTreatmentsService { - public Mono generateFrom(InputStream specificTreatmentStream) { + public LunaticPostProcessing generateFrom(InputStream specificTreatmentStream) { LunaticPostProcessing lunaticPostProcessings = new LunaticPostProcessing(); - try { - SpecificTreatmentsDeserializer deserializer = new SpecificTreatmentsDeserializer(); - SpecificTreatments treatments = deserializer.deserialize(specificTreatmentStream); + SpecificTreatmentsDeserializer deserializer = new SpecificTreatmentsDeserializer(); + SpecificTreatments treatments = deserializer.deserialize(specificTreatmentStream); - List suggesters = treatments.suggesters(); - if(suggesters != null && !suggesters.isEmpty()) - lunaticPostProcessings.addPostProcessing(new LunaticSuggesterSpecificTreatment(suggesters)); + List suggesters = treatments.suggesters(); + if(suggesters != null && !suggesters.isEmpty()) + lunaticPostProcessings.addPostProcessing(new LunaticSuggesterSpecificTreatment(suggesters)); - List regroupements = treatments.regroupements(); - if(regroupements != null && !regroupements.isEmpty()) { - lunaticPostProcessings.addPostProcessing(new LunaticRegroupingSpecificTreatment(regroupements)); - } - - return Mono.just(lunaticPostProcessings); + List regroupements = treatments.regroupements(); + if(regroupements != null && !regroupements.isEmpty()) { + lunaticPostProcessings.addPostProcessing(new LunaticRegroupingSpecificTreatment(regroupements)); } - catch (SpecificTreatmentsDeserializationException | SpecificTreatmentsValidationException ex) { - return Mono.error(ex); - } + return lunaticPostProcessings; } } diff --git a/eno-ws/src/main/resources/application.properties b/eno-ws/src/main/resources/application.properties index c3652702b..efd9d7958 100644 --- a/eno-ws/src/main/resources/application.properties +++ b/eno-ws/src/main/resources/application.properties @@ -5,6 +5,13 @@ springdoc.swagger-ui.tagsSorter=alpha # Sort API endpoints within tags alphabetically in the swagger ui springdoc.swagger-ui.operationsSorter=alpha +# Maximum size for files in controllers +spring.servlet.multipart.max-file-size=${eno.ws.max.upload.size} +# Maximum size for requests +spring.servlet.multipart.max-request-size=${eno.ws.max.upload.size} +# Maximum size for WebClient buffering +spring.codec.max-in-memory-size=${eno.ws.max.upload.size} + ### Customizable properties ### # URL of the deployed web-service app (used for swagger config) @@ -19,6 +26,9 @@ eno.legacy.ws.url=https://eno-url.insee.fr # Timeout for the web-client eno.webclient.timeout=600 +# Maximum size of requests +eno.ws.max.upload.size=12MB + # CORS, for services that want to call eno , default: no one eno.cors.origins= diff --git a/eno-ws/src/test/java/fr/insee/eno/ws/service/DDIToLunaticServiceTest.java b/eno-ws/src/test/java/fr/insee/eno/ws/service/DDIToLunaticServiceTest.java index 96f995a08..b39992e7f 100644 --- a/eno-ws/src/test/java/fr/insee/eno/ws/service/DDIToLunaticServiceTest.java +++ b/eno-ws/src/test/java/fr/insee/eno/ws/service/DDIToLunaticServiceTest.java @@ -33,19 +33,18 @@ class DDIToLunaticServiceTest { "kzguw1v7", // non-numeric controls "kanye31s_1", // declarations }) - void nonRegression_DefaultCAWI(String questionnaireId) { + void nonRegression_DefaultCAWI(String questionnaireId) throws Exception { // DDIToLunaticService ddiToLunaticService = new DDIToLunaticService(); String result = ddiToLunaticService.transformToJson( this.getClass().getClassLoader().getResourceAsStream("non-regression/ddi-"+questionnaireId+".xml"), - EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC)) - .block(); + EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC)); // assertNotNull(result); } @Test - void nonRegression_suggesterProcessing() { + void nonRegression_suggesterProcessing() throws Exception { // SpecificTreatmentsDeserializer deserializer = new SpecificTreatmentsDeserializer(); SpecificTreatments postProcessingInput = deserializer.deserialize( @@ -59,8 +58,7 @@ void nonRegression_suggesterProcessing() { String result = ddiToLunaticService.transformToJson( this.getClass().getClassLoader().getResourceAsStream("non-regression/suggester-processing/ddi-l7ugetj0.xml"), EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC), - lunaticPostProcessing) - .block(); + lunaticPostProcessing); // assertNotNull(result); @@ -71,7 +69,7 @@ void nonRegression_suggesterProcessing() { "group-input.json", "group-input-outside-loop.json" }) - void nonRegression_groupProcessing(String groupProcessingFileName) { + void nonRegression_groupProcessing(String groupProcessingFileName) throws Exception { // SpecificTreatmentsDeserializer deserializer = new SpecificTreatmentsDeserializer(); SpecificTreatments postProcessingInput = deserializer.deserialize( @@ -85,8 +83,7 @@ void nonRegression_groupProcessing(String groupProcessingFileName) { DDIToLunaticService ddiToLunaticService = new DDIToLunaticService(); String result = ddiToLunaticService.transformToJson( this.getClass().getClassLoader().getResourceAsStream("non-regression/group-processing/ddi-lhpz68wp.xml"), - EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC)) - .block(); + EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC)); // assertNotNull(result);