From 1db0ad25fd883c8a9db0890889e4f2530438b0d7 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Fri, 21 Feb 2025 06:52:48 +0530 Subject: [PATCH] chore: Support String inputs for import service methods (#39337) --- .../imports/internal/ImportServiceCE.java | 21 ++++--- .../imports/internal/ImportServiceCEImpl.java | 59 +++++++++++-------- .../partial/PartialImportServiceCE.java | 3 + .../partial/PartialImportServiceCEImpl.java | 10 +++- .../ApplicationControllerTest.java | 6 +- .../ServerSchemaMigrationEnforcerTest.java | 32 ++++------ .../imports/internal/ImportServiceTests.java | 19 ++++-- .../solutions/PartialImportServiceTest.java | 48 +++++++-------- 8 files changed, 110 insertions(+), 88 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCE.java index 7c66c89061..5cff3aae35 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCE.java @@ -31,13 +31,9 @@ public interface ImportServiceCE { ArtifactBasedImportService getArtifactBasedImportService(ArtifactType artifactType); - /** - * This method takes a file part and makes a Json entity which implements the ArtifactExchangeJson interface - * - * @param filePart : filePart from which the contents would be made - * @return : Json entity which implements ArtifactExchangeJson - */ - Mono extractArtifactExchangeJson(Part filePart); + Mono readFilePartToString(Part file); + + Mono extractArtifactExchangeJson(String jsonString); /** * Hydrates an Artifact within the specified workspace by saving the provided JSON file. @@ -50,6 +46,17 @@ public interface ImportServiceCE { Mono extractArtifactExchangeJsonAndSaveArtifact( Part filePart, String workspaceId, String artifactId); + /** + * Hydrates an Artifact within the specified workspace by saving the provided JSON file. + * + * @param jsonContents The JSON representing the Artifact object to be saved. + * The Artifact implements the Artifact interface. + * @param workspaceId The identifier for the destination workspace. + * @param artifactId + */ + Mono extractArtifactExchangeJsonAndSaveArtifact( + String jsonContents, String workspaceId, String artifactId); + /** * Saves the provided ArtifactExchangeJson within the specified workspace. * diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java index 66a3d8c73e..696aa38b0e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java @@ -100,50 +100,61 @@ public class ImportServiceCEImpl implements ImportServiceCE { }; } - /** - * This method takes a file part and makes a Json entity which implements the ArtifactExchangeJson interface - * - * @param filePart : filePart from which the contents would be made - * @return : Json entity which implements ArtifactExchangeJson - */ - public Mono extractArtifactExchangeJson(Part filePart) { - - final MediaType contentType = filePart.headers().getContentType(); + @Override + public Mono readFilePartToString(Part file) { + final MediaType contentType = file.headers().getContentType(); if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { log.error("Invalid content type, {}", contentType); return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, INVALID_JSON_FILE)); } - return DataBufferUtils.join(filePart.content()) - .map(dataBuffer -> { - byte[] data = new byte[dataBuffer.readableByteCount()]; - dataBuffer.read(data); - DataBufferUtils.release(dataBuffer); - return new String(data); - }) - .map(jsonString -> { - gsonBuilder.registerTypeAdapter(ArtifactExchangeJson.class, artifactExchangeJsonAdapter); - Gson gson = gsonBuilder.create(); - return gson.fromJson(jsonString, ArtifactExchangeJson.class); - }); + return DataBufferUtils.join(file.content()).map(dataBuffer -> { + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + return new String(data); + }); + } + + /** + * This method takes JSON content and makes a JSON entity which implements the ArtifactExchangeJson interface. + * + * @param jsonString JSON string to parse + * @return JSON entity which implements ArtifactExchangeJson + */ + @Override + public Mono extractArtifactExchangeJson(String jsonString) { + return Mono.fromCallable(() -> { + gsonBuilder.registerTypeAdapter(ArtifactExchangeJson.class, artifactExchangeJsonAdapter); + Gson gson = gsonBuilder.create(); + return gson.fromJson(jsonString, ArtifactExchangeJson.class); + }); + } + + @Override + public Mono extractArtifactExchangeJsonAndSaveArtifact( + Part filePart, String workspaceId, String artifactId) { + return readFilePartToString(filePart) + .flatMap(jsonContents -> + extractArtifactExchangeJsonAndSaveArtifact(jsonContents, workspaceId, artifactId)); } /** * Hydrates an Artifact within the specified workspace by saving the provided JSON file. * - * @param filePart The filePart representing the Artifact object to be saved. + * @param jsonContents The jsonContents representing the Artifact object to be saved. * The Artifact implements the Artifact interface. * @param workspaceId The identifier for the destination workspace. */ @Override public Mono extractArtifactExchangeJsonAndSaveArtifact( - Part filePart, String workspaceId, String artifactId) { + String jsonContents, String workspaceId, String artifactId) { if (StringUtils.isEmpty(workspaceId)) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.WORKSPACE_ID)); } - Mono importedContextMono = extractArtifactExchangeJson(filePart) + Mono importedContextMono = extractArtifactExchangeJson(jsonContents) .zipWhen(contextJson -> { if (StringUtils.isEmpty(artifactId)) { return importNewArtifactInWorkspaceFromJson(workspaceId, contextJson); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCE.java index a207e9f472..547059d466 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCE.java @@ -10,5 +10,8 @@ public interface PartialImportServiceCE { Mono importResourceInPage(String workspaceId, String applicationId, String pageId, Part file); + Mono importResourceInPage( + String workspaceId, String applicationId, String pageId, String fileContents); + Mono importBuildingBlock(BuildingBlockDTO buildingBlockDTO); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java index 29c9a04ec7..181068f83a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java @@ -106,9 +106,17 @@ public class PartialImportServiceCEImpl implements PartialImportServiceCE { @Override public Mono importResourceInPage(String workspaceId, String applicationId, String pageId, Part file) { + return importService + .readFilePartToString(file) + .flatMap(fileContents -> importResourceInPage(workspaceId, applicationId, pageId, fileContents)); + } + + @Override + public Mono importResourceInPage( + String workspaceId, String applicationId, String pageId, String fileContents) { Mono currUserMono = sessionUserService.getCurrentUser(); return importService - .extractArtifactExchangeJson(file) + .extractArtifactExchangeJson(fileContents) .flatMap(artifactExchangeJson -> { if (artifactExchangeJson instanceof ApplicationJson && isImportableResource((ApplicationJson) artifactExchangeJson)) { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java index 09c30c0d6d..8f169f5986 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java @@ -21,6 +21,7 @@ import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.http.codec.multipart.Part; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.BodyInserters; @@ -29,6 +30,7 @@ import reactor.core.publisher.Mono; import java.io.IOException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; @SpringBootTest @AutoConfigureWebTestClient @@ -69,7 +71,7 @@ public class ApplicationControllerTest { @WithMockUser public void whenFileUploadedWithLongHeader_thenVerifyErrorStatus() throws IOException { - Mockito.when(importService.extractArtifactExchangeJsonAndSaveArtifact(any(), any(), any())) + Mockito.when(importService.extractArtifactExchangeJsonAndSaveArtifact(anyString(), any(), any())) .thenAnswer(importableArtifactDTOAnswer(new ApplicationImportDTO())); final String fileName = getFileName(130 * 1024); @@ -100,7 +102,7 @@ public class ApplicationControllerTest { @WithMockUser public void whenFileUploadedWithShortHeader_thenVerifySuccessStatus() throws IOException { - Mockito.when(importService.extractArtifactExchangeJsonAndSaveArtifact(any(), any(), any())) + Mockito.when(importService.extractArtifactExchangeJsonAndSaveArtifact(any(Part.class), any(), any())) .thenAnswer(importableArtifactDTOAnswer(new ApplicationImportDTO())); final String fileName = getFileName(2 * 1024); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ServerSchemaMigrationEnforcerTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ServerSchemaMigrationEnforcerTest.java index 8dffa58481..490eaea377 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ServerSchemaMigrationEnforcerTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/git/ServerSchemaMigrationEnforcerTest.java @@ -27,6 +27,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -41,20 +42,16 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.MediaType; -import org.springframework.http.codec.multipart.FilePart; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; -import reactor.core.publisher.Flux; +import org.springframework.util.StreamUtils; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.io.IOException; import java.net.URISyntaxException; -import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -228,13 +225,13 @@ public class ServerSchemaMigrationEnforcerTest { @WithUserDetails(value = "api_user") public void importApplication_ThenExportApplication_MatchJson_equals_Success() throws URISyntaxException { String filePath = "ce-automation-test.json"; - FilePart filePart = createFilePart(filePath); + String jsonContents = readResource(filePath); Workspace newWorkspace = new Workspace(); newWorkspace.setName("Template Workspace"); Mono workspaceMono = workspaceService.create(newWorkspace).cache(); ApplicationJson applicationJsonToBeImported = importService - .extractArtifactExchangeJson(filePart) + .extractArtifactExchangeJson(jsonContents) .map(artifactExchangeJson -> (ApplicationJson) artifactExchangeJson) .block(); @@ -249,7 +246,7 @@ public class ServerSchemaMigrationEnforcerTest { final Mono resultMono = workspaceMono .flatMap(workspace -> - importService.extractArtifactExchangeJsonAndSaveArtifact(filePart, workspace.getId(), null)) + importService.extractArtifactExchangeJsonAndSaveArtifact(jsonContents, workspace.getId(), null)) .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); final Mono exportApplicationMono = resultMono.flatMap(applicationImportDTO -> { @@ -280,17 +277,10 @@ public class ServerSchemaMigrationEnforcerTest { .verifyComplete(); } - private FilePart createFilePart(String filePath) throws URISyntaxException { - FilePart filePart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); - URL resource = this.getClass().getResource(filePath); - Flux dataBufferFlux = DataBufferUtils.read( - Path.of(resource.toURI()), new DefaultDataBufferFactory(), 4096) - .cache(); - - Mockito.when(filePart.content()).thenReturn(dataBufferFlux); - Mockito.when(filePart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON); - - return filePart; + @SneakyThrows + private String readResource(String filePath) { + return StreamUtils.copyToString( + new DefaultResourceLoader().getResource(filePath).getInputStream(), StandardCharsets.UTF_8); } private void removeCustomJsLibsEntries(JsonObject applicationObjectNode) { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java index 1ac66965af..317f121db5 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java @@ -78,6 +78,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; @@ -93,6 +94,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -102,6 +104,7 @@ import org.springframework.http.codec.multipart.FilePart; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.StreamUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -110,6 +113,7 @@ import reactor.util.function.Tuple4; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; @@ -859,10 +863,8 @@ public class ImportServiceTests { @Test @WithUserDetails(value = "api_user") public void importArtifactWithNullWorkspaceIdTest() { - FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); - Mono resultMono = - importService.extractArtifactExchangeJsonAndSaveArtifact(filepart, null, null); + importService.extractArtifactExchangeJsonAndSaveArtifact("", null, null); StepVerifier.create(resultMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException @@ -4943,9 +4945,10 @@ public class ImportServiceTests { return applicationRepository.save(application); }) .flatMap(application -> { - FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + final String validAppJsonContents = + readResource("test_assets/ImportExportServiceTest/valid-application.json"); return importService - .extractArtifactExchangeJson(filePart) + .extractArtifactExchangeJson(validAppJsonContents) .map(artifactExchangeJson -> (ApplicationJson) artifactExchangeJson) .flatMap(applicationJson -> importService.mergeArtifactExchangeJsonWithImportableArtifact( workspaceId, application.getId(), null, applicationJson, null)) @@ -4957,6 +4960,12 @@ public class ImportServiceTests { .verify(); } + @SneakyThrows + private String readResource(String filePath) { + return StreamUtils.copyToString( + new DefaultResourceLoader().getResource(filePath).getInputStream(), StandardCharsets.UTF_8); + } + @Test @WithUserDetails(value = "api_user") public void extractFileAndUpdateNonGitConnectedApplication_WhenNoPermissionToCreatePage_Fails() { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java index 2f8d1eda2a..646d60b177 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java @@ -37,6 +37,7 @@ import com.appsmith.server.services.PermissionGroupService; import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.WorkspaceService; import com.google.gson.Gson; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,19 +46,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.MediaType; -import org.springframework.http.codec.multipart.FilePart; -import org.springframework.http.codec.multipart.Part; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.security.test.context.support.WithUserDetails; -import reactor.core.publisher.Flux; +import org.springframework.util.StreamUtils; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.function.Tuple3; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.HashMap; import java.util.List; @@ -252,16 +248,10 @@ public class PartialImportServiceTest { .block(); } - private FilePart createFilePart(String filePath) { - FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); - Flux dataBufferFlux = DataBufferUtils.read( - new ClassPathResource(filePath), new DefaultDataBufferFactory(), 4096) - .cache(); - - Mockito.when(filepart.content()).thenReturn(dataBufferFlux); - Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON); - - return filepart; + @SneakyThrows + private String readResource(String filePath) { + return StreamUtils.copyToString( + new DefaultResourceLoader().getResource(filePath).getInputStream(), StandardCharsets.UTF_8); } @Test @@ -282,10 +272,10 @@ public class PartialImportServiceTest { .block() .getId(); - Part filePart = createFilePart("test_assets/ImportExportServiceTest/partial-export-resource.json"); + String jsonContents = readResource("test_assets/ImportExportServiceTest/partial-export-resource.json"); Mono, List>> result = partialImportService - .importResourceInPage(workspaceId, testApplication.getId(), pageId, filePart) + .importResourceInPage(workspaceId, testApplication.getId(), pageId, jsonContents) .flatMap(application -> { Mono> actionList = newActionService .findByPageId(pageId, Optional.empty()) @@ -334,11 +324,12 @@ public class PartialImportServiceTest { savedPage.setRefName("master"); savedPage = applicationPageService.createPage(savedPage).block(); - Part filePart = createFilePart("test_assets/ImportExportServiceTest/partial-export-valid-without-widget.json"); + String jsonContents = + readResource("test_assets/ImportExportServiceTest/partial-export-valid-without-widget.json"); PageDTO finalSavedPage = savedPage; Mono, List>> result = partialImportService - .importResourceInPage(workspaceId, application.getId(), savedPage.getId(), filePart) + .importResourceInPage(workspaceId, application.getId(), savedPage.getId(), jsonContents) .flatMap(application1 -> { Mono> actionList = newActionService .findByPageId(finalSavedPage.getId(), Optional.empty()) @@ -395,11 +386,12 @@ public class PartialImportServiceTest { .block() .getId(); - Part filePart = createFilePart("test_assets/ImportExportServiceTest/partial-export-resource.json"); + String jsonContents = readResource("test_assets/ImportExportServiceTest/partial-export-resource.json"); Mono, List>> result = partialImportService - .importResourceInPage(workspaceId, testApplication.getId(), pageId, filePart) - .then(partialImportService.importResourceInPage(workspaceId, testApplication.getId(), pageId, filePart)) + .importResourceInPage(workspaceId, testApplication.getId(), pageId, jsonContents) + .then(partialImportService.importResourceInPage( + workspaceId, testApplication.getId(), pageId, jsonContents)) .flatMap(application -> { Mono> actionList = newActionService .findByPageId(pageId, Optional.empty()) @@ -449,15 +441,15 @@ public class PartialImportServiceTest { @WithUserDetails(value = "api_user") public void testPartialImportWithBuildingBlock_nameClash_success() { - Part filePart = createFilePart("test_assets/ImportExportServiceTest/building-block.json"); + String jsonContents = readResource("test_assets/ImportExportServiceTest/building-block.json"); ApplicationJson applicationJson = (ApplicationJson) - importService.extractArtifactExchangeJson(filePart).block(); + importService.extractArtifactExchangeJson(jsonContents).block(); // Mock the call to fetch the json file from CS Mockito.when(applicationTemplateService.getApplicationJsonFromTemplate("templatedId")) .thenReturn(Mono.just(applicationJson)); ApplicationJson applicationJson1 = (ApplicationJson) - importService.extractArtifactExchangeJson(filePart).block(); + importService.extractArtifactExchangeJson(jsonContents).block(); Mockito.when(applicationTemplateService.getApplicationJsonFromTemplate("templatedId1")) .thenReturn(Mono.just(applicationJson1));