diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java index 8a32e8a0fe7f0322f78843501acc28dd87c2e1be..02eae1754da169817e6371696bc1b814d34a1dc7 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java @@ -25,6 +25,7 @@ package de.ozgcloud.document.bescheid.smartdocuments; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Optional; @@ -34,14 +35,15 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.BodyExtractors; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.client.RestClient; import org.w3c.dom.Document; import org.w3c.dom.Text; import org.xml.sax.SAXException; @@ -69,7 +71,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import reactor.core.publisher.Mono; @Log4j2 @Service @@ -87,7 +88,7 @@ class SmartDocumentsBescheidRemoteService implements BescheidRemoteService { private static final MediaType JSON_MEDIA_TYPE_FOR_SD = MediaType.APPLICATION_JSON_UTF8; @Qualifier("smartDocuments") - private final WebClient smartDocumentsWebClient; + private final RestClient smartDocumentsRestClient; private final SmartDocumentsProperties properties; @@ -96,19 +97,24 @@ class SmartDocumentsBescheidRemoteService implements BescheidRemoteService { var sdRequest = createRequest(request, vorgang); LOG.debug(() -> buildLogRequest(sdRequest)); - return smartDocumentsWebClient.post().accept(MediaType.APPLICATION_JSON) + var response = smartDocumentsRestClient.post().accept(MediaType.APPLICATION_JSON) .contentType(JSON_MEDIA_TYPE_FOR_SD) - .bodyValue(sdRequest) + .body(sdRequest) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, this::handleClientError) - .bodyToMono(SmartDocumentsResponse.class) - .map(response -> buildBescheid(request, response)) - .block(); + .toEntity(SmartDocumentsResponse.class) + .getBody(); + return buildBescheid(request, response); + } - Mono<Throwable> handleClientError(ClientResponse response) { - return response.body(BodyExtractors.toMono(String.class)) - .map(content -> new TechnicalException("Client-Error: " + content)); + void handleClientError(HttpRequest request, ClientHttpResponse response) { + try { + var responseBody = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8); + throw new TechnicalException("Client-Error: " + response.getStatusCode() + " " + responseBody); + } catch (IOException e) { + throw new TechnicalException("Couldn't read response body", e); + } } private String buildLogRequest(SmartDocumentsRequest request) { diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java index 01255a15d544124d4e28975319b103614c2f52eb..0462d99ab07a8d2ec4af61f9a6b034df974ef4e3 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java @@ -23,16 +23,20 @@ */ package de.ozgcloud.document.bescheid.smartdocuments; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; + +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; +import org.apache.hc.core5.http.HttpHost; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; - -import reactor.netty.http.client.HttpClient; -import reactor.netty.transport.ProxyProvider; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestClient; @Configuration @ConditionalOnProperty("ozgcloud.bescheid.smart-documents.url") @@ -42,35 +46,43 @@ class SmartDocumentsConfiguration { private SmartDocumentsProperties properties; @Bean("smartDocuments") - WebClient smartDocumentsWebClient() { - ReactorClientHttpConnector connector = new ReactorClientHttpConnector(buildHttpClient()); - - return WebClient.builder() + RestClient smartDocumentsRestClient() { + return RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory(buildHttpClient())) .baseUrl(properties.getUrl()) - .filter(ExchangeFilterFunctions.basicAuthentication(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword())) - .clientConnector(connector) + .defaultHeaders(headers -> headers.setBasicAuth(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword())) .build(); } - private HttpClient buildHttpClient() { + CloseableHttpClient buildHttpClient() { if (properties.getProxy() != null) { return createProxyHttpClient(); - } else { - return createNoProxyHttpClient(); } + return createNoProxyHttpClient(); } - private HttpClient createNoProxyHttpClient() { - return HttpClient.create(); + CloseableHttpClient createNoProxyHttpClient() { + return HttpClients.createDefault(); } - private HttpClient createProxyHttpClient() { - return HttpClient.create() - .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP) - .host(properties.getProxy().getHost()) - .port(properties.getProxy().getPort()) - .username(properties.getProxy().getUsername()) - .password(username -> properties.getProxy().getPassword())); + CloseableHttpClient createProxyHttpClient() { + return HttpClients.custom() + .setRoutePlanner(createProxyRoutePlanner()) + .setDefaultCredentialsProvider(createProxyCredentialsProvider()) + .build(); + } + + DefaultProxyRoutePlanner createProxyRoutePlanner() { + return new DefaultProxyRoutePlanner(new HttpHost(properties.getProxy().getHost(), properties.getProxy().getPort())); + } + + CredentialsProvider createProxyCredentialsProvider() { + var host = properties.getProxy().getHost(); + var port = properties.getProxy().getPort(); + var username = properties.getProxy().getUsername(); + var password = properties.getProxy().getPassword().toCharArray(); + + return CredentialsProviderBuilder.create().add(new AuthScope(host, port), username, password).build(); } } diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..79739379d26d965c568a6c06f6600f530b8130aa --- /dev/null +++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java @@ -0,0 +1,118 @@ +package de.ozgcloud.document.bescheid.smartdocuments; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.ProxyConfiguration; + +class SmartDocumentsConfigurationTest { + + @Spy + @InjectMocks + private SmartDocumentsConfiguration smartDocumentsConfiguration; + + @Mock + private SmartDocumentsProperties properties; + + @Nested + class CreateProxyCredentialsProviderTest { + + private final String user = "max"; + private final String password = "max2"; + private final String host = "test-proxy.local"; + private final int port = 8080; + private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration(); + + @BeforeEach + void setUp() { + proxyConfiguration.setHost(host); + proxyConfiguration.setPort(port); + proxyConfiguration.setUsername(user); + proxyConfiguration.setPassword(password); + when(properties.getProxy()).thenReturn(proxyConfiguration); + } + + @Test + void shouldCreateWithUserAndPassword() { + var credentialsProvider = smartDocumentsConfiguration.createProxyCredentialsProvider(); + + var credentialsForProxy = (UsernamePasswordCredentials) credentialsProvider.getCredentials(new AuthScope(host, port), null); + + assertThat(credentialsForProxy).extracting( + UsernamePasswordCredentials::getUserName, + UsernamePasswordCredentials::getUserPassword) + .containsExactly(user, password.toCharArray()); + } + } + + @Nested + class CreateProxyRoutePlannerTest { + + private final String host = "test-proxy.local"; + private final int port = 8080; + private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration(); + + @BeforeEach + void setUp() { + proxyConfiguration.setHost(host); + proxyConfiguration.setPort(port); + when(properties.getProxy()).thenReturn(proxyConfiguration); + } + + @Test + void shouldCreateWithProxySettings() throws HttpException { + var routePlanner = smartDocumentsConfiguration.createProxyRoutePlanner(); + + var proxyHost = routePlanner.determineRoute(new HttpHost("http://any"), new HttpClientContext()).getProxyHost(); + + assertThat(proxyHost).extracting( + HttpHost::getHostName, + HttpHost::getPort + ).containsExactly(host, port); + } + } + + @Nested + class BuildHttpClientTest { + private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault(); + private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault(); + + @BeforeEach + void setUp() { + } + + @Test + void shouldReturnProxyHttpClient() { + doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createProxyHttpClient(); + when(properties.getProxy()).thenReturn(new ProxyConfiguration()); + + var httpClient = smartDocumentsConfiguration.buildHttpClient(); + + assertThat(httpClient).isSameAs(proxyHttpClient); + } + + @Test + void shouldReturnNonProxyHttpClient() { + doReturn(nonProxyHttpClient).when(smartDocumentsConfiguration).createNoProxyHttpClient(); + when(properties.getProxy()).thenReturn(null); + + var httpClient = smartDocumentsConfiguration.buildHttpClient(); + + assertThat(httpClient).isSameAs(nonProxyHttpClient); + } + } +} \ No newline at end of file