From 16a0f8ece00c5a97a49643606e16b9af774b1828 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 3 Jun 2016 00:19:15 -0700 Subject: [PATCH 01/21] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index eccf95f7fd..067ee826ce 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.76 + 1.77-SNAPSHOT GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -16,7 +16,7 @@ scm:git:git@github.com/kohsuke/${project.artifactId}.git scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git http://${project.artifactId}.kohsuke.org/ - github-api-1.76 + HEAD From 0415326d0935ae63c1f4cf56b8a29f6f810dad71 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 3 Jun 2016 20:27:29 -0700 Subject: [PATCH 02/21] Issue #261: handle 204 no content correctly --- src/main/java/org/kohsuke/github/Requester.java | 4 ++++ src/test/java/Foo.java | 8 ++++---- src/test/java/org/kohsuke/github/RepositoryTest.java | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index dfbee6256f..62fb78339f 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -512,6 +512,10 @@ private T parse(Class type, T instance) throws IOException { if (responseCode == 304) { return null; // special case handling for 304 unmodified, as the content will be "" } + if (responseCode == 204 && type.isArray()) { + // no content + return type.cast(Array.newInstance(type.getComponentType(),0)); + } r = new InputStreamReader(wrapStream(uc.getInputStream()), "UTF-8"); String data = IOUtils.toString(r); diff --git a/src/test/java/Foo.java b/src/test/java/Foo.java index a29baac4b4..35e5eb5955 100644 --- a/src/test/java/Foo.java +++ b/src/test/java/Foo.java @@ -1,4 +1,5 @@ import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHRepository.Contributor; import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; @@ -9,11 +10,10 @@ */ public class Foo { public static void main(String[] args) throws Exception { - Collection lst = GitHub.connect().getUser("kohsuke").getRepositories().values(); - for (GHRepository r : lst) { - System.out.println(r.getName()); + GitHub gh = GitHub.connect(); + for (Contributor c : gh.getRepository("kohsuke/yo").listContributors()) { + System.out.println(c); } - System.out.println(lst.size()); } private static void testRateLimit() throws Exception { diff --git a/src/test/java/org/kohsuke/github/RepositoryTest.java b/src/test/java/org/kohsuke/github/RepositoryTest.java index 61cdc09dca..dae2892a45 100644 --- a/src/test/java/org/kohsuke/github/RepositoryTest.java +++ b/src/test/java/org/kohsuke/github/RepositoryTest.java @@ -50,4 +50,12 @@ public void listLanguages() throws IOException { String mainLanguage = r.getLanguage(); assertTrue(r.listLanguages().containsKey(mainLanguage)); } + + @Test // Issue #261 + public void listEmptyContributors() throws IOException { + GitHub gh = GitHub.connect(); + for (Contributor c : gh.getRepository("github-api-test-org/empty").listContributors()) { + System.out.println(c); + } + } } From c7f6889534b01f60da441bcadef8036ecc4317bb Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 3 Jun 2016 20:31:12 -0700 Subject: [PATCH 03/21] Issue #258: updated OkHttp that handles Cache control headers better --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 067ee826ce..fa6bbacf9b 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ com.squareup.okhttp okhttp-urlconnection - 2.0.0 + 2.7.5 true From a2f0837d14147ff2a2fa87de404381c9fa383bff Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 3 Jun 2016 20:56:17 -0700 Subject: [PATCH 04/21] Issue #279: added another overload that takes permission --- src/main/java/org/kohsuke/github/GHTeam.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kohsuke/github/GHTeam.java b/src/main/java/org/kohsuke/github/GHTeam.java index 172f859657..ae950aed70 100644 --- a/src/main/java/org/kohsuke/github/GHTeam.java +++ b/src/main/java/org/kohsuke/github/GHTeam.java @@ -124,7 +124,13 @@ public void remove(GHUser u) throws IOException { } public void add(GHRepository r) throws IOException { - org.root.retrieve().method("PUT").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); + add(r,null); + } + + public void add(GHRepository r, GHOrganization.Permission permission) throws IOException { + org.root.retrieve().method("PUT") + .with("permission",permission) + .to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); } public void remove(GHRepository r) throws IOException { From 9f5a6ee5497896ccf1a5e097833b533fb9325de1 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Thu, 21 Jul 2016 13:53:23 -0700 Subject: [PATCH 05/21] Implement an abuse handler If too many requests are made within X amount of time (not the traditional hourly rate limit), github may begin returning 403. Then we should wait for a bit to attempt to access the API again. In this case, we parse out the Retry-After field returned and sleep until that (it's usually 60 seconds) --- src/main/java/org/kohsuke/github/GitHub.java | 4 +++- .../org/kohsuke/github/GitHubBuilder.java | 3 ++- .../org/kohsuke/github/RateLimitHandler.java | 21 +++++++++++++++++++ .../java/org/kohsuke/github/Requester.java | 8 +++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 22792a8798..47388c93f2 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -83,6 +83,7 @@ public class GitHub { private final String apiUrl; /*package*/ final RateLimitHandler rateLimitHandler; + /*package*/ final RateLimitHandler abuseLimitHandler; private HttpConnector connector = HttpConnector.DEFAULT; @@ -122,7 +123,7 @@ public class GitHub { * @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) throws IOException { + /* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler, RateLimitHandler abuseLimitHandler) throws IOException { if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize this.apiUrl = apiUrl; if (null != connector) this.connector = connector; @@ -140,6 +141,7 @@ public class GitHub { } this.rateLimitHandler = rateLimitHandler; + this.abuseLimitHandler = abuseLimitHandler; if (login==null && encodedAuthorization!=null) login = getMyself().getLogin(); diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java index 2abe16f9c1..9ff603e199 100644 --- a/src/main/java/org/kohsuke/github/GitHubBuilder.java +++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java @@ -30,6 +30,7 @@ public class GitHubBuilder { private HttpConnector connector; private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT; + private RateLimitHandler abuseLimitHandler = RateLimitHandler.ABUSE; public GitHubBuilder() { } @@ -193,6 +194,6 @@ public HttpURLConnection connect(URL url) throws IOException { } public GitHub build() throws IOException { - return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler); + return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler, abuseLimitHandler); } } diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java index b00624ce2b..8ff47a5ab4 100644 --- a/src/main/java/org/kohsuke/github/RateLimitHandler.java +++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java @@ -48,6 +48,27 @@ private long parseWaitTime(HttpURLConnection uc) { return Math.max(10000, Long.parseLong(v)*1000 - System.currentTimeMillis()); } }; + + /** + * Wait until the API abuse "wait time" is passed. + */ + public static final RateLimitHandler ABUSE = new RateLimitHandler() { + @Override + public void onError(IOException e, HttpURLConnection uc) throws IOException { + try { + Thread.sleep(parseWaitTime(uc)); + } catch (InterruptedException _) { + throw (InterruptedIOException)new InterruptedIOException().initCause(e); + } + } + + private long parseWaitTime(HttpURLConnection uc) { + String v = uc.getHeaderField("Retry-After"); + if (v==null) return 60 * 1000; // can't tell, return 1 min + + return Math.max(1000, Long.parseLong(v)*1000); + } + }; /** * Fail immediately. diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 62fb78339f..a0d5e27196 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -572,6 +572,14 @@ private InputStream wrapStream(InputStream in) throws IOException { root.rateLimitHandler.onError(e,uc); return; } + + // Check to see whether we hit a 403, and the Retry-After field is + // available. + if (responseCode == HttpURLConnection.HTTP_FORBIDDEN && + uc.getHeaderField("Retry-After") != null) { + this.root.abuseLimitHandler.onError(e,uc); + return; + } InputStream es = wrapStream(uc.getErrorStream()); try { From 856cf5e56887685b2979e3177ee5574e77cd1ac0 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 19:58:04 -0700 Subject: [PATCH 06/21] Better type safety by splitting RateLimitHandler and AbuseLimitHandler While the signature is the same, headers that they expect are different, so any non-trivial logic cannot be reused. --- .../org/kohsuke/github/AbuseLimitHandler.java | 63 +++++++++++++++++++ src/main/java/org/kohsuke/github/GitHub.java | 4 +- .../org/kohsuke/github/GitHubBuilder.java | 6 +- .../org/kohsuke/github/RateLimitHandler.java | 22 +------ .../java/org/kohsuke/github/Requester.java | 7 +-- 5 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/AbuseLimitHandler.java diff --git a/src/main/java/org/kohsuke/github/AbuseLimitHandler.java b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java new file mode 100644 index 0000000000..1a158ee140 --- /dev/null +++ b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java @@ -0,0 +1,63 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; + +/** + * Pluggable strategy to determine what to do when the API abuse limit is hit. + * + * @author Kohsuke Kawaguchi + * @see GitHubBuilder#withAbuseLimitHandler(AbuseLimitHandler) + * @see documentation + * @see RateLimitHandler + */ +public abstract class AbuseLimitHandler { + /** + * Called when the library encounters HTTP error indicating that the API rate limit is reached. + * + *

+ * Any exception thrown from this method will cause the request to fail, and the caller of github-api + * will receive an exception. If this method returns normally, another request will be attempted. + * For that to make sense, the implementation needs to wait for some time. + * + * @see API documentation from GitHub + * @param e + * Exception from Java I/O layer. If you decide to fail the processing, you can throw + * this exception (or wrap this exception into another exception and throw it.) + * @param uc + * Connection that resulted in an error. Useful for accessing other response headers. + */ + public abstract void onError(IOException e, HttpURLConnection uc) throws IOException; + + /** + * Wait until the API abuse "wait time" is passed. + */ + public static final AbuseLimitHandler WAIT = new AbuseLimitHandler() { + @Override + public void onError(IOException e, HttpURLConnection uc) throws IOException { + try { + Thread.sleep(parseWaitTime(uc)); + } catch (InterruptedException _) { + throw (InterruptedIOException)new InterruptedIOException().initCause(e); + } + } + + private long parseWaitTime(HttpURLConnection uc) { + String v = uc.getHeaderField("Retry-After"); + if (v==null) return 60 * 1000; // can't tell, return 1 min + + return Math.max(1000, Long.parseLong(v)*1000); + } + }; + + /** + * Fail immediately. + */ + public static final AbuseLimitHandler FAIL = new AbuseLimitHandler() { + @Override + public void onError(IOException e, HttpURLConnection uc) throws IOException { + throw (IOException)new IOException("Abust limit reached").initCause(e); + } + }; +} diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 47388c93f2..297596d87e 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -83,7 +83,7 @@ public class GitHub { private final String apiUrl; /*package*/ final RateLimitHandler rateLimitHandler; - /*package*/ final RateLimitHandler abuseLimitHandler; + /*package*/ final AbuseLimitHandler abuseLimitHandler; private HttpConnector connector = HttpConnector.DEFAULT; @@ -123,7 +123,7 @@ public class GitHub { * @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, RateLimitHandler abuseLimitHandler) throws IOException { + /* 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; diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java index 9ff603e199..62a99a6272 100644 --- a/src/main/java/org/kohsuke/github/GitHubBuilder.java +++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java @@ -30,7 +30,7 @@ public class GitHubBuilder { private HttpConnector connector; private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT; - private RateLimitHandler abuseLimitHandler = RateLimitHandler.ABUSE; + private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT; public GitHubBuilder() { } @@ -179,6 +179,10 @@ public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) { this.rateLimitHandler = handler; return this; } + public GitHubBuilder withAbuseLimitHandler(AbuseLimitHandler handler) { + this.abuseLimitHandler = handler; + return this; + } /** * Configures {@linkplain #withConnector(HttpConnector) connector} diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java index 8ff47a5ab4..053fd783e0 100644 --- a/src/main/java/org/kohsuke/github/RateLimitHandler.java +++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java @@ -9,6 +9,7 @@ * * @author Kohsuke Kawaguchi * @see GitHubBuilder#withRateLimitHandler(RateLimitHandler) + * @see AbuseLimitHandler */ public abstract class RateLimitHandler { /** @@ -48,27 +49,6 @@ private long parseWaitTime(HttpURLConnection uc) { return Math.max(10000, Long.parseLong(v)*1000 - System.currentTimeMillis()); } }; - - /** - * Wait until the API abuse "wait time" is passed. - */ - public static final RateLimitHandler ABUSE = new RateLimitHandler() { - @Override - public void onError(IOException e, HttpURLConnection uc) throws IOException { - try { - Thread.sleep(parseWaitTime(uc)); - } catch (InterruptedException _) { - throw (InterruptedIOException)new InterruptedIOException().initCause(e); - } - } - - private long parseWaitTime(HttpURLConnection uc) { - String v = uc.getHeaderField("Retry-After"); - if (v==null) return 60 * 1000; // can't tell, return 1 min - - return Math.max(1000, Long.parseLong(v)*1000); - } - }; /** * Fail immediately. diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index a0d5e27196..43c4e07694 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -572,10 +572,9 @@ private InputStream wrapStream(InputStream in) throws IOException { root.rateLimitHandler.onError(e,uc); return; } - - // Check to see whether we hit a 403, and the Retry-After field is - // available. - if (responseCode == HttpURLConnection.HTTP_FORBIDDEN && + + // Retry-After is not documented but apparently that field exists + if (responseCode == HttpURLConnection.HTTP_FORBIDDEN && uc.getHeaderField("Retry-After") != null) { this.root.abuseLimitHandler.onError(e,uc); return; From d82397a173cb0d576edc0bf02505dcefebac43ae Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:00:05 -0700 Subject: [PATCH 07/21] doc fix --- src/main/java/org/kohsuke/github/AbuseLimitHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kohsuke/github/AbuseLimitHandler.java b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java index 1a158ee140..4cb6bfe6c5 100644 --- a/src/main/java/org/kohsuke/github/AbuseLimitHandler.java +++ b/src/main/java/org/kohsuke/github/AbuseLimitHandler.java @@ -14,14 +14,14 @@ */ public abstract class AbuseLimitHandler { /** - * Called when the library encounters HTTP error indicating that the API rate limit is reached. + * Called when the library encounters HTTP error indicating that the API abuse limit is reached. * *

* Any exception thrown from this method will cause the request to fail, and the caller of github-api * will receive an exception. If this method returns normally, another request will be attempted. * For that to make sense, the implementation needs to wait for some time. * - * @see API documentation from GitHub + * @see API documentation from GitHub * @param e * Exception from Java I/O layer. If you decide to fail the processing, you can throw * this exception (or wrap this exception into another exception and throw it.) From bb1cecb95bf24577ccebaaf321022caab5605fa2 Mon Sep 17 00:00:00 2001 From: Duncan Dickinson Date: Fri, 5 Aug 2016 20:11:33 -0700 Subject: [PATCH 08/21] PR-284: license API support Had to do git-diff | git-apply to avoid whitespe changes to GHRepository --- .../java/org/kohsuke/github/GHLicense.java | 110 +++++++++ .../org/kohsuke/github/GHLicenseBase.java | 107 +++++++++ .../java/org/kohsuke/github/GHRepository.java | 48 ++++ src/main/java/org/kohsuke/github/GitHub.java | 28 +++ .../github/extras/PreviewHttpConnector.java | 67 ++++++ .../org/kohsuke/github/GHLicenseTest.java | 220 ++++++++++++++++++ 6 files changed, 580 insertions(+) create mode 100644 src/main/java/org/kohsuke/github/GHLicense.java create mode 100644 src/main/java/org/kohsuke/github/GHLicenseBase.java create mode 100644 src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java create mode 100644 src/test/java/org/kohsuke/github/GHLicenseTest.java diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java new file mode 100644 index 0000000000..baeb6c3d7d --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -0,0 +1,110 @@ +/* + * The MIT License + * + * Copyright (c) 2016, Duncan Dickinson + * + * 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * The GitHub Preview API's license information + *

+ * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @author Duncan Dickinson + * @see GitHub#getLicense(String) + * @see GHRepository#getFullLicense() + * @see https://developer.github.com/v3/licenses/ + */ +@SuppressWarnings({"UnusedDeclaration"}) +@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", + "NP_UNWRITTEN_FIELD"}, justification = "JSON API") +public class GHLicense extends GHLicenseBase { + + protected String html_url, description, category, implementation, body; + + protected List required = new ArrayList(); + protected List permitted = new ArrayList(); + protected List forbidden = new ArrayList(); + + public URL getHtmlUrl() { + return GitHub.parseURL(html_url); + } + + public String getDescription() { + return description; + } + + public String getCategory() { + return category; + } + + public String getImplementation() { + return implementation; + } + + public List getRequired() { + return required; + } + + public List getPermitted() { + return permitted; + } + + public List getForbidden() { + return forbidden; + } + + public String getBody() { + return body; + } + + @Override + public String toString() { + return "GHLicense{" + + "html_url='" + html_url + '\'' + + ", description='" + description + '\'' + + ", category='" + category + '\'' + + ", implementation='" + implementation + '\'' + + ", body='" + body + '\'' + + ", required=" + required + + ", permitted=" + permitted + + ", forbidden=" + forbidden + + ", htmlUrl=" + getHtmlUrl() + + "} " + super.toString(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/src/main/java/org/kohsuke/github/GHLicenseBase.java b/src/main/java/org/kohsuke/github/GHLicenseBase.java new file mode 100644 index 0000000000..e6c92359d8 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHLicenseBase.java @@ -0,0 +1,107 @@ +/* + * The MIT License + * + * Copyright (c) 2016, Duncan Dickinson + * + * 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 com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.net.URL; + +/** + * The basic information for GitHub API licenses - as use in a number of + * API calls that only return the basic details + *

+ * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @author Duncan Dickinson + * @see https://developer.github.com/v3/licenses/ + * @see GitHub#listLicenses() + * @see GHRepository#getLicense() + * @see GHLicense GHLicense subclass for the more comprehensive listing of properties + */ +@SuppressWarnings({"UnusedDeclaration"}) +@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", + "NP_UNWRITTEN_FIELD"}, justification = "JSON API") +public class GHLicenseBase { + + protected String key, name, url; + protected Boolean featured; + + /** + * @return a mnemonic for the license + */ + public String getKey() { + return key; + } + + /** + * @return the license name + */ + public String getName() { + return name; + } + + /** + * @return API URL of this object. + */ + @WithBridgeMethods(value = String.class, adapterMethod = "urlToString") + public URL getUrl() { + return GitHub.parseURL(url); + } + + /** + * Featured licenses are bold in the new repository drop-down + * + * @return True if the license is featured, false otherwise + */ + public Boolean isFeatured() { + return featured; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GHLicenseBase)) return false; + + GHLicenseBase that = (GHLicenseBase) o; + + return getUrl().toString().equals(that.getUrl().toString()); + } + + @Override + public int hashCode() { + return getUrl().toString().hashCode(); + } + + @Override + public String toString() { + return "GHLicenseBase{" + + "key='" + key + '\'' + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", featured=" + featured + + '}'; + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 41a44e9dab..0b7424311b 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -65,6 +65,16 @@ public class GHRepository extends GHObject { private String description, homepage, name, full_name; private String html_url; // this is the UI + /* + * The license information makes use of the preview API. + * + * See: https://developer.github.com/v3/licenses/ + */ + /** + * The basic license details as returned from {@link GitHub#getRepository(String)} + */ + private GHLicenseBase license; + private String git_url, ssh_url, clone_url, svn_url, mirror_url; private GHUser owner; // not fully populated. beware. private boolean has_issues, has_wiki, fork, has_downloads; @@ -839,6 +849,44 @@ protected void wrapUp(GHCommitComment[] page) { } }; } + * Gets the basic license details for the repository. + *

+ * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} + *

+ * Warning: Only returns the basic license details. Use {@link GitHub#getLicense(String)} + * to get the full license information (hint: pass it {@link GHLicenseBase#getKey()}). + * + * @throws IOException as usual but also if you don't use the preview connector + */ + public GHLicenseBase getLicense() { + return license; + } + + /** + * Access the full license details - makes an additional API call + *

+ * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @return the license details + * @throws IOException as usual but also if you don't use the preview connector + */ + public GHLicense getFullLicense() throws IOException { + return root.getLicense(license.getKey()); + } + + /** + * Retrieves the contents of the repository's license file - makes an additional API call + *

+ * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @return details regarding the license contents + * @throws IOException as usual but also if you don't use the preview connector + */ + public GHContent getLicenseContent() throws IOException { + return root.retrieve().to(getApiTailUrl("license"), GHContent.class).wrap(this); + } + + /** /** * Lists all the commit statues attached to the given commit, newer ones first. diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 297596d87e..dd1e3593e3 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -56,6 +56,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; + import java.util.logging.Logger; /** @@ -339,6 +340,33 @@ 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 - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @see GitHub API - Licenses + * + * @return a list of popular open source licenses + * @throws IOException if the HttpConnector doesn't pass in the preview header or other IO issue + */ + public List listLicenses() throws IOException { + return Arrays.asList(retrieve().to("/licenses", GHLicenseBase[].class)); + } + + /** + * Returns the full details for a license + * + * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * + * @param key The license key provided from the API + * @return The license details + * @throws IOException + */ + public GHLicense getLicense(String key) throws IOException { + return retrieve().to("/licenses/" + key, GHLicense.class); + } + + /** /** * This method returns a shallowly populated organizations. diff --git a/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java b/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java new file mode 100644 index 0000000000..e1be7855f4 --- /dev/null +++ b/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java @@ -0,0 +1,67 @@ +/* + * Copyright $year slavinson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kohsuke.github.extras; + +import org.kohsuke.github.HttpConnector; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +public class PreviewHttpConnector implements HttpConnector { + private final HttpConnector base; + private final int readTimeout, connectTimeout; + + /** + * @param connectTimeout HTTP connection timeout in milliseconds + * @param readTimeout HTTP read timeout in milliseconds + */ + public PreviewHttpConnector(HttpConnector base, int connectTimeout, int readTimeout) { + this.base = base; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + } + + public PreviewHttpConnector(HttpConnector base, int timeout) { + this(base, timeout, timeout); + } + + public PreviewHttpConnector(HttpConnector base) { + this(base, ImpatientHttpConnector.CONNECT_TIMEOUT, ImpatientHttpConnector.READ_TIMEOUT); + } + + public PreviewHttpConnector() { + this(new HttpConnector() { + public HttpURLConnection connect(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + }, ImpatientHttpConnector.CONNECT_TIMEOUT, ImpatientHttpConnector.READ_TIMEOUT); + } + + public HttpURLConnection connect(URL url) throws IOException { + HttpURLConnection con = base.connect(url); + con.setConnectTimeout(connectTimeout); + con.setReadTimeout(readTimeout); + con.addRequestProperty("Accept", PREVIEW_MEDIA_TYPE); + return con; + } + + /** + * Default connection timeout in milliseconds + */ + public static final String PREVIEW_MEDIA_TYPE = "application/vnd.github.drax-preview+json"; +} diff --git a/src/test/java/org/kohsuke/github/GHLicenseTest.java b/src/test/java/org/kohsuke/github/GHLicenseTest.java new file mode 100644 index 0000000000..e27f8057cd --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHLicenseTest.java @@ -0,0 +1,220 @@ +/* + * The MIT License + * + * Copyright (c) 2016, Duncan Dickinson + * + * 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 org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.kohsuke.github.extras.PreviewHttpConnector; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +/** + * @author Duncan Dickinson + */ +public class GHLicenseTest extends Assert { + private GitHub gitHub; + + @Before + public void setUp() throws Exception { + gitHub = new GitHubBuilder() + .fromCredentials() + .withConnector(new PreviewHttpConnector()) + .build(); + } + + /** + * Basic test to ensure that the list of licenses from {@link GitHub#listLicenses()} is returned + * + * @throws IOException + */ + @Test + public void listLicenses() throws IOException { + List licenses = gitHub.listLicenses(); + assertTrue(licenses.size() > 0); + } + + /** + * Tests that {@link GitHub#listLicenses()} returns the MIT license + * in the expected manner. + * + * @throws IOException + */ + @Test + public void listLicensesCheckIndividualLicense() throws IOException { + List licenses = gitHub.listLicenses(); + for (GHLicenseBase lic : licenses) { + if (lic.getKey().equals("mit")) { + assertTrue(lic.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); + return; + } + } + fail("The MIT license was not found"); + } + + /** + * Checks that the request for an individual license using {@link GitHub#getLicense(String)} + * returns expected values (not all properties are checked) + * + * @throws IOException + */ + @Test + public void getLicense() throws IOException { + String key = "mit"; + GHLicense license = gitHub.getLicense(key); + assertNotNull(license); + assertTrue("The name is correct", license.getName().equals("MIT License")); + assertTrue("The HTML URL is correct", license.getHtmlUrl().equals(new URL("http://choosealicense.com/licenses/mit/"))); + } + + /** + * Attempts to list the licenses with a non-preview connection + * + * @throws IOException is expected to be thrown + */ + @Test(expected = IOException.class) + public void ListLicensesWithoutPreviewConnection() throws IOException { + GitHub.connect().listLicenses(); + } + + /** + * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} + * and checks that the license is correct + * + * @throws IOException + */ + @Test + public void checkRepositoryLicense() throws IOException { + GHRepository repo = gitHub.getRepository("kohsuke/github-api"); + GHLicenseBase license = repo.getLicense(); + assertNotNull("The license is populated", license); + assertTrue("The key is correct", license.getKey().equals("mit")); + assertTrue("The name is correct", license.getName().equals("MIT License")); + assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); + } + + /** + * Accesses the 'atom/atom' repo using {@link GitHub#getRepository(String)} + * and checks that the license is correct + * + * @throws IOException + */ + @Test + public void checkRepositoryLicenseAtom() throws IOException { + GHRepository repo = gitHub.getRepository("atom/atom"); + GHLicenseBase license = repo.getLicense(); + assertNotNull("The license is populated", license); + assertTrue("The key is correct", license.getKey().equals("mit")); + assertTrue("The name is correct", license.getName().equals("MIT License")); + assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); + } + + /** + * Accesses the 'pomes/pomes' repo using {@link GitHub#getRepository(String)} + * and checks that the license is correct + * + * @throws IOException + */ + @Test + public void checkRepositoryLicensePomes() throws IOException { + GHRepository repo = gitHub.getRepository("pomes/pomes"); + GHLicenseBase license = repo.getLicense(); + assertNotNull("The license is populated", license); + assertTrue("The key is correct", license.getKey().equals("apache-2.0")); + assertTrue("The name is correct", license.getName().equals("Apache License 2.0")); + assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/apache-2.0"))); + } + + /** + * Accesses the 'dedickinson/test-repo' repo using {@link GitHub#getRepository(String)} + * and checks that *no* license is returned as the repo doesn't have one + * + * @throws IOException + */ + @Test + public void checkRepositoryWithoutLicense() throws IOException { + GHRepository repo = gitHub.getRepository("dedickinson/test-repo"); + GHLicenseBase license = repo.getLicense(); + assertNull("There is no license", license); + } + + /** + * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} + * and then calls {@link GHRepository#getFullLicense()} and checks that certain + * properties are correct + * + * @throws IOException + */ + @Test + public void checkRepositoryFullLicense() throws IOException { + GHRepository repo = gitHub.getRepository("kohsuke/github-api"); + GHLicense license = repo.getFullLicense(); + assertNotNull("The license is populated", license); + assertTrue("The key is correct", license.getKey().equals("mit")); + assertTrue("The name is correct", license.getName().equals("MIT License")); + assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); + assertTrue("The HTML URL is correct", license.getHtmlUrl().equals(new URL("http://choosealicense.com/licenses/mit/"))); + } + + /** + * Accesses the 'pomes/pomes' repo using {@link GitHub#getRepository(String)} + * and then calls {@link GHRepository#getLicenseContent()} and checks that certain + * properties are correct + * + * @throws IOException + */ + @Test + public void checkRepositoryLicenseContent() throws IOException { + GHRepository repo = gitHub.getRepository("pomes/pomes"); + GHContent content = repo.getLicenseContent(); + assertNotNull("The license content is populated", content); + assertTrue("The type is 'file'", content.getType().equals("file")); + assertTrue("The license file is 'LICENSE'", content.getName().equals("LICENSE")); + + if (content.getEncoding().equals("base64")) { + String licenseText = new String(IOUtils.toByteArray(content.read())); + assertTrue("The license appears to be an Apache License", licenseText.contains("Apache License")); + } else { + fail("Expected the license to be Base64 encoded but instead it was " + content.getEncoding()); + } + } + + /** + * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} + * but without using {@link PreviewHttpConnector} and ensures that the {@link GHRepository#getLicense()} + * call just returns null rather than raising an exception. This should indicate that + * non-preview connection requests aren't affected by the change in functionality + * + * @throws IOException + */ + @Test + public void checkRepositoryLicenseWithoutPreviewConnection() throws IOException { + GHRepository repo = GitHub.connect().getRepository("kohsuke/github-api"); + assertNull(repo.getLicense()); + } +} From 1de02a509948d2d0dea8a38b135a4bc83709217d Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:19:36 -0700 Subject: [PATCH 09/21] Added a marker for preview APIs --- src/main/java/org/kohsuke/github/GHBranch.java | 3 +++ src/main/java/org/kohsuke/github/Preview.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/main/java/org/kohsuke/github/Preview.java diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index 1de47a156e..f4e90f6556 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -53,6 +53,7 @@ public String getSHA1() { /** * Disables branch protection and allows anyone with push access to push changes. */ + @Preview @Deprecated public void disableProtection() throws IOException { BranchProtection bp = new BranchProtection(); bp.enabled = false; @@ -64,6 +65,7 @@ public void disableProtection() throws IOException { * * @see GHCommitStatus#getContext() */ + @Preview @Deprecated public void enableProtection(EnforcementLevel level, Collection contexts) throws IOException { BranchProtection bp = new BranchProtection(); bp.enabled = true; @@ -73,6 +75,7 @@ public void enableProtection(EnforcementLevel level, Collection contexts setProtection(bp); } + @Preview @Deprecated public void enableProtection(EnforcementLevel level, String... contexts) throws IOException { enableProtection(level, Arrays.asList(contexts)); } diff --git a/src/main/java/org/kohsuke/github/Preview.java b/src/main/java/org/kohsuke/github/Preview.java new file mode 100644 index 0000000000..2eeed60456 --- /dev/null +++ b/src/main/java/org/kohsuke/github/Preview.java @@ -0,0 +1,18 @@ +package org.kohsuke.github; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Indicates that the method/class/etc marked maps to GitHub API in the preview period. + * + * These APIs are subject to change and not a part of the backward compatibility commitment. + * Always used in conjunction with 'deprecated' to raise awareness to clients. + * + * @author Kohsuke Kawaguchi + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Preview { +} From 0cf4211aa54b74a489e49ca1b6c53b7c61c1b04b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:36:46 -0700 Subject: [PATCH 10/21] Merged GHLicense & GHLicenseBase Elsewhere in this library, whenever there are multiple forms of the same object, we map that to the same class and use lazy data retrieval to fill missing fields. --- .../kohsuke/github/GHContentWithLicense.java | 19 ++++ .../java/org/kohsuke/github/GHLicense.java | 103 ++++++++++++----- .../org/kohsuke/github/GHLicenseBase.java | 107 ------------------ .../java/org/kohsuke/github/GHObject.java | 2 +- .../java/org/kohsuke/github/GHRepository.java | 27 ++--- src/main/java/org/kohsuke/github/GitHub.java | 12 +- .../org/kohsuke/github/GHLicenseTest.java | 18 +-- 7 files changed, 123 insertions(+), 165 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/GHContentWithLicense.java delete mode 100644 src/main/java/org/kohsuke/github/GHLicenseBase.java diff --git a/src/main/java/org/kohsuke/github/GHContentWithLicense.java b/src/main/java/org/kohsuke/github/GHContentWithLicense.java new file mode 100644 index 0000000000..a7dfa843d3 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHContentWithLicense.java @@ -0,0 +1,19 @@ +package org.kohsuke.github; + +/** + * {@link GHContent} with license information. + * + * @author Kohsuke Kawaguchi + * @see documentation + * @see GHRepository#getLicense() + */ +@Preview @Deprecated +class GHContentWithLicense extends GHContent { + GHLicense license; + + @Override + GHContentWithLicense wrap(GHRepository owner) { + super.wrap(owner); + return this; + } +} diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index baeb6c3d7d..24ca8bbbaa 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -24,8 +24,10 @@ package org.kohsuke.github; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -33,17 +35,25 @@ /** * The GitHub Preview API's license information *

- * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * WARNING: This uses a PREVIEW API - subject to change. * * @author Duncan Dickinson * @see GitHub#getLicense(String) - * @see GHRepository#getFullLicense() + * @see GHRepository#getLicense() * @see https://developer.github.com/v3/licenses/ */ +@Preview @Deprecated @SuppressWarnings({"UnusedDeclaration"}) @SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API") -public class GHLicense extends GHLicenseBase { +public class GHLicense extends GHObject { + /*package almost final*/ GitHub root; + + // these fields are always present, even in the short form + protected String key, name; + + // the rest is only after populated + protected Boolean featured; protected String html_url, description, category, implementation, body; @@ -51,60 +61,101 @@ public class GHLicense extends GHLicenseBase { protected List permitted = new ArrayList(); protected List forbidden = new ArrayList(); - public URL getHtmlUrl() { + /** + * @return a mnemonic for the license + */ + public String getKey() { + return key; + } + + /** + * @return the license name + */ + public String getName() { + return name; + } + + /** + * @return API URL of this object. + */ + @WithBridgeMethods(value = String.class, adapterMethod = "urlToString") + public URL getUrl() { + return GitHub.parseURL(url); + } + + /** + * Featured licenses are bold in the new repository drop-down + * + * @return True if the license is featured, false otherwise + */ + public Boolean isFeatured() throws IOException { + populate(); + return featured; + } + + public URL getHtmlUrl() throws IOException { + populate(); return GitHub.parseURL(html_url); } - public String getDescription() { + public String getDescription() throws IOException { + populate(); return description; } - public String getCategory() { + public String getCategory() throws IOException { + populate(); return category; } - public String getImplementation() { + public String getImplementation() throws IOException { + populate(); return implementation; } - public List getRequired() { + public List getRequired() throws IOException { + populate(); return required; } - public List getPermitted() { + public List getPermitted() throws IOException { + populate(); return permitted; } - public List getForbidden() { + public List getForbidden() throws IOException { + populate(); return forbidden; } - public String getBody() { + public String getBody() throws IOException { + populate(); return body; } - @Override - public String toString() { - return "GHLicense{" + - "html_url='" + html_url + '\'' + - ", description='" + description + '\'' + - ", category='" + category + '\'' + - ", implementation='" + implementation + '\'' + - ", body='" + body + '\'' + - ", required=" + required + - ", permitted=" + permitted + - ", forbidden=" + forbidden + - ", htmlUrl=" + getHtmlUrl() + - "} " + super.toString(); + /** + * Fully populate the data by retrieving missing data. + * + * Depending on the original API call where this object is created, it may not contain everything. + */ + protected synchronized void populate() throws IOException { + if (description!=null) return; // already populated + + root.retrieve().to(url, this); } @Override public boolean equals(Object o) { - return super.equals(o); + if (this == o) return true; + if (!(o instanceof GHLicense)) return false; + + GHLicense that = (GHLicense) o; + return this.url.equals(that.url); } @Override public int hashCode() { - return super.hashCode(); + return url.hashCode(); } + } diff --git a/src/main/java/org/kohsuke/github/GHLicenseBase.java b/src/main/java/org/kohsuke/github/GHLicenseBase.java deleted file mode 100644 index e6c92359d8..0000000000 --- a/src/main/java/org/kohsuke/github/GHLicenseBase.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, Duncan Dickinson - * - * 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 com.infradna.tool.bridge_method_injector.WithBridgeMethods; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import java.net.URL; - -/** - * The basic information for GitHub API licenses - as use in a number of - * API calls that only return the basic details - *

- * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} - * - * @author Duncan Dickinson - * @see https://developer.github.com/v3/licenses/ - * @see GitHub#listLicenses() - * @see GHRepository#getLicense() - * @see GHLicense GHLicense subclass for the more comprehensive listing of properties - */ -@SuppressWarnings({"UnusedDeclaration"}) -@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", - "NP_UNWRITTEN_FIELD"}, justification = "JSON API") -public class GHLicenseBase { - - protected String key, name, url; - protected Boolean featured; - - /** - * @return a mnemonic for the license - */ - public String getKey() { - return key; - } - - /** - * @return the license name - */ - public String getName() { - return name; - } - - /** - * @return API URL of this object. - */ - @WithBridgeMethods(value = String.class, adapterMethod = "urlToString") - public URL getUrl() { - return GitHub.parseURL(url); - } - - /** - * Featured licenses are bold in the new repository drop-down - * - * @return True if the license is featured, false otherwise - */ - public Boolean isFeatured() { - return featured; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof GHLicenseBase)) return false; - - GHLicenseBase that = (GHLicenseBase) o; - - return getUrl().toString().equals(that.getUrl().toString()); - } - - @Override - public int hashCode() { - return getUrl().toString().hashCode(); - } - - @Override - public String toString() { - return "GHLicenseBase{" + - "key='" + key + '\'' + - ", name='" + name + '\'' + - ", url='" + url + '\'' + - ", featured=" + featured + - '}'; - } -} diff --git a/src/main/java/org/kohsuke/github/GHObject.java b/src/main/java/org/kohsuke/github/GHObject.java index 323e72cd29..a81d913a5c 100644 --- a/src/main/java/org/kohsuke/github/GHObject.java +++ b/src/main/java/org/kohsuke/github/GHObject.java @@ -51,7 +51,7 @@ public URL getUrl() { * URL of this object for humans, which renders some HTML. */ @WithBridgeMethods(value=String.class, adapterMethod="urlToString") - public abstract URL getHtmlUrl(); + public abstract URL getHtmlUrl() throws IOException; /** * When was this resource last updated? diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 0b7424311b..5430603363 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -70,10 +70,7 @@ public class GHRepository extends GHObject { * * See: https://developer.github.com/v3/licenses/ */ - /** - * The basic license details as returned from {@link GitHub#getRepository(String)} - */ - private GHLicenseBase license; + private GHLicense license; private String git_url, ssh_url, clone_url, svn_url, mirror_url; private GHUser owner; // not fully populated. beware. @@ -849,6 +846,8 @@ protected void wrapUp(GHCommitComment[] page) { } }; } + + /** * Gets the basic license details for the repository. *

* This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} @@ -858,20 +857,12 @@ protected void wrapUp(GHCommitComment[] page) { * * @throws IOException as usual but also if you don't use the preview connector */ - public GHLicenseBase getLicense() { - return license; - } - - /** - * Access the full license details - makes an additional API call - *

- * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} - * - * @return the license details - * @throws IOException as usual but also if you don't use the preview connector - */ - public GHLicense getFullLicense() throws IOException { - return root.getLicense(license.getKey()); + @Preview @Deprecated + public GHLicense getLicense() throws IOException{ + return root.retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .to(getApiTailUrl("license"), GHContentWithLicense.class) + .wrap(this).license; } /** diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index dd1e3593e3..a630e1f6d3 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -340,28 +340,32 @@ 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 - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * WARNING: This uses a PREVIEW API. * * @see GitHub API - Licenses * * @return a list of popular open source licenses * @throws IOException if the HttpConnector doesn't pass in the preview header or other IO issue */ - public List listLicenses() throws IOException { - return Arrays.asList(retrieve().to("/licenses", GHLicenseBase[].class)); + @Preview @Deprecated + public List listLicenses() throws IOException { + return Arrays.asList(retrieve().to("/licenses", GHLicense[].class)); } /** * Returns the full details for a license * - * WARNING: This uses a PREVIEW API - you must use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * 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().to("/licenses/" + key, GHLicense.class); } diff --git a/src/test/java/org/kohsuke/github/GHLicenseTest.java b/src/test/java/org/kohsuke/github/GHLicenseTest.java index e27f8057cd..43a97b5ed9 100644 --- a/src/test/java/org/kohsuke/github/GHLicenseTest.java +++ b/src/test/java/org/kohsuke/github/GHLicenseTest.java @@ -55,7 +55,7 @@ public void setUp() throws Exception { */ @Test public void listLicenses() throws IOException { - List licenses = gitHub.listLicenses(); + List licenses = gitHub.listLicenses(); assertTrue(licenses.size() > 0); } @@ -67,8 +67,8 @@ public void listLicenses() throws IOException { */ @Test public void listLicensesCheckIndividualLicense() throws IOException { - List licenses = gitHub.listLicenses(); - for (GHLicenseBase lic : licenses) { + List licenses = gitHub.listLicenses(); + for (GHLicense lic : licenses) { if (lic.getKey().equals("mit")) { assertTrue(lic.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); return; @@ -111,7 +111,7 @@ public void ListLicensesWithoutPreviewConnection() throws IOException { @Test public void checkRepositoryLicense() throws IOException { GHRepository repo = gitHub.getRepository("kohsuke/github-api"); - GHLicenseBase license = repo.getLicense(); + GHLicense license = repo.getLicense(); assertNotNull("The license is populated", license); assertTrue("The key is correct", license.getKey().equals("mit")); assertTrue("The name is correct", license.getName().equals("MIT License")); @@ -127,7 +127,7 @@ public void checkRepositoryLicense() throws IOException { @Test public void checkRepositoryLicenseAtom() throws IOException { GHRepository repo = gitHub.getRepository("atom/atom"); - GHLicenseBase license = repo.getLicense(); + GHLicense license = repo.getLicense(); assertNotNull("The license is populated", license); assertTrue("The key is correct", license.getKey().equals("mit")); assertTrue("The name is correct", license.getName().equals("MIT License")); @@ -143,7 +143,7 @@ public void checkRepositoryLicenseAtom() throws IOException { @Test public void checkRepositoryLicensePomes() throws IOException { GHRepository repo = gitHub.getRepository("pomes/pomes"); - GHLicenseBase license = repo.getLicense(); + GHLicense license = repo.getLicense(); assertNotNull("The license is populated", license); assertTrue("The key is correct", license.getKey().equals("apache-2.0")); assertTrue("The name is correct", license.getName().equals("Apache License 2.0")); @@ -159,13 +159,13 @@ public void checkRepositoryLicensePomes() throws IOException { @Test public void checkRepositoryWithoutLicense() throws IOException { GHRepository repo = gitHub.getRepository("dedickinson/test-repo"); - GHLicenseBase license = repo.getLicense(); + GHLicense license = repo.getLicense(); assertNull("There is no license", license); } /** * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} - * and then calls {@link GHRepository#getFullLicense()} and checks that certain + * and then calls {@link GHRepository#getLicense()} and checks that certain * properties are correct * * @throws IOException @@ -173,7 +173,7 @@ public void checkRepositoryWithoutLicense() throws IOException { @Test public void checkRepositoryFullLicense() throws IOException { GHRepository repo = gitHub.getRepository("kohsuke/github-api"); - GHLicense license = repo.getFullLicense(); + GHLicense license = repo.getLicense(); assertNotNull("The license is populated", license); assertTrue("The key is correct", license.getKey().equals("mit")); assertTrue("The name is correct", license.getName().equals("MIT License")); From 80aa75aab1af674d8e1662ec074c0fbc026ce290 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:40:23 -0700 Subject: [PATCH 11/21] Added the wrap() method for a backpointer --- src/main/java/org/kohsuke/github/GHContentWithLicense.java | 2 ++ src/main/java/org/kohsuke/github/GHLicense.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/org/kohsuke/github/GHContentWithLicense.java b/src/main/java/org/kohsuke/github/GHContentWithLicense.java index a7dfa843d3..bbd115e649 100644 --- a/src/main/java/org/kohsuke/github/GHContentWithLicense.java +++ b/src/main/java/org/kohsuke/github/GHContentWithLicense.java @@ -14,6 +14,8 @@ class GHContentWithLicense extends GHContent { @Override GHContentWithLicense wrap(GHRepository owner) { super.wrap(owner); + if (license!=null) + license.wrap(owner.root); return this; } } diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index 24ca8bbbaa..b4dec87d32 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -158,4 +158,8 @@ public int hashCode() { return url.hashCode(); } + /*package*/ GHLicense wrap(GitHub root) { + this.root = root; + return this; + } } From 07b527a0f297e4ae909ba7c324e7d9c09df53e48 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:40:34 -0700 Subject: [PATCH 12/21] Enumeration in GitHub should be PagedIterable --- src/main/java/org/kohsuke/github/GitHub.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index a630e1f6d3..8466ae6d54 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -348,11 +348,20 @@ public GHRepository getRepository(String name) throws IOException { * @see GitHub API - Licenses * * @return a list of popular open source licenses - * @throws IOException if the HttpConnector doesn't pass in the preview header or other IO issue */ @Preview @Deprecated - public List listLicenses() throws IOException { - return Arrays.asList(retrieve().to("/licenses", GHLicense[].class)); + public PagedIterable listLicenses() throws IOException { + return new PagedIterable() { + public PagedIterator _iterator(int pageSize) { + return new PagedIterator(retrieve().asIterator("/licenses", GHLicense[].class, pageSize)) { + @Override + protected void wrapUp(GHLicense[] page) { + for (GHLicense c : page) + c.wrap(GitHub.this); + } + }; + } + }; } /** From 70f0f5714a6bb1fa92ba6a52fa3692e87c02bea8 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:44:10 -0700 Subject: [PATCH 13/21] While a use of custom HttpConnector is clever, it doesn't fit the current idiom of this library. --- .../github/extras/PreviewHttpConnector.java | 67 ------------------- .../org/kohsuke/github/GHLicenseTest.java | 15 ----- 2 files changed, 82 deletions(-) delete mode 100644 src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java diff --git a/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java b/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java deleted file mode 100644 index e1be7855f4..0000000000 --- a/src/main/java/org/kohsuke/github/extras/PreviewHttpConnector.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright $year slavinson - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.kohsuke.github.extras; - -import org.kohsuke.github.HttpConnector; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; - -public class PreviewHttpConnector implements HttpConnector { - private final HttpConnector base; - private final int readTimeout, connectTimeout; - - /** - * @param connectTimeout HTTP connection timeout in milliseconds - * @param readTimeout HTTP read timeout in milliseconds - */ - public PreviewHttpConnector(HttpConnector base, int connectTimeout, int readTimeout) { - this.base = base; - this.connectTimeout = connectTimeout; - this.readTimeout = readTimeout; - } - - public PreviewHttpConnector(HttpConnector base, int timeout) { - this(base, timeout, timeout); - } - - public PreviewHttpConnector(HttpConnector base) { - this(base, ImpatientHttpConnector.CONNECT_TIMEOUT, ImpatientHttpConnector.READ_TIMEOUT); - } - - public PreviewHttpConnector() { - this(new HttpConnector() { - public HttpURLConnection connect(URL url) throws IOException { - return (HttpURLConnection) url.openConnection(); - } - }, ImpatientHttpConnector.CONNECT_TIMEOUT, ImpatientHttpConnector.READ_TIMEOUT); - } - - public HttpURLConnection connect(URL url) throws IOException { - HttpURLConnection con = base.connect(url); - con.setConnectTimeout(connectTimeout); - con.setReadTimeout(readTimeout); - con.addRequestProperty("Accept", PREVIEW_MEDIA_TYPE); - return con; - } - - /** - * Default connection timeout in milliseconds - */ - public static final String PREVIEW_MEDIA_TYPE = "application/vnd.github.drax-preview+json"; -} diff --git a/src/test/java/org/kohsuke/github/GHLicenseTest.java b/src/test/java/org/kohsuke/github/GHLicenseTest.java index 43a97b5ed9..5335e1a9d3 100644 --- a/src/test/java/org/kohsuke/github/GHLicenseTest.java +++ b/src/test/java/org/kohsuke/github/GHLicenseTest.java @@ -44,7 +44,6 @@ public class GHLicenseTest extends Assert { public void setUp() throws Exception { gitHub = new GitHubBuilder() .fromCredentials() - .withConnector(new PreviewHttpConnector()) .build(); } @@ -203,18 +202,4 @@ public void checkRepositoryLicenseContent() throws IOException { fail("Expected the license to be Base64 encoded but instead it was " + content.getEncoding()); } } - - /** - * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} - * but without using {@link PreviewHttpConnector} and ensures that the {@link GHRepository#getLicense()} - * call just returns null rather than raising an exception. This should indicate that - * non-preview connection requests aren't affected by the change in functionality - * - * @throws IOException - */ - @Test - public void checkRepositoryLicenseWithoutPreviewConnection() throws IOException { - GHRepository repo = GitHub.connect().getRepository("kohsuke/github-api"); - assertNull(repo.getLicense()); - } } From 6a356c82a5129df674be98c2a73d5c1edf08b1bb Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:44:45 -0700 Subject: [PATCH 14/21] Fixed up tests --- src/main/java/org/kohsuke/github/GHRepository.java | 8 +++----- src/test/java/org/kohsuke/github/GHLicenseTest.java | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 5430603363..2ee35b4f74 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -850,10 +850,7 @@ protected void wrapUp(GHCommitComment[] page) { /** * Gets the basic license details for the repository. *

- * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} - *

- * Warning: Only returns the basic license details. Use {@link GitHub#getLicense(String)} - * to get the full license information (hint: pass it {@link GHLicenseBase#getKey()}). + * This is a preview item and subject to change. * * @throws IOException as usual but also if you don't use the preview connector */ @@ -868,11 +865,12 @@ public GHLicense getLicense() throws IOException{ /** * Retrieves the contents of the repository's license file - makes an additional API call *

- * This is a preview item and requires you to use {@link org.kohsuke.github.extras.PreviewHttpConnector} + * This is a preview item and subject to change. * * @return details regarding the license contents * @throws IOException as usual but also if you don't use the preview connector */ + @Preview @Deprecated public GHContent getLicenseContent() throws IOException { return root.retrieve().to(getApiTailUrl("license"), GHContent.class).wrap(this); } diff --git a/src/test/java/org/kohsuke/github/GHLicenseTest.java b/src/test/java/org/kohsuke/github/GHLicenseTest.java index 5335e1a9d3..d32325f11d 100644 --- a/src/test/java/org/kohsuke/github/GHLicenseTest.java +++ b/src/test/java/org/kohsuke/github/GHLicenseTest.java @@ -28,11 +28,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.kohsuke.github.extras.PreviewHttpConnector; import java.io.IOException; import java.net.URL; -import java.util.List; /** * @author Duncan Dickinson @@ -54,8 +52,8 @@ public void setUp() throws Exception { */ @Test public void listLicenses() throws IOException { - List licenses = gitHub.listLicenses(); - assertTrue(licenses.size() > 0); + Iterable licenses = gitHub.listLicenses(); + assertTrue(licenses.iterator().hasNext()); } /** @@ -66,7 +64,7 @@ public void listLicenses() throws IOException { */ @Test public void listLicensesCheckIndividualLicense() throws IOException { - List licenses = gitHub.listLicenses(); + PagedIterable licenses = gitHub.listLicenses(); for (GHLicense lic : licenses) { if (lic.getKey().equals("mit")) { assertTrue(lic.getUrl().equals(new URL("https://api.github.com/licenses/mit"))); From 59324b008267ee9e3fdb3e5d7dc88a4c69bfd3e0 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:47:08 -0700 Subject: [PATCH 15/21] Add preview media type header explicitly --- src/main/java/org/kohsuke/github/GHLicense.java | 4 +++- src/main/java/org/kohsuke/github/GHRepository.java | 14 +++++++++----- src/main/java/org/kohsuke/github/GitHub.java | 8 ++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index b4dec87d32..283a60edff 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -141,7 +141,9 @@ public String getBody() throws IOException { protected synchronized void populate() throws IOException { if (description!=null) return; // already populated - root.retrieve().to(url, this); + root.retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .to(url, this); } @Override diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 2ee35b4f74..a4a739f896 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -856,10 +856,7 @@ protected void wrapUp(GHCommitComment[] page) { */ @Preview @Deprecated public GHLicense getLicense() throws IOException{ - return root.retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") - .to(getApiTailUrl("license"), GHContentWithLicense.class) - .wrap(this).license; + return getLicenseContent_().license; } /** @@ -872,7 +869,14 @@ public GHLicense getLicense() throws IOException{ */ @Preview @Deprecated public GHContent getLicenseContent() throws IOException { - return root.retrieve().to(getApiTailUrl("license"), GHContent.class).wrap(this); + return getLicenseContent_(); + } + + @Preview @Deprecated + private GHContentWithLicense getLicenseContent_() throws IOException { + return root.retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .to(getApiTailUrl("license"), GHContentWithLicense.class).wrap(this); } /** diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 8466ae6d54..dff5f59148 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -353,7 +353,9 @@ public GHRepository getRepository(String name) throws IOException { public PagedIterable listLicenses() throws IOException { return new PagedIterable() { public PagedIterator _iterator(int pageSize) { - return new PagedIterator(retrieve().asIterator("/licenses", GHLicense[].class, pageSize)) { + return new PagedIterator(retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .asIterator("/licenses", GHLicense[].class, pageSize)) { @Override protected void wrapUp(GHLicense[] page) { for (GHLicense c : page) @@ -376,7 +378,9 @@ protected void wrapUp(GHLicense[] page) { */ @Preview @Deprecated public GHLicense getLicense(String key) throws IOException { - return retrieve().to("/licenses/" + key, GHLicense.class); + return retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .to("/licenses/" + key, GHLicense.class); } /** From cabbbf7f0203c4f22f8e0fd1baad488b623d83c8 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:50:39 -0700 Subject: [PATCH 16/21] Handle 404 that represents "no license" --- .../java/org/kohsuke/github/GHRepository.java | 16 +++++++++++----- .../java/org/kohsuke/github/GHLicenseTest.java | 10 ---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index a4a739f896..04e2b85101 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -853,10 +853,12 @@ protected void wrapUp(GHCommitComment[] page) { * This is a preview item and subject to change. * * @throws IOException as usual but also if you don't use the preview connector + * @return null if there's no license. */ @Preview @Deprecated public GHLicense getLicense() throws IOException{ - return getLicenseContent_().license; + GHContentWithLicense lic = getLicenseContent_(); + return lic!=null ? lic.license : null; } /** @@ -864,7 +866,7 @@ public GHLicense getLicense() throws IOException{ *

* This is a preview item and subject to change. * - * @return details regarding the license contents + * @return details regarding the license contents, or null if there's no license. * @throws IOException as usual but also if you don't use the preview connector */ @Preview @Deprecated @@ -874,9 +876,13 @@ public GHContent getLicenseContent() throws IOException { @Preview @Deprecated private GHContentWithLicense getLicenseContent_() throws IOException { - return root.retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") - .to(getApiTailUrl("license"), GHContentWithLicense.class).wrap(this); + try { + return root.retrieve() + .withHeader("Accept","application/vnd.github.drax-preview+json") + .to(getApiTailUrl("license"), GHContentWithLicense.class).wrap(this); + } catch (FileNotFoundException e) { + return null; + } } /** diff --git a/src/test/java/org/kohsuke/github/GHLicenseTest.java b/src/test/java/org/kohsuke/github/GHLicenseTest.java index d32325f11d..b2cc73f928 100644 --- a/src/test/java/org/kohsuke/github/GHLicenseTest.java +++ b/src/test/java/org/kohsuke/github/GHLicenseTest.java @@ -89,16 +89,6 @@ public void getLicense() throws IOException { assertTrue("The HTML URL is correct", license.getHtmlUrl().equals(new URL("http://choosealicense.com/licenses/mit/"))); } - /** - * Attempts to list the licenses with a non-preview connection - * - * @throws IOException is expected to be thrown - */ - @Test(expected = IOException.class) - public void ListLicensesWithoutPreviewConnection() throws IOException { - GitHub.connect().listLicenses(); - } - /** * Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)} * and checks that the license is correct From a9fb4546e19e453766a46256c43232ed2ea35e6a Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 20:56:11 -0700 Subject: [PATCH 17/21] Constants for preview media types --- src/main/java/org/kohsuke/github/GHBranch.java | 6 +++--- src/main/java/org/kohsuke/github/GHLicense.java | 6 +++--- src/main/java/org/kohsuke/github/GHRepository.java | 3 ++- src/main/java/org/kohsuke/github/GitHub.java | 9 +++------ src/main/java/org/kohsuke/github/Previews.java | 9 +++++++++ src/main/java/org/kohsuke/github/Requester.java | 4 ++++ 6 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/Previews.java diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index f4e90f6556..4c80db9ef4 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -7,6 +7,8 @@ import java.util.Arrays; import java.util.Collection; +import static org.kohsuke.github.Previews.LOKI; + /** * A branch in a repository. * @@ -81,9 +83,7 @@ public void enableProtection(EnforcementLevel level, String... contexts) throws } private void setProtection(BranchProtection bp) throws IOException { - new Requester(root).method("PATCH") - .withHeader("Accept","application/vnd.github.loki-preview+json") - ._with("protection",bp).to(getApiRoute()); + new Requester(root).method("PATCH").withPreview(LOKI)._with("protection",bp).to(getApiRoute()); } String getApiRoute() { diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index 283a60edff..47bec0f71c 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -32,6 +32,8 @@ import java.util.ArrayList; import java.util.List; +import static org.kohsuke.github.Previews.DRAX; + /** * The GitHub Preview API's license information *

@@ -141,9 +143,7 @@ public String getBody() throws IOException { protected synchronized void populate() throws IOException { if (description!=null) return; // already populated - root.retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") - .to(url, this); + root.retrieve().withPreview(DRAX).to(url, this); } @Override diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 04e2b85101..472adf41eb 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -51,6 +51,7 @@ import java.util.TreeMap; import static java.util.Arrays.asList; +import static org.kohsuke.github.Previews.DRAX; /** * A repository on GitHub. @@ -878,7 +879,7 @@ public GHContent getLicenseContent() throws IOException { private GHContentWithLicense getLicenseContent_() throws IOException { try { return root.retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") + .withPreview(DRAX) .to(getApiTailUrl("license"), GHContentWithLicense.class).wrap(this); } catch (FileNotFoundException e) { return null; diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index dff5f59148..e5bf16b1de 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -27,6 +27,7 @@ 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; @@ -353,9 +354,7 @@ public GHRepository getRepository(String name) throws IOException { public PagedIterable listLicenses() throws IOException { return new PagedIterable() { public PagedIterator _iterator(int pageSize) { - return new PagedIterator(retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") - .asIterator("/licenses", GHLicense[].class, pageSize)) { + return new PagedIterator(retrieve().withPreview(DRAX).asIterator("/licenses", GHLicense[].class, pageSize)) { @Override protected void wrapUp(GHLicense[] page) { for (GHLicense c : page) @@ -378,9 +377,7 @@ protected void wrapUp(GHLicense[] page) { */ @Preview @Deprecated public GHLicense getLicense(String key) throws IOException { - return retrieve() - .withHeader("Accept","application/vnd.github.drax-preview+json") - .to("/licenses/" + key, GHLicense.class); + return retrieve().withPreview(DRAX).to("/licenses/" + key, GHLicense.class); } /** diff --git a/src/main/java/org/kohsuke/github/Previews.java b/src/main/java/org/kohsuke/github/Previews.java new file mode 100644 index 0000000000..a5d061cbd4 --- /dev/null +++ b/src/main/java/org/kohsuke/github/Previews.java @@ -0,0 +1,9 @@ +package org.kohsuke.github; + +/** + * @author Kohsuke Kawaguchi + */ +/*package*/ class Previews { + static final String LOKI = "application/vnd.github.loki-preview+json"; + static final String DRAX = "application/vnd.github.drax-preview+json"; +} diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 43c4e07694..ac492579ce 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -110,6 +110,10 @@ public Requester withHeader(String name, String value) { return this; } + /*package*/ Requester withPreview(String name) { + return withHeader("Accept",name); + } + /** * Makes a request with authentication credential. */ From 63f500ad7fff182662f35e3588f1cee4b3e72e2d Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 21:11:00 -0700 Subject: [PATCH 18/21] Fixed issue #286 List commit API (https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository) already populates short info, and so populate() call could be excessive. It's possible that the short info is always available and therefore there's never a need to call populate(), but that assumption is hard to test, so I'm leaving that in --- src/main/java/org/kohsuke/github/GHCommit.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kohsuke/github/GHCommit.java b/src/main/java/org/kohsuke/github/GHCommit.java index 91510fc88c..6e606ef603 100644 --- a/src/main/java/org/kohsuke/github/GHCommit.java +++ b/src/main/java/org/kohsuke/github/GHCommit.java @@ -179,7 +179,8 @@ static class User { public ShortInfo getCommitShortInfo() throws IOException { - populate(); + if (commit==null) + populate(); return commit; } From 4f15b7c9fac50bfc280f7fad2aa11467aab63ca5 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 21:19:32 -0700 Subject: [PATCH 19/21] NPE fix. type can be null --- src/main/java/org/kohsuke/github/Requester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index ac492579ce..6175191ea6 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -516,7 +516,7 @@ private T parse(Class type, T instance) throws IOException { if (responseCode == 304) { return null; // special case handling for 304 unmodified, as the content will be "" } - if (responseCode == 204 && type.isArray()) { + if (responseCode == 204 && type!=null && type.isArray()) { // no content return type.cast(Array.newInstance(type.getComponentType(),0)); } From e2a1630cf4d555d9ecc3d07ec49f5315c2d24dc5 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 21:28:54 -0700 Subject: [PATCH 20/21] findbug taming --- src/main/java/org/kohsuke/github/GHLicense.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index 47bec0f71c..2c5c4299c1 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -49,6 +49,7 @@ @SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API") public class GHLicense extends GHObject { + @SuppressFBWarnings("IS2_INCONSISTENT_SYNC") // root is set before the object is returned to the app /*package almost final*/ GitHub root; // these fields are always present, even in the short form From e9368fb04eaaa58ecaa6c3863140647b0a3f6f4f Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Fri, 5 Aug 2016 21:32:10 -0700 Subject: [PATCH 21/21] [maven-release-plugin] prepare release github-api-1.77 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fa6bbacf9b..3a637f2227 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.77-SNAPSHOT + 1.77 GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -16,7 +16,7 @@ scm:git:git@github.com/kohsuke/${project.artifactId}.git scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git http://${project.artifactId}.kohsuke.org/ - HEAD + github-api-1.77