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())