From b17fae8e44679f85a3b0708a6dc72ee80b0848bd Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Wed, 11 Mar 2020 17:16:15 +0000 Subject: [PATCH] 1. Add to Page changes : In case the sample response is null, don't set the cached response for the action. 2. Add to Page changes : Documentation object has been added in Action to handle the extra Template documentation for actions that have been imported from 3p marketplace 3. Added basic structure for rapid api plugin by copy pasting the rest api plugin --- app/server/appsmith-plugins/pom.xml | 1 + .../appsmith-plugins/rapidApiPlugin/pom.xml | 85 ++++++ .../com/external/plugins/RapidApiPlugin.java | 266 ++++++++++++++++++ .../external/plugins/RapidApiPluginTest.java | 18 ++ .../com/appsmith/server/domains/Action.java | 2 + .../server/domains/Documentation.java | 13 + .../server/services/ItemServiceImpl.java | 10 +- 7 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 app/server/appsmith-plugins/rapidApiPlugin/pom.xml create mode 100644 app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java create mode 100644 app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 11ebe088e9..eb1160af2c 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -18,6 +18,7 @@ postgresPlugin restApiPlugin mongoPlugin + rapidApiPlugin \ No newline at end of file diff --git a/app/server/appsmith-plugins/rapidApiPlugin/pom.xml b/app/server/appsmith-plugins/rapidApiPlugin/pom.xml new file mode 100644 index 0000000000..32621c6e67 --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + com.external.plugins + rapidApiPlugin + 1.0-SNAPSHOT + + rapidApiPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + rapidapi-plugin + com.external.plugins.RapidApiPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + junit + junit + 4.11 + test + + + + org.pf4j + pf4j-spring + 0.5.0 + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + + + + org.springframework + spring-webflux + 5.1.5.RELEASE + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + + + + + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + + + diff --git a/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java new file mode 100644 index 0000000000..420f4e3cc8 --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java @@ -0,0 +1,266 @@ +package com.external.plugins; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Property; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.plugins.BasePlugin; +import com.appsmith.external.plugins.PluginExecutor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.bson.internal.Base64; +import org.json.JSONObject; +import org.pf4j.Extension; +import org.pf4j.PluginWrapper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RapidApiPlugin extends BasePlugin { + private static int MAX_REDIRECTS = 5; + private static ObjectMapper objectMapper; + private static String rapidApiKeyName = "X-RapidAPI-Key"; + private static String rapidApiKeyValue = "f2a61def63msh9d6582090d01286p157197jsnade6f31fcae8"; + + public RapidApiPlugin(PluginWrapper wrapper) { + super(wrapper); + this.objectMapper = new ObjectMapper(); + } + + @Slf4j + @Extension + public static class RapidApiPluginExecutor implements PluginExecutor { + + @Override + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { + + String requestBody = (actionConfiguration.getBody() == null) ? "" : actionConfiguration.getBody(); + String path = (actionConfiguration.getPath() == null) ? "" : actionConfiguration.getPath(); + String url = datasourceConfiguration.getUrl() + path; + + HttpMethod httpMethod = actionConfiguration.getHttpMethod(); + if (httpMethod == null) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "HTTPMethod must be set.")); + } + + WebClient.Builder webClientBuilder = WebClient.builder(); + + if (datasourceConfiguration.getHeaders() != null) { + addHeadersToRequest(webClientBuilder, datasourceConfiguration.getHeaders()); + } + + if (actionConfiguration.getHeaders() != null) { + addHeadersToRequest(webClientBuilder, actionConfiguration.getHeaders()); + } + + // Add the rapid api headers + webClientBuilder.defaultHeader(rapidApiKeyName, rapidApiKeyValue); + + URI uri = null; + try { + uri = createFinalUriWithQueryParams(url, actionConfiguration.getQueryParameters()); + System.out.println("Final URL is : " + uri.toString()); + } catch (URISyntaxException e) { + e.printStackTrace(); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); + } + + // Build the body of the request in case of bodyFormData is not null + if (actionConfiguration.getBodyFormData() != null) { + // First set the header to specify the content type + webClientBuilder.defaultHeader("Content-Type", "application/json"); + + Map strStrMap = new HashMap(); + + List bodyFormData = actionConfiguration.getBodyFormData(); + + bodyFormData + .stream() + .map(property -> strStrMap.put(property.getKey(), property.getValue())); + + log.debug("str str map from body form data is : {}", strStrMap); + + JSONObject bodyJson = new JSONObject(strStrMap); + String jsonString = bodyJson.toString(); + log.debug("Json string from body form data is : {}", jsonString); + + } + + WebClient client = webClientBuilder.build(); + return httpCall(client, httpMethod, uri, requestBody, 0) + .flatMap(clientResponse -> clientResponse.toEntity(byte[].class)) + .map(stringResponseEntity -> { + HttpHeaders headers = stringResponseEntity.getHeaders(); + // Find the media type of the response to parse the body as required. + MediaType contentType = headers.getContentType(); + byte[] body = stringResponseEntity.getBody(); + HttpStatus statusCode = stringResponseEntity.getStatusCode(); + + ActionExecutionResult result = new ActionExecutionResult(); + result.setStatusCode(statusCode.toString()); + // If the HTTP response is 200, only then cache the response. + // We shouldn't cache the response even for other 2xx statuses like 201, 204 etc. + if (statusCode.equals(HttpStatus.OK)) { + result.setShouldCacheResponse(true); + } + + if (headers != null) { + // Convert the headers into json tree to store in the results + String headerInJsonString; + try { + headerInJsonString = objectMapper.writeValueAsString(headers); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + } + try { + // Set headers in the result now + result.setHeaders(objectMapper.readTree(headerInJsonString)); + } catch (IOException e) { + e.printStackTrace(); + return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + } + } + + if (body != null) { + /**TODO + * Handle XML response. Currently we only handle JSON & Image responses. The other kind of responses + * are kept as is and returned as a string. + */ + if (MediaType.APPLICATION_JSON.equals(contentType) || + MediaType.APPLICATION_JSON_UTF8.equals(contentType) || + MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) { + try { + String jsonBody = new String(body); + result.setBody(objectMapper.readTree(jsonBody)); + } catch (IOException e) { + e.printStackTrace(); + return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + } + } else if (MediaType.IMAGE_GIF.equals(contentType) || + MediaType.IMAGE_JPEG.equals(contentType) || + MediaType.IMAGE_PNG.equals(contentType)) { + String encode = Base64.encode(body); + result.setBody(encode); + } else { + // If the body is not of JSON type, just set it as is. + String bodyString = new String(body); + result.setBody(bodyString.trim()); + } + } + return result; + }) + .doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + } + + private Mono httpCall(WebClient webClient, HttpMethod httpMethod, URI uri, String requestBody, int iteration) { + if (iteration == MAX_REDIRECTS) { + System.out.println("Exceeded the http redirect limits. Returning error"); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Exceeded the HTTO redirect limits of " + MAX_REDIRECTS)); + } + return webClient + .method(httpMethod) + .uri(uri) + .body(BodyInserters.fromObject(requestBody)) + .exchange() + .doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))) + .flatMap(res -> { + ClientResponse response = (ClientResponse) res; + if (response.statusCode().is3xxRedirection()) { + String redirectUrl = response.headers().header("Location").get(0); + /** + * TODO + * In case the redirected URL is not absolute (complete), create the new URL using the relative path + * This particular scenario is seen in the URL : https://rickandmortyapi.com/api/character + * It redirects to partial URI : /api/character/ + * In this scenario we should convert the partial URI to complete URI + */ + URI redirectUri = null; + try { + redirectUri = new URI(redirectUrl); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return httpCall(webClient, httpMethod, redirectUri, requestBody, iteration + 1); + } + return Mono.just(response); + }); + } + + @Override + public Object datasourceCreate(DatasourceConfiguration datasourceConfiguration) { + return null; + } + + @Override + public void datasourceDestroy(Object connection) { + + } + + @Override + public Boolean isDatasourceValid(DatasourceConfiguration datasourceConfiguration) { + if (datasourceConfiguration.getUrl() == null) { + System.out.println("URL is null. Data validation failed"); + return false; + } + // Check for URL validity + try { + new URL(datasourceConfiguration.getUrl()).toURI(); + return true; + } catch (Exception e) { + System.out.println("URL is invalid. Data validation failed"); + return false; + } + } + + private void addHeadersToRequest(WebClient.Builder webClientBuilder, List headers) { + for (Property header : headers) { + if (header.getKey() != null && !header.getKey().isEmpty()) { + webClientBuilder.defaultHeader(header.getKey(), header.getValue()); + } + } + } + + private URI createFinalUriWithQueryParams(String url, List queryParams) throws URISyntaxException { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance(); + uriBuilder.uri(new URI(url)); + + if (queryParams != null) { + for (Property queryParam : queryParams) { + if (queryParam.getKey() != null && !queryParam.getKey().isEmpty()) { + uriBuilder.queryParam(queryParam.getKey(), queryParam.getValue()); + } + } + } + return uriBuilder.build(true).toUri(); + } + + /** + * TODO : + * Add a function which is called during import of a template to an action. As part of that do the following : + * 1. Get the provider and the template + * 2. Check if the provider is subscribed to, and if not, subscribe. + * 3. Set Property field isRedacted for fields like host, etc. These fields in turn would not be displayed to + * the user during GET Actions. + */ + } +} diff --git a/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java b/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java new file mode 100644 index 0000000000..6436bda5ab --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java @@ -0,0 +1,18 @@ +package com.external.plugins; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Unit test for simple App. + */ +public class RapidApiPluginTest { + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() { + assertTrue(true); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java index 9af67ec1e5..3cf56314ac 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java @@ -50,6 +50,8 @@ public class Action extends BaseDomain { String templateId; //If action is created via a template, store the id here. + Documentation documentation; + /** * If the Datasource is null, create one and set the autoGenerated flag to true. This is required because spring-data * cannot add the createdAt and updatedAt properties for null embedded objects. At this juncture, we couldn't find diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java new file mode 100644 index 0000000000..b4f39fe3b2 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java @@ -0,0 +1,13 @@ +package com.appsmith.server.domains; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Documentation { + String text; + String url; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java index ea23434000..e8855557dc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.ApiTemplate; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.Documentation; import com.appsmith.server.dtos.AddItemToPageDTO; import com.appsmith.server.dtos.ItemDTO; import com.appsmith.server.dtos.ItemType; @@ -66,13 +67,20 @@ public class ItemServiceImpl implements ItemService { action.setName(addItemToPageDTO.getName()); action.setPageId(addItemToPageDTO.getPageId()); action.setTemplateId(apiTemplate.getId()); + + Documentation documentation = new Documentation(); + documentation.setText(apiTemplate.getApiTemplateConfiguration().getDocumentation()); + documentation.setUrl(apiTemplate.getApiTemplateConfiguration().getDocumentationUrl()); + action.setDocumentation(documentation); /** TODO * Also hit the Marketplace to update the number of imports. */ // Set Action Fields action.setActionConfiguration(apiTemplate.getActionConfiguration()); - action.setCacheResponse(apiTemplate.getApiTemplateConfiguration().getSampleResponse().getBody().toString()); + if (apiTemplate.getApiTemplateConfiguration().getSampleResponse() != null) { + action.setCacheResponse(apiTemplate.getApiTemplateConfiguration().getSampleResponse().getBody().toString()); + } return pluginService .findByPackageName(apiTemplate.getPackageName())