/* * Copyright © 2014 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License */ package co.cask.cdap.cli.command; import co.cask.cdap.api.service.Service; import co.cask.cdap.cli.ArgumentName; import co.cask.cdap.cli.CLIConfig; import co.cask.cdap.cli.Categorized; import co.cask.cdap.cli.CommandCategory; import co.cask.cdap.cli.ElementType; import co.cask.cdap.cli.english.Article; import co.cask.cdap.cli.english.Fragment; import co.cask.cdap.cli.exception.CommandInputError; import co.cask.cdap.cli.util.AbstractCommand; import co.cask.cdap.cli.util.FilePathResolver; import co.cask.cdap.client.ServiceClient; import co.cask.cdap.client.config.ClientConfig; import co.cask.cdap.client.util.RESTClient; import co.cask.cdap.common.conf.StringUtils; import co.cask.cdap.proto.Id; import co.cask.common.cli.Arguments; import co.cask.common.http.HttpMethod; import co.cask.common.http.HttpRequest; import co.cask.common.http.HttpResponse; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.inject.Inject; import java.io.PrintStream; import java.net.URL; import java.util.Collection; import java.util.Map; /** * Call an endpoint of a {@link Service}. */ public class CallServiceCommand extends AbstractCommand implements Categorized { private static final Gson GSON = new Gson(); private final ClientConfig clientConfig; private final RESTClient restClient; private final ServiceClient serviceClient; private final FilePathResolver filePathResolver; @Inject public CallServiceCommand(ClientConfig clientConfig, RESTClient restClient, ServiceClient serviceClient, CLIConfig cliConfig, FilePathResolver filePathResolver) { super(cliConfig); this.clientConfig = clientConfig; this.restClient = restClient; this.serviceClient = serviceClient; this.filePathResolver = filePathResolver; } @Override public void perform(Arguments arguments, PrintStream output) throws Exception { String[] appAndServiceId = arguments.get(ArgumentName.SERVICE.toString()).split("\\."); if (appAndServiceId.length < 2) { throw new CommandInputError(this); } String appId = appAndServiceId[0]; String serviceId = appAndServiceId[1]; Id.Service service = Id.Service.from( Id.Application.from(cliConfig.getCurrentNamespace(), appId), serviceId); String method = arguments.get(ArgumentName.HTTP_METHOD.toString()); String path = arguments.get(ArgumentName.ENDPOINT.toString()); path = path.startsWith("/") ? path.substring(1) : path; String headers = arguments.get(ArgumentName.HEADERS.toString(), ""); String bodyString = arguments.get(ArgumentName.HTTP_BODY.toString(), ""); String bodyFile = arguments.get(ArgumentName.LOCAL_FILE_PATH.toString(), ""); if (!bodyString.isEmpty() && !bodyFile.isEmpty()) { String message = String.format("Please provide either [body <%s>] or [body:file <%s>], " + "but not both", ArgumentName.HTTP_BODY.toString(), ArgumentName.LOCAL_FILE_PATH.toString()); throw new CommandInputError(this, message); } Map<String, String> headerMap = GSON.fromJson(headers, new TypeToken<Map<String, String>>() { }.getType()); URL url = new URL(serviceClient.getServiceURL(service), path); HttpMethod httpMethod = HttpMethod.valueOf(method); HttpRequest.Builder builder = HttpRequest.builder(httpMethod, url).addHeaders(headerMap); if (httpMethod == HttpMethod.GET && (!bodyFile.isEmpty() || !bodyString.isEmpty())) { throw new UnsupportedOperationException("Sending body in a GET request is not supported"); } if (!bodyFile.isEmpty()) { builder.withBody(filePathResolver.resolvePathToFile(bodyFile)); } else if (!bodyString.isEmpty()) { builder.withBody(bodyString); } HttpResponse response = restClient.execute(builder.build(), clientConfig.getAccessToken()); output.printf("< %s %s\n", response.getResponseCode(), response.getResponseMessage()); for (Map.Entry<String, String> header : response.getHeaders().entries()) { output.printf("< %s: %s\n", header.getKey(), header.getValue()); } output.print(response.getResponseBodyAsString()); } @Override public String getPattern() { return String.format("call service <%s> <%s> <%s> [headers <%s>] [body <%s>] [body:file <%s>]", ArgumentName.SERVICE, ArgumentName.HTTP_METHOD, ArgumentName.ENDPOINT, ArgumentName.HEADERS, ArgumentName.HTTP_BODY, ArgumentName.LOCAL_FILE_PATH); } @Override public String getDescription() { return String.format("Calls %s endpoint. The '<%s>' are formatted as '{\"key\":\"value\", ...}'. " + "The request body may be provided as either a string or a file. " + "To provide the body as a string, use 'body <%s>'. " + "To provide the body as a file, use 'body:file <%s>'.", Fragment.of(Article.A, ElementType.SERVICE.getName()), ArgumentName.HEADERS, ArgumentName.HTTP_BODY, ArgumentName.LOCAL_FILE_PATH); } /** * Format multiple header values as a comma separated list of the values. * This is a valid formatting: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html */ private String formatHeaders(HttpResponse response) { Multimap<String, String> headers = response.getHeaders(); ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>(); for (String key : headers.keySet()) { Collection<String> value = headers.get(key); builder.put(key, StringUtils.arrayToString(value.toArray(new String[value.size()]))); } return formatHeader(builder.build()); } @Override public String getCategory() { return CommandCategory.EGRESS.getName(); } }