diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomServerOAuth2AuthorizationRequestResolver.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomServerOAuth2AuthorizationRequestResolver.java index 0e3f92b0e4..6f8046c230 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomServerOAuth2AuthorizationRequestResolver.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomServerOAuth2AuthorizationRequestResolver.java @@ -2,8 +2,6 @@ package com.appsmith.server.authentication.handlers; import com.appsmith.server.configurations.CommonConfig; import com.appsmith.server.constants.Security; -import com.appsmith.server.exceptions.AppsmithError; -import com.appsmith.server.exceptions.AppsmithException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -20,7 +18,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.util.Assert; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java index 73d72a8297..499cba64ad 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java @@ -3,8 +3,10 @@ package com.appsmith.server.controllers; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.server.constants.Url; import com.appsmith.server.domains.Action; +import com.appsmith.server.domains.Layout; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.ExecuteActionDTO; +import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.services.ActionCollectionService; @@ -68,4 +70,10 @@ public class ActionController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), action, null)); } + + @PutMapping("/refactor") + public Mono> refactorActionName(@RequestBody RefactorNameDTO refactorNameDTO) { + return layoutActionService.refactorActionName(refactorNameDTO) + .map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/LayoutController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/LayoutController.java index 61fbed4972..18996edb58 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/LayoutController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/LayoutController.java @@ -2,6 +2,7 @@ package com.appsmith.server.controllers; import com.appsmith.server.constants.Url; import com.appsmith.server.domains.Layout; +import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.LayoutService; @@ -56,4 +57,10 @@ public class LayoutController { .map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null)); } + @PutMapping("/refactor") + public Mono> refactorWidgetName(@RequestBody RefactorNameDTO refactorNameDTO) { + return layoutActionService.refactorWidgetName(refactorNameDTO) + .map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null)); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Layout.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Layout.java index 25bc78101a..2a7268a403 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Layout.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Layout.java @@ -38,6 +38,9 @@ public class Layout extends BaseDomain { @JsonIgnore Set publishedLayoutOnLoadActions; + @JsonIgnore + Set widgetNames; + /** * If view mode, the dsl returned should be the publishedDSL, else if the edit mode is on (view mode = false) * the dsl returned should be JSONObject dsl diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/RefactorNameDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/RefactorNameDTO.java new file mode 100644 index 0000000000..47b080c8c5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/RefactorNameDTO.java @@ -0,0 +1,13 @@ +package com.appsmith.server.dtos; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RefactorNameDTO { + String pageId; + String layoutId; + String oldName; + String newName; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index f7c7ba86c6..d2a24f3fbe 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -22,6 +22,7 @@ public enum AppsmithError { INVALID_DATASOURCE(400, 4013, "Datasource is invalid. Please edit to make it valid"), INVALID_DATASOURCE_CONFIGURATION(400, 4015, "Datasource configuration is invalid"), NO_CONFIGURATION_FOUND_IN_ACTION(400, 4016, "Action without any configuration is invalid. Please try again with actionConfiguration"), + NAME_CLASH_NOT_ALLOWED_IN_REFACTOR(400, 4017, "Unable to change the name {0} to {1} because {1} already exists in the current page"), UNAUTHORIZED_DOMAIN(401, 4012, "Invalid email domain provided. Please sign in with a valid work email ID"), UNAUTHORIZED_ACCESS(401, 4013, "Unauthorized access"), INVALID_ACTION_NAME(401, 4014, "Action name is invalid. Please input syntactically correct name"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java index 92d1163fd3..ae6cf9debf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/MustacheHelper.java @@ -9,7 +9,7 @@ import java.util.stream.Collectors; public class MustacheHelper { // This regex matches mustache template keys of the form {{somekey}} - private static Pattern pattern = Pattern.compile("\\{\\{\\s*([^{}]+)\\s*}}"); + private static Pattern pattern = Pattern.compile("\\{\\{([\\s\\S]*?)}}"); public static Set extractMustacheKeys(String template) { if (template == null || template.isEmpty()) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java index 6993a621bc..7723421e4d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ActionRepository.java @@ -13,7 +13,7 @@ public interface ActionRepository extends BaseRepository { Mono findById(String id); - Mono findByName(String name); + Mono findByNameAndPageId(String name, String pageId); Flux findDistinctActionsByNameInAndPageId(Set names, String pageId); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java index ef128b64fa..4fd9c6915e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java @@ -15,11 +15,13 @@ public interface ActionService extends CrudService { Mono save(Action action); - Mono findByName(String name); + Mono findByNameAndPageId(String name, String pageId); Flux findDistinctActionsByNameInAndPageId(Set names, String pageId); Flux findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set names, String pageId, String httpMethod); Flux saveAll(List actions); + + public Action extractAndSetJsonPathKeys(Action action); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java index 14a59bb1fa..7e35bedabc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java @@ -262,7 +262,7 @@ public class ActionServiceImpl extends BaseService actionKeys = extractKeysFromAction(action); Set datasourceKeys = datasourceService.extractKeysFromDatasource(action.getDatasource()); Set keys = new HashSet() {{ @@ -418,8 +418,8 @@ public class ActionServiceImpl extends BaseService findByName(String name) { - return repository.findByName(name); + public Mono findByNameAndPageId(String name, String pageId) { + return repository.findByNameAndPageId(name, pageId); } @Override @@ -436,7 +436,7 @@ public class ActionServiceImpl extends BaseService saveAll(List actions) { return repository.saveAll(actions); } - + /** * This function replaces the variables in the Object with the actual params */ diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java index 73223bdc73..6b0cb56acb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java @@ -3,10 +3,15 @@ package com.appsmith.server.services; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Layout; import com.appsmith.server.dtos.ActionMoveDTO; +import com.appsmith.server.dtos.RefactorNameDTO; import reactor.core.publisher.Mono; public interface LayoutActionService { public Mono updateLayout(String pageId, String layoutId, Layout layout); public Mono moveAction(ActionMoveDTO actionMoveDTO); + + public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO); + + public Mono refactorActionName(RefactorNameDTO refactorNameDTO); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java index 45d9c24387..c5644fd233 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java @@ -1,24 +1,32 @@ package com.appsmith.server.services; +import com.appsmith.external.models.ActionConfiguration; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.Page; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import net.minidev.json.parser.ParseException; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -41,6 +49,13 @@ public class LayoutActionServiceImpl implements LayoutActionService { */ private final Pattern pattern = Pattern.compile("[a-zA-Z0-9._]+"); + /* + * To replace fetchUsers in `{{JSON.stringify(fetchUsers)}}` with getUsers, the following regex is required : + * `\\b(fetchUsers)\\b`. To achieve this the following strings preWord and postWord are declared here to be used + * at run time to create the regex pattern. + */ + private final String preWord = "\\b("; + private final String postWord = ")\\b"; public LayoutActionServiceImpl(ActionService actionService, PageService pageService, @@ -64,6 +79,10 @@ public class LayoutActionServiceImpl implements LayoutActionService { log.debug("Exception caught during conversion of DSL Json object to String. ", e); } + Set widgetNames = new HashSet<>(); + extractAllWidgetNamesFromDSL(dsl, widgetNames); + layout.setWidgetNames(widgetNames); + Mono> dynamicBindingNamesMono = Mono.just(dslString) // Extract all the mustache keys in the DSL to get the dynamic bindings used in the DSL. .map(dslString1 -> extractMustacheKeys(dslString1)) @@ -196,4 +215,226 @@ public class LayoutActionServiceImpl implements LayoutActionService { .collect(toSet())) .thenReturn(savedAction)); } + + @Override + public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO) { + String pageId = refactorNameDTO.getPageId(); + String layoutId = refactorNameDTO.getLayoutId(); + String oldName = refactorNameDTO.getOldName(); + String newName = refactorNameDTO.getNewName(); + return isNameAllowed(pageId, layoutId, newName) + .flatMap(allowed -> { + if (!allowed) { + return Mono.error(new AppsmithException(AppsmithError.NAME_CLASH_NOT_ALLOWED_IN_REFACTOR, oldName, newName)); + } + return refactorName(pageId, layoutId, oldName, newName); + }); + } + + @Override + public Mono refactorActionName(RefactorNameDTO refactorNameDTO) { + String pageId = refactorNameDTO.getPageId(); + String layoutId = refactorNameDTO.getLayoutId(); + String oldName = refactorNameDTO.getOldName(); + String newName = refactorNameDTO.getNewName(); + return isNameAllowed(pageId, layoutId, newName) + .flatMap(allowed -> { + if (!allowed) { + return Mono.error(new AppsmithException(AppsmithError.NAME_CLASH_NOT_ALLOWED_IN_REFACTOR, oldName, newName)); + } + return actionService + .findByNameAndPageId(oldName, pageId); + }) + .flatMap(action -> { + action.setName(newName); + return actionService.update(action.getId(), action); + }) + .then(refactorName(pageId, layoutId, oldName, newName)); + } + + /** + * Assumption here is that the refactoring name provided is indeed unique and is fit to be replaced everywhere. + * + * @param pageId + * @param layoutId + * @param oldName + * @param newName + * @return + */ + private Mono refactorName(String pageId, String layoutId, String oldName, String newName) { + String regexPattern = preWord + oldName + postWord; + Pattern oldNamePattern = Pattern.compile(regexPattern); + + MultiValueMap params = new LinkedMultiValueMap<>(); + if (pageId != null) { + params.add(FieldName.PAGE_ID, pageId); + } + Flux actionsInPageFlux = actionService.get(params); + + Mono updatePageMono = pageService + .findById(pageId) + .flatMap(page -> { + List layouts = page.getLayouts(); + for (Layout layout : layouts) { + if (layout.getId().equals(layoutId) && layout.getDsl() != null) { + String dslString = ""; + try { + dslString = objectMapper.writeValueAsString(layout.getDsl()); + } catch (JsonProcessingException e) { + log.debug("Exception caught during conversion of DSL Json object to String. ", e); + } + Matcher matcher = oldNamePattern.matcher(dslString); + String newDslString = matcher.replaceAll(newName); + try { + JSONParser parser = new JSONParser(JSONParser.MODE_PERMISSIVE); + JSONObject json = (JSONObject) parser.parse(newDslString); + layout.setDsl(json); + } catch (ParseException e) { + log.debug("Exception caught during DSL conversion from string to Json object. ", e); + } + page.setLayouts(layouts); + // Since the page has most probably changed, save the page and return. + return pageService.save(page); + } + } + // If we have reached here, the layout was not found and the page should be returned as is. + return Mono.just(page); + }); + + Mono> updateActionsMono = actionsInPageFlux + /* + * Assuming that the datasource should not be dependent on the widget and hence not going through the same + * to look for replacement pattern. + */ + .flatMap(action -> { + Boolean actionUpdateRequired = false; + ActionConfiguration actionConfiguration = action.getActionConfiguration(); + Set jsonPathKeys = action.getJsonPathKeys(); + // Since json path keys actually contain the entire inline js function instead of just the widget/action + // name, we can not simply use the set.contains(obj) function. We need to iterate over all the keys + // in the set and see if the old name is a substring of the json path key. + for (String key : jsonPathKeys) { + if (key.contains(oldName)) { + actionUpdateRequired = true; + } + } + + if (!actionUpdateRequired || actionConfiguration == null) { + return Mono.just(action); + } + // if actionupdateRequired is true AND actionConfiguration is not null + try { + String actionConfigurationAsString = objectMapper.writeValueAsString(actionConfiguration); + Matcher matcher = oldNamePattern.matcher(actionConfigurationAsString); + String newActionConfigurationAsString = matcher.replaceAll(newName); + ActionConfiguration newActionConfiguration = objectMapper.readValue(newActionConfigurationAsString, ActionConfiguration.class); + action.setActionConfiguration(newActionConfiguration); + action = actionService.extractAndSetJsonPathKeys(action); + return actionService.save(action); + } catch (JsonProcessingException e) { + log.debug("Exception caught during conversion between string and action configuration object ", e); + return Mono.just(action); + } + }) + .collect(toSet()); + + return updateActionsMono + .then(updatePageMono) + .flatMap(page -> { + List layouts = page.getLayouts(); + for (Layout layout : layouts) { + if (layout.getId().equals(layoutId)) { + return updateLayout(pageId, layout.getId(), layout); + } + } + return Mono.empty(); + }); + } + + /** + * Walks the DSL and extracts all the widget names from it. + * + * @param dsl + * @param widgetNames + */ + private void extractAllWidgetNamesFromDSL(JSONObject dsl, Set widgetNames) { + if (dsl.get(FieldName.WIDGET_NAME) == null) { + //This isnt a valid widget configuration. No need to traverse this. + return; + } + + String widgetName = dsl.getAsString(FieldName.WIDGET_NAME); + + //Since we are parsing this widget in this, add it. + widgetNames.add(widgetName); + + ArrayList children = (ArrayList) dsl.get(FieldName.CHILDREN); + if (children != null) { + for (int i = 0; i < children.size(); i++) { + Map data = (Map) children.get(i); + JSONObject object = new JSONObject(); + object.putAll(data); + extractAllWidgetNamesFromDSL(object, widgetNames); + } + } + } + + /** + * Compares the new name with the existing widget and action names for this page. If they match, then it returns + * false to signify that refactoring can not be allowed. Else, refactoring should be allowed and hence true is + * returned. + * + * @param pageId + * @param layoutId + * @param newName + * @return + */ + private Mono isNameAllowed(String pageId, String layoutId, String newName) { + MultiValueMap params = new LinkedMultiValueMap<>(); + if (pageId != null) { + params.add(FieldName.PAGE_ID, pageId); + } + + Mono> actionNamesInPageMono = actionService + .get(params) + .map(action -> action.getName()) + .collect(toSet()); + + /* + * TODO : Execute this check directly on the DB server. We can query array of arrays by: + * https://stackoverflow.com/questions/12629692/querying-an-array-of-arrays-in-mongodb + */ + Mono> widgetNamesMono = pageService + .findById(pageId) + .flatMap(page -> { + List layouts = page.getLayouts(); + for (Layout layout : layouts) { + if (layout.getId().equals(layoutId)) { + return Mono.just(layout.getWidgetNames()); + } + } + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.LAYOUT_ID, layoutId)); + }); + + return actionNamesInPageMono + .map(actionNames -> { + if (actionNames.contains(newName)) { + return false; + } + return true; + }) + .zipWith(widgetNamesMono) + .map(tuple -> { + Boolean allowed = tuple.getT1(); + if (allowed.equals(false)) { + return false; + } + + Set widgetNames = tuple.getT2(); + if (widgetNames.contains(newName)) { + return false; + } + return true; + }); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java index 4bb26d5dc3..41ead6804b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java @@ -9,7 +9,6 @@ import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; import org.bson.types.ObjectId; import org.jgrapht.Graph; -import org.jgrapht.graph.DefaultEdge; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -87,38 +86,6 @@ public class LayoutServiceImpl implements LayoutService { } - /** - * Walks the DSL and extracts all the widget names from it and adds it to the graph. - * - * @param dsl - * @param graph - */ - @Deprecated - private void extractAllWidgetNamesAndAddThemAsVerticesToTheGraph(JSONObject dsl, Graph graph) { - if (dsl.get(FieldName.WIDGET_NAME) == null) { - //This isnt a valid widget configuration. No need to traverse this. - return; - } - - String widgetName = dsl.getAsString(FieldName.WIDGET_NAME); - - //Since we are parsing this widget in this, add it to the graph as a vertex. - if (!graph.vertexSet().contains(widgetName)) { - graph.addVertex(widgetName); - } - - ArrayList children = (ArrayList) dsl.get(FieldName.CHILDREN); - if (children != null) { - for (int i = 0; i < children.size(); i++) { - Map data = (Map) children.get(i); - JSONObject object = new JSONObject(); - object.putAll(data); - extractAllWidgetNamesAndAddThemAsVerticesToTheGraph(object, graph); - } - } - - } - /** * Walks the DSL and adds relationship between widgets and actions. Widgets have at this point already been added * to the graph by function extractAllWidgetNamesAndAddThemAsVerticesToTheGraph. Actions are recognized by comparing