package org.springframework.data.rest.shell.commands;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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.util.CollectionUtils;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Commands that discover resources and create local helpers for defined links.
*
* @author Jon Brisbin
*/
@Component
public class DiscoveryCommands implements CommandMarker, ApplicationEventPublisherAware {
private static final MediaType COMPACT_JSON = MediaType.valueOf("application/x-spring-data-compact+json");
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryCommands.class);
@Autowired
private ConfigurationCommands configCmds;
@Autowired
private ContextCommands contextCmds;
@Autowired
private SslCommands sslCmds;
private SslAwareClientHttpRequestFactory requestFactory = new SslAwareClientHttpRequestFactory();
@Autowired(required = false)
private RestTemplate client = new RestTemplate(requestFactory);
@Autowired(required = false)
private ObjectMapper mapper = new ObjectMapper();
private Map<String, String> resources = new HashMap<String, String>();
private ApplicationEventPublisher ctx;
private static String pad(String s, int len) {
char[] pad = new char[len - s.length()];
Arrays.fill(pad, ' ');
return s + new String(pad);
}
@Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.ctx = applicationEventPublisher;
}
/**
* Get the discovered resources.
*
* @return
*/
public Map<String, String> getResources() {
return resources;
}
@CliAvailabilityIndicator({"discover", "list", "follow"})
public boolean available() {
return true;
}
/**
* Issue a GET and discover what resources are available by looking in the links property of the JSON.
*
* @param path
* URI to resource.
*
* @return
*
* @throws IOException
*/
@CliCommand(value = "discover", help = "Discover the resources available at a given URI.")
public String discover(
@CliOption(key = {"", "rel"},
mandatory = false,
help = "The URI at which to discover resources.",
unspecifiedDefaultValue = "/") PathOrRel path) throws IOException, URISyntaxException {
URI requestUri;
if("/".equals(path)) {
requestUri = configCmds.getBaseUri();
} else if(path.getPath().startsWith("http")) {
requestUri = URI.create(path.getPath());
} else {
requestUri = UriComponentsBuilder.fromUri(configCmds.getBaseUri()).path(path.getPath()).build().toUri();
}
configCmds.setBaseUri(requestUri.toString());
return list(new PathOrRel(requestUri.toString()), null);
}
@CliCommand(value = "list", help = "Discover the resources available at a given URI.")
public String list(
@CliOption(key = {"", "rel"},
mandatory = false,
help = "The URI at which to discover resources.",
unspecifiedDefaultValue = "/") PathOrRel path,
@CliOption(key = "params",
mandatory = false,
help = "Query parameters to add to the URL.") Map params) {
URI requestUri;
if("/".equals(path)) {
requestUri = configCmds.getBaseUri();
} else if(path.getPath().startsWith("http")) {
requestUri = URI.create(path.getPath());
} else if(resources.containsKey(path)) {
requestUri = UriComponentsBuilder.fromUriString(resources.get(path))
.build()
.toUri();
} else if("/".equals(configCmds.getBaseUri().getPath())) {
requestUri = UriComponentsBuilder.fromUri(configCmds.getBaseUri())
.path(path.getPath())
.build()
.toUri();
} else {
requestUri = UriComponentsBuilder.fromUri(configCmds.getBaseUri())
.pathSegment(path.getPath())
.build()
.toUri();
}
if(null != params) {
UriComponentsBuilder urib = UriComponentsBuilder.fromUri(requestUri);
for(Object key : params.keySet()) {
urib.queryParam(key.toString(), params.get(key));
}
requestUri = urib.build().toUri();
}
ExtractLinksHelper elh = new ExtractLinksHelper();
List<Link> links = client.execute(requestUri, HttpMethod.GET, elh, elh);
if(links.size() == 0) {
return "No resources found...";
}
StringBuilder sb = new StringBuilder();
int maxRelLen = 0;
int maxHrefLen = 0;
// First get max lengths
for(Link l : links) {
if(maxRelLen < l.getRel().length()) {
maxRelLen = l.getRel().length();
}
if(maxHrefLen < l.getHref().length()) {
maxHrefLen = l.getHref().length();
}
}
maxRelLen += 4;
sb.append(pad("rel", maxRelLen))
.append(pad("href", maxHrefLen))
.append(OsUtils.LINE_SEPARATOR);
char[] line = new char[maxRelLen + maxHrefLen];
Arrays.fill(line, '=');
sb.append(new String(line))
.append(OsUtils.LINE_SEPARATOR);
// Now build a table
for(Link l : links) {
resources.put(l.getRel(), l.getHref());
sb.append(pad(l.getRel(), maxRelLen))
.append(pad(l.getHref(), maxHrefLen))
.append(OsUtils.LINE_SEPARATOR);
}
return sb.toString();
}
/**
* Follow a URI by setting the baseUri to this path, then discovering what resources are available there.
*
* @param path
* URI to resource.
*
* @return
*
* @throws IOException
* @throws URISyntaxException
*/
@CliCommand(value = "follow",
help = "Follows a URI path, sets the base to that new path, and discovers what resources are available.")
public void follow(
@CliOption(key = {"", "rel"},
mandatory = true,
help = "The URI to follow.") PathOrRel path) throws IOException, URISyntaxException {
configCmds.setBaseUri(path.getPath());
}
private class ExtractLinksHelper implements RequestCallback, ResponseExtractor<List<Link>> {
@Override public void doWithRequest(ClientHttpRequest request) throws IOException {
request.getHeaders().setAll(configCmds.getHeaders().toSingleValueMap());
if(CollectionUtils.isEmpty(request.getHeaders().getAccept())) {
if(LOG.isDebugEnabled()) {
LOG.debug("No 'Accept' header specified, using " + COMPACT_JSON);
}
request.getHeaders().setAccept(Arrays.asList(COMPACT_JSON, MediaType.APPLICATION_JSON));
}
}
@Override public List<Link> extractData(ClientHttpResponse response) throws IOException {
List<Link> links = new ArrayList<Link>();
MediaType ct = response.getHeaders().getContentType();
if(null != ct && ct.getSubtype().endsWith("json")) {
Map m = mapper.readValue(response.getBody(), Map.class);
Object o = m.get("links");
if(o instanceof List) {
for(Object lnk : (List)o) {
if(lnk instanceof Map) {
Map lnkmap = (Map)lnk;
String href = String.format("%s", lnkmap.get("href"));
String rel = String.format("%s", lnkmap.get("rel"));
links.add(new Link(href, rel));
}
}
}
} else if(null != ct && ct.getSubtype().endsWith("uri-list")) {
BufferedReader rdr = new BufferedReader(new InputStreamReader(response.getBody()));
String line;
while(null != (line = rdr.readLine())) {
links.add(new Link(URI.create(line).toString(), ""));
}
}
if(LOG.isDebugEnabled()) {
LOG.debug("Returning links: " + links);
}
return links;
}
}
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);
}
}
}