Merge branch 'feature/mustache-key-extraction' into 'master'

Extract mustache keys from action object

When an action is created, we extract the mustache keys from various parts of the action and save it in the field `jsonPathKeys`. This same thing also happens in the update flow.

Since mustache library doesn't have an extraction function, we use a regex template to extract those keys.

We also modify the data type of the `body` parameter in the `ActionConfiguration` to String. This is because there are reserved characters that are not allowed as Mongo keys but are valid JSON keys. Converting to a string will remove all these problems.

Fixes #31 

See merge request theappsmith/internal-tools-server!58
This commit is contained in:
Arpit Mohan 2019-11-05 10:40:09 +00:00
commit 29c7b7cc01
4 changed files with 105 additions and 10 deletions

View File

@ -31,7 +31,7 @@ public class ActionConfiguration {
String path;
List<Property> headers;
List<Property> queryParameters;
Map<String, Object> body;
String body;
HttpMethod httpMethod;
// DB action fields

View File

@ -39,10 +39,7 @@ public class RestApiPlugin extends BasePlugin {
public Mono<ActionExecutionResult> execute(Object connection,
ResourceConfiguration resourceConfiguration,
ActionConfiguration actionConfiguration) {
Map<String, Object> requestBody = actionConfiguration.getBody();
if (requestBody == null) {
requestBody = (Map<String, Object>) new HashMap<String, Object>();
}
String requestBody = actionConfiguration.getBody();
String path = (actionConfiguration.getPath() == null) ? "" : actionConfiguration.getPath();
String url = resourceConfiguration.getUrl() + path;

View File

@ -1,13 +1,14 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.ActionConfiguration;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
import java.util.Set;
@Getter
@Setter
@ -30,5 +31,6 @@ public class Action extends BaseDomain {
// This is a list of keys that the client whose values the client needs to send during action execution.
// These are the Mustache keys that the server will replace before invoking the API
List<String> jsonPathKeys;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
Set<String> jsonPathKeys;
}

View File

@ -6,6 +6,7 @@ import com.appsmith.external.models.Param;
import com.appsmith.external.models.ResourceConfiguration;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.PageAction;
@ -15,6 +16,7 @@ import com.appsmith.server.dtos.ExecuteActionDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.ActionRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
@ -34,11 +36,17 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collection;
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;
import java.util.stream.Collectors;
import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject;
@Slf4j
@Service
public class ActionServiceImpl extends BaseService<ActionRepository, Action, String> implements ActionService {
@ -51,6 +59,9 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
private final ObjectMapper objectMapper;
private final ResourceContextService resourceContextService;
// This regex matches mustache template keys of the form {{somekey}}
private final Pattern pattern = Pattern.compile("\\{\\{\\s*([^{}]+)\\s*}}");
@Autowired
public ActionServiceImpl(Scheduler scheduler,
Validator validator,
@ -74,6 +85,40 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
this.resourceContextService = resourceContextService;
}
/**
* This function updates an existing action in the DB. We are completely overriding the base update function to
* ensure that we can populate the JsonPathKeys field in the ActionConfiguration based on any changes that may
* have happened in the action object.
* <p>
* Calling the base function would make redundant DB calls and slow down this API unnecessarily.
*
* @param id
* @param action
* @return
*/
@Override
public Mono<Action> update(String id, Action action) {
if (id == null) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID));
}
return repository.findById(id)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "action", id)))
.map(dbAction -> {
copyNewFieldValuesIntoOldObject(action, dbAction);
return dbAction;
})
.map(act -> extractAndSetJsonPathKeys(act))
.flatMap(repository::save)
.map(act -> {
analyticsService
.sendEvent(AnalyticsEvents.UPDATE + "_" + act.getClass().getSimpleName().toUpperCase(),
act);
return act;
}
);
}
@Override
public Mono<Action> create(@NotNull Action action) {
if (action.getId() != null) {
@ -92,11 +137,62 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
.map(plugin -> {
action.setPluginId(plugin.getId());
return action;
})
}).map(act -> extractAndSetJsonPathKeys(act))
.flatMap(super::create)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.REPOSITORY_SAVE_FAILED)));
}
private Set<String> extractMustacheKeys(String template) {
if (template == null || template.isEmpty()) {
return new HashSet<>();
}
Matcher matcher = pattern.matcher(template);
if (matcher.groupCount() > 0) {
Set<String> collect = matcher.results()
.map(result -> result.group(1))
.collect(Collectors.toSet());
return collect;
}
return new HashSet<>();
}
/**
* This function extracts all the mustache template keys (as per the regex) and returns them to the calling fxn
* This set of keys is stored separately in the field `jsonPathKeys` in the action object. The client
* uses the set `jsonPathKeys` to simplify it's value substitution.
*
* @param action
* @return
*/
private Set<String> extractKeysFromAction(Action action) {
if(action.getActionConfiguration() == null) {
return new HashSet<>();
}
// Convert the object to String as a preparation to send it to mustache extraction
try {
String actionConfigStr = objectMapper.writeValueAsString(action.getActionConfiguration());
return extractMustacheKeys(actionConfigStr);
} catch (JsonProcessingException e) {
log.error("Exception caught while extracting mustache keys from action configuration. ", e);
}
return new HashSet<>();
}
/**
* This function extracts the mustache keys and sets them in the field jsonPathKeys in the action object
*
* @param action
* @return
*/
private Action extractAndSetJsonPathKeys(Action action) {
Set<String> keys = extractKeysFromAction(action);
action.setJsonPathKeys(keys);
return action;
}
public Mono<Page> bindPageToAction(Action action, String pageId) {
Mono<Page> pageMono = pageService.findById(pageId);
action.setPageId(pageId);
@ -208,7 +304,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
objectInJsonString = mustacheReplacement(objectInJsonString, configuration.getClass().getSimpleName(), replaceParamsMap);
return objectMapper.readValue(objectInJsonString, configuration.getClass());
} catch (Exception e) {
e.printStackTrace();
log.error("Exception caught while substituting values in mustache template.", e);
}
return configuration;
}