diff --git a/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsRemoteCaller.java b/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsRemoteCaller.java index f017474496..973f5a7a3b 100644 --- a/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsRemoteCaller.java +++ b/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsRemoteCaller.java @@ -16,7 +16,9 @@ import static org.entando.entando.aps.system.services.tenants.ITenantManager.PRIMARY_CODE; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.net.SocketTimeoutException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +26,7 @@ import java.util.Map; import java.util.Optional; import java.util.WeakHashMap; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.entando.entando.aps.system.services.tenants.TenantConfig; import org.entando.entando.ent.exception.EntRuntimeException; @@ -45,6 +48,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import org.entando.entando.aps.system.services.storage.CdsActive; @@ -60,7 +64,7 @@ public class CdsRemoteCaller { private final RestTemplate restTemplateWithRedirect; private final CdsConfiguration configuration; - // used weak map to skip excessive growth + // used weak map to skip excessive growthclear private Map tenantsToken = new WeakHashMap<>(); @Autowired @@ -76,8 +80,8 @@ public CdsCreateResponseDto executePostCall(URI url, boolean isProtectedResource, Optional fileInputStream , Optional config, - boolean forceTokenRetrieve) { - + boolean forceTokenRetrieve, + int cdsRetry) { try { logger.debug("Trying to call POST on url:'{}' with isProtectedResource:'{}' forceTokenRetrieve:'{}' isFile:'{}' and is config tenant empty:'{}'", url, @@ -90,6 +94,7 @@ public CdsCreateResponseDto executePostCall(URI url, headers.setContentType(MediaType.MULTIPART_FORM_DATA); MultiValueMap body = fileInputStream + .map(this::cloneInputStream) .map(is -> buildFileBodyRequest(subPath, isProtectedResource, is)) .orElseGet(() -> buildDirectoryBodyRequest(subPath, isProtectedResource)); @@ -98,7 +103,8 @@ public CdsCreateResponseDto executePostCall(URI url, ResponseEntity> fullResponse = restTemplate.exchange(url, HttpMethod.POST, entity, - new ParameterizedTypeReference>(){}); + new ParameterizedTypeReference<>() { + }); List responseList = Optional .ofNullable(fullResponse.getBody()) @@ -115,15 +121,28 @@ public CdsCreateResponseDto executePostCall(URI url, return response; } catch (HttpClientErrorException e) { if (!forceTokenRetrieve && (e.getStatusCode().equals(HttpStatus.UNAUTHORIZED))) { - return this.executePostCall(url, subPath, isProtectedResource, fileInputStream, config, true); + return this.executePostCall(url, subPath, isProtectedResource, fileInputStream, config, true, + cdsRetry + 1); } else { - throw buildExceptionWithMessage("POST", e.getStatusCode() , url.toString()); + throw buildExceptionWithMessage("POST", e.getStatusCode(), url.toString()); } } catch(Exception ex){ + if (ex instanceof ResourceAccessException && ex.getCause() instanceof SocketTimeoutException + && cdsRetry < 2) { + return this.executePostCall(url, subPath, isProtectedResource, fileInputStream, config, true, + cdsRetry + 1); + } throw new EntRuntimeException(String.format(GENERIC_REST_ERROR_MSG, url.toString()), ex); } } + private InputStream cloneInputStream(InputStream is) { + try { + return IOUtils.toBufferedInputStream(is); + } catch (IOException e) { + throw new EntRuntimeException("Error in cloning input stream", e); + } + } private MultiValueMap buildDirectoryBodyRequest(String subPath, boolean isProtectedResource){ MultiValueMap body = new LinkedMultiValueMap<>(); body.add("path", subPath); diff --git a/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManager.java b/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManager.java index dd255c3ade..269c129312 100644 --- a/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManager.java +++ b/cds-plugin/src/main/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManager.java @@ -80,12 +80,14 @@ private void create(String subPath, boolean isProtectedResource, Optional configMap = Map.of("cdsPublicUrl","http://my-server/tenant1/cms-resources", @@ -233,7 +246,8 @@ void shouldCreateFileForPrimary() throws Exception { false, Optional.ofNullable(is), Optional.empty(), - false); + false, + 0); Assertions.assertTrue(ret.isStatusOk()); @@ -545,4 +559,103 @@ void shouldRetrieveFileAttribute() { eq(new ParameterizedTypeReference>(){})); } + @Test + void shouldThrowExceptionIfAnErrorOccurInCloningFileInputStream() throws Exception { + // set the config map for tenant + Map configMap = Map.of("cdsPublicUrl", "http://my-server/tenant1/cms-resources", + "cdsPrivateUrl", "http://cds-kube-service:8081/", + "cdsPath", "/mytenant/api/v1", + "kcAuthUrl", "http://tenant1.server.com/auth", + "kcRealm", "tenant1", + "kcClientId", "id", + "kcClientSecret", "secret", + "tenantCode", "my-tenant1"); + TenantConfig tc = new TenantConfig(configMap); + Optional tenantConfig = Optional.of(tc); + // set tenant + ApsTenantApplicationUtils.setTenant("my-tenant"); + + // the input stream used to execute the post call + Optional fileInputStream = Optional.of( + new ByteArrayInputStream("fake".getBytes(StandardCharsets.UTF_8))); + + Mockito.when(restTemplate.exchange(anyString(), + eq(HttpMethod.POST), + any(), + eq(new ParameterizedTypeReference>() { + }))).thenReturn( + ResponseEntity.status(HttpStatus.OK).body(Map.of(OAuth2AccessToken.ACCESS_TOKEN, "entando"))); + + try (MockedStatic utilities = Mockito.mockStatic(IOUtils.class)) { + utilities.when(() -> IOUtils.toBufferedInputStream(any())) + .thenThrow(new IOException()); + + URI url = URI.create("http://cds-kube-service:8081/mytenant/api/v1/upload/"); + Exception ex = assertThrows(EntRuntimeException.class, + () -> cdsRemoteCaller.executePostCall( + url, + "/sub-path-testy", + false, + fileInputStream, + tenantConfig, + false, + 0) + ); + assertEquals("Generic error in a rest call for url:'http://cds-kube-service:8081/mytenant/api/v1/upload/'", + ex.getMessage()); + assertEquals("Error in cloning input stream", ex.getCause().getMessage()); + + } + } + + @Test + void shouldRetryOneTimeIfPostTimeOutOccurs() throws Exception { + // set the config map for tenant + Map configMap = Map.of("cdsPublicUrl", "http://my-server/tenant1/cms-resources", + "cdsPrivateUrl", "http://cds-kube-service:8081/", + "cdsPath", "/mytenant/api/v1", + "kcAuthUrl", "http://tenant1.server.com/auth", + "kcRealm", "tenant1", + "kcClientId", "id", + "kcClientSecret", "secret", + "tenantCode", "my-tenant1"); + TenantConfig tc = new TenantConfig(configMap); + Optional tenantConfig = Optional.of(tc); + // set tenant + ApsTenantApplicationUtils.setTenant("my-tenant"); + // the input stream used to execute the post call + Optional fileInputStream = Optional.of( + new ByteArrayInputStream("fake".getBytes(StandardCharsets.UTF_8))); + + CdsCreateRowResponseDto resp = new CdsCreateRowResponseDto(); + resp.setStatus("OK"); + List list = new ArrayList<>(); + list.add(resp); + URI url = URI.create("http://cds-kube-service:8081/mytenant/api/v1/upload/"); + Mockito.when(restTemplate.exchange(eq(url), eq(HttpMethod.POST), any(), + eq(new ParameterizedTypeReference>() { + }))) + + .thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED)) + .thenThrow(new ResourceAccessException("", new SocketTimeoutException(""))) + .thenReturn(ResponseEntity.ok(list)); + + URI auth = URI.create("http://tenant1.server.com/auth/realms/tenant1/protocol/openid-connect/token"); + Mockito.when(restTemplate.exchange(eq(auth.toString()), + eq(HttpMethod.POST), + any(), + eq(new ParameterizedTypeReference>() { + }))).thenReturn( + ResponseEntity.status(HttpStatus.OK).body(Map.of("access_token", "xxxxxx"))); + + CdsCreateResponseDto responseDto = cdsRemoteCaller.executePostCall( + url, + "/sub-path-testy", + false, + fileInputStream, + tenantConfig, + false, + 0); + assertTrue(responseDto.isStatusOk()); + } } diff --git a/cds-plugin/src/test/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManagerTest.java b/cds-plugin/src/test/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManagerTest.java index 3e4e0a483c..20908741f7 100644 --- a/cds-plugin/src/test/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManagerTest.java +++ b/cds-plugin/src/test/java/org/entando/entando/plugins/jpcds/aps/system/storage/CdsStorageManagerTest.java @@ -80,7 +80,8 @@ void shouldCreateDirectory() throws Exception { eq(false), any(), any(), - eq(false))).thenReturn(ret); + eq(false), + eq(0))).thenReturn(ret); ApsTenantApplicationUtils.setTenant("my-tenant"); cdsStorageManager.createDirectory("/sub-path-testy",false); @@ -96,7 +97,8 @@ void shouldCreateDirectory() throws Exception { eq(false), any(), any(), - eq(false)); + eq(false), + eq(0)); } @@ -126,7 +128,8 @@ void shouldNotCreateDirectory() throws Exception { eq(false), any(), any(), - eq(false))).thenReturn(ret); + eq(false), + eq(0))).thenReturn(ret); ApsTenantApplicationUtils.setTenant("my-tenant"); Assertions.assertThatThrownBy( @@ -157,7 +160,8 @@ void shouldCreateFile() throws Exception { eq(false), any(), any(), - eq(false))).thenReturn(ret); + eq(false), + eq(0))).thenReturn(ret); ApsTenantApplicationUtils.setTenant("my-tenant"); InputStream is = new ByteArrayInputStream("testo a casos".getBytes(StandardCharsets.UTF_8)); @@ -169,7 +173,8 @@ void shouldCreateFile() throws Exception { eq(false), any(), any(), - eq(false)); + eq(false), + eq(0)); } @@ -533,7 +538,8 @@ void shouldEditFile() throws Exception { eq(false), any(), any(), - eq(false))).thenReturn(ret); + eq(false), + eq(0))).thenReturn(ret); ApsTenantApplicationUtils.setTenant("my-tenant"); InputStream is = new ByteArrayInputStream("testo a casos".getBytes(StandardCharsets.UTF_8)); @@ -552,7 +558,8 @@ void shouldEditFile() throws Exception { eq(false), any(), any(), - eq(false)); + eq(false), + eq(0)); }