diff --git a/src/main/java/org/kohsuke/github/GHMeta.java b/src/main/java/org/kohsuke/github/GHMeta.java new file mode 100644 index 0000000000..b2a9e22fe1 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHMeta.java @@ -0,0 +1,56 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class that wraps the list of GitHub's IP addresses. + * + * @author Paulo Miguel Almeida + * + * @see GitHub#getMeta() + * @see Get Meta + */ + +public class GHMeta { + + @JsonProperty("verifiable_password_authentication") + private boolean verifiablePasswordAuthentication; + private List hooks; + private List git; + private List web; + private List api; + private List pages; + private List importer = new ArrayList<>(); + + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + public List getHooks() { + return Collections.unmodifiableList(hooks); + } + + public List getGit() { + return Collections.unmodifiableList(git); + } + + public List getWeb() { + return Collections.unmodifiableList(web); + } + + public List getApi() { + return Collections.unmodifiableList(api); + } + + public List getPages() { + return Collections.unmodifiableList(pages); + } + + public List getImporter() { + return Collections.unmodifiableList(importer); + } +} diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index e1bd982d42..875cb314c2 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -26,13 +26,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import org.apache.commons.codec.Charsets; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -266,7 +264,7 @@ public static GitHub offline() { /** * Is this an anonymous connection - * + * * @return {@code true} if operations that require authentication will fail. */ public boolean isAnonymous() { @@ -275,7 +273,7 @@ public boolean isAnonymous() { /** * Is this an always offline "connection". - * + * * @return {@code true} if this is an always offline "connection". */ public boolean isOffline() { @@ -764,7 +762,7 @@ public GHAuthorization resetAuth(@Nonnull String clientId, @Nonnull String acces /** * Returns a list of all authorizations. - * + * * @see List your * authorizations */ @@ -802,6 +800,20 @@ public boolean isCredentialValid() { } } + /** + * Provides a list of GitHub's IP addresses. + * + * @see Get Meta + * + * @return an instance of {@link GHMeta} + * @throws IOException + * if the credentials supplied are invalid or if you're trying to access it as a GitHub App via the JWT + * authentication + */ + public GHMeta getMeta() throws IOException { + return retrieve().to("/meta", GHMeta.class); + } + GHUser intern(GHUser user) throws IOException { if (user == null) return user; @@ -866,7 +878,7 @@ public void checkApiUrlValidity() throws IOException { * Checks if a GitHub Enterprise server is configured in private mode. * * In private mode response looks like: - * + * *
      *  $ curl -i https://github.mycompany.com/api/v3/
      *     HTTP/1.1 401 Unauthorized
@@ -955,7 +967,7 @@ public GHNotificationStream listNotifications() {
 
     /**
      * This provides a dump of every public repository, in the order that they were created.
-     * 
+     *
      * @see documentation
      */
     public PagedIterable listAllPublicRepositories() {
diff --git a/src/main/java/org/kohsuke/github/example/dataobject/ReadOnlyObjects.java b/src/main/java/org/kohsuke/github/example/dataobject/ReadOnlyObjects.java
new file mode 100644
index 0000000000..e56f405979
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/example/dataobject/ReadOnlyObjects.java
@@ -0,0 +1,475 @@
+package org.kohsuke.github.example.dataobject;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link org.kohsuke.github.GHMeta} wraps the list of GitHub's IP addresses.
+ * 

+ * This class is used to show examples of different ways to create simple read-only data objects. For data objects that + * can be modified, perform actions, or get other objects we'll need other examples. + *

+ * IMPORTANT: There is no one right way to do this, but there are better and worse. + *

    + *
  • Better: {@link GHMetaGettersUnmodifiable} is a good balance of clarity and brevity
  • + *
  • Worse: {@link GHMetaPublic} exposes setters that are not needed, making it unclear that fields are actually + * read-only
  • + *
+ * + * @author Liam Newman + * + * @see org.kohsuke.github.GHMeta + * @see Get Meta + */ + +public final class ReadOnlyObjects { + + /** + * All GHMeta data objects should expose these values. + * + * @author Liam Newman + */ + public interface GHMetaExample { + boolean isVerifiablePasswordAuthentication(); + + List getHooks(); + + List getGit(); + + List getWeb(); + + List getApi(); + + List getPages(); + + List getImporter(); + } + + /** + * This version uses public getters and setters and leaves it up to Jackson how it wants to fill them. + *

+ * Pro: + *

    + *
  • Easy to create
  • + *
  • Not much code
  • + *
  • Mininal annotations
  • + *
+ * Con: + *
    + *
  • Exposes public setters for fields that should not be changed
  • + *
  • Lists modifiable when they should not be changed
  • + *
  • Jackson generally doesn't call the setters, it just sets the fields directly
  • + *
+ * + * @author Paulo Miguel Almeida + * @see org.kohsuke.github.GHMeta + */ + + public static class GHMetaPublic implements GHMetaExample { + + @JsonProperty("verifiable_password_authentication") + private boolean verifiablePasswordAuthentication; + private List hooks; + private List git; + private List web; + private List api; + private List pages; + private List importer; + + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + public void setVerifiablePasswordAuthentication(boolean verifiablePasswordAuthentication) { + this.verifiablePasswordAuthentication = verifiablePasswordAuthentication; + } + + public List getHooks() { + return hooks; + } + + public void setHooks(List hooks) { + this.hooks = hooks; + } + + public List getGit() { + return git; + } + + public void setGit(List git) { + this.git = git; + } + + public List getWeb() { + return web; + } + + public void setWeb(List web) { + this.web = web; + } + + public List getApi() { + return api; + } + + public void setApi(List api) { + this.api = api; + } + + public List getPages() { + return pages; + } + + public void setPages(List pages) { + this.pages = pages; + } + + public List getImporter() { + return importer; + } + + public void setImporter(List importer) { + this.importer = importer; + } + + } + + /** + * This version uses public getters and shows that package or private setters both can be used by jackson. You can + * check this by running in debug and setting break points in the setters. + * + *

+ * Pro: + *

    + *
  • Easy to create
  • + *
  • Not much code
  • + *
  • Some annotations
  • + *
+ * Con: + *
    + *
  • Exposes some package setters for fields that should not be changed, better than public
  • + *
  • Lists modifiable when they should not be changed
  • + *
+ * + * @author Liam Newman + * @see org.kohsuke.github.GHMeta + */ + + public static class GHMetaPackage implements GHMetaExample { + + private boolean verifiablePasswordAuthentication; + private List hooks; + private List git; + private List web; + private List api; + private List pages; + + /** + * Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters. + */ + @JsonProperty + private List importer; + + @JsonProperty("verifiable_password_authentication") + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + private void setVerifiablePasswordAuthentication(boolean verifiablePasswordAuthentication) { + this.verifiablePasswordAuthentication = verifiablePasswordAuthentication; + } + + @JsonProperty + public List getHooks() { + return hooks; + } + + /** + * Setters can be private (or package local) and will still be called by Jackson. The {@link JsonProperty} can + * got on the getter or setter and still work. + * + * @param hooks + * list of hooks + */ + private void setHooks(List hooks) { + this.hooks = hooks; + } + + public List getGit() { + return git; + } + + /** + * Since we mostly use Jackson for deserialization, {@link JsonSetter} is also okay, but {@link JsonProperty} is + * preferred. + * + * @param git + * list of git addresses + */ + @JsonSetter + void setGit(List git) { + this.git = git; + } + + public List getWeb() { + return web; + } + + /** + * The {@link JsonProperty} can got on the getter or setter and still work. + * + * @param web + * list of web addresses + */ + void setWeb(List web) { + this.web = web; + } + + @JsonProperty + public List getApi() { + return api; + } + + void setApi(List api) { + this.api = api; + } + + @JsonProperty + public List getPages() { + return pages; + } + + void setPages(List pages) { + this.pages = pages; + } + + /** + * Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters. + * + * @return list of importer addresses + */ + public List getImporter() { + return importer; + } + + /** + * Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters. + * + * @param importer + * list of importer addresses + */ + void setImporter(List importer) { + this.importer = importer; + } + + } + + /** + * This version uses only public getters and returns unmodifiable lists. + * + * + *

+ * Pro: + *

    + *
  • Very Easy to create
  • + *
  • Minimal code
  • + *
  • Mininal annotations
  • + *
  • Fields effectively final and lists unmodifiable
  • + *
+ * Con: + *
    + *
  • Effectively final is not quite really final
  • + *
  • If one of the lists were missing (an option member, for example), it will throw NPE but we could mitigate by + * checking for null or assigning a default.
  • + *
+ * + * @author Liam Newman + * @see org.kohsuke.github.GHMeta + */ + public static class GHMetaGettersUnmodifiable implements GHMetaExample { + + @JsonProperty("verifiable_password_authentication") + private boolean verifiablePasswordAuthentication; + private List hooks; + private List git; + private List web; + private List api; + private List pages; + /** + * If this were an optional member, we could fill it with an empty list by default. + */ + private List importer = new ArrayList<>(); + + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + public List getHooks() { + return Collections.unmodifiableList(hooks); + } + + public List getGit() { + return Collections.unmodifiableList(git); + } + + public List getWeb() { + return Collections.unmodifiableList(web); + } + + public List getApi() { + return Collections.unmodifiableList(api); + } + + public List getPages() { + return Collections.unmodifiableList(pages); + } + + public List getImporter() { + return Collections.unmodifiableList(importer); + } + } + + /** + * This version uses only public getters and returns unmodifiable lists and has final fields + *

+ * Pro: + *

    + *
  • Moderate amount of code
  • + *
  • More annotations
  • + *
  • Fields final and lists unmodifiable
  • + *
+ * Con: + *
    + *
  • Extra allocations - default array lists will be replaced by Jackson (yes, even though they are final)
  • + *
  • Added constructor is annoying
  • + *
  • If this object could be refreshed or populated, then the final is misleading (and possibly buggy)
  • + *
+ * + * @author Liam Newman + * @see org.kohsuke.github.GHMeta + */ + public static class GHMetaGettersFinal implements GHMetaExample { + + private final boolean verifiablePasswordAuthentication; + private final List hooks = new ArrayList<>(); + private final List git = new ArrayList<>(); + private final List web = new ArrayList<>(); + private final List api = new ArrayList<>(); + private final List pages = new ArrayList<>(); + private final List importer = new ArrayList<>(); + + @JsonCreator + private GHMetaGettersFinal( + @JsonProperty("verifiable_password_authentication") boolean verifiablePasswordAuthentication) { + // boolean fields when final seem to be really final, so we have to switch to constructor + this.verifiablePasswordAuthentication = verifiablePasswordAuthentication; + } + + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + public List getHooks() { + return Collections.unmodifiableList(hooks); + } + + public List getGit() { + return Collections.unmodifiableList(git); + } + + public List getWeb() { + return Collections.unmodifiableList(web); + } + + public List getApi() { + return Collections.unmodifiableList(api); + } + + public List getPages() { + return Collections.unmodifiableList(pages); + } + + public List getImporter() { + return Collections.unmodifiableList(importer); + } + } + + /** + * This version uses only public getters and returns unmodifiable lists + *

+ * Pro: + *

    + *
  • Fields final and lists unmodifiable
  • + *
  • Construction behavior can be controlled - if values depended on each other or needed to be set in a specific + * order, this could do that.
  • + *
+ * Con: + *
    + *
  • There is no way you'd know about this without some research
  • + *
  • Specific annotations needed
  • + *
  • Brittle and verbose - not friendly to optional fields or large number of fields
  • + *
+ * + * @author Liam Newman + * @see org.kohsuke.github.GHMeta + */ + public static class GHMetaGettersFinalCreator implements GHMetaExample { + + private final boolean verifiablePasswordAuthentication; + private final List hooks; + private final List git; + private final List web; + private final List api; + private final List pages; + private final List importer; + + @JsonCreator + private GHMetaGettersFinalCreator(@Nonnull @JsonProperty("hooks") List hooks, + @Nonnull @JsonProperty("git") List git, @Nonnull @JsonProperty("web") List web, + @Nonnull @JsonProperty("api") List api, @Nonnull @JsonProperty("pages") List pages, + @Nonnull @JsonProperty("importer") List importer, + @JsonProperty("verifiable_password_authentication") boolean verifiablePasswordAuthentication) { + this.verifiablePasswordAuthentication = verifiablePasswordAuthentication; + this.hooks = Collections.unmodifiableList(hooks); + this.git = Collections.unmodifiableList(git); + this.web = Collections.unmodifiableList(web); + this.api = Collections.unmodifiableList(api); + this.pages = Collections.unmodifiableList(pages); + this.importer = Collections.unmodifiableList(importer); + } + + public boolean isVerifiablePasswordAuthentication() { + return verifiablePasswordAuthentication; + } + + public List getHooks() { + return hooks; + } + + public List getGit() { + return git; + } + + public List getWeb() { + return web; + } + + public List getApi() { + return api; + } + + public List getPages() { + return pages; + } + + public List getImporter() { + return importer; + } + } +} \ No newline at end of file diff --git a/src/test/java/org/kohsuke/github/GitHubTest.java b/src/test/java/org/kohsuke/github/GitHubTest.java index 41e6ef9830..81cfeba27b 100644 --- a/src/test/java/org/kohsuke/github/GitHubTest.java +++ b/src/test/java/org/kohsuke/github/GitHubTest.java @@ -1,18 +1,13 @@ package org.kohsuke.github; -import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.reflect.Field; import java.util.*; import com.google.common.collect.Iterables; -import org.apache.commons.io.IOUtils; -import org.junit.Ignore; import org.junit.Test; +import org.kohsuke.github.example.dataobject.ReadOnlyObjects; import static org.hamcrest.CoreMatchers.*; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; /** * Unit test for {@link GitHub}. @@ -79,4 +74,33 @@ public void testListMyAuthorizations() throws IOException { assertNotNull(auth.getAppName()); } } + + @Test + public void getMeta() throws IOException { + GHMeta meta = gitHub.getMeta(); + assertTrue(meta.isVerifiablePasswordAuthentication()); + assertEquals(19, meta.getApi().size()); + assertEquals(19, meta.getGit().size()); + assertEquals(3, meta.getHooks().size()); + assertEquals(6, meta.getImporter().size()); + assertEquals(6, meta.getPages().size()); + assertEquals(19, meta.getWeb().size()); + + // Also test examples here + Class[] examples = new Class[] { ReadOnlyObjects.GHMetaPublic.class, ReadOnlyObjects.GHMetaPackage.class, + ReadOnlyObjects.GHMetaGettersUnmodifiable.class, ReadOnlyObjects.GHMetaGettersFinal.class, + ReadOnlyObjects.GHMetaGettersFinalCreator.class, }; + + for (Class metaClass : examples) { + ReadOnlyObjects.GHMetaExample metaExample = gitHub.retrieve().to("/meta", + (Class) metaClass); + assertTrue(metaExample.isVerifiablePasswordAuthentication()); + assertEquals(19, metaExample.getApi().size()); + assertEquals(19, metaExample.getGit().size()); + assertEquals(3, metaExample.getHooks().size()); + assertEquals(6, metaExample.getImporter().size()); + assertEquals(6, metaExample.getPages().size()); + assertEquals(19, metaExample.getWeb().size()); + } + } } diff --git a/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/__files/meta-d7546658-0019-4aac-ad1a-502d7abd8050.json b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/__files/meta-d7546658-0019-4aac-ad1a-502d7abd8050.json new file mode 100644 index 0000000000..02db520957 --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/__files/meta-d7546658-0019-4aac-ad1a-502d7abd8050.json @@ -0,0 +1,87 @@ +{ + "verifiable_password_authentication": true, + "hooks": [ + "192.30.252.0/22", + "185.199.108.0/22", + "140.82.112.0/20" + ], + "web": [ + "192.30.252.0/22", + "185.199.108.0/22", + "140.82.112.0/20", + "13.114.40.48/32", + "13.229.188.59/32", + "13.234.176.102/32", + "13.234.210.38/32", + "13.236.229.21/32", + "13.237.44.5/32", + "13.250.177.223/32", + "15.164.81.167/32", + "18.194.104.89/32", + "18.195.85.27/32", + "35.159.8.160/32", + "52.192.72.89/32", + "52.64.108.95/32", + "52.69.186.44/32", + "52.74.223.119/32", + "52.78.231.108/32" + ], + "api": [ + "192.30.252.0/22", + "185.199.108.0/22", + "140.82.112.0/20", + "13.209.163.61/32", + "13.230.158.120/32", + "13.233.76.15/32", + "13.234.168.60/32", + "13.236.14.80/32", + "13.238.54.232/32", + "13.250.168.23/32", + "13.250.94.254/32", + "18.179.245.253/32", + "18.194.201.191/32", + "18.195.135.122/32", + "52.58.203.252/32", + "52.63.231.178/32", + "52.69.239.207/32", + "54.169.195.247/32", + "54.180.75.25/32" + ], + "git": [ + "192.30.252.0/22", + "185.199.108.0/22", + "140.82.112.0/20", + "13.114.40.48/32", + "13.229.188.59/32", + "13.234.176.102/32", + "13.234.210.38/32", + "13.236.229.21/32", + "13.237.44.5/32", + "13.250.177.223/32", + "15.164.81.167/32", + "18.194.104.89/32", + "18.195.85.27/32", + "35.159.8.160/32", + "52.192.72.89/32", + "52.64.108.95/32", + "52.69.186.44/32", + "52.74.223.119/32", + "52.78.231.108/32" + ], + "pages": [ + "192.30.252.153/32", + "192.30.252.154/32", + "185.199.108.153/32", + "185.199.109.153/32", + "185.199.110.153/32", + "185.199.111.153/32" + ], + "importer": [ + "54.87.5.173", + "54.166.52.62", + "23.20.92.3", + "192.30.252.0/22", + "185.199.108.0/22", + "140.82.112.0/20" + ] +} \ No newline at end of file diff --git a/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/mappings/meta-1-d75466.json b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/mappings/meta-1-d75466.json new file mode 100644 index 0000000000..37c886c2cc --- /dev/null +++ b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/getMeta/mappings/meta-1-d75466.json @@ -0,0 +1,40 @@ +{ + "id": "d7546658-0019-4aac-ad1a-502d7abd8050", + "name": "meta", + "request": { + "url": "/meta", + "method": "GET" + }, + "response": { + "status": 200, + "bodyFileName": "meta-d7546658-0019-4aac-ad1a-502d7abd8050.json", + "headers": { + "Date": "Thu, 14 Nov 2019 02:46:14 GMT", + "Content-Type": "application/json; charset=utf-8", + "Server": "GitHub.com", + "Status": "200 OK", + "X-RateLimit-Limit": "60", + "X-RateLimit-Remaining": "59", + "X-RateLimit-Reset": "1573703174", + "Cache-Control": "public, max-age=60, s-maxage=60", + "Vary": [ + "Accept", + "Accept-Encoding" + ], + "ETag": "W/\"eb00a8c05920e4cb21eb0d018c339fe1\"", + "X-GitHub-Media-Type": "unknown, github.v3", + "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type", + "Access-Control-Allow-Origin": "*", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Frame-Options": "deny", + "X-Content-Type-Options": "nosniff", + "X-XSS-Protection": "1; mode=block", + "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", + "Content-Security-Policy": "default-src 'none'", + "X-GitHub-Request-Id": "FC2E:3B7E:AD9E9:C9032:5DCCBFF6" + } + }, + "uuid": "d7546658-0019-4aac-ad1a-502d7abd8050", + "persistent": true, + "insertionIndex": 1 +} \ No newline at end of file