mirror of
https://github.com/jlengrand/github-api.git
synced 2026-03-10 08:21:21 +00:00
784 lines
29 KiB
Java
784 lines
29 KiB
Java
/*
|
||
* The MIT License
|
||
*
|
||
* Copyright (c) 2010, Kohsuke Kawaguchi
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to deal
|
||
* in the Software without restriction, including without limitation the rights
|
||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
* copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in
|
||
* all copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
* THE SOFTWARE.
|
||
*/
|
||
package org.kohsuke.github;
|
||
|
||
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
|
||
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
|
||
import static java.util.logging.Level.FINE;
|
||
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
|
||
import static org.kohsuke.github.Previews.DRAX;
|
||
|
||
import java.io.ByteArrayInputStream;
|
||
import java.io.FileNotFoundException;
|
||
import java.io.IOException;
|
||
import java.io.InputStreamReader;
|
||
import java.io.Reader;
|
||
import java.net.HttpURLConnection;
|
||
import java.net.MalformedURLException;
|
||
import java.net.URL;
|
||
import java.text.ParseException;
|
||
import java.text.SimpleDateFormat;
|
||
import java.util.Arrays;
|
||
import java.util.Collection;
|
||
import java.util.Date;
|
||
import java.util.HashMap;
|
||
import java.util.HashSet;
|
||
import java.util.Hashtable;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
import java.util.TimeZone;
|
||
|
||
import org.apache.commons.codec.Charsets;
|
||
import org.apache.commons.codec.binary.Base64;
|
||
|
||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
|
||
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
|
||
|
||
import javax.annotation.Nonnull;
|
||
import java.util.logging.Logger;
|
||
|
||
/**
|
||
* Root of the GitHub API.
|
||
*
|
||
* <h2>Thread safety</h2>
|
||
* <p>
|
||
* This library aims to be safe for use by multiple threads concurrently, although
|
||
* the library itself makes no attempt to control/serialize potentially conflicting
|
||
* operations to GitHub, such as updating & deleting a repository at the same time.
|
||
*
|
||
* @author Kohsuke Kawaguchi
|
||
*/
|
||
public class GitHub {
|
||
/*package*/ final String login;
|
||
|
||
/**
|
||
* Value of the authorization header to be sent with the request.
|
||
*/
|
||
/*package*/ final String encodedAuthorization;
|
||
|
||
private final Map<String,GHUser> users = new Hashtable<String, GHUser>();
|
||
private final Map<String,GHOrganization> orgs = new Hashtable<String, GHOrganization>();
|
||
|
||
private final String apiUrl;
|
||
|
||
/*package*/ final RateLimitHandler rateLimitHandler;
|
||
/*package*/ final AbuseLimitHandler abuseLimitHandler;
|
||
|
||
private HttpConnector connector = HttpConnector.DEFAULT;
|
||
|
||
/**
|
||
* Creates a client API root object.
|
||
*
|
||
* <p>
|
||
* Several different combinations of the login/oauthAccessToken/password parameters are allowed
|
||
* to represent different ways of authentication.
|
||
*
|
||
* <dl>
|
||
* <dt>Loging anonymously
|
||
* <dd>Leave all three parameters null and you will be making HTTP requests without any authentication.
|
||
*
|
||
* <dt>Log in with password
|
||
* <dd>Specify the login and password, then leave oauthAccessToken null.
|
||
* This will use the HTTP BASIC auth with the GitHub API.
|
||
*
|
||
* <dt>Log in with OAuth token
|
||
* <dd>Specify oauthAccessToken, and optionally specify the login. Leave password null.
|
||
* This will send OAuth token to the GitHub API. If the login parameter is null,
|
||
* The constructor makes an API call to figure out the user name that owns the token.
|
||
* </dl>
|
||
*
|
||
* @param apiUrl
|
||
* The URL of GitHub (or GitHub enterprise) API endpoint, such as "https://api.github.com" or
|
||
* "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <tt>/api/v3</tt> in the URL.
|
||
* For historical reasons, this parameter still accepts the bare domain name, but that's considered deprecated.
|
||
* Password is also considered deprecated as it is no longer required for api usage.
|
||
* @param login
|
||
* The use ID on GitHub that you are logging in as. Can be omitted if the OAuth token is
|
||
* provided or if logging in anonymously. Specifying this would save one API call.
|
||
* @param oauthAccessToken
|
||
* Secret OAuth token.
|
||
* @param password
|
||
* User's password. Always used in conjunction with the {@code login} parameter
|
||
* @param connector
|
||
* HttpConnector to use. Pass null to use default connector.
|
||
*/
|
||
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler, AbuseLimitHandler abuseLimitHandler) throws IOException {
|
||
if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize
|
||
this.apiUrl = apiUrl;
|
||
if (null != connector) this.connector = connector;
|
||
|
||
if (oauthAccessToken!=null) {
|
||
encodedAuthorization = "token "+oauthAccessToken;
|
||
} else {
|
||
if (password!=null) {
|
||
String authorization = (login + ':' + password);
|
||
String charsetName = Charsets.UTF_8.name();
|
||
encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes(charsetName)), charsetName);
|
||
} else {// anonymous access
|
||
encodedAuthorization = null;
|
||
}
|
||
}
|
||
|
||
this.rateLimitHandler = rateLimitHandler;
|
||
this.abuseLimitHandler = abuseLimitHandler;
|
||
|
||
if (login==null && encodedAuthorization!=null)
|
||
login = getMyself().getLogin();
|
||
this.login = login;
|
||
}
|
||
|
||
/**
|
||
* Obtains the credential from "~/.github" or from the System Environment Properties.
|
||
*/
|
||
public static GitHub connect() throws IOException {
|
||
return GitHubBuilder.fromCredentials().build();
|
||
}
|
||
|
||
/**
|
||
* Version that connects to GitHub Enterprise.
|
||
*
|
||
* @param apiUrl
|
||
* The URL of GitHub (or GitHub enterprise) API endpoint, such as "https://api.github.com" or
|
||
* "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <tt>/api/v3</tt> in the URL.
|
||
* For historical reasons, this parameter still accepts the bare domain name, but that's considered deprecated.
|
||
*/
|
||
public static GitHub connectToEnterprise(String apiUrl, String oauthAccessToken) throws IOException {
|
||
return new GitHubBuilder().withEndpoint(apiUrl).withOAuthToken(oauthAccessToken).build();
|
||
}
|
||
|
||
public static GitHub connectToEnterprise(String apiUrl, String login, String password) throws IOException {
|
||
return new GitHubBuilder().withEndpoint(apiUrl).withPassword(login, password).build();
|
||
}
|
||
|
||
public static GitHub connect(String login, String oauthAccessToken) throws IOException {
|
||
return new GitHubBuilder().withOAuthToken(oauthAccessToken, login).build();
|
||
}
|
||
|
||
/**
|
||
* @deprecated
|
||
* Either OAuth token or password is sufficient, so there's no point in passing both.
|
||
* Use {@link #connectUsingPassword(String, String)} or {@link #connectUsingOAuth(String)}.
|
||
*/
|
||
public static GitHub connect(String login, String oauthAccessToken, String password) throws IOException {
|
||
return new GitHubBuilder().withOAuthToken(oauthAccessToken, login).withPassword(login, password).build();
|
||
}
|
||
|
||
public static GitHub connectUsingPassword(String login, String password) throws IOException {
|
||
return new GitHubBuilder().withPassword(login, password).build();
|
||
}
|
||
|
||
public static GitHub connectUsingOAuth(String oauthAccessToken) throws IOException {
|
||
return new GitHubBuilder().withOAuthToken(oauthAccessToken).build();
|
||
}
|
||
|
||
public static GitHub connectUsingOAuth(String githubServer, String oauthAccessToken) throws IOException {
|
||
return new GitHubBuilder().withEndpoint(githubServer).withOAuthToken(oauthAccessToken).build();
|
||
}
|
||
/**
|
||
* Connects to GitHub anonymously.
|
||
*
|
||
* All operations that requires authentication will fail.
|
||
*/
|
||
public static GitHub connectAnonymously() throws IOException {
|
||
return new GitHubBuilder().build();
|
||
}
|
||
|
||
/**
|
||
* Connects to GitHub Enterprise anonymously.
|
||
*
|
||
* All operations that requires authentication will fail.
|
||
*/
|
||
public static GitHub connectToEnterpriseAnonymously(String apiUrl) throws IOException {
|
||
return new GitHubBuilder().withEndpoint(apiUrl).build();
|
||
}
|
||
|
||
/**
|
||
* An offline-only {@link GitHub} useful for parsing event notification from an unknown source.
|
||
*
|
||
* All operations that require a connection will fail.
|
||
*
|
||
* @return An offline-only {@link GitHub}.
|
||
*/
|
||
public static GitHub offline() {
|
||
try {
|
||
return new GitHubBuilder()
|
||
.withEndpoint("https://api.github.invalid")
|
||
.withConnector(HttpConnector.OFFLINE)
|
||
.build();
|
||
} catch (IOException e) {
|
||
throw new IllegalStateException("The offline implementation constructor should not connect", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Is this an anonymous connection
|
||
* @return {@code true} if operations that require authentication will fail.
|
||
*/
|
||
public boolean isAnonymous() {
|
||
return login==null && encodedAuthorization==null;
|
||
}
|
||
|
||
/**
|
||
* Is this an always offline "connection".
|
||
* @return {@code true} if this is an always offline "connection".
|
||
*/
|
||
public boolean isOffline() {
|
||
return connector == HttpConnector.OFFLINE;
|
||
}
|
||
|
||
public HttpConnector getConnector() {
|
||
return connector;
|
||
}
|
||
|
||
/**
|
||
* Sets the custom connector used to make requests to GitHub.
|
||
*/
|
||
public void setConnector(HttpConnector connector) {
|
||
this.connector = connector;
|
||
}
|
||
|
||
/*package*/ void requireCredential() {
|
||
if (isAnonymous())
|
||
throw new IllegalStateException("This operation requires a credential but none is given to the GitHub constructor");
|
||
}
|
||
|
||
/*package*/ URL getApiURL(String tailApiUrl) throws IOException {
|
||
if (tailApiUrl.startsWith("/")) {
|
||
if ("github.com".equals(apiUrl)) {// backward compatibility
|
||
return new URL(GITHUB_URL + tailApiUrl);
|
||
} else {
|
||
return new URL(apiUrl + tailApiUrl);
|
||
}
|
||
} else {
|
||
return new URL(tailApiUrl);
|
||
}
|
||
}
|
||
|
||
/*package*/ Requester retrieve() {
|
||
return new Requester(this).method("GET");
|
||
}
|
||
|
||
/**
|
||
* Gets the current rate limit.
|
||
*/
|
||
public GHRateLimit getRateLimit() throws IOException {
|
||
try {
|
||
return retrieve().to("/rate_limit", JsonRateLimit.class).rate;
|
||
} catch (FileNotFoundException e) {
|
||
// GitHub Enterprise doesn't have the rate limit, so in that case
|
||
// return some big number that's not too big.
|
||
// see issue #78
|
||
GHRateLimit r = new GHRateLimit();
|
||
r.limit = r.remaining = 1000000;
|
||
long hours = 1000L * 60 * 60;
|
||
r.reset = new Date(System.currentTimeMillis() + 1 * hours );
|
||
return r;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets the {@link GHUser} that represents yourself.
|
||
*/
|
||
@WithBridgeMethods(GHUser.class)
|
||
public GHMyself getMyself() throws IOException {
|
||
requireCredential();
|
||
|
||
GHMyself u = retrieve().to("/user", GHMyself.class);
|
||
|
||
u.root = this;
|
||
users.put(u.getLogin(), u);
|
||
|
||
return u;
|
||
}
|
||
|
||
/**
|
||
* Obtains the object that represents the named user.
|
||
*/
|
||
public GHUser getUser(String login) throws IOException {
|
||
GHUser u = users.get(login);
|
||
if (u == null) {
|
||
u = retrieve().to("/users/" + login, GHUser.class);
|
||
u.root = this;
|
||
users.put(u.getLogin(), u);
|
||
}
|
||
return u;
|
||
}
|
||
|
||
|
||
/**
|
||
* clears all cached data in order for external changes (modifications and del
|
||
*/
|
||
public void refreshCache() {
|
||
users.clear();
|
||
orgs.clear();
|
||
}
|
||
|
||
/**
|
||
* Interns the given {@link GHUser}.
|
||
*/
|
||
protected GHUser getUser(GHUser orig) {
|
||
GHUser u = users.get(orig.getLogin());
|
||
if (u==null) {
|
||
orig.root = this;
|
||
users.put(orig.getLogin(),orig);
|
||
return orig;
|
||
}
|
||
return u;
|
||
}
|
||
|
||
public GHOrganization getOrganization(String name) throws IOException {
|
||
GHOrganization o = orgs.get(name);
|
||
if (o==null) {
|
||
o = retrieve().to("/orgs/" + name, GHOrganization.class).wrapUp(this);
|
||
orgs.put(name,o);
|
||
}
|
||
return o;
|
||
}
|
||
|
||
/**
|
||
* Gets the repository object from 'user/reponame' string that GitHub calls as "repository name"
|
||
*
|
||
* @see GHRepository#getName()
|
||
*/
|
||
public GHRepository getRepository(String name) throws IOException {
|
||
String[] tokens = name.split("/");
|
||
return retrieve().to("/repos/" + tokens[0] + '/' + tokens[1], GHRepository.class).wrap(this);
|
||
}
|
||
/**
|
||
* Returns a list of popular open source licenses
|
||
*
|
||
* WARNING: This uses a PREVIEW API.
|
||
*
|
||
* @see <a href="https://developer.github.com/v3/licenses/">GitHub API - Licenses</a>
|
||
*
|
||
* @return a list of popular open source licenses
|
||
*/
|
||
@Preview @Deprecated
|
||
public PagedIterable<GHLicense> listLicenses() throws IOException {
|
||
return new PagedIterable<GHLicense>() {
|
||
public PagedIterator<GHLicense> _iterator(int pageSize) {
|
||
return new PagedIterator<GHLicense>(retrieve().withPreview(DRAX).asIterator("/licenses", GHLicense[].class, pageSize)) {
|
||
@Override
|
||
protected void wrapUp(GHLicense[] page) {
|
||
for (GHLicense c : page)
|
||
c.wrap(GitHub.this);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Returns a list of all users.
|
||
*/
|
||
public PagedIterable<GHUser> listUsers() throws IOException {
|
||
return new PagedIterable<GHUser>() {
|
||
public PagedIterator<GHUser> _iterator(int pageSize) {
|
||
return new PagedIterator<GHUser>(retrieve().asIterator("/users", GHUser[].class, pageSize)) {
|
||
@Override
|
||
protected void wrapUp(GHUser[] page) {
|
||
for (GHUser u : page)
|
||
u.wrapUp(GitHub.this);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Returns the full details for a license
|
||
*
|
||
* WARNING: This uses a PREVIEW API.
|
||
*
|
||
* @param key The license key provided from the API
|
||
* @return The license details
|
||
* @throws IOException
|
||
* @see GHLicense#getKey()
|
||
*/
|
||
@Preview @Deprecated
|
||
public GHLicense getLicense(String key) throws IOException {
|
||
return retrieve().withPreview(DRAX).to("/licenses/" + key, GHLicense.class);
|
||
}
|
||
|
||
/**
|
||
|
||
/**
|
||
* This method returns a shallowly populated organizations.
|
||
*
|
||
* To retrieve full organization details, you need to call {@link #getOrganization(String)}
|
||
* TODO: make this automatic.
|
||
*/
|
||
public Map<String, GHOrganization> getMyOrganizations() throws IOException {
|
||
GHOrganization[] orgs = retrieve().to("/user/orgs", GHOrganization[].class);
|
||
Map<String, GHOrganization> r = new HashMap<String, GHOrganization>();
|
||
for (GHOrganization o : orgs) {
|
||
// don't put 'o' into orgs because they are shallow
|
||
r.put(o.getLogin(),o.wrapUp(this));
|
||
}
|
||
return r;
|
||
}
|
||
|
||
/**
|
||
* Gets complete map of organizations/teams that current user belongs to.
|
||
*
|
||
* Leverages the new GitHub API /user/teams made available recently to
|
||
* get in a single call the complete set of organizations, teams and permissions
|
||
* in a single call.
|
||
*/
|
||
public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
|
||
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
|
||
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
|
||
team.wrapUp(this);
|
||
String orgLogin = team.getOrganization().getLogin();
|
||
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
|
||
if (teamsPerOrg == null) {
|
||
teamsPerOrg = new HashSet<GHTeam>();
|
||
}
|
||
teamsPerOrg.add(team);
|
||
allMyTeams.put(orgLogin, teamsPerOrg);
|
||
}
|
||
return allMyTeams;
|
||
}
|
||
|
||
/**
|
||
* Public events visible to you. Equivalent of what's displayed on https://github.com/
|
||
*/
|
||
public List<GHEventInfo> getEvents() throws IOException {
|
||
GHEventInfo[] events = retrieve().to("/events", GHEventInfo[].class);
|
||
for (GHEventInfo e : events)
|
||
e.wrapUp(this);
|
||
return Arrays.asList(events);
|
||
}
|
||
|
||
/**
|
||
* Gets a sigle gist by ID.
|
||
*/
|
||
public GHGist getGist(String id) throws IOException {
|
||
return retrieve().to("/gists/"+id,GHGist.class).wrapUp(this);
|
||
}
|
||
|
||
public GHGistBuilder createGist() {
|
||
return new GHGistBuilder(this);
|
||
}
|
||
|
||
/**
|
||
* Parses the GitHub event object.
|
||
*
|
||
* This is primarily intended for receiving a POST HTTP call from a hook.
|
||
* Unfortunately, hook script payloads aren't self-descriptive, so you need
|
||
* to know the type of the payload you are expecting.
|
||
*/
|
||
public <T extends GHEventPayload> T parseEventPayload(Reader r, Class<T> type) throws IOException {
|
||
T t = MAPPER.readValue(r, type);
|
||
t.wrapUp(this);
|
||
return t;
|
||
}
|
||
|
||
/**
|
||
* Creates a new repository.
|
||
*
|
||
* @return
|
||
* Newly created repository.
|
||
* @deprecated
|
||
* Use {@link #createRepository(String)} that uses a builder pattern to let you control every aspect.
|
||
*/
|
||
public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException {
|
||
return createRepository(name).description(description).homepage(homepage).private_(!isPublic).create();
|
||
}
|
||
|
||
/**
|
||
* Starts a builder that creates a new repository.
|
||
*
|
||
* <p>
|
||
* You use the returned builder to set various properties, then call {@link GHCreateRepositoryBuilder#create()}
|
||
* to finally createa repository.
|
||
*
|
||
* <p>
|
||
* To create a repository in an organization, see
|
||
* {@link GHOrganization#createRepository(String, String, String, GHTeam, boolean)}
|
||
*/
|
||
public GHCreateRepositoryBuilder createRepository(String name) {
|
||
return new GHCreateRepositoryBuilder(this,"/user/repos",name);
|
||
}
|
||
|
||
/**
|
||
* Creates a new authorization.
|
||
*
|
||
* The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future.
|
||
*
|
||
* @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">Documentation</a>
|
||
*/
|
||
public GHAuthorization createToken(Collection<String> scope, String note, String noteUrl) throws IOException{
|
||
Requester requester = new Requester(this)
|
||
.with("scopes", scope)
|
||
.with("note", note)
|
||
.with("note_url", noteUrl);
|
||
|
||
return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
|
||
}
|
||
|
||
/**
|
||
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#get-or-create-an-authorization-for-a-specific-app">docs</a>
|
||
*/
|
||
public GHAuthorization createOrGetAuth(String clientId, String clientSecret, List<String> scopes, String note,
|
||
String note_url)
|
||
throws IOException {
|
||
Requester requester = new Requester(this)
|
||
.with("client_secret", clientSecret)
|
||
.with("scopes", scopes)
|
||
.with("note", note)
|
||
.with("note_url", note_url);
|
||
|
||
return requester.method("PUT").to("/authorizations/clients/" + clientId, GHAuthorization.class);
|
||
}
|
||
|
||
/**
|
||
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#delete-an-authorization">Delete an authorization</a>
|
||
*/
|
||
public void deleteAuth(long id) throws IOException {
|
||
retrieve().method("DELETE").to("/authorizations/" + id);
|
||
}
|
||
|
||
/**
|
||
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#check-an-authorization">Check an authorization</a>
|
||
*/
|
||
public GHAuthorization checkAuth(@Nonnull String clientId, @Nonnull String accessToken) throws IOException {
|
||
return retrieve().to("/applications/" + clientId + "/tokens/" + accessToken, GHAuthorization.class);
|
||
}
|
||
|
||
/**
|
||
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#reset-an-authorization">Reset an authorization</a>
|
||
*/
|
||
public GHAuthorization resetAuth(@Nonnull String clientId, @Nonnull String accessToken) throws IOException {
|
||
return retrieve().method("POST").to("/applications/" + clientId + "/tokens/" + accessToken, GHAuthorization.class);
|
||
}
|
||
|
||
/**
|
||
* Ensures that the credential is valid.
|
||
*/
|
||
public boolean isCredentialValid() throws IOException {
|
||
try {
|
||
retrieve().to("/user", GHUser.class);
|
||
return true;
|
||
} catch (IOException e) {
|
||
if (LOGGER.isLoggable(FINE))
|
||
LOGGER.log(FINE, "Exception validating credentials on " + this.apiUrl + " with login '" + this.login + "' " + e, e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static class GHApiInfo {
|
||
private String rate_limit_url;
|
||
|
||
void check(String apiUrl) throws IOException {
|
||
if (rate_limit_url==null)
|
||
throw new IOException(apiUrl+" doesn't look like GitHub API URL");
|
||
|
||
// make sure that the URL is legitimate
|
||
new URL(rate_limit_url);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Tests the connection.
|
||
*
|
||
* <p>
|
||
* Verify that the API URL and credentials are valid to access this GitHub.
|
||
*
|
||
* <p>
|
||
* This method returns normally if the endpoint is reachable and verified to be GitHub API URL.
|
||
* Otherwise this method throws {@link IOException} to indicate the problem.
|
||
*/
|
||
public void checkApiUrlValidity() throws IOException {
|
||
try {
|
||
retrieve().to("/", GHApiInfo.class).check(apiUrl);
|
||
} catch (IOException e) {
|
||
if (isPrivateModeEnabled()) {
|
||
throw (IOException)new IOException("GitHub Enterprise server (" + apiUrl + ") with private mode enabled").initCause(e);
|
||
}
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ensures if a GitHub Enterprise server is configured in private mode.
|
||
*
|
||
* @return {@code true} if private mode is enabled. If it tries to use this method with GitHub, returns {@code
|
||
* false}.
|
||
*/
|
||
private boolean isPrivateModeEnabled() {
|
||
try {
|
||
HttpURLConnection uc = getConnector().connect(getApiURL("/"));
|
||
/*
|
||
$ curl -i https://github.mycompany.com/api/v3/
|
||
HTTP/1.1 401 Unauthorized
|
||
Server: GitHub.com
|
||
Date: Sat, 05 Mar 2016 19:45:01 GMT
|
||
Content-Type: application/json; charset=utf-8
|
||
Content-Length: 130
|
||
Status: 401 Unauthorized
|
||
X-GitHub-Media-Type: github.v3
|
||
X-XSS-Protection: 1; mode=block
|
||
X-Frame-Options: deny
|
||
Content-Security-Policy: default-src 'none'
|
||
Access-Control-Allow-Credentials: true
|
||
Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
|
||
Access-Control-Allow-Origin: *
|
||
X-GitHub-Request-Id: dbc70361-b11d-4131-9a7f-674b8edd0411
|
||
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
|
||
X-Content-Type-Options: nosniff
|
||
*/
|
||
return uc.getResponseCode() == HTTP_UNAUTHORIZED
|
||
&& uc.getHeaderField("X-GitHub-Media-Type") != null;
|
||
} catch (IOException e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Search issues.
|
||
*/
|
||
public GHIssueSearchBuilder searchIssues() {
|
||
return new GHIssueSearchBuilder(this);
|
||
}
|
||
|
||
/**
|
||
* Search users.
|
||
*/
|
||
public GHUserSearchBuilder searchUsers() {
|
||
return new GHUserSearchBuilder(this);
|
||
}
|
||
|
||
/**
|
||
* Search repositories.
|
||
*/
|
||
public GHRepositorySearchBuilder searchRepositories() {
|
||
return new GHRepositorySearchBuilder(this);
|
||
}
|
||
|
||
/**
|
||
* Search content.
|
||
*/
|
||
public GHContentSearchBuilder searchContent() {
|
||
return new GHContentSearchBuilder(this);
|
||
}
|
||
|
||
/**
|
||
* List all the notifications.
|
||
*/
|
||
public GHNotificationStream listNotifications() {
|
||
return new GHNotificationStream(this,"/notifications");
|
||
}
|
||
|
||
/**
|
||
* This provides a dump of every public repository, in the order that they were created.
|
||
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
|
||
*/
|
||
public PagedIterable<GHRepository> listAllPublicRepositories() {
|
||
return listAllPublicRepositories(null);
|
||
}
|
||
|
||
/**
|
||
* This provides a dump of every public repository, in the order that they were created.
|
||
*
|
||
* @param since
|
||
* The integer ID of the last Repository that you’ve seen. See {@link GHRepository#getId()}
|
||
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
|
||
*/
|
||
public PagedIterable<GHRepository> listAllPublicRepositories(final String since) {
|
||
return new PagedIterable<GHRepository>() {
|
||
public PagedIterator<GHRepository> _iterator(int pageSize) {
|
||
return new PagedIterator<GHRepository>(retrieve().with("since",since).asIterator("/repositories", GHRepository[].class, pageSize)) {
|
||
@Override
|
||
protected void wrapUp(GHRepository[] page) {
|
||
for (GHRepository c : page)
|
||
c.wrap(GitHub.this);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Render a Markdown document in raw mode.
|
||
*
|
||
* <p>
|
||
* It takes a Markdown document as plaintext and renders it as plain Markdown
|
||
* without a repository context (just like a README.md file is rendered – this
|
||
* is the simplest way to preview a readme online).
|
||
*
|
||
* @see GHRepository#renderMarkdown(String, MarkdownMode)
|
||
*/
|
||
public Reader renderMarkdown(String text) throws IOException {
|
||
return new InputStreamReader(
|
||
new Requester(this)
|
||
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
|
||
.contentType("text/plain;charset=UTF-8")
|
||
.asStream("/markdown/raw"),
|
||
"UTF-8");
|
||
}
|
||
|
||
/*package*/ static URL parseURL(String s) {
|
||
try {
|
||
return s==null ? null : new URL(s);
|
||
} catch (MalformedURLException e) {
|
||
throw new IllegalStateException("Invalid URL: "+s);
|
||
}
|
||
}
|
||
|
||
/*package*/ static Date parseDate(String timestamp) {
|
||
if (timestamp==null) return null;
|
||
for (String f : TIME_FORMATS) {
|
||
try {
|
||
SimpleDateFormat df = new SimpleDateFormat(f);
|
||
df.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||
return df.parse(timestamp);
|
||
} catch (ParseException e) {
|
||
// try next
|
||
}
|
||
}
|
||
throw new IllegalStateException("Unable to parse the timestamp: "+timestamp);
|
||
}
|
||
|
||
/*package*/ static String printDate(Date dt) {
|
||
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(dt);
|
||
}
|
||
|
||
/*package*/ static final ObjectMapper MAPPER = new ObjectMapper();
|
||
|
||
private static final String[] TIME_FORMATS = {"yyyy/MM/dd HH:mm:ss ZZZZ","yyyy-MM-dd'T'HH:mm:ss'Z'"};
|
||
|
||
static {
|
||
MAPPER.setVisibilityChecker(new Std(NONE, NONE, NONE, NONE, ANY));
|
||
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||
}
|
||
|
||
/* package */ static final String GITHUB_URL = "https://api.github.com";
|
||
|
||
private static final Logger LOGGER = Logger.getLogger(GitHub.class.getName());
|
||
}
|