diff --git a/bin/tests/scenarios/github-api-matching-representation/base.java b/bin/tests/scenarios/github-api-matching-representation/base.java new file mode 100644 index 0000000..2488c98 --- /dev/null +++ b/bin/tests/scenarios/github-api-matching-representation/base.java @@ -0,0 +1,446 @@ +/* + * 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.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +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.List; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; + +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; + +/** + * Root of the GitHub API. + * + * @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 users = new HashMap(); + private final Map orgs = new HashMap(); + + private final String apiUrl; + + private HttpConnector connector = HttpConnector.DEFAULT; + + /** + * Connects to GitHub.com + */ + private GitHub(String login, String oauthAccessToken, String password) throws IOException { + this (GITHUB_URL, login, oauthAccessToken, password); + } + + /** + * Creates a client API root object. + * + *

+ * Several different combinations of the login/oauthAccessToken/password parameters are allowed + * to represent different ways of authentication. + * + *

+ *
Loging anonymously + *
Leave all three parameters null and you will be making HTTP requests without any authentication. + * + *
Log in with password + *
Specify the login and password, then leave oauthAccessToken null. + * This will use the HTTP BASIC auth with the GitHub API. + * + *
Log in with OAuth token + *
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. + *
+ * + * @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 /api/v3 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 + */ + private GitHub(String apiUrl, String login, String oauthAccessToken, String password) throws IOException { + if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize + this.apiUrl = apiUrl; + + if (oauthAccessToken!=null) { + encodedAuthorization = "token "+oauthAccessToken; + } else { + if (password!=null) { + String authorization = (login + ':' + password); + encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes())); + } else {// anonymous access + encodedAuthorization = null; + } + } + + if (login==null && encodedAuthorization!=null) + login = getMyself().getLogin(); + this.login = login; + } + + /** + * Obtains the credential from "~/.github" + */ + public static GitHub connect() throws IOException { + Properties props = new Properties(); + File homeDir = new File(System.getProperty("user.home")); + FileInputStream in = new FileInputStream(new File(homeDir, ".github")); + try { + props.load(in); + } finally { + IOUtils.closeQuietly(in); + } + return new GitHub(GITHUB_URL,props.getProperty("login"), props.getProperty("oauth"),props.getProperty("password")); + } + + /** + * 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 /api/v3 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 connectUsingOAuth(apiUrl, oauthAccessToken); + } + + public static GitHub connectToEnterprise(String apiUrl, String login, String password) throws IOException { + return new GitHub(apiUrl, login, null, password); + } + + public static GitHub connect(String login, String oauthAccessToken) throws IOException { + return new GitHub(login,oauthAccessToken,null); + } + + /** + * @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 GitHub(login,oauthAccessToken,password); + } + + public static GitHub connectUsingPassword(String login, String password) throws IOException { + return new GitHub(login,null,password); + } + + public static GitHub connectUsingOAuth(String oauthAccessToken) throws IOException { + return new GitHub(null, oauthAccessToken, null); + } + + public static GitHub connectUsingOAuth(String githubServer, String oauthAccessToken) throws IOException { + return new GitHub(githubServer,null, oauthAccessToken,null); + } + /** + * Connects to GitHub anonymously. + * + * All operations that requires authentication will fail. + */ + public static GitHub connectAnonymously() throws IOException { + return new GitHub(null,null,null); + } + + /** + * Is this an anonymous connection + * @return {@code true} if operations that require authentication will fail. + */ + public boolean isAnonymous() { + return login==null && encodedAuthorization==null; + } + + 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; + 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; + } + + /** + * Interns the given {@link GHUser}. + */ + protected GHUser getUser(GHUser orig) throws IOException { + GHUser u = users.get(orig.getLogin()); + if (u==null) { + orig.root = this; + users.put(login,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); + } + + /** + * 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 getMyOrganizations() throws IOException { + GHOrganization[] orgs = retrieve().to("/user/orgs", GHOrganization[].class); + Map r = new HashMap(); + for (GHOrganization o : orgs) { + // don't put 'o' into orgs because they are shallow + r.put(o.getLogin(),o.wrapUp(this)); + } + return r; + } + + /** + * Public events visible to you. Equivalent of what's displayed on https://github.com/ + */ + public List getEvents() throws IOException { + // TODO: pagination + 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 parseEventPayload(Reader r, Class type) throws IOException { + T t = MAPPER.readValue(r, type); + t.wrapUp(this); + return t; + } + + /** + * Creates a new repository. + * + * To create a repository in an organization, see + * {@link GHOrganization#createRepository(String, String, String, GHTeam, boolean)} + * + * @return + * Newly created repository. + */ + public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException { + Requester requester = new Requester(this) + .with("name", name).with("description", description).with("homepage", homepage) + .with("public", isPublic ? 1 : 0); + return requester.method("POST").to("/user/repos", GHRepository.class).wrap(this); + } + + /** + * Creates a new authorization. + * + * The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future. + * + * @see Documentation + */ + public GHAuthorization createToken(Collection 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); + } + + /** + * Ensures that the credential is valid. + */ + public boolean isCredentialValid() throws IOException { + try { + retrieve().to("/user", GHUser.class); + return true; + } catch (IOException e) { + return false; + } + } + + /*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); + } + + private static final String GITHUB_URL = "https://api.github.com"; +} diff --git a/bin/tests/scenarios/github-api-matching-representation/left.java b/bin/tests/scenarios/github-api-matching-representation/left.java new file mode 100644 index 0000000..136daf0 --- /dev/null +++ b/bin/tests/scenarios/github-api-matching-representation/left.java @@ -0,0 +1,471 @@ +/* + * 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 java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +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.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; + +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; + +/** + * Root of the GitHub API. + * + * @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 users = new HashMap(); + private final Map orgs = new HashMap(); + + private final String apiUrl; + + private HttpConnector connector = HttpConnector.DEFAULT; + + /** + * Connects to GitHub.com + */ + private GitHub(String login, String oauthAccessToken, String password) throws IOException { + this (GITHUB_URL, login, oauthAccessToken, password); + } + + /** + * Creates a client API root object. + * + *

+ * Several different combinations of the login/oauthAccessToken/password parameters are allowed + * to represent different ways of authentication. + * + *

+ *
Loging anonymously + *
Leave all three parameters null and you will be making HTTP requests without any authentication. + * + *
Log in with password + *
Specify the login and password, then leave oauthAccessToken null. + * This will use the HTTP BASIC auth with the GitHub API. + * + *
Log in with OAuth token + *
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. + *
+ * + * @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 /api/v3 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 + */ + private GitHub(String apiUrl, String login, String oauthAccessToken, String password) throws IOException { + if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize + this.apiUrl = apiUrl; + + if (oauthAccessToken!=null) { + encodedAuthorization = "token "+oauthAccessToken; + } else { + if (password!=null) { + String authorization = (login + ':' + password); + encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes())); + } else {// anonymous access + encodedAuthorization = null; + } + } + + if (login==null && encodedAuthorization!=null) + login = getMyself().getLogin(); + this.login = login; + } + + /** + * Obtains the credential from "~/.github" + */ + public static GitHub connect() throws IOException { + Properties props = new Properties(); + File homeDir = new File(System.getProperty("user.home")); + FileInputStream in = new FileInputStream(new File(homeDir, ".github")); + try { + props.load(in); + } finally { + IOUtils.closeQuietly(in); + } + return new GitHub(GITHUB_URL,props.getProperty("login"), props.getProperty("oauth"),props.getProperty("password")); + } + + /** + * 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 /api/v3 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 connectUsingOAuth(apiUrl, oauthAccessToken); + } + + public static GitHub connectToEnterprise(String apiUrl, String login, String password) throws IOException { + return new GitHub(apiUrl, login, null, password); + } + + public static GitHub connect(String login, String oauthAccessToken) throws IOException { + return new GitHub(login,oauthAccessToken,null); + } + + /** + * @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 GitHub(login,oauthAccessToken,password); + } + + public static GitHub connectUsingPassword(String login, String password) throws IOException { + return new GitHub(login,null,password); + } + + public static GitHub connectUsingOAuth(String oauthAccessToken) throws IOException { + return new GitHub(null, oauthAccessToken, null); + } + + public static GitHub connectUsingOAuth(String githubServer, String oauthAccessToken) throws IOException { + return new GitHub(githubServer,null, oauthAccessToken,null); + } + /** + * Connects to GitHub anonymously. + * + * All operations that requires authentication will fail. + */ + public static GitHub connectAnonymously() throws IOException { + return new GitHub(null,null,null); + } + + /** + * Is this an anonymous connection + * @return {@code true} if operations that require authentication will fail. + */ + public boolean isAnonymous() { + return login==null && encodedAuthorization==null; + } + + 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; + 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; + } + + /** + * Interns the given {@link GHUser}. + */ + protected GHUser getUser(GHUser orig) throws IOException { + GHUser u = users.get(orig.getLogin()); + if (u==null) { + orig.root = this; + users.put(login,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); + } + + /** + * 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 getMyOrganizations() throws IOException { + GHOrganization[] orgs = retrieve().to("/user/orgs", GHOrganization[].class); + Map r = new HashMap(); + 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> getMyTeams() throws IOException { + Map> allMyTeams = new HashMap>(); + for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) { + team.wrapUp(this); + String orgLogin = team.getOrganization().getLogin(); + Set teamsPerOrg = allMyTeams.get(orgLogin); + if (teamsPerOrg == null) { + teamsPerOrg = new HashSet(); + } + 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 getEvents() throws IOException { + // TODO: pagination + 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 parseEventPayload(Reader r, Class type) throws IOException { + T t = MAPPER.readValue(r, type); + t.wrapUp(this); + return t; + } + + /** + * Creates a new repository. + * + * To create a repository in an organization, see + * {@link GHOrganization#createRepository(String, String, String, GHTeam, boolean)} + * + * @return + * Newly created repository. + */ + public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException { + Requester requester = new Requester(this) + .with("name", name).with("description", description).with("homepage", homepage) + .with("public", isPublic ? 1 : 0); + return requester.method("POST").to("/user/repos", GHRepository.class).wrap(this); + } + + /** + * Creates a new authorization. + * + * The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future. + * + * @see Documentation + */ + public GHAuthorization createToken(Collection 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); + } + + /** + * Ensures that the credential is valid. + */ + public boolean isCredentialValid() throws IOException { + try { + retrieve().to("/user", GHUser.class); + return true; + } catch (IOException e) { + return false; + } + } + + /*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); + } + + private static final String GITHUB_URL = "https://api.github.com"; +} diff --git a/bin/tests/scenarios/github-api-matching-representation/merge.java b/bin/tests/scenarios/github-api-matching-representation/merge.java new file mode 100644 index 0000000..65c5566 --- /dev/null +++ b/bin/tests/scenarios/github-api-matching-representation/merge.java @@ -0,0 +1 @@ + package org . kohsuke . github ; import static com . fasterxml . jackson . annotation . JsonAutoDetect . Visibility . ANY ; import static com . fasterxml . jackson . annotation . JsonAutoDetect . Visibility . NONE ; import java . io . File ; import java . io . FileInputStream ; import java . io . FileNotFoundException ; import java . io . IOException ; import java . io . Reader ; 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 . List ; import java . util . Map ; import java . util . Properties ; import java . util . Set ; import java . util . TimeZone ; import org . apache . commons . codec . binary . Base64 ; import org . apache . commons . io . IOUtils ; 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 ; public class GitHub { final String login ; final String encodedAuthorization ; private final Map < String , GHUser > users = new HashMap < String , GHUser > ( ) ; private final Map < String , GHOrganization > orgs = new HashMap < String , GHOrganization > ( ) ; private final String apiUrl ; private HttpConnector connector = HttpConnector . DEFAULT ; private GitHub ( String login , String oauthAccessToken , String password ) throws IOException { this ( GITHUB_URL , login , oauthAccessToken , password ) ; } private GitHub ( String apiUrl , String login , String oauthAccessToken , String password ) throws IOException { if ( apiUrl . endsWith ( "/" ) ) apiUrl = apiUrl . substring ( 0 , apiUrl . length ( ) - 1 ) ; this . apiUrl = apiUrl ; if ( oauthAccessToken != null ) { encodedAuthorization = "token " + oauthAccessToken ; } else { if ( password != null ) { String authorization = ( login + ':' + password ) ; encodedAuthorization = "Basic " + new String ( Base64 . encodeBase64 ( authorization . getBytes ( ) ) ) ; } else { encodedAuthorization = null ; } } if ( login == null && encodedAuthorization != null ) login = getMyself ( ) . getLogin ( ) ; this . login = login ; } public static GitHub connect ( ) throws IOException { Properties props = new Properties ( ) ; File homeDir = new File ( System . getProperty ( "user.home" ) ) ; FileInputStream in = new FileInputStream ( new File ( homeDir , ".github" ) ) ; try { props . load ( in ) ; } finally { IOUtils . closeQuietly ( in ) ; } return new GitHub ( GITHUB_URL , props . getProperty ( "login" ) , props . getProperty ( "oauth" ) , props . getProperty ( "password" ) ) ; } public static GitHub connectToEnterprise ( String apiUrl , String oauthAccessToken ) throws IOException { return connectUsingOAuth ( apiUrl , oauthAccessToken ) ; } public static GitHub connectToEnterprise ( String apiUrl , String login , String password ) throws IOException { return new GitHub ( apiUrl , login , null , password ) ; } public static GitHub connect ( String login , String oauthAccessToken ) throws IOException { return new GitHub ( login , oauthAccessToken , null ) ; } public static GitHub connect ( String login , String oauthAccessToken , String password ) throws IOException { return new GitHub ( login , oauthAccessToken , password ) ; } public static GitHub connectUsingPassword ( String login , String password ) throws IOException { return new GitHub ( login , null , password ) ; } public static GitHub connectUsingOAuth ( String oauthAccessToken ) throws IOException { return new GitHub ( null , oauthAccessToken , null ) ; } public static GitHub connectUsingOAuth ( String githubServer , String oauthAccessToken ) throws IOException { return new GitHub ( githubServer , null , oauthAccessToken , null ) ; } public static GitHub connectAnonymously ( ) throws IOException { return new GitHub ( null , null , null ) ; } public boolean isAnonymous ( ) { return login == null && encodedAuthorization == null ; } public HttpConnector getConnector ( ) { return connector ; } public void setConnector ( HttpConnector connector ) { this . connector = connector ; } void requireCredential ( ) { if ( isAnonymous ( ) ) throw new IllegalStateException ( "This operation requires a credential but none is given to the GitHub constructor" ) ; } URL getApiURL ( String tailApiUrl ) throws IOException { if ( tailApiUrl . startsWith ( "/" ) ) { if ( "github.com" . equals ( apiUrl ) ) { return new URL ( GITHUB_URL + tailApiUrl ) ; } else { return new URL ( apiUrl + tailApiUrl ) ; } } else { return new URL ( tailApiUrl ) ; } } Requester retrieve ( ) { return new Requester ( this ) . method ( "GET" ) ; } public GHRateLimit getRateLimit ( ) throws IOException { try { return retrieve ( ) . to ( "/rate_limit" , JsonRateLimit . class ) . rate ; } catch ( FileNotFoundException e ) { GHRateLimit r = new GHRateLimit ( ) ; r . limit = r . remaining = 1000000 ; return r ; } } @ 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 ; } 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 ; } protected GHUser getUser ( GHUser orig ) throws IOException { GHUser u = users . get ( orig . getLogin ( ) ) ; if ( u == null ) { orig . root = this ; users . put ( login , 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 ; } public GHRepository getRepository ( String name ) throws IOException { String [ ] tokens = name . split ( "/" ) ; return retrieve ( ) . to ( "/repos/" + tokens [ 0 ] + '/' + tokens [ 1 ] , GHRepository . class ) . wrap ( this ) ; } 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 ) { r . put ( o . getLogin ( ) , o . wrapUp ( this ) ) ; } return r ; } 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 List < GHEventInfo > getEvents ( ) throws IOException { GHEventInfo [ ] events = retrieve ( ) . to ( "/events" , GHEventInfo [ ] . class ) ; for ( GHEventInfo e : events ) e . wrapUp ( this ) ; return Arrays . asList ( events ) ; } public GHGist getGist ( String id ) throws IOException { return retrieve ( ) . to ( "/gists/" + id , GHGist . class ) . wrapUp ( this ) ; } public GHGistBuilder createGist ( ) { return new GHGistBuilder ( this ) ; } public < T extends GHEventPayload > T parseEventPayload ( Reader r , Class < T > type ) throws IOException { T t = MAPPER . readValue ( r , type ) ; t . wrapUp ( this ) ; return t ; } public GHRepository createRepository ( String name , String description , String homepage , boolean isPublic ) throws IOException { Requester requester = new Requester ( this ) . with ( "name" , name ) . with ( "description" , description ) . with ( "homepage" , homepage ) . with ( "public" , isPublic ? 1 : 0 ) ; return requester . method ( "POST" ) . to ( "/user/repos" , GHRepository . class ) . wrap ( this ) ; } 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 ) ; } public boolean isCredentialValid ( ) throws IOException { try { retrieve ( ) . to ( "/user" , GHUser . class ) ; return true ; } catch ( IOException e ) { return false ; } } static URL parseURL ( String s ) { try { return s == null ? null : new URL ( s ) ; } catch ( MalformedURLException e ) { throw new IllegalStateException ( "Invalid URL: " + s ) ; } } 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 ) { } } throw new IllegalStateException ( "Unable to parse the timestamp: " + timestamp ) ; } static String printDate ( Date dt ) { return new SimpleDateFormat ( "yyyy-MM-dd'T'HH:mm:ss'Z'" ) . format ( dt ) ; } 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 ) ; } private static final String GITHUB_URL = "https://api.github.com" ; public void refreshCache ( ) { users . clear ( ) ; orgs . clear ( ) ; } } \ No newline at end of file diff --git a/bin/tests/scenarios/github-api-matching-representation/right.java b/bin/tests/scenarios/github-api-matching-representation/right.java new file mode 100644 index 0000000..dce87ef --- /dev/null +++ b/bin/tests/scenarios/github-api-matching-representation/right.java @@ -0,0 +1,454 @@ +/* + * 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.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +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.List; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; + +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; + +/** + * Root of the GitHub API. + * + * @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 users = new HashMap(); + private final Map orgs = new HashMap(); + + private final String apiUrl; + + private HttpConnector connector = HttpConnector.DEFAULT; + + /** + * Connects to GitHub.com + */ + private GitHub(String login, String oauthAccessToken, String password) throws IOException { + this (GITHUB_URL, login, oauthAccessToken, password); + } + + /** + * Creates a client API root object. + * + *

+ * Several different combinations of the login/oauthAccessToken/password parameters are allowed + * to represent different ways of authentication. + * + *

+ *
Loging anonymously + *
Leave all three parameters null and you will be making HTTP requests without any authentication. + * + *
Log in with password + *
Specify the login and password, then leave oauthAccessToken null. + * This will use the HTTP BASIC auth with the GitHub API. + * + *
Log in with OAuth token + *
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. + *
+ * + * @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 /api/v3 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 + */ + private GitHub(String apiUrl, String login, String oauthAccessToken, String password) throws IOException { + if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize + this.apiUrl = apiUrl; + + if (oauthAccessToken!=null) { + encodedAuthorization = "token "+oauthAccessToken; + } else { + if (password!=null) { + String authorization = (login + ':' + password); + encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes())); + } else {// anonymous access + encodedAuthorization = null; + } + } + + if (login==null && encodedAuthorization!=null) + login = getMyself().getLogin(); + this.login = login; + } + + /** + * Obtains the credential from "~/.github" + */ + public static GitHub connect() throws IOException { + Properties props = new Properties(); + File homeDir = new File(System.getProperty("user.home")); + FileInputStream in = new FileInputStream(new File(homeDir, ".github")); + try { + props.load(in); + } finally { + IOUtils.closeQuietly(in); + } + return new GitHub(GITHUB_URL,props.getProperty("login"), props.getProperty("oauth"),props.getProperty("password")); + } + + /** + * 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 /api/v3 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 connectUsingOAuth(apiUrl, oauthAccessToken); + } + + public static GitHub connectToEnterprise(String apiUrl, String login, String password) throws IOException { + return new GitHub(apiUrl, login, null, password); + } + + public static GitHub connect(String login, String oauthAccessToken) throws IOException { + return new GitHub(login,oauthAccessToken,null); + } + + /** + * @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 GitHub(login,oauthAccessToken,password); + } + + public static GitHub connectUsingPassword(String login, String password) throws IOException { + return new GitHub(login,null,password); + } + + public static GitHub connectUsingOAuth(String oauthAccessToken) throws IOException { + return new GitHub(null, oauthAccessToken, null); + } + + public static GitHub connectUsingOAuth(String githubServer, String oauthAccessToken) throws IOException { + return new GitHub(githubServer,null, oauthAccessToken,null); + } + /** + * Connects to GitHub anonymously. + * + * All operations that requires authentication will fail. + */ + public static GitHub connectAnonymously() throws IOException { + return new GitHub(null,null,null); + } + + /** + * Is this an anonymous connection + * @return {@code true} if operations that require authentication will fail. + */ + public boolean isAnonymous() { + return login==null && encodedAuthorization==null; + } + + 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; + 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) throws IOException { + GHUser u = users.get(orig.getLogin()); + if (u==null) { + orig.root = this; + users.put(login,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); + } + + /** + * 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 getMyOrganizations() throws IOException { + GHOrganization[] orgs = retrieve().to("/user/orgs", GHOrganization[].class); + Map r = new HashMap(); + for (GHOrganization o : orgs) { + // don't put 'o' into orgs because they are shallow + r.put(o.getLogin(),o.wrapUp(this)); + } + return r; + } + + /** + * Public events visible to you. Equivalent of what's displayed on https://github.com/ + */ + public List 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 parseEventPayload(Reader r, Class type) throws IOException { + T t = MAPPER.readValue(r, type); + t.wrapUp(this); + return t; + } + + /** + * Creates a new repository. + * + * To create a repository in an organization, see + * {@link GHOrganization#createRepository(String, String, String, GHTeam, boolean)} + * + * @return + * Newly created repository. + */ + public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException { + Requester requester = new Requester(this) + .with("name", name).with("description", description).with("homepage", homepage) + .with("public", isPublic ? 1 : 0); + return requester.method("POST").to("/user/repos", GHRepository.class).wrap(this); + } + + /** + * Creates a new authorization. + * + * The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future. + * + * @see Documentation + */ + public GHAuthorization createToken(Collection 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); + } + + /** + * Ensures that the credential is valid. + */ + public boolean isCredentialValid() throws IOException { + try { + retrieve().to("/user", GHUser.class); + return true; + } catch (IOException e) { + return false; + } + } + + /*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); + } + + private static final String GITHUB_URL = "https://api.github.com"; +}