diff --git a/src/main/java/com/sap/cloudfoundry/client/facade/Messages.java b/src/main/java/com/sap/cloudfoundry/client/facade/Messages.java index b4bb2b1ea..34d42690e 100644 --- a/src/main/java/com/sap/cloudfoundry/client/facade/Messages.java +++ b/src/main/java/com/sap/cloudfoundry/client/facade/Messages.java @@ -11,8 +11,6 @@ private Messages() { public static final String CF_ROOT_REQUEST_FINISHED = "CF root request finished"; public static final String CALLING_LOG_CACHE_ENDPOINT_TO_GET_APP_LOGS = "Calling log-cache endpoint to get app logs"; public static final String APP_LOGS_WERE_FETCHED_SUCCESSFULLY = "App logs were fetched successfully"; - public static final String STARTED_READING_LOG_RESPONSE_INPUT_STREAM = "Started reading log response input stream"; - public static final String ENDED_READING_LOG_RESPONSE_INPUT_STREAM = "Ended reading log response input stream"; // WARN messages public static final String RETRYING_OPERATION = "Retrying operation that failed with: {0}"; @@ -25,7 +23,5 @@ private Messages() { public static final String SERVICE_PLAN_WITH_GUID_0_NOT_AVAILABLE_FOR_SERVICE_INSTANCE_1 = "Service plan with guid \"{0}\" is not available for service instance \"{1}\"."; public static final String SERVICE_OFFERING_WITH_GUID_0_IS_NOT_AVAILABLE = "Service offering with guid \"{0}\" is not available."; public static final String SERVICE_OFFERING_WITH_GUID_0_NOT_FOUND = "Service offering with guid \"{0}\" not found."; - public static final String CANT_READ_APP_LOGS_RESPONSE = "Failed to read application recent logs response: %s"; - public static final String CANT_DESERIALIZE_APP_LOGS_RESPONSE = "Failed to deserialize application recent logs response: %s"; } diff --git a/src/main/java/com/sap/cloudfoundry/client/facade/adapters/CloudFoundryClientFactory.java b/src/main/java/com/sap/cloudfoundry/client/facade/adapters/CloudFoundryClientFactory.java index 0541b883a..017ccb82c 100644 --- a/src/main/java/com/sap/cloudfoundry/client/facade/adapters/CloudFoundryClientFactory.java +++ b/src/main/java/com/sap/cloudfoundry/client/facade/adapters/CloudFoundryClientFactory.java @@ -1,17 +1,13 @@ package com.sap.cloudfoundry.client.facade.adapters; -import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.text.MessageFormat; import java.time.Duration; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; import java.util.function.Function; import org.cloudfoundry.client.CloudFoundryClient; @@ -25,31 +21,38 @@ import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; - +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cloudfoundry.client.facade.CloudException; -import com.sap.cloudfoundry.client.facade.CloudOperationException; import com.sap.cloudfoundry.client.facade.Messages; import com.sap.cloudfoundry.client.facade.oauth2.OAuthClient; import com.sap.cloudfoundry.client.facade.rest.CloudSpaceClient; import com.sap.cloudfoundry.client.facade.util.CloudUtil; import com.sap.cloudfoundry.client.facade.util.JsonUtil; +import io.netty.channel.ChannelOption; import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; @Value.Immutable public abstract class CloudFoundryClientFactory { private static final Logger LOGGER = LoggerFactory.getLogger(CloudFoundryClientFactory.class); - static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() - .executor(Executors.newSingleThreadExecutor()) - .followRedirects(HttpClient.Redirect.NORMAL) - .connectTimeout(Duration.ofMinutes(10)) - .build(); - private final Map connectionContextCache = new ConcurrentHashMap<>(); + private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); + + static final WebClient WEB_CLIENT = buildWebClient(); + public abstract Optional getSslHandshakeTimeout(); public abstract Optional getConnectTimeout(); @@ -85,30 +88,38 @@ public LogCacheClient createLogCacheClient(URL controllerUrl, OAuthClient oAuthC @SuppressWarnings("unchecked") private Map callCfRoot(URL controllerUrl, Map requestTags) { - HttpResponse response; + LOGGER.info(MessageFormat.format(Messages.CALLING_CF_ROOT_0_TO_ACCESS_LOG_CACHE_URL, controllerUrl)); + String response = WEB_CLIENT.get() + .uri(getControllerUri(controllerUrl)) + .headers(httpHeaders -> httpHeaders.addAll(getAdditionalRequestHeaders(requestTags))) + .exchangeToMono(this::handleClientResponse) + .block(); + LOGGER.info(Messages.CF_ROOT_REQUEST_FINISHED); + var map = JsonUtil.convertJsonToMap(response); + return (Map) map.get("links"); + } + + private URI getControllerUri(URL controllerUrl) { try { - HttpRequest request = buildCfRootRequest(controllerUrl, requestTags); - LOGGER.info(MessageFormat.format(Messages.CALLING_CF_ROOT_0_TO_ACCESS_LOG_CACHE_URL, controllerUrl)); - response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() / 100 != 2) { - var status = HttpStatus.valueOf(response.statusCode()); - throw new CloudOperationException(status, status.getReasonPhrase(), response.body()); - } - LOGGER.info(Messages.CF_ROOT_REQUEST_FINISHED); - } catch (InterruptedException | URISyntaxException | IOException e) { + return controllerUrl.toURI(); + } catch (URISyntaxException e) { throw new CloudException(e.getMessage(), e); } - var map = JsonUtil.convertJsonToMap(response.body()); - return (Map) map.get("links"); } - private HttpRequest buildCfRootRequest(URL controllerUrl, Map requestTags) throws URISyntaxException { - var requestBuilder = HttpRequest.newBuilder() - .GET() - .uri(controllerUrl.toURI()) - .timeout(Duration.ofMinutes(5)); - requestTags.forEach(requestBuilder::header); - return requestBuilder.build(); + private LinkedMultiValueMap getAdditionalRequestHeaders(Map requestTags) { + LinkedMultiValueMap additionalHeaders = new LinkedMultiValueMap<>(); + requestTags.forEach(additionalHeaders::add); + return additionalHeaders; + } + + private Mono handleClientResponse(ClientResponse clientResponse) { + if (clientResponse.statusCode() + .is2xxSuccessful()) { + return clientResponse.bodyToMono(String.class); + } + return clientResponse.createException() + .flatMap(Mono::error); } public CloudSpaceClient createSpaceClient(URL controllerUrl, OAuthClient oAuthClient, Map requestTags) { @@ -165,4 +176,19 @@ private reactor.netty.http.client.HttpClient getAdditionalHttpClientConfiguratio return clientWithOptions; } + private static WebClient buildWebClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) Duration.ofMinutes(10) + .toMillis()) + .responseTimeout(Duration.ofMinutes(5)); + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .exchangeStrategies(ExchangeStrategies.builder() + .codecs(configurer -> configurer.defaultCodecs() + .jackson2JsonDecoder(new Jackson2JsonDecoder(MAPPER))) + .build()) + .build(); + + } + } diff --git a/src/main/java/com/sap/cloudfoundry/client/facade/adapters/LogCacheClient.java b/src/main/java/com/sap/cloudfoundry/client/facade/adapters/LogCacheClient.java index fd597fcff..e801f1587 100644 --- a/src/main/java/com/sap/cloudfoundry/client/facade/adapters/LogCacheClient.java +++ b/src/main/java/com/sap/cloudfoundry/client/facade/adapters/LogCacheClient.java @@ -1,11 +1,8 @@ package com.sap.cloudfoundry.client.facade.adapters; -import java.io.IOException; -import java.io.InputStream; +import static com.sap.cloudfoundry.client.facade.adapters.CloudFoundryClientFactory.WEB_CLIENT; + import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -18,27 +15,22 @@ import java.util.Map; import java.util.UUID; -import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.util.UriComponentsBuilder; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cloudfoundry.client.facade.CloudException; -import com.sap.cloudfoundry.client.facade.CloudOperationException; import com.sap.cloudfoundry.client.facade.Messages; import com.sap.cloudfoundry.client.facade.domain.ApplicationLog; import com.sap.cloudfoundry.client.facade.domain.ImmutableApplicationLog; import com.sap.cloudfoundry.client.facade.oauth2.OAuthClient; import com.sap.cloudfoundry.client.facade.util.CloudUtil; -public class LogCacheClient { +import reactor.core.publisher.Mono; - private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); +public class LogCacheClient { private static final Logger LOGGER = LoggerFactory.getLogger(LogCacheClient.class); @@ -53,40 +45,21 @@ public LogCacheClient(String logCacheApi, OAuthClient oAuthClient, Map getRecentLogs(UUID applicationGuid, LocalDateTime offset) { - HttpResponse response = CloudUtil.executeWithRetry(() -> executeRequest(applicationGuid, offset)); - - return parseBody(response.body()).getLogs() - .stream() - .map(this::mapToAppLog) - // we use a linked list so that the log messages can be a LIFO sequence - // that way, we avoid unnecessary sorting and copying to and from another collection/array - .collect(LinkedList::new, LinkedList::addFirst, LinkedList::addAll); + ApplicationLogsResponse applicationLogsResponse = CloudUtil.executeWithRetry(() -> executeApplicationLogsRequest(applicationGuid, + offset)); + return applicationLogsResponse.getLogs() + .stream() + .map(this::mapToAppLog) + // we use a linked list so that the log messages can be a LIFO sequence + // that way, we avoid unnecessary sorting and copying to and from another collection/array + .collect(LinkedList::new, LinkedList::addFirst, LinkedList::addAll); } - private HttpResponse executeRequest(UUID applicationGuid, LocalDateTime offset) { - try { - HttpRequest request = buildGetLogsRequest(applicationGuid, offset); - LOGGER.info(Messages.CALLING_LOG_CACHE_ENDPOINT_TO_GET_APP_LOGS); - var response = CloudFoundryClientFactory.HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); - if (response.statusCode() / 100 != 2) { - var status = HttpStatus.valueOf(response.statusCode()); - throw new CloudOperationException(status, status.getReasonPhrase(), parseBodyToString(response.body())); - } - LOGGER.info(Messages.APP_LOGS_WERE_FETCHED_SUCCESSFULLY); - return response; - } catch (IOException | InterruptedException e) { - throw new CloudException(e.getMessage(), e); - } - } - - private HttpRequest buildGetLogsRequest(UUID applicationGuid, LocalDateTime offset) { - var requestBuilder = HttpRequest.newBuilder() - .GET() - .uri(buildGetLogsUrl(applicationGuid, offset)) - .timeout(Duration.ofMinutes(5)) - .header(HttpHeaders.AUTHORIZATION, oAuthClient.getAuthorizationHeaderValue()); - requestTags.forEach(requestBuilder::header); - return requestBuilder.build(); + private ApplicationLogsResponse executeApplicationLogsRequest(UUID applicationGuid, LocalDateTime offset) { + LOGGER.info(Messages.CALLING_LOG_CACHE_ENDPOINT_TO_GET_APP_LOGS); + ApplicationLogsResponse applicationLogsResponse = executeApplicationLogsRequest(buildGetLogsUrl(applicationGuid, offset)); + LOGGER.info(Messages.APP_LOGS_WERE_FETCHED_SUCCESSFULLY); + return applicationLogsResponse; } private URI buildGetLogsUrl(UUID applicationGuid, LocalDateTime offset) { @@ -103,23 +76,28 @@ private URI buildGetLogsUrl(UUID applicationGuid, LocalDateTime offset) { .toUri(); } - private String parseBodyToString(InputStream is) { - try (InputStream wrapped = is) { - return IOUtils.toString(wrapped, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new CloudException(String.format(Messages.CANT_READ_APP_LOGS_RESPONSE, e.getMessage()), e); - } + private ApplicationLogsResponse executeApplicationLogsRequest(URI logsUrl) { + return WEB_CLIENT.get() + .uri(logsUrl) + .headers(httpHeaders -> httpHeaders.addAll(getAdditionalRequestHeaders(requestTags))) + .header(HttpHeaders.AUTHORIZATION, oAuthClient.getAuthorizationHeaderValue()) + .exchangeToMono(this::handleClientResponse) + .block(); + } + + private LinkedMultiValueMap getAdditionalRequestHeaders(Map requestTags) { + LinkedMultiValueMap additionalHeaders = new LinkedMultiValueMap<>(); + requestTags.forEach(additionalHeaders::add); + return additionalHeaders; } - private ApplicationLogsResponse parseBody(InputStream is) { - LOGGER.info(Messages.STARTED_READING_LOG_RESPONSE_INPUT_STREAM); - try (InputStream wrapped = is) { - var appLogsResponse = MAPPER.readValue(wrapped, ApplicationLogsResponse.class); - LOGGER.info(Messages.ENDED_READING_LOG_RESPONSE_INPUT_STREAM); - return appLogsResponse; - } catch (IOException e) { - throw new CloudException(String.format(Messages.CANT_DESERIALIZE_APP_LOGS_RESPONSE, e.getMessage()), e); + private Mono handleClientResponse(ClientResponse clientResponse) { + if (clientResponse.statusCode() + .is2xxSuccessful()) { + return clientResponse.bodyToMono(ApplicationLogsResponse.class); } + return clientResponse.createException() + .flatMap(Mono::error); } private ApplicationLog mapToAppLog(ApplicationLogEntity log) { diff --git a/src/main/java/com/sap/cloudfoundry/client/facade/domain/CloudRoute.java b/src/main/java/com/sap/cloudfoundry/client/facade/domain/CloudRoute.java index 1f1cf498f..c2f3c210a 100644 --- a/src/main/java/com/sap/cloudfoundry/client/facade/domain/CloudRoute.java +++ b/src/main/java/com/sap/cloudfoundry/client/facade/domain/CloudRoute.java @@ -1,13 +1,13 @@ package com.sap.cloudfoundry.client.facade.domain; +import java.util.Objects; + import org.immutables.value.Value; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.sap.cloudfoundry.client.facade.Nullable; -import java.util.Objects; - @Value.Immutable @JsonSerialize(as = ImmutableCloudRoute.class) @JsonDeserialize(as = ImmutableCloudRoute.class) @@ -58,10 +58,8 @@ public boolean equals(Object object) { var thisDomain = getDomain().getName(); var otherDomain = otherRoute.getDomain() .getName(); - return thisDomain.equals(otherDomain) - && areEmptyOrEqual(getHost(), otherRoute.getHost()) - && areEmptyOrEqual(getPath(), otherRoute.getPath()) - && Objects.equals(getPort(), otherRoute.getPort()); + return thisDomain.equals(otherDomain) && areEmptyOrEqual(getHost(), otherRoute.getHost()) + && areEmptyOrEqual(getPath(), otherRoute.getPath()) && Objects.equals(getPort(), otherRoute.getPort()); } private static boolean areEmptyOrEqual(String lhs, String rhs) { diff --git a/src/main/java/com/sap/cloudfoundry/client/facade/rest/CloudControllerRestClient.java b/src/main/java/com/sap/cloudfoundry/client/facade/rest/CloudControllerRestClient.java index 61773a17e..e9155bf2b 100644 --- a/src/main/java/com/sap/cloudfoundry/client/facade/rest/CloudControllerRestClient.java +++ b/src/main/java/com/sap/cloudfoundry/client/facade/rest/CloudControllerRestClient.java @@ -7,7 +7,6 @@ import java.util.Set; import java.util.UUID; -import com.sap.cloudfoundry.client.facade.dto.ApplicationToCreateDto; import org.cloudfoundry.client.v3.Metadata; import com.sap.cloudfoundry.client.facade.UploadStatusCallback; @@ -34,6 +33,7 @@ import com.sap.cloudfoundry.client.facade.domain.Staging; import com.sap.cloudfoundry.client.facade.domain.Upload; import com.sap.cloudfoundry.client.facade.domain.UserRole; +import com.sap.cloudfoundry.client.facade.dto.ApplicationToCreateDto; /** * Interface defining operations available for the cloud controller REST client implementations diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e39b0b9bd..5e669e978 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -20,6 +20,7 @@ requires org.apache.commons.logging; requires org.reactivestreams; requires java.net.http; + requires io.netty.transport; requires static java.compiler; requires static org.immutables.value;