diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml index 3104da8338..087b756340 100644 --- a/app/server/appsmith-interfaces/pom.xml +++ b/app/server/appsmith-interfaces/pom.xml @@ -58,6 +58,7 @@ org.projectlombok lombok + 1.18.8 provided diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java index 2eaf58594d..c431ca7fc2 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java @@ -5,6 +5,7 @@ import com.appsmith.external.constants.DisplayDataType; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.ParsedDataType; +import com.appsmith.external.plugins.SmartSubstitutionInterface; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -188,13 +189,15 @@ public class DataTypeStringUtils { public static String jsonSmartReplacementPlaceholderWithValue(String input, String replacement, - List> insertedParams) { + List> insertedParams, + SmartSubstitutionInterface smartSubstitutionUtils) { DataType dataType = DataTypeStringUtils.stringToKnownDataTypeConverter(replacement); Map.Entry parameter = new SimpleEntry<>(replacement, dataType.toString()); insertedParams.add(parameter); + String updatedReplacement; switch (dataType) { case INTEGER: case LONG: @@ -202,12 +205,12 @@ public class DataTypeStringUtils { case DOUBLE: case NULL: case BOOLEAN: - input = placeholderPattern.matcher(input).replaceFirst(String.valueOf(replacement)); + updatedReplacement = String.valueOf(replacement); break; case ARRAY: try { JSONArray jsonArray = (JSONArray) parser.parse(replacement); - input = placeholderPattern.matcher(input).replaceFirst(String.valueOf(objectMapper.writeValueAsString(jsonArray))); + updatedReplacement = String.valueOf(objectMapper.writeValueAsString(jsonArray)); } catch (net.minidev.json.parser.ParseException | JsonProcessingException e) { throw Exceptions.propagate( new AppsmithPluginException( @@ -223,7 +226,7 @@ public class DataTypeStringUtils { JSONObject jsonObject = (JSONObject) parser.parse(replacement); String jsonString = String.valueOf(objectMapper.writeValueAsString(jsonObject)); // Adding Matcher.quoteReplacement so that "/" and "$" in the string are escaped during replacement - input = placeholderPattern.matcher(input).replaceFirst(Matcher.quoteReplacement(jsonString)); + updatedReplacement = Matcher.quoteReplacement(jsonString); } catch (net.minidev.json.parser.ParseException | JsonProcessingException e) { throw Exceptions.propagate( new AppsmithPluginException( @@ -235,7 +238,7 @@ public class DataTypeStringUtils { } break; case BSON: - input = placeholderPattern.matcher(input).replaceFirst(Matcher.quoteReplacement(replacement)); + updatedReplacement = Matcher.quoteReplacement(replacement); break; case DATE: case TIME: @@ -246,7 +249,7 @@ public class DataTypeStringUtils { default: try { String valueAsString = objectMapper.writeValueAsString(replacement); - input = placeholderPattern.matcher(input).replaceFirst(Matcher.quoteReplacement(valueAsString)); + updatedReplacement = Matcher.quoteReplacement(valueAsString); } catch (JsonProcessingException e) { throw Exceptions.propagate( new AppsmithPluginException( @@ -258,6 +261,11 @@ public class DataTypeStringUtils { } } + if (smartSubstitutionUtils != null) { + updatedReplacement = smartSubstitutionUtils.sanitizeReplacement(updatedReplacement); + } + + input = placeholderPattern.matcher(input).replaceFirst(updatedReplacement); return input; } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java index 42fc69c946..716c4cc2aa 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java @@ -36,6 +36,7 @@ public interface SmartSubstitutionInterface { // If the evaluated value of the mustache binding is present, set it in the prepared statement if (matchingParam.isPresent()) { String value = matchingParam.get().getValue(); + input = substituteValueInInput(i + 1, key, value, input, insertedParams, args); } else { @@ -54,4 +55,18 @@ public interface SmartSubstitutionInterface { List> insertedParams, Object... args) throws AppsmithPluginException { return input; } + + + /** + * This method is part of the pre-processing of the replacement value before the final substitution that + * happens as part of smart substitution process. + * This is the default implementation. Each plugin that implements `SmartSubstitutionInterface` is supposed to + * override this method to provide plugin specific implementation. + * + * @param replacementValue - value to be substituted + * @return - updated replacement value + */ + default String sanitizeReplacement(String replacementValue) { + return replacementValue; + } } diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java index ea94d90508..00bc20c684 100644 --- a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java +++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java @@ -983,7 +983,7 @@ public class AmazonS3Plugin extends BasePlugin { List> insertedParams, Object... args) { String jsonBody = (String) input; - return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams); + return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams, null); } } diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java index 103a0399d4..032ee0bf63 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java @@ -299,7 +299,7 @@ public class GoogleSheetsPlugin extends BasePlugin { List> insertedParams, Object... args) { String jsonBody = (String) input; - return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams); + return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams, null); } } } \ No newline at end of file diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index bd17698610..2a063117a4 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -117,6 +117,17 @@ public class MongoPlugin extends BasePlugin { */ private static final String MONGO_URI_REGEX = "^(mongodb(\\+srv)?:\\/\\/)((.+):(.+))(@.+\\/(.+))$"; + /** + * This regex matches the following two patterns: + * - "ObjectId(someId)" // will not match without outer double quotes + * - Group 1 = "ObjectId(someId)" + * - Group 2 = ObjectId(someId) // no quotes + * - 'ObjectId(someId)' // will not match without outer single quotes + * - Group 3 = 'ObjectId(someId)' + * - Group 4 = ObjectId(someId) // not quotes + */ + private static final String OBJECT_ID_INSIDE_QUOTES_REGEX = "(\\\"(ObjectId\\(.*?\\))\\\")|('(ObjectId\\(.*?\\))')"; + private static final int REGEX_GROUP_HEAD = 1; private static final int REGEX_GROUP_USERNAME = 4; @@ -406,6 +417,65 @@ public class MongoPlugin extends BasePlugin { .subscribeOn(scheduler); } + /** + * This method is part of the pre-processing of the replacement value before the final substitution that + * happens as part of smart substitution process. + * + * @param replacementValue - value to be substituted + * @return - updated replacement value + */ + @Override + public String sanitizeReplacement(String replacementValue) { + replacementValue = removeQuotesAroundObjectId(replacementValue); + + return replacementValue; + } + + /** + * This method is meant to remove extra quotes around the `ObjectId(...)` string. E.g. if the input query is + * "... {$in: [\"ObjectId(\"123\")\"]}" then the output query will be "... {$in: [ObjectId(\"123\")]}". + * + * @param query - input query + * @return - query obtained after removing quotes around ObjectId string. + */ + private String removeQuotesAroundObjectId(String query) { + Map objectIdMap = new HashMap(); + + Pattern pattern = Pattern.compile(OBJECT_ID_INSIDE_QUOTES_REGEX); + Matcher matcher = pattern.matcher(query); + while (matcher.find()) { + String objectIdWithQuotes; + String objectIdWithoutQuotes; + + /** + * `If` branch will match when ObjectId is wrapped within double quotes e.g. "ObjectId(someId)" + * - Group 1 = "ObjectId(someId)" + * - Group 2 = ObjectId(someId) // no quotes + * `Else` branch will match when ObjectId is wrapped within single quotes e.g. 'ObjectId(someId)' + * - Group 3 = 'ObjectId(someId)' + * - Group 4 = ObjectId(someId) // no quotes + */ + if (matcher.group(1) != null) { + objectIdWithQuotes = matcher.group(1); + objectIdWithoutQuotes = matcher.group(2); + } + else { + objectIdWithQuotes = matcher.group(3); + objectIdWithoutQuotes = matcher.group(4); + } + + objectIdMap.put(objectIdWithQuotes, objectIdWithoutQuotes); + } + + for (Map.Entry entry : objectIdMap.entrySet()) { + String objectIdWithQuotes = (entry).getKey(); + String objectIdWithoutQuotes = (entry).getValue(); + query = query.replace(objectIdWithQuotes, objectIdWithoutQuotes); + } + + return query; + } + private String smartSubstituteBSON(String rawQuery, List params, List> parameters) throws AppsmithPluginException { @@ -878,7 +948,7 @@ public class MongoPlugin extends BasePlugin { List> insertedParams, Object... args) { String jsonBody = (String) input; - return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams); + return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams, this); } @Override diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java index 65a9315cf7..b8429d4b53 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java @@ -89,6 +89,7 @@ public class MongoPluginTest { private static String address; private static Integer port; private JsonNode value; + private static MongoClient mongoClient; @SuppressWarnings("rawtypes") @ClassRule @@ -99,14 +100,14 @@ public class MongoPluginTest { public static void setUp() { address = mongoContainer.getContainerIpAddress(); port = mongoContainer.getFirstMappedPort(); - String uri = "mongodb://" + address + ":" + Integer.toString(port); - final MongoClient mongoClient = MongoClients.create(uri); + String uri = "mongodb://" + address + ":" + port; + mongoClient = MongoClients.create(uri); Flux.from(mongoClient.getDatabase("test").listCollectionNames()).collectList(). flatMap(collectionNamesList -> { - final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection( - "users"); if (collectionNamesList.size() == 0) { + final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection( + "users"); Mono.from(usersCollection.insertMany(List.of( new Document(Map.of( "name", "Cierra Vega", @@ -122,7 +123,7 @@ public class MongoPluginTest { ))).block(); } - return Mono.just(usersCollection); + return Mono.empty(); }).block(); } @@ -1816,4 +1817,116 @@ public class MongoPluginTest { }) .verifyComplete(); } + + @Test + public void testSmartSubstitutionWithObjectIdInDoubleQuotes() { + final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection("users"); + List documentIds = new ArrayList<>(); + Flux.from(usersCollection.find()) + .map(doc -> documentIds.add(doc.get("_id").toString())) + .collectList() + .block(); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + String findQuery = "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"_id\": {\n" + + " $in: {{Input1.text}}\n" + + " }\n" + + " }\n" + + "}"; + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(findQuery); + + StringBuilder sb = new StringBuilder(); + documentIds.stream() + .forEach(id -> sb.append(" \"ObjectId(\\\"" + id + "\\\")\",")); + sb.setLength(sb.length() - 1); + String objectIdsAsArray = "[" + sb + "]"; + + Map configMap = new HashMap<>(); + setValueSafelyInFormData(configMap, SMART_SUBSTITUTION, Boolean.TRUE); + setValueSafelyInFormData(configMap, COMMAND, "RAW"); + actionConfiguration.setFormData(configMap); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("Input1.text"); + param1.setValue(objectIdsAsArray); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + executeActionDTO, dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertEquals(3, ((ArrayNode) result.getBody()).size()); + }) + .verifyComplete(); + + } + + @Test + public void testSmartSubstitutionWithObjectIdInSingleQuotes() { + final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection("users"); + List documentIds = new ArrayList<>(); + Flux.from(usersCollection.find()) + .map(doc -> documentIds.add(doc.get("_id").toString())) + .collectList() + .block(); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + String findQuery = "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"_id\": {\n" + + " $in: {{Input1.text}}\n" + + " }\n" + + " }\n" + + "}"; + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody(findQuery); + + StringBuilder sb = new StringBuilder(); + documentIds.stream() + .forEach(id -> sb.append(" \'ObjectId(\\\"" + id + "\\\")\',")); + sb.setLength(sb.length() - 1); + String objectIdsAsArray = "[" + sb + "]"; + + Map configMap = new HashMap<>(); + setValueSafelyInFormData(configMap, SMART_SUBSTITUTION, Boolean.TRUE); + setValueSafelyInFormData(configMap, COMMAND, "RAW"); + actionConfiguration.setFormData(configMap); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + List params = new ArrayList<>(); + Param param1 = new Param(); + param1.setKey("Input1.text"); + param1.setValue(objectIdsAsArray); + params.add(param1); + executeActionDTO.setParams(params); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, + executeActionDTO, dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertEquals(3, ((ArrayNode) result.getBody()).size()); + }) + .verifyComplete(); + + } } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index c3c15b6aa0..f27edf6bba 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -671,7 +671,7 @@ public class RestApiPlugin extends BasePlugin { List> insertedParams, Object... args) { String jsonBody = (String) input; - return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams); + return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(jsonBody, value, insertedParams, null); } @Override