chore: Support String inputs for import service methods (#39337)

This commit is contained in:
Shrikant Sharat Kandula 2025-02-21 06:52:48 +05:30 committed by GitHub
parent f18064e030
commit 1db0ad25fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 110 additions and 88 deletions

View File

@ -31,13 +31,9 @@ public interface ImportServiceCE {
ArtifactBasedImportService<? extends Artifact, ? extends ArtifactImportDTO, ? extends ArtifactExchangeJson>
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<? extends ArtifactExchangeJson> extractArtifactExchangeJson(Part filePart);
Mono<String> readFilePartToString(Part file);
Mono<? extends ArtifactExchangeJson> extractArtifactExchangeJson(String jsonString);
/**
* Hydrates an Artifact within the specified workspace by saving the provided JSON file.
@ -50,6 +46,17 @@ public interface ImportServiceCE {
Mono<? extends ArtifactImportDTO> 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<? extends ArtifactImportDTO> extractArtifactExchangeJsonAndSaveArtifact(
String jsonContents, String workspaceId, String artifactId);
/**
* Saves the provided ArtifactExchangeJson within the specified workspace.
*

View File

@ -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<? extends ArtifactExchangeJson> extractArtifactExchangeJson(Part filePart) {
final MediaType contentType = filePart.headers().getContentType();
@Override
public Mono<String> 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<? extends ArtifactExchangeJson> extractArtifactExchangeJson(String jsonString) {
return Mono.fromCallable(() -> {
gsonBuilder.registerTypeAdapter(ArtifactExchangeJson.class, artifactExchangeJsonAdapter);
Gson gson = gsonBuilder.create();
return gson.fromJson(jsonString, ArtifactExchangeJson.class);
});
}
@Override
public Mono<? extends ArtifactImportDTO> 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<? extends ArtifactImportDTO> 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<ArtifactImportDTO> importedContextMono = extractArtifactExchangeJson(filePart)
Mono<ArtifactImportDTO> importedContextMono = extractArtifactExchangeJson(jsonContents)
.zipWhen(contextJson -> {
if (StringUtils.isEmpty(artifactId)) {
return importNewArtifactInWorkspaceFromJson(workspaceId, contextJson);

View File

@ -10,5 +10,8 @@ public interface PartialImportServiceCE {
Mono<Application> importResourceInPage(String workspaceId, String applicationId, String pageId, Part file);
Mono<Application> importResourceInPage(
String workspaceId, String applicationId, String pageId, String fileContents);
Mono<BuildingBlockResponseDTO> importBuildingBlock(BuildingBlockDTO buildingBlockDTO);
}

View File

@ -106,9 +106,17 @@ public class PartialImportServiceCEImpl implements PartialImportServiceCE {
@Override
public Mono<Application> importResourceInPage(String workspaceId, String applicationId, String pageId, Part file) {
return importService
.readFilePartToString(file)
.flatMap(fileContents -> importResourceInPage(workspaceId, applicationId, pageId, fileContents));
}
@Override
public Mono<Application> importResourceInPage(
String workspaceId, String applicationId, String pageId, String fileContents) {
Mono<User> currUserMono = sessionUserService.getCurrentUser();
return importService
.extractArtifactExchangeJson(file)
.extractArtifactExchangeJson(fileContents)
.flatMap(artifactExchangeJson -> {
if (artifactExchangeJson instanceof ApplicationJson
&& isImportableResource((ApplicationJson) artifactExchangeJson)) {

View File

@ -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);

View File

@ -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<Workspace> 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<ApplicationImportDTO> resultMono = workspaceMono
.flatMap(workspace ->
importService.extractArtifactExchangeJsonAndSaveArtifact(filePart, workspace.getId(), null))
importService.extractArtifactExchangeJsonAndSaveArtifact(jsonContents, workspace.getId(), null))
.map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO);
final Mono<ApplicationJson> 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<DataBuffer> 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) {

View File

@ -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<? extends ArtifactImportDTO> 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() {

View File

@ -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<DataBuffer> 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<Tuple3<Application, List<NewAction>, List<ActionCollection>>> result = partialImportService
.importResourceInPage(workspaceId, testApplication.getId(), pageId, filePart)
.importResourceInPage(workspaceId, testApplication.getId(), pageId, jsonContents)
.flatMap(application -> {
Mono<List<NewAction>> 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<Tuple3<Application, List<NewAction>, List<ActionCollection>>> result = partialImportService
.importResourceInPage(workspaceId, application.getId(), savedPage.getId(), filePart)
.importResourceInPage(workspaceId, application.getId(), savedPage.getId(), jsonContents)
.flatMap(application1 -> {
Mono<List<NewAction>> 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<Tuple3<Application, List<NewAction>, List<ActionCollection>>> 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<List<NewAction>> 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));