package org.springframework.data.rest.shell.commands; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.rest.shell.context.ResponseEvent; import org.springframework.data.rest.shell.formatter.FormatProvider; import org.springframework.data.rest.shell.formatter.Formatter; import org.springframework.hateoas.Link; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.shell.core.CommandMarker; import org.springframework.shell.core.annotation.CliAvailabilityIndicator; import org.springframework.shell.core.annotation.CliCommand; import org.springframework.shell.core.annotation.CliOption; import org.springframework.shell.support.util.OsUtils; import org.springframework.stereotype.Component; import org.springframework.web.client.*; import org.springframework.web.util.UriComponentsBuilder; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import java.io.*; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * Commands that issue the HTTP requests. * * @author Jon Brisbin */ @Component public class HttpCommands implements CommandMarker, ApplicationEventPublisherAware, InitializingBean { private static final Logger LOG = LoggerFactory.getLogger(HttpCommands.class); private static final String LOCATION_HEADER = "Location"; @Autowired private ConfigurationCommands configCmds; @Autowired private DiscoveryCommands discoveryCmds; @Autowired private ContextCommands contextCmds; @Autowired private SslCommands sslCmds; private SslAwareClientHttpRequestFactory requestFactory = new SslAwareClientHttpRequestFactory(); @Autowired(required = false) private RestTemplate restTemplate = new RestTemplate(requestFactory); @Autowired(required = false) private ObjectMapper mapper = new ObjectMapper(); private ApplicationEventPublisher ctx; private Object lastResult; private URI requestUri; @Autowired private FormatProvider formatProvider; private static String encode(String s) { try { return URLEncoder.encode(s, "ISO-8859-1"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.ctx = applicationEventPublisher; } @Override public void afterPropertiesSet() throws Exception { mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true); restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { @Override public void handleError(ClientHttpResponse response) throws IOException { } }); } @CliAvailabilityIndicator({"timeout", "get", "post", "put", "delete"}) public boolean isHttpCommandAvailable() { return true; } @CliCommand(value = "timeout", help = "Set the read timeout for requests.") public void timeout(@CliOption(key = "", mandatory = true, help = "The timeout (in milliseconds) to wait for a response.", unspecifiedDefaultValue = "30000") int timeout) { requestFactory.setReadTimeout(timeout); } /** * HTTP GET to retrieve a resource. * * @param path URI to resource. * @param params URL query parameters to pass for paging and search. * @return */ @CliCommand(value = "get", help = "Issue HTTP GET to a resource.") public String get( @CliOption(key = {"", "rel"}, mandatory = false, help = "The path to the resource to GET.", unspecifiedDefaultValue = "") PathOrRel path, @CliOption(key = "follow", mandatory = false, help = "If a Location header is returned, immediately follow it.", unspecifiedDefaultValue = "false") final String follow, @CliOption(key = "params", mandatory = false, help = "Query parameters to add to the URL as a simplified JSON fragment '{paramName:\"paramValue\"}'.") Map params, @CliOption(key = "output", mandatory = false, help = "The path to dump the output to.") String outputPath) { outputPath = contextCmds.evalAsString(outputPath); UriComponentsBuilder ucb = createUriComponentsBuilder(path.getPath()); if (null != params) { for (Object key : params.keySet()) { Object o = params.get(key); ucb.queryParam(key.toString(), encode(o.toString())); } } requestUri = ucb.build().toUri(); return execute(HttpMethod.GET, null, follow, outputPath); } /** * HTTP POST to create a new resource. * * @param path URI to resource. * @param data The JSON data to send. * @return */ @CliCommand(value = "post", help = "Issue HTTP POST to create a new resource.") public String post( @CliOption(key = {"", "rel"}, mandatory = false, help = "The path to the resource collection.", unspecifiedDefaultValue = "") PathOrRel path, @CliOption(key = "data", mandatory = false, help = "The JSON data to use as the resource.") String data, @CliOption(key = "from", mandatory = false, help = "The directory from which to read JSON files to POST to the server.") String fromDir, @CliOption(key = "follow", mandatory = false, help = "If a Location header is returned, immediately follow it.", unspecifiedDefaultValue = "false") final String follow, @CliOption(key = "params", mandatory = false, help = "Query parameters to add to the URL as a simplified JSON fragment '{paramName:\"paramValue\"}'.") Map params, @CliOption(key = "output", mandatory = false, help = "The path to dump the output to.") String outputTo) throws IOException { fromDir = contextCmds.evalAsString(fromDir); final String outputPath = contextCmds.evalAsString(outputTo); UriComponentsBuilder ucb = createUriComponentsBuilder(path.getPath()); if (null != params) { for (Object key : params.keySet()) { Object o = params.get(key); ucb.queryParam(key.toString(), encode(o.toString())); } } requestUri = ucb.build().toUri(); Object obj = null; if (null != data) { if (data.contains("#{")) { obj = contextCmds.eval(data); } else if (data.startsWith("[") || data.startsWith("{")) { Class<?> targetType = Map.class; if (data.startsWith("[")) { targetType = List.class; } obj = mapper.readValue(data.replaceAll("\\\\", "").replaceAll("'", "\""), targetType); } else { obj = data; } } if (null != fromDir) { fromDir = contextCmds.evalAsString(fromDir); try { return readFileOrFiles(HttpMethod.POST, fromDir, follow, outputPath); } catch (IOException e) { throw new IllegalStateException(e); } } return execute(HttpMethod.POST, obj, follow, outputPath); } /** * HTTP PUT to update a resource. * * @param path URI to resource. * @param data The JSON data to send. * @return */ @CliCommand(value = "put", help = "Issue HTTP PUT to update a resource.") public String put( @CliOption(key = {"", "rel"}, mandatory = false, help = "The path to the resource.", unspecifiedDefaultValue = "") PathOrRel path, @CliOption(key = "data", mandatory = false, help = "The JSON data to use as the resource.") String data, @CliOption(key = "from", mandatory = false, help = "The directory from which to read JSON files to POST to the server.") String fromDir, @CliOption(key = "follow", mandatory = false, help = "If a Location header is returned, immediately follow it.", unspecifiedDefaultValue = "false") final String follow, @CliOption(key = "params", mandatory = false, help = "Query parameters to add to the URL as a simplified JSON fragment '{paramName:\"paramValue\"}'.") Map params, @CliOption(key = "output", mandatory = false, help = "The path to dump the output to.") String outputPath) throws IOException { fromDir = contextCmds.evalAsString(fromDir); outputPath = contextCmds.evalAsString(outputPath); UriComponentsBuilder ucb = createUriComponentsBuilder(path.getPath()); if (null != params) { for (Object key : params.keySet()) { Object o = params.get(key); ucb.queryParam(key.toString(), encode(o.toString())); } } requestUri = ucb.build().toUri(); Object obj; if (null != data) { if (data.contains("#{")) { obj = contextCmds.eval(data); } else if (data.startsWith("[") || data.startsWith("{")) { Class<?> targetType = Map.class; if (data.startsWith("[")) { targetType = List.class; } try { obj = mapper.readValue(data.replaceAll("\\\\", "").replaceAll("'", "\""), targetType); } catch (JsonParseException e) { LOG.error(e.getMessage(), e); throw new IllegalStateException(e.getMessage(), e); } } else { obj = data; } return execute(HttpMethod.PUT, obj, follow, outputPath); } if (null != fromDir) { fromDir = contextCmds.evalAsString(fromDir); try { return readFileOrFiles(HttpMethod.PUT, fromDir, "false", outputPath); } catch (IOException e) { throw new IllegalStateException(e); } } return null; } /** * HTTP DELETE to delete a resource. * * @param path URI to resource. * @return */ @CliCommand(value = "delete", help = "Issue HTTP DELETE to delete a resource.") public String delete( @CliOption(key = {"", "rel"}, mandatory = false, help = "Issue HTTP DELETE to delete a resource.", unspecifiedDefaultValue = "") PathOrRel path, @CliOption(key = "follow", mandatory = false, help = "If a Location header is returned, immediately follow it.", unspecifiedDefaultValue = "false") final String follow, @CliOption(key = "params", mandatory = false, help = "Query parameters to add to the URL as a simplified JSON fragment '{paramName:\"paramValue\"}'.") Map params, @CliOption(key = "output", mandatory = false, help = "The path to dump the output to.") String outputPath) { outputPath = contextCmds.evalAsString(outputPath); UriComponentsBuilder ucb = createUriComponentsBuilder(path.getPath()); if (null != params) { for (Object key : params.keySet()) { Object o = params.get(key); ucb.queryParam(key.toString(), encode(o.toString())); } } requestUri = ucb.build().toUri(); return execute(HttpMethod.DELETE, null, follow, outputPath); } public String execute(final HttpMethod method, final Object data, final String follow, final String outputPath) { final StringBuilder buffer = new StringBuilder(); MediaType contentType = configCmds.getHeaders().getContentType(); if (contentType == null) { contentType = MediaType.APPLICATION_JSON; } ResponseErrorHandler origErrHandler = restTemplate.getErrorHandler(); RequestHelper helper = (null == data ? new RequestHelper() : new RequestHelper(data, contentType)); ResponseEntity<String> response; try { restTemplate.setErrorHandler(new ResponseErrorHandler() { @Override public boolean hasError(ClientHttpResponse response) throws IOException { HttpStatus status = response.getStatusCode(); return (status == HttpStatus.BAD_GATEWAY || status == HttpStatus.GATEWAY_TIMEOUT); } @Override public void handleError(ClientHttpResponse response) throws IOException { if (LOG.isWarnEnabled()) { LOG.warn("Client encountered an error " + response.getRawStatusCode() + ". Retrying..."); } System.out.println(execute(method, data, follow, outputPath)); } }); if (LOG.isInfoEnabled()) { LOG.info("Sending " + method + " to " + requestUri + " using " + data); } response = restTemplate.execute(requestUri, method, helper, helper); } catch (ResourceAccessException e) { if (LOG.isWarnEnabled()) { LOG.warn("Client encountered an error. Retrying. (" + e.getMessage() + ")", e); } // Calling this method recursively results in hang, so just retry once. response = restTemplate.execute(requestUri, method, helper, helper); } finally { restTemplate.setErrorHandler(origErrHandler); } if ("true".equals(follow) && response.getHeaders().containsKey(LOCATION_HEADER)) { try { configCmds.setBaseUri(response.getHeaders().getFirst(LOCATION_HEADER)); } catch (URISyntaxException e) { LOG.error("Error following Location header: " + e.getMessage(), e); } } outputRequest(method.name(), requestUri, buffer); contextCmds.variables.put("response", response); ctx.publishEvent(new ResponseEvent(requestUri, method, response)); outputResponse(response, buffer); if (null != outputPath) { FileWriter writer = null; try { writer = new FileWriter(new File(outputPath)); writer.write(buffer.toString()); writer.flush(); } catch (IOException e) { LOG.error(e.getMessage(), e); throw new IllegalArgumentException(e); } finally { if (null != writer) { try { writer.close(); } catch (IOException e) { } } } return "\n>> " + outputPath + "\n"; } else { switch (response.getStatusCode()) { case BAD_REQUEST: case INTERNAL_SERVER_ERROR: { System.err.println(buffer.toString()); return null; } default: return buffer.toString(); } } } private String readFileOrFiles(final HttpMethod method, final String fromPath, final String follow, final String outputPath) throws IOException { String output; File fromFile = new File(fromPath); if (!fromFile.exists()) { throw new IllegalArgumentException("Path " + fromPath + " not found."); } if (fromFile.isDirectory()) { final AtomicInteger numItems = new AtomicInteger(0); FilenameFilter jsonFilter = new FilenameFilter() { @Override public boolean accept(File file, String s) { return s.endsWith(".json"); } }; for (File file : fromFile.listFiles(jsonFilter)) { Object body = readFile(file); String response = execute(method, body, follow, outputPath); if (LOG.isDebugEnabled()) { LOG.debug(response); } if (null != response) { numItems.incrementAndGet(); } } output = "\n" + numItems.get() + " files successfully uploaded to the server using " + method + "\n"; } else { Object body = readFile(fromFile); String response = execute(method, body, follow, outputPath); if (LOG.isDebugEnabled()) { LOG.debug(response); } output = response; } return output; } private Object readFile(File file) throws IOException { StringBuilder builder = new StringBuilder(); FileReader reader = new FileReader(file); char[] buffer = new char[8 * 1024]; int read; while (-1 < (read = reader.read(buffer))) { String s = new String(buffer, 0, read); builder.append(s); } String bodyAsString = builder.toString(); Object body = ""; if (bodyAsString.length() > 0) { try { if (bodyAsString.charAt(0) == '{') { body = mapper.readValue(bodyAsString, Map.class); } else if (bodyAsString.charAt(0) == '[') { body = mapper.readValue(bodyAsString, List.class); } else { body = bodyAsString; } } catch (JsonParseException e) { LOG.error(e.getMessage(), e); throw new IllegalStateException(e.getMessage(), e); } } return body; } private UriComponentsBuilder createUriComponentsBuilder(String path) { UriComponentsBuilder ucb; if (discoveryCmds.getResources().containsKey(path)) { ucb = UriComponentsBuilder.fromUriString(discoveryCmds.getResources().get(path)); } else { if (path.startsWith("http")) { ucb = UriComponentsBuilder.fromUriString(path); } else { ucb = UriComponentsBuilder.fromUri(configCmds.getBaseUri()).pathSegment(path); } } return ucb; } private void outputRequest(String method, URI requestUri, StringBuilder buffer) { buffer.append("> ") .append(method) .append(" ") .append(requestUri.toString()) .append(OsUtils.LINE_SEPARATOR); for (Map.Entry<String, String> entry : configCmds.getHeaders().toSingleValueMap().entrySet()) { buffer.append("> ") .append(entry.getKey()) .append(": ") .append(entry.getValue()) .append(OsUtils.LINE_SEPARATOR); } buffer.append(OsUtils.LINE_SEPARATOR); } private void outputResponse(ResponseEntity<String> response, StringBuilder buffer) { buffer.append("< ") .append(response.getStatusCode().value()) .append(" ") .append(response.getStatusCode().name()) .append(OsUtils.LINE_SEPARATOR); for (Map.Entry<String, List<String>> entry : response.getHeaders().entrySet()) { buffer.append("< ") .append(entry.getKey()) .append(": "); boolean first = true; for (String s : entry.getValue()) { if (!first) { buffer.append(","); } else { first = false; } buffer.append(s); } buffer.append(OsUtils.LINE_SEPARATOR); } buffer.append("< ").append(OsUtils.LINE_SEPARATOR); if (null != response.getBody()) { final Formatter formatter = formatProvider.getFormatter(response.getHeaders().getContentType().getSubtype()); buffer.append(formatter.format(response.getBody())); } } private class RequestHelper implements RequestCallback, ResponseExtractor<ResponseEntity<String>> { private Object body; private MediaType contentType; private HttpMessageConverterExtractor<String> extractor = new HttpMessageConverterExtractor<String>(String.class, restTemplate.getMessageConverters()); private ObjectMapper mapper = new ObjectMapper(); { mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true); } private RequestHelper() { } private RequestHelper(Object body, MediaType contentType) { this.body = body; this.contentType = contentType; } @Override public void doWithRequest(ClientHttpRequest request) throws IOException { request.getHeaders().setAll(configCmds.getHeaders().toSingleValueMap()); if (null != contentType) { request.getHeaders().setContentType(contentType); } if (null != body) { if (body instanceof String) { request.getBody().write(((String) body).getBytes()); } else if (body instanceof byte[]) { request.getBody().write((byte[]) body); } else { try { mapper.writeValue(request.getBody(), body); } catch (JsonParseException e) { LOG.error(e.getMessage(), e); throw new IllegalStateException(e.getMessage(), e); } } } //contextCmds.variables.put("request", request); } @SuppressWarnings({"unchecked"}) @Override public ResponseEntity<String> extractData(ClientHttpResponse response) throws IOException { String body = extractor.extractData(response); contextCmds.variables.put("requestUrl", requestUri.toString()); contextCmds.variables.put("responseHeaders", response.getHeaders()); contextCmds.variables.put("responseBody", null); MediaType ct = response.getHeaders().getContentType(); if (null != body && null != ct && ct.getSubtype().endsWith("json")) { // Pretty-print the JSON try { if (body.startsWith("{")) { lastResult = mapper.readValue(body.getBytes(), Map.class); } else if (body.startsWith("[")) { lastResult = mapper.readValue(body.getBytes(), List.class); } else { lastResult = new String(body.getBytes()); } } catch (JsonParseException e) { LOG.error(e.getMessage(), e); throw new IllegalStateException(e.getMessage(), e); } contextCmds.variables.put("responseBody", lastResult); if (lastResult instanceof Map && ((Map) lastResult).containsKey("links")) { Links linksobj; if (contextCmds.variables.containsKey("links")) { linksobj = (Links) contextCmds.variables.get("links"); } else { linksobj = new Links(); contextCmds.evalCtx.addPropertyAccessor(linksobj.getPropertyAccessor()); } linksobj.getLinks().clear(); for (Map<String, String> linkmap : (List<Map<String, String>>) ((Map) lastResult).get("links")) { linksobj.addLink(new Link(linkmap.get("href"), linkmap.get("rel"))); } contextCmds.variables.put("links", linksobj); } StringWriter sw = new StringWriter(); try { mapper.writeValue(sw, lastResult); } catch (JsonParseException e) { LOG.error(e.getMessage(), e); throw new IllegalStateException(e.getMessage(), e); } body = sw.toString(); } return new ResponseEntity<String>(body, response.getHeaders(), response.getStatusCode()); } } private class SslAwareClientHttpRequestFactory extends SimpleClientHttpRequestFactory { @Override protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException { if (connection instanceof HttpsURLConnection) { HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; if (!sslCmds.getValidate()) { httpsConnection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { return true; } }); httpsConnection.setSSLSocketFactory(sslCmds.getCustomContext().getSocketFactory()); } } super.prepareConnection(connection, httpMethod); } } }