/*******************************************************************************
* Copyright (c) 2013, 2016 GoPivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* GoPivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.wizard.github;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.ide.eclipse.boot.wizard.BootWizardActivator;
import org.springframework.ide.eclipse.boot.wizard.github.auth.BasicAuthCredentials;
import org.springframework.ide.eclipse.boot.wizard.github.auth.Credentials;
import org.springframework.ide.eclipse.boot.wizard.github.auth.NullCredentials;
import org.springframework.ide.eclipse.boot.wizard.util.Spring3MappingJacksonHttpMessageConverter;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil;
/**
* A GithubClient instance needs to configured with some credentials and then it is able to
* talk to github using its rest api to obtain information about github repos, users,
* organisations etc.
*
* @author Kris De Volder
*/
public class GithubClient {
private static final Pattern GITHUB_HOST = Pattern.compile("(.*\\.|)github\\.com");
//pattern should match 'github.com' and api.github.com'
private static final int CONNECT_TIMEOUT = 10000;
private static final boolean DEBUG = false;
private static final boolean LOG_GITHUB_RATE_LIMIT = false;
private final Credentials credentials;
private final RestTemplate client;
/**
* Create a GithubClient with default credentials. The default credentials
* are a basic authentication username plus password read from a "user.properties"
* fetched from the classloader.
*/
public GithubClient() {
this(createDefaultCredentials());
}
public GithubClient(Credentials c) {
this.credentials = c;
this.client= createRestTemplate();
}
public static Credentials createDefaultCredentials() {
//Try system properties
String username = System.getProperty("github.user.name");
String password = System.getProperty("github.user.password");
if (username!=null && password!=null) {
return new BasicAuthCredentials(GITHUB_HOST, username, password);
}
//No credentials found. Try proceeding without credentials.
return new NullCredentials();
}
private String addHost(String path) {
if (path.startsWith("http")) {
return path;
}
if (!path.startsWith("/")) {
path = "/"+path;
}
return "https://api.github.com"+path;
}
/**
* Fetch info about repos under a given organization.
*/
public Repo[] getOrgRepos(String orgName) {
return get("/orgs/{orgName}/repos", Repo[].class, orgName);
// return get("/orgs/{orgName}/repos?per_page=100", Repo[].class, orgName);
}
/**
* Fetch the remaining rate limit.
*/
public RateLimitResponse getRateLimit() throws IOException {
return get("/rate_limit", RateLimitResponse.class);
}
/**
* Fetch info about repos under a given user name
*/
public Repo[] getUserRepos(String userName) {
return get("/users/{userName}/repos", Repo[].class, userName);
}
/**
* Get repos for the authenticated user. This seems to be the only way to list private repos
* associated with a user. This only works over an authenticated github connection.
*/
public Repo[] getMyRepos() {
try {
return get("/user/repos", Repo[].class);
} catch (Throwable e) {
BootWizardActivator.log(e);
}
return new Repo[0];
}
/**
* Fetch info about a repo identified by an owner and a name
*/
public Repo getRepo(String owner, String repo) {
return get("/repos/{owner}/{repo}", Repo.class, owner, repo);
}
/**
* Helper method to fetch json data from some url (or url template)
* and parse the data into an object of a given type.
*/
@SuppressWarnings("unchecked")
public <T> T get(String url, Class<T> type, Object... vars) {
url = addHost(url);
if (type.isArray()) {
Class<?> componentType = type.getComponentType();
//Assume this means we have to support response pagination as described in:
//http://developer.github.com/v3/#pagination
ArrayList<Object> results = new ArrayList<>();
do {
ResponseEntity<T> entity = client.getForEntity(url, type, vars);
Object[] pageResults = (Object[])entity.getBody(); //cast is safe because T is an array type.
for (Object r : pageResults) {
results.add(r);
}
url = getNextPageUrl(entity);
} while (url!=null);
return (T) results.toArray((Object[])Array.newInstance(componentType, results.size()));
} else {
try {
return client.getForObject(url, type, vars);
} catch (HttpServerErrorException e) {
throw new Error("Error reading: "+url);
}
}
}
/**
* Get the url of the next page in a paginated result.
* May return null if there is no next page.
* <p>
* See http://developer.github.com/v3/#pagination
*/
private static <T> String getNextPageUrl(ResponseEntity<T> entity) {
List<String> linkHeader = entity.getHeaders().get("Link");
if (linkHeader!=null) {
//Example of header String:
//<https://api.github.com/organizations/4161866/repos?page=2>; rel="next", <https://api.github.com/organizations/4161866/repos?page=2>; rel="last"
Pattern nextPat = Pattern.compile("<([^<]*)>;\\s*rel=\"next\"");
for (String string : linkHeader) {
System.out.println(string);
Matcher m = nextPat.matcher(string);
if (m.find()) {
return m.group(1);
}
}
}
return null; //no pagination info found
}
protected static String getNormalisedProtocol(String protocol) {
return protocol.toUpperCase();
}
private RestTemplate createRestTemplate() {
RestTemplate rest = new RestTemplate();
// IProxyService proxyService = GettingStartedActivator.getDefault().getProxyService();
// if (proxyService!=null && proxyService.isProxiesEnabled()) {
// final IProxyData[] existingProxies = proxyService.getProxyData();
// if (existingProxies != null && existingProxies.length>0) {
// //TODO: Do some magic to configure proxies on the http request based on its url.
//
// //some interesting code in here:
// //org.cloudfoundry.ide.eclipse.internal.server.core.CloudFoundryClientFactory.getProxy(URL)
// }
// }
//Add authentication
rest = credentials.apply(rest);
//Add json parsing capability using Jackson mapper.
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
messageConverters.add(new Spring3MappingJacksonHttpMessageConverter());
rest.setMessageConverters(messageConverters);
//Add rate limit logging
if (LOG_GITHUB_RATE_LIMIT) {
rest.getInterceptors().add(new ClientHttpRequestInterceptor() {
//@Override
public ClientHttpResponse intercept(HttpRequest request,
byte[] body, ClientHttpRequestExecution execution)
throws IOException {
ClientHttpResponse res = execution.execute(request, body);
System.out.println("==== Github: "+request.getURI()+ " =========");
for (Entry<String, List<String>> header : res.getHeaders().entrySet()) {
if (header.getKey().contains("RateLimit")) {
System.out.print(header.getKey()+":");
for (String value : header.getValue()) {
System.out.print(" "+value);
}
System.out.println();
}
}
System.out.println("======================= ");
return res;
}
});
}
return rest;
}
// /**
// * Add hoc testing code.
// */
// public static void main(String[] args) throws Exception {
// GithubClient github = new GithubClient();
// Repo[] resp = github.getGuidesRepos();
//
// for (Repo repo : resp) {
// System.out.println(repo.getName());
// System.out.println(" "+repo.getDescription());
// System.out.println(" "+repo.getUrl());
// }
//
// System.out.println(github.getRateLimit());
// }
/**
* Download content from a url and save to an outputstream. Use same credentials as
* other operations in this client. May need to use this to download stuff like
* zip file from github if the repo it comes from is private.
*/
public void fetch(URL url, OutputStream writeTo) throws IOException {
URLConnection conn = null;
InputStream input = null;
try {
conn = url.openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT);
credentials.apply(conn);
conn.connect();
if (DEBUG) {
System.out.println(">>> "+url);
Map<String, List<String>> headers = conn.getHeaderFields();
for (Entry<String, List<String>> header : headers.entrySet()) {
System.out.println(header.getKey()+":");
for (String value : header.getValue()) {
System.out.println(" "+value);
}
}
System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
}
input = conn.getInputStream();
IOUtil.pipe(input, writeTo);
} finally {
if (input!=null) {
try {
input.close();
} catch (Throwable e) {
//ignore.
}
}
}
}
/**
* For some quick add-hoc testing
*/
public static void main(String[] args) {
GithubClient gh = new GithubClient();
for (int i = 0; i < 5; i++) {
Repo[] repos = gh.getOrgRepos("spring-guides");
System.out.println(repos.length);
}
}
}