Merge branch 'feature/refactor' into 'release'

APIs added for refactoring widget names and action names

See merge request theappsmith/internal-tools-server!161
This commit is contained in:
Trisha Anand 2020-01-20 12:26:13 +00:00
commit c8ee68b7aa
13 changed files with 287 additions and 43 deletions

View File

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

View File

@ -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<ActionService, Action, Stri
return layoutActionService.moveAction(actionMoveDTO)
.map(action -> new ResponseDTO<>(HttpStatus.OK.value(), action, null));
}
@PutMapping("/refactor")
public Mono<ResponseDTO<Layout>> refactorActionName(@RequestBody RefactorNameDTO refactorNameDTO) {
return layoutActionService.refactorActionName(refactorNameDTO)
.map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null));
}
}

View File

@ -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<ResponseDTO<Layout>> refactorWidgetName(@RequestBody RefactorNameDTO refactorNameDTO) {
return layoutActionService.refactorWidgetName(refactorNameDTO)
.map(created -> new ResponseDTO<>(HttpStatus.OK.value(), created, null));
}
}

View File

@ -38,6 +38,9 @@ public class Layout extends BaseDomain {
@JsonIgnore
Set<DslActionDTO> publishedLayoutOnLoadActions;
@JsonIgnore
Set<String> 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

View File

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

View File

@ -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"),

View File

@ -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<String> extractMustacheKeys(String template) {
if (template == null || template.isEmpty()) {

View File

@ -13,7 +13,7 @@ public interface ActionRepository extends BaseRepository<Action, String> {
Mono<Action> findById(String id);
Mono<Action> findByName(String name);
Mono<Action> findByNameAndPageId(String name, String pageId);
Flux<Action> findDistinctActionsByNameInAndPageId(Set<String> names, String pageId);

View File

@ -15,11 +15,13 @@ public interface ActionService extends CrudService<Action, String> {
Mono<Action> save(Action action);
Mono<Action> findByName(String name);
Mono<Action> findByNameAndPageId(String name, String pageId);
Flux<Action> findDistinctActionsByNameInAndPageId(Set<String> names, String pageId);
Flux<Action> findDistinctRestApiActionsByNameInAndPageIdAndHttpMethod(Set<String> names, String pageId, String httpMethod);
Flux<Action> saveAll(List<Action> actions);
public Action extractAndSetJsonPathKeys(Action action);
}

View File

@ -262,7 +262,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
* @param action
* @return
*/
private Action extractAndSetJsonPathKeys(Action action) {
public Action extractAndSetJsonPathKeys(Action action) {
Set<String> actionKeys = extractKeysFromAction(action);
Set<String> datasourceKeys = datasourceService.extractKeysFromDatasource(action.getDatasource());
Set<String> keys = new HashSet<String>() {{
@ -418,8 +418,8 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
}
@Override
public Mono<Action> findByName(String name) {
return repository.findByName(name);
public Mono<Action> findByNameAndPageId(String name, String pageId) {
return repository.findByNameAndPageId(name, pageId);
}
@Override
@ -436,7 +436,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
public Flux<Action> saveAll(List<Action> actions) {
return repository.saveAll(actions);
}
/**
* This function replaces the variables in the Object with the actual params
*/

View File

@ -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<Layout> updateLayout(String pageId, String layoutId, Layout layout);
public Mono<Action> moveAction(ActionMoveDTO actionMoveDTO);
public Mono<Layout> refactorWidgetName(RefactorNameDTO refactorNameDTO);
public Mono<Layout> refactorActionName(RefactorNameDTO refactorNameDTO);
}

View File

@ -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<String> widgetNames = new HashSet<>();
extractAllWidgetNamesFromDSL(dsl, widgetNames);
layout.setWidgetNames(widgetNames);
Mono<Set<String>> 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<Layout> 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<Layout> 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<Layout> refactorName(String pageId, String layoutId, String oldName, String newName) {
String regexPattern = preWord + oldName + postWord;
Pattern oldNamePattern = Pattern.compile(regexPattern);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
if (pageId != null) {
params.add(FieldName.PAGE_ID, pageId);
}
Flux<Action> actionsInPageFlux = actionService.get(params);
Mono<Page> updatePageMono = pageService
.findById(pageId)
.flatMap(page -> {
List<Layout> 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<Set<Object>> 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<String> 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<Layout> 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<String> 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<Object> children = (ArrayList<Object>) 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<Boolean> isNameAllowed(String pageId, String layoutId, String newName) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
if (pageId != null) {
params.add(FieldName.PAGE_ID, pageId);
}
Mono<Set<String>> 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<Set<String>> widgetNamesMono = pageService
.findById(pageId)
.flatMap(page -> {
List<Layout> 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<String> widgetNames = tuple.getT2();
if (widgetNames.contains(newName)) {
return false;
}
return true;
});
}
}

View File

@ -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<String, DefaultEdge> 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<Object> children = (ArrayList<Object>) 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