diff --git a/pom.xml b/pom.xml index 7d5af32649..f8ac35a431 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.31 + 1.32 GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -64,7 +64,7 @@ org.codehaus.jackson jackson-mapper-asl - 1.5.0 + 1.9.9 commons-io diff --git a/src/main/java/org/kohsuke/github/GHCommit.java b/src/main/java/org/kohsuke/github/GHCommit.java index 3cf7f1437c..8069066780 100644 --- a/src/main/java/org/kohsuke/github/GHCommit.java +++ b/src/main/java/org/kohsuke/github/GHCommit.java @@ -203,7 +203,7 @@ private GHUser resolveUser(User author) throws IOException { public PagedIterable listComments() { return new PagedIterable() { public PagedIterator iterator() { - return new PagedIterator(owner.root.retrievePaged(String.format("/repos/%s/%s/commits/%s/comments",owner.getOwnerName(),owner.getName(),sha),GHCommitComment[].class,false)) { + return new PagedIterator(owner.root.retrieve().asIterator(String.format("/repos/%s/%s/commits/%s/comments", owner.getOwnerName(), owner.getName(), sha), GHCommitComment[].class)) { @Override protected void wrapUp(GHCommitComment[] page) { for (GHCommitComment c : page) @@ -220,7 +220,7 @@ protected void wrapUp(GHCommitComment[] page) { * I'm not sure how path/line/position parameters interact with each other. */ public GHCommitComment createComment(String body, String path, Integer line, Integer position) throws IOException { - GHCommitComment r = new Poster(owner.root) + GHCommitComment r = new Requester(owner.root) .with("body",body) .with("path",path) .with("line",line) @@ -234,6 +234,20 @@ public GHCommitComment createComment(String body) throws IOException { return createComment(body,null,null,null); } + /** + * Gets the status of this commit, newer ones first. + */ + public PagedIterable listStatuses() throws IOException { + return owner.listCommitStatuses(sha); + } + + /** + * Gets the last status of this commit, which is what gets shown in the UI. + */ + public GHCommitStatus getLastStatus() throws IOException { + return owner.getLastCommitStatus(sha); + } + GHCommit wrapUp(GHRepository owner) { this.owner = owner; return this; diff --git a/src/main/java/org/kohsuke/github/GHCommitComment.java b/src/main/java/org/kohsuke/github/GHCommitComment.java index f4621c2b01..9ff07304eb 100644 --- a/src/main/java/org/kohsuke/github/GHCommitComment.java +++ b/src/main/java/org/kohsuke/github/GHCommitComment.java @@ -97,10 +97,9 @@ public GHCommit getCommit() throws IOException { * Updates the body of the commit message. */ public void update(String body) throws IOException { - GHCommitComment r = new Poster(owner.root) - .with("body",body) - .withCredential() - .to(getApiTail(),GHCommitComment.class,"PATCH"); + GHCommitComment r = new Requester(owner.root) + .with("body", body) + .withCredential().method("PATCH").to(getApiTail(), GHCommitComment.class); this.body = body; } @@ -108,7 +107,7 @@ public void update(String body) throws IOException { * Deletes this comment. */ public void delete() throws IOException { - new Poster(owner.root).withCredential().to(getApiTail(),null,"DELETE"); + new Requester(owner.root).withCredential().method("DELETE").to(getApiTail()); } private String getApiTail() { diff --git a/src/main/java/org/kohsuke/github/GHCommitState.java b/src/main/java/org/kohsuke/github/GHCommitState.java new file mode 100644 index 0000000000..e716ea02e1 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHCommitState.java @@ -0,0 +1,11 @@ +package org.kohsuke.github; + +/** + * Represents the state of commit + * + * @author Kohsuke Kawaguchi + * @see GHCommitStatus + */ +public enum GHCommitState { + PENDING, SUCCESS, ERROR, FAILURE +} diff --git a/src/main/java/org/kohsuke/github/GHCommitStatus.java b/src/main/java/org/kohsuke/github/GHCommitStatus.java new file mode 100644 index 0000000000..319e13cc64 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHCommitStatus.java @@ -0,0 +1,72 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.util.Date; + +/** + * Represents a status of a commit. + * + * @author Kohsuke Kawaguchi + * @see GHRepository#getCommitStatus(String) + * @see GHCommit#getStatus() + */ +public class GHCommitStatus { + String created_at, updated_at; + String state; + String target_url,description; + int id; + String url; + GHUser creator; + + private GitHub root; + + /*package*/ GHCommitStatus wrapUp(GitHub root) { + if (creator!=null) creator.wrapUp(root); + this.root = root; + return this; + } + + public Date getCreatedAt() { + return GitHub.parseDate(created_at); + } + + public Date getUpdatedAt() { + return GitHub.parseDate(updated_at); + } + + public GHCommitState getState() { + for (GHCommitState s : GHCommitState.values()) { + if (s.name().equalsIgnoreCase(state)) + return s; + } + throw new IllegalStateException("Unexpected state: "+state); + } + + /** + * The URL that this status is linked to. + * + * This is the URL specified when creating a commit status. + */ + public String getTargetUrl() { + return target_url; + } + + public String getDescription() { + return description; + } + + public int getId() { + return id; + } + + /** + * API URL of this commit status. + */ + public String getUrl() { + return url; + } + + public GHUser getCreator() { + return creator; + } +} diff --git a/src/main/java/org/kohsuke/github/GHHook.java b/src/main/java/org/kohsuke/github/GHHook.java index 94144e6f07..8d1e5da027 100644 --- a/src/main/java/org/kohsuke/github/GHHook.java +++ b/src/main/java/org/kohsuke/github/GHHook.java @@ -54,7 +54,6 @@ public int getId() { * Deletes this hook. */ public void delete() throws IOException { - new Poster(repository.root).withCredential() - .to(String.format("/repos/%s/%s/hooks/%d",repository.getOwnerName(),repository.getName(),id),null,"DELETE"); + new Requester(repository.root).withCredential().method("DELETE").to(String.format("/repos/%s/%s/hooks/%d", repository.getOwnerName(), repository.getName(), id)); } } diff --git a/src/main/java/org/kohsuke/github/GHIssue.java b/src/main/java/org/kohsuke/github/GHIssue.java index 3224e76f29..18f2594e3a 100644 --- a/src/main/java/org/kohsuke/github/GHIssue.java +++ b/src/main/java/org/kohsuke/github/GHIssue.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.net.URL; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -42,15 +41,30 @@ public class GHIssue { GitHub root; GHRepository owner; - - private String gravatar_id,body,title,state,created_at,updated_at,html_url; - private List labels; - private int number,votes,comments; - private int position; + + // API v3 + protected GHUser assignee; + protected String state; + protected int number; + protected String closed_at; + protected int comments; + protected String body; + protected List labels; + protected GHUser user; + protected String title, created_at, html_url; + protected GHIssue.PullRequest pull_request; + protected GHMilestone milestone; + protected String url, updated_at; + protected int id; + protected GHUser closed_by; /*package*/ GHIssue wrap(GHRepository owner) { this.owner = owner; this.root = owner.root; + if(milestone != null) milestone.wrap(owner); + if(assignee != null) assignee.wrapUp(root); + if(user != null) user.wrapUp(root); + if(closed_by != null) closed_by.wrapUp(root); return this; } @@ -112,16 +126,23 @@ public Date getUpdatedAt() { return GitHub.parseDate(updated_at); } + public Date getClosedAt() { + return GitHub.parseDate(closed_at); + } + + public URL getApiURL(){ + return GitHub.parseURL(url); + } + /** * Updates the issue by adding a comment. */ public void comment(String message) throws IOException { - new Poster(root).withCredential().with("body",message).to(getApiRoute()+"/comments",null,"POST"); + new Requester(root).withCredential().with("body",message).to(getApiRoute() + "/comments"); } private void edit(String key, Object value) throws IOException { - new Poster(root).withCredential()._with(key, value) - .to(getApiRoute(),null,"PATCH"); + new Requester(root).withCredential()._with(key, value).method("PATCH").to(getApiRoute()); } /** @@ -169,7 +190,7 @@ public List getComments() throws IOException { public PagedIterable listComments() throws IOException { return new PagedIterable() { public PagedIterator iterator() { - return new PagedIterator(root.retrievePaged(getApiRoute() + "/comments",GHIssueComment[].class,false)) { + return new PagedIterator(root.retrieve().asIterator(getApiRoute() + "/comments", GHIssueComment[].class)) { protected void wrapUp(GHIssueComment[] page) { for (GHIssueComment c : page) c.wrapUp(GHIssue.this); @@ -182,4 +203,52 @@ protected void wrapUp(GHIssueComment[] page) { private String getApiRoute() { return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/issues/"+number; } + + public GHUser getAssignee() { + return assignee; + } + + /** + * User who submitted the issue. + */ + @Deprecated + public GHUser getUser() { + return user; + } + + public GHUser getClosedBy() { + if(!"closed".equals(state)) return null; + if(closed_by != null) return closed_by; + + //TODO closed_by = owner.getIssue(number).getClosed_by(); + return closed_by; + } + + public int getCommentsCount(){ + return comments; + } + + public PullRequest getPullRequest() { + return pull_request; + } + + public GHMilestone getMilestone() { + return milestone; + } + + public static class PullRequest{ + private String diff_url, patch_url, html_url; + + public URL getDiffUrl() { + return GitHub.parseURL(diff_url); + } + + public URL getPatchUrl() { + return GitHub.parseURL(patch_url); + } + + public URL getUrl() { + return GitHub.parseURL(html_url); + } + } } \ No newline at end of file diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java index ddceccd70f..2a16abcdad 100644 --- a/src/main/java/org/kohsuke/github/GHMyself.java +++ b/src/main/java/org/kohsuke/github/GHMyself.java @@ -22,7 +22,7 @@ public class GHMyself extends GHUser { * Always non-null. */ public List getEmails() throws IOException { - String[] addresses = root.retrieveWithAuth("/user/emails", String[].class); + String[] addresses = root.retrieve().withCredential().to("/user/emails", String[].class); return Collections.unmodifiableList(Arrays.asList(addresses)); } @@ -33,11 +33,11 @@ public List getEmails() throws IOException { * Always non-null. */ public List getPublicKeys() throws IOException { - return Collections.unmodifiableList(Arrays.asList(root.retrieveWithAuth("/user/keys", GHKey[].class))); + return Collections.unmodifiableList(Arrays.asList(root.retrieve().withCredential().to("/user/keys", GHKey[].class))); } // public void addEmails(Collection emails) throws IOException { -//// new Poster(root,ApiVersion.V3).withCredential().to("/user/emails"); +//// new Requester(root,ApiVersion.V3).withCredential().to("/user/emails"); // root.retrieveWithAuth3() // } } diff --git a/src/main/java/org/kohsuke/github/GHOrganization.java b/src/main/java/org/kohsuke/github/GHOrganization.java index f15626370c..8328c72235 100644 --- a/src/main/java/org/kohsuke/github/GHOrganization.java +++ b/src/main/java/org/kohsuke/github/GHOrganization.java @@ -33,7 +33,7 @@ public GHRepository createRepository(String name, String description, String hom public GHRepository createRepository(String name, String description, String homepage, GHTeam team, boolean isPublic) throws IOException { // such API doesn't exist, so fall back to HTML scraping - return new Poster(root).withCredential() + return new Requester(root).withCredential() .with("name", name).with("description", description).with("homepage", homepage) .with("public", isPublic).with("team_id",team.getId()).to("/orgs/"+login+"/repos", GHRepository.class).wrap(root); } @@ -42,7 +42,7 @@ public GHRepository createRepository(String name, String description, String hom * Teams by their names. */ public Map getTeams() throws IOException { - GHTeam[] teams = root.retrieveWithAuth("/orgs/" + login + "/teams", GHTeam[].class); + GHTeam[] teams = root.retrieve().withCredential().to("/orgs/" + login + "/teams", GHTeam[].class); Map r = new TreeMap(); for (GHTeam t : teams) { r.put(t.getName(),t.wrapUp(this)); @@ -54,7 +54,7 @@ public Map getTeams() throws IOException { * Publicizes the membership. */ public void publicize(GHUser u) throws IOException { - root.retrieveWithAuth("/orgs/" + login + "/public_members/" + u.getLogin(), null, "PUT"); + root.retrieve().withCredential().method("PUT").to("/orgs/" + login + "/public_members/" + u.getLogin(), null); } /** @@ -64,7 +64,7 @@ public List getMembers() throws IOException { return new AbstractList() { // these are shallow objects with only some limited values filled out // TODO: it's better to allow objects to fill themselves in later when missing values are requested - final GHUser[] shallow = root.retrieveWithAuth("/orgs/" + login + "/members", GHUser[].class); + final GHUser[] shallow = root.retrieve().withCredential().to("/orgs/" + login + "/members", GHUser[].class); @Override public GHUser get(int index) { @@ -86,7 +86,7 @@ public int size() { * Conceals the membership. */ public void conceal(GHUser u) throws IOException { - root.retrieveWithAuth("/orgs/" + login + "/public_members/" + u.getLogin(), null, "DELETE"); + root.retrieve().withCredential().method("DELETE").to("/orgs/" + login + "/public_members/" + u.getLogin(), null); } public enum Permission { ADMIN, PUSH, PULL } @@ -95,13 +95,13 @@ public enum Permission { ADMIN, PUSH, PULL } * Creates a new team and assigns the repositories. */ public GHTeam createTeam(String name, Permission p, Collection repositories) throws IOException { - Poster post = new Poster(root).withCredential().with("name", name).with("permission", p.name().toLowerCase()); + Requester post = new Requester(root).withCredential().with("name", name).with("permission", p.name().toLowerCase()); List repo_names = new ArrayList(); for (GHRepository r : repositories) { repo_names.add(r.getName()); } post.with("repo_names",repo_names); - return post.to("/orgs/"+login+"/teams",GHTeam.class,"POST").wrapUp(this); + return post.method("POST").to("/orgs/" + login + "/teams", GHTeam.class).wrapUp(this); } public GHTeam createTeam(String name, Permission p, GHRepository... repositories) throws IOException { diff --git a/src/main/java/org/kohsuke/github/GHPerson.java b/src/main/java/org/kohsuke/github/GHPerson.java index 4811839c0a..71c7c54c6f 100644 --- a/src/main/java/org/kohsuke/github/GHPerson.java +++ b/src/main/java/org/kohsuke/github/GHPerson.java @@ -17,13 +17,13 @@ public abstract class GHPerson { /*package almost final*/ GitHub root; - // common - protected String login,location,blog,email,name,created_at,company; + // core data fields that exist even for "small" user data (such as the user info in pull request) + protected String login, avatar_url, url, gravatar_id; protected int id; - protected String gravatar_id; // appears in V3 as well but presumably subsumed by avatar_url? - // V3 - protected String avatar_url,html_url; + // other fields (that only show up in full data) + protected String location,blog,email,name,created_at,company; + protected String html_url; protected int followers,following,public_repos,public_gists; /*package*/ GHPerson wrapUp(GitHub root) { @@ -31,6 +31,17 @@ public abstract class GHPerson { return this; } + /** + * 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 void populate() throws IOException { + if (created_at!=null) return; // already populated + + root.retrieve().to(url, this); + } + /** * Gets the repositories this user owns. */ @@ -56,7 +67,7 @@ public synchronized Map getRepositories() throws IOExceptio public synchronized Iterable> iterateRepositories(final int pageSize) { return new Iterable>() { public Iterator> iterator() { - final Iterator pager = root.retrievePaged("/users/" + login + "/repos?per_page="+pageSize,GHRepository[].class,false); + final Iterator pager = root.retrieve().asIterator("/users/" + login + "/repos?per_page="+pageSize,GHRepository[].class); return new Iterator>() { public boolean hasNext() { @@ -85,7 +96,7 @@ public void remove() { */ public GHRepository getRepository(String name) throws IOException { try { - return root.retrieveWithAuth("/repos/" + login + '/' + name, GHRepository.class).wrap(root); + return root.retrieve().withCredential().to("/repos/" + login + '/' + name, GHRepository.class).wrap(root); } catch (FileNotFoundException e) { return null; } @@ -123,51 +134,60 @@ public String getLogin() { /** * Gets the human-readable name of the user, like "Kohsuke Kawaguchi" */ - public String getName() { + public String getName() throws IOException { + populate(); return name; } /** * Gets the company name of this user, like "Sun Microsystems, Inc." */ - public String getCompany() { + public String getCompany() throws IOException { + populate(); return company; } /** * Gets the location of this user, like "Santa Clara, California" */ - public String getLocation() { + public String getLocation() throws IOException { + populate(); return location; } - public String getCreatedAt() { + public String getCreatedAt() throws IOException { + populate(); return created_at; } /** * Gets the blog URL of this user. */ - public String getBlog() { + public String getBlog() throws IOException { + populate(); return blog; } /** * Gets the e-mail address of the user. */ - public String getEmail() { + public String getEmail() throws IOException { + populate(); return email; } - public int getPublicGistCount() { + public int getPublicGistCount() throws IOException { + populate(); return public_gists; } - public int getPublicRepoCount() { + public int getPublicRepoCount() throws IOException { + populate(); return public_repos; } - public int getFollowingCount() { + public int getFollowingCount() throws IOException { + populate(); return following; } @@ -178,7 +198,8 @@ public int getId() { return id; } - public int getFollowersCount() { + public int getFollowersCount() throws IOException { + populate(); return followers; } diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index 1f86981e0f..32523005e2 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -23,7 +23,9 @@ */ package org.kohsuke.github; +import java.io.IOException; import java.net.URL; +import java.util.Collection; import java.util.Date; /** @@ -33,12 +35,35 @@ */ @SuppressWarnings({"UnusedDeclaration"}) public class GHPullRequest extends GHIssue { - private String closed_at, patch_url, issue_updated_at; - private GHUser issue_user, user; - // labels?? - private GHCommitPointer base, head; - private String mergeable, diff_url; + + private String patch_url, diff_url, issue_url; + private GHCommitPointer base; + private String merged_at; + private GHCommitPointer head; + // details that are only available when obtained from ID + private GHUser merged_by; + private int review_comments, additions; + private boolean merged; + private Boolean mergeable; + private int deletions; + private String mergeable_state; + private int changed_files; + + + GHPullRequest wrapUp(GHRepository owner) { + this.wrap(owner); + return wrapUp(owner.root); + } + + GHPullRequest wrapUp(GitHub root) { + if (owner!=null) owner.wrap(root); + if (base!=null) base.wrapUp(root); + if (head!=null) head.wrapUp(root); + if (merged_by != null) merged_by.wrapUp(root); + return this; + } + /** * The URL of the patch file. * like https://github.com/jenkinsci/jenkins/pull/100.patch @@ -46,12 +71,13 @@ public class GHPullRequest extends GHIssue { public URL getPatchUrl() { return GitHub.parseURL(patch_url); } - - /** - * User who submitted a pull request. + + /** + * The URL of the patch file. + * like https://github.com/jenkinsci/jenkins/pull/100.patch */ - public GHUser getUser() { - return user; + public URL getIssueUrl() { + return GitHub.parseURL(issue_url); } /** @@ -69,16 +95,9 @@ public GHCommitPointer getHead() { return head; } + @Deprecated public Date getIssueUpdatedAt() { - return GitHub.parseDate(issue_updated_at); - } - - /** - * The HTML page of this pull request, - * like https://github.com/jenkinsci/jenkins/pull/100 - */ - public URL getUrl() { - return super.getUrl(); + return super.getUpdatedAt(); } /** @@ -89,22 +108,77 @@ public URL getDiffUrl() { return GitHub.parseURL(diff_url); } - public Date getClosedAt() { - return GitHub.parseDate(closed_at); + public Date getMergedAt() { + return GitHub.parseDate(merged_at); } - GHPullRequest wrapUp(GHRepository owner) { - this.owner = owner; - return wrapUp(owner.root); - } + @Override + public Collection getLabels() { + return super.getLabels(); + } - GHPullRequest wrapUp(GitHub root) { - this.root = root; - if (owner!=null) owner.wrap(root); - if (issue_user!=null) issue_user.root=root; - if (user!=null) user.root=root; - if (base!=null) base.wrapUp(root); - if (head!=null) head.wrapUp(root); - return this; + @Override + public GHUser getClosedBy() { + return null; + } + + @Override + public PullRequest getPullRequest() { + return null; + } + +// +// details that are only available via get with ID +// +// + public GHUser getMergedBy() throws IOException { + populate(); + return merged_by; + } + + public int getReviewComments() throws IOException { + populate(); + return review_comments; + } + + public int getAdditions() throws IOException { + populate(); + return additions; + } + + public boolean isMerged() throws IOException { + populate(); + return merged; + } + + public Boolean getMergeable() throws IOException { + populate(); + return mergeable; + } + + public int getDeletions() throws IOException { + populate(); + return deletions; + } + + public String getMergeableState() throws IOException { + populate(); + return mergeable_state; + } + + public int getChangedFiles() throws IOException { + populate(); + return changed_files; + } + + /** + * Fully populate the data by retrieving missing data. + * + * Depending on the original API call where this object is created, it may not contain everything. + */ + private void populate() throws IOException { + if (merged_by!=null) return; // already populated + + root.retrieve().to(url, this); } } diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 15877e2bd7..291b616c3a 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -141,7 +141,7 @@ public GHUser getOwner() throws IOException { } public List getIssues(GHIssueState state) throws IOException { - return Arrays.asList(GHIssue.wrap(root.retrieve("/repos/" + owner.login + "/" + name + "/issues?state=" + state.toString().toLowerCase(), GHIssue[].class), this)); + return Arrays.asList(GHIssue.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/issues?state=" + state.toString().toLowerCase(), GHIssue[].class), this)); } protected String getOwnerName() { @@ -213,7 +213,7 @@ public int getSize() { */ @WithBridgeMethods(Set.class) public GHPersonSet getCollaborators() throws IOException { - return new GHPersonSet(GHUser.wrap(root.retrieve("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class),root)); + return new GHPersonSet(GHUser.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class),root)); } /** @@ -222,7 +222,7 @@ public GHPersonSet getCollaborators() throws IOException { */ public Set getCollaboratorNames() throws IOException { Set r = new HashSet(); - for (GHUser u : GHUser.wrap(root.retrieve("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class),root)) + for (GHUser u : GHUser.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class),root)) r.add(u.login); return r; } @@ -231,7 +231,7 @@ public Set getCollaboratorNames() throws IOException { * If this repository belongs to an organization, return a set of teams. */ public Set getTeams() throws IOException { - return Collections.unmodifiableSet(new HashSet(Arrays.asList(GHTeam.wrapUp(root.retrieveWithAuth("/repos/" + owner.login + "/" + name + "/teams", GHTeam[].class), root.getOrganization(owner.login))))); + return Collections.unmodifiableSet(new HashSet(Arrays.asList(GHTeam.wrapUp(root.retrieve().withCredential().to("/repos/" + owner.login + "/" + name + "/teams", GHTeam[].class), root.getOrganization(owner.login))))); } public void addCollaborators(GHUser... users) throws IOException { @@ -253,7 +253,7 @@ public void removeCollaborators(Collection users) throws IOException { private void modifyCollaborators(Collection users, String method) throws IOException { verifyMine(); for (GHUser user : users) { - new Poster(root).withCredential().to("/repos/"+owner.login+"/"+name+"/collaborators/"+user.getLogin(),null,method); + new Requester(root).withCredential().method(method).to("/repos/" + owner.login + "/" + name + "/collaborators/" + user.getLogin()); } } @@ -270,11 +270,10 @@ public void setEmailServiceHook(String address) throws IOException { } private void edit(String key, String value) throws IOException { - Poster poster = new Poster(root).withCredential(); + Requester requester = new Requester(root).withCredential(); if (!key.equals("name")) - poster.with("name", name); // even when we don't change the name, we need to send it in - poster.with(key, value) - .to("/repos/" + owner.login + "/" + name, null, "PATCH"); + requester.with("name", name); // even when we don't change the name, we need to send it in + requester.with(key, value).method("PATCH").to("/repos/" + owner.login + "/" + name); } /** @@ -314,7 +313,7 @@ public void setHomepage(String value) throws IOException { * Deletes this repository. */ public void delete() throws IOException { - new Poster(root).withCredential().to("/repos/" + owner.login +"/"+name, null, "DELETE"); + new Requester(root).withCredential().method("DELETE").to("/repos/" + owner.login + "/" + name); } /** @@ -324,7 +323,7 @@ public void delete() throws IOException { * Newly forked repository that belong to you. */ public GHRepository fork() throws IOException { - return new Poster(root).withCredential().to("/repos/" + owner.login + "/" + name + "/forks", GHRepository.class, "POST").wrap(root); + return new Requester(root).withCredential().method("POST").to("/repos/" + owner.login + "/" + name + "/forks", GHRepository.class).wrap(root); } /** @@ -334,7 +333,7 @@ public GHRepository fork() throws IOException { * Newly forked repository that belong to you. */ public GHRepository forkTo(GHOrganization org) throws IOException { - new Poster(root).withCredential().to(String.format("/repos/%s/%s/forks?org=%s",owner.login,name,org.getLogin())); + new Requester(root).withCredential().to(String.format("/repos/%s/%s/forks?org=%s",owner.login,name,org.getLogin())); // this API is asynchronous. we need to wait for a bit for (int i=0; i<10; i++) { @@ -353,7 +352,7 @@ public GHRepository forkTo(GHOrganization org) throws IOException { * Retrieves a specified pull request. */ public GHPullRequest getPullRequest(int i) throws IOException { - return root.retrieveWithAuth("/repos/" + owner.login + '/' + name + "/pulls/" + i, GHPullRequest.class).wrapUp(this); + return root.retrieve().withCredential().to("/repos/" + owner.login + '/' + name + "/pulls/" + i, GHPullRequest.class).wrapUp(this); } /** @@ -371,7 +370,7 @@ public List getPullRequests(GHIssueState state) throws IOExceptio public PagedIterable listPullRequests(final GHIssueState state) { return new PagedIterable() { public PagedIterator iterator() { - return new PagedIterator(root.retrievePaged(String.format("/repos/%s/%s/pulls?state=%s", owner.login,name,state.name().toLowerCase(Locale.ENGLISH)), GHPullRequest[].class, false)) { + return new PagedIterator(root.retrieve().asIterator(String.format("/repos/%s/%s/pulls?state=%s", owner.login, name, state.name().toLowerCase(Locale.ENGLISH)), GHPullRequest[].class)) { @Override protected void wrapUp(GHPullRequest[] page) { for (GHPullRequest pr : page) @@ -387,14 +386,14 @@ protected void wrapUp(GHPullRequest[] page) { */ public List getHooks() throws IOException { List list = new ArrayList(Arrays.asList( - root.retrieveWithAuth(String.format("/repos/%s/%s/hooks", owner.login, name), GHHook[].class))); + root.retrieve().withCredential().to(String.format("/repos/%s/%s/hooks", owner.login, name), GHHook[].class))); for (GHHook h : list) h.wrap(this); return list; } public GHHook getHook(int id) throws IOException { - return root.retrieveWithAuth(String.format("/repos/%s/%s/hooks/%d", owner.login, name, id), GHHook.class).wrap(this); + return root.retrieve().withCredential().to(String.format("/repos/%s/%s/hooks/%d", owner.login, name, id), GHHook.class).wrap(this); } /** @@ -403,7 +402,7 @@ public GHHook getHook(int id) throws IOException { public GHCommit getCommit(String sha1) throws IOException { GHCommit c = commits.get(sha1); if (c==null) { - c = root.retrieve(String.format("/repos/%s/%s/commits/%s", owner.login, name, sha1), GHCommit.class).wrapUp(this); + c = root.retrieve().to(String.format("/repos/%s/%s/commits/%s", owner.login, name, sha1), GHCommit.class).wrapUp(this); commits.put(sha1,c); } return c; @@ -415,7 +414,7 @@ public GHCommit getCommit(String sha1) throws IOException { public PagedIterable listCommits() { return new PagedIterable() { public PagedIterator iterator() { - return new PagedIterator(root.retrievePaged(String.format("/repos/%s/%s/commits",owner.login,name),GHCommit[].class,false)) { + return new PagedIterator(root.retrieve().asIterator(String.format("/repos/%s/%s/commits", owner.login, name), GHCommit[].class)) { protected void wrapUp(GHCommit[] page) { for (GHCommit c : page) c.wrapUp(GHRepository.this); @@ -431,7 +430,7 @@ protected void wrapUp(GHCommit[] page) { public PagedIterable listCommitComments() { return new PagedIterable() { public PagedIterator iterator() { - return new PagedIterator(root.retrievePaged(String.format("/repos/%s/%s/comments",owner.login,name),GHCommitComment[].class,false)) { + return new PagedIterator(root.retrieve().asIterator(String.format("/repos/%s/%s/comments", owner.login, name), GHCommitComment[].class)) { @Override protected void wrapUp(GHCommitComment[] page) { for (GHCommitComment c : page) @@ -442,6 +441,49 @@ protected void wrapUp(GHCommitComment[] page) { }; } + /** + * Lists all the commit statues attached to the given commit, newer ones first. + */ + public PagedIterable listCommitStatuses(final String sha1) throws IOException { + return new PagedIterable() { + public PagedIterator iterator() { + return new PagedIterator(root.retrieve().asIterator(String.format("/repos/%s/%s/statuses/%s", owner.login, name, sha1), GHCommitStatus[].class)) { + @Override + protected void wrapUp(GHCommitStatus[] page) { + for (GHCommitStatus c : page) + c.wrapUp(root); + } + }; + } + }; + } + + /** + * Gets the last status of this commit, which is what gets shown in the UI. + */ + public GHCommitStatus getLastCommitStatus(String sha1) throws IOException { + List v = listCommitStatuses(sha1).asList(); + return v.isEmpty() ? null : v.get(0); + } + + /** + * Creates a commit status + * + * @param targetUrl + * Optional parameter that points to the URL that has more details. + * @param description + * Optional short description. + */ + public GHCommitStatus createCommitStatus(String sha1, GHCommitState state, String targetUrl, String description) throws IOException { + return new Requester(root) + .withCredential() + .with("state",state.name().toLowerCase(Locale.ENGLISH)) + .with("target_url", targetUrl) + .with("description", description) + .to(String.format("/repos/%s/%s/statuses/%s",owner.login,this.name,sha1),GHCommitStatus.class).wrapUp(root); + } + + /** * * See https://api.github.com/hooks for possible names and their configuration scheme. @@ -462,7 +504,7 @@ public GHHook createHook(String name, Map config, Collection getBranches() throws IOException { Map r = new TreeMap(); - for (GHBranch p : root.retrieve("/repos/" + owner.login + "/" + name + "/branches", GHBranch[].class)) { + for (GHBranch p : root.retrieve().to("/repos/" + owner.login + "/" + name + "/branches", GHBranch[].class)) { p.wrap(this); r.put(p.getName(),p); } @@ -577,7 +619,7 @@ public Map getBranches() throws IOException { public Map getMilestones() throws IOException { Map milestones = new TreeMap(); - GHMilestone[] ms = root.retrieve("/repos/" + owner.login + "/" + name + "/milestones", GHMilestone[].class); + GHMilestone[] ms = root.retrieve().to("/repos/" + owner.login + "/" + name + "/milestones", GHMilestone[].class); for (GHMilestone m : ms) { m.owner = this; m.root = root; @@ -589,7 +631,7 @@ public Map getMilestones() throws IOException { public GHMilestone getMilestone(int number) throws IOException { GHMilestone m = milestones.get(number); if (m == null) { - m = root.retrieve("/repos/" + owner.login + "/" + name + "/milestones/" + number, GHMilestone.class); + m = root.retrieve().to("/repos/" + owner.login + "/" + name + "/milestones/" + number, GHMilestone.class); m.owner = this; m.root = root; milestones.put(m.getNumber(), m); @@ -598,9 +640,8 @@ public GHMilestone getMilestone(int number) throws IOException { } public GHMilestone createMilestone(String title, String description) throws IOException { - return new Poster(root).withCredential() - .with("title", title).with("description", description) - .to("/repos/"+owner.login+"/"+name+"/milestones", GHMilestone.class,"POST").wrap(this); + return new Requester(root).withCredential() + .with("title", title).with("description", description).method("POST").to("/repos/" + owner.login + "/" + name + "/milestones", GHMilestone.class).wrap(this); } @Override diff --git a/src/main/java/org/kohsuke/github/GHTeam.java b/src/main/java/org/kohsuke/github/GHTeam.java index d4bf99d582..dca37e3313 100644 --- a/src/main/java/org/kohsuke/github/GHTeam.java +++ b/src/main/java/org/kohsuke/github/GHTeam.java @@ -46,11 +46,11 @@ public int getId() { * Retrieves the current members. */ public Set getMembers() throws IOException { - return new HashSet(Arrays.asList(GHUser.wrap(org.root.retrieveWithAuth(api("/members"), GHUser[].class), org.root))); + return new HashSet(Arrays.asList(GHUser.wrap(org.root.retrieve().withCredential().to(api("/members"), GHUser[].class), org.root))); } public Map getRepositories() throws IOException { - GHRepository[] repos = org.root.retrieveWithAuth(api("/repos"), GHRepository[].class); + GHRepository[] repos = org.root.retrieve().withCredential().to(api("/repos"), GHRepository[].class); Map m = new TreeMap(); for (GHRepository r : repos) { m.put(r.getName(),r.wrap(org.root)); @@ -62,22 +62,22 @@ public Map getRepositories() throws IOException { * Adds a member to the team. */ public void add(GHUser u) throws IOException { - org.root.retrieveWithAuth(api("/members/" + u.getLogin()), null, "PUT"); + org.root.retrieve().withCredential().method("PUT").to(api("/members/" + u.getLogin()), null); } /** * Removes a member to the team. */ public void remove(GHUser u) throws IOException { - org.root.retrieveWithAuth(api("/members/" + u.getLogin()), null, "DELETE"); + org.root.retrieve().withCredential().method("DELETE").to(api("/members/" + u.getLogin()), null); } public void add(GHRepository r) throws IOException { - org.root.retrieveWithAuth(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null, "PUT"); + org.root.retrieve().withCredential().method("PUT").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); } public void remove(GHRepository r) throws IOException { - org.root.retrieveWithAuth(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null, "DELETE"); + org.root.retrieve().withCredential().method("DELETE").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); } private String api(String tail) { diff --git a/src/main/java/org/kohsuke/github/GHUser.java b/src/main/java/org/kohsuke/github/GHUser.java index 07f44f71c4..329434f38c 100644 --- a/src/main/java/org/kohsuke/github/GHUser.java +++ b/src/main/java/org/kohsuke/github/GHUser.java @@ -41,14 +41,14 @@ public class GHUser extends GHPerson { * Follow this user. */ public void follow() throws IOException { - new Poster(root).withCredential().to("/user/following/"+login,null,"PUT"); + new Requester(root).withCredential().method("PUT").to("/user/following/" + login); } /** * Unfollow this user. */ public void unfollow() throws IOException { - new Poster(root).withCredential().to("/user/following/"+login,null,"DELETE"); + new Requester(root).withCredential().method("DELETE").to("/user/following/" + login); } /** @@ -56,7 +56,7 @@ public void unfollow() throws IOException { */ @WithBridgeMethods(Set.class) public GHPersonSet getFollows() throws IOException { - GHUser[] followers = root.retrieve("/users/" + login + "/following", GHUser[].class); + GHUser[] followers = root.retrieve().to("/users/" + login + "/following", GHUser[].class); return new GHPersonSet(Arrays.asList(wrap(followers,root))); } @@ -65,7 +65,7 @@ public GHPersonSet getFollows() throws IOException { */ @WithBridgeMethods(Set.class) public GHPersonSet getFollowers() throws IOException { - GHUser[] followers = root.retrieve("/users/" + login + "/followers", GHUser[].class); + GHUser[] followers = root.retrieve().to("/users/" + login + "/followers", GHUser[].class); return new GHPersonSet(Arrays.asList(wrap(followers,root))); } @@ -82,7 +82,7 @@ public GHPersonSet getFollowers() throws IOException { public GHPersonSet getOrganizations() throws IOException { GHPersonSet orgs = new GHPersonSet(); Set names = new HashSet(); - for (GHOrganization o : root.retrieve("/users/" + login + "/orgs", GHOrganization[].class)) { + for (GHOrganization o : root.retrieve().to("/users/" + login + "/orgs", GHOrganization[].class)) { if (names.add(o.getLogin())) // I've seen some duplicates in the data orgs.add(root.getOrganization(o.getLogin())); } diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 5893de9c41..886239820b 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -23,18 +23,20 @@ */ package org.kohsuke.github; -import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.ANY; -import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import org.apache.commons.io.IOUtils; +import org.codehaus.jackson.map.DeserializationConfig.Feature; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.introspect.VisibilityChecker.Std; +import sun.misc.BASE64Encoder; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.InterruptedIOException; import java.io.Reader; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; @@ -42,25 +44,12 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Properties; import java.util.TimeZone; -import java.util.zip.GZIPInputStream; -import com.infradna.tool.bridge_method_injector.WithBridgeMethods; -import org.apache.commons.io.IOUtils; -import org.codehaus.jackson.map.DeserializationConfig.Feature; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.introspect.VisibilityChecker.Std; - -import sun.misc.BASE64Encoder; - -import com.gargoylesoftware.htmlunit.WebClient; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; +import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.*; /** * Root of the GitHub API. @@ -163,196 +152,22 @@ public static GitHub connectAnonymously() { // append the access token tailApiUrl = tailApiUrl + (tailApiUrl.indexOf('?')>=0 ?'&':'?') + "access_token=" + oauthAccessToken; } - - return new URL("https://api."+githubServer+tailApiUrl); - } - - /*package*/ T retrieve(String tailApiUrl, Class type) throws IOException { - return _retrieve(tailApiUrl, type, "GET", false); - } - - /*package*/ T retrieveWithAuth(String tailApiUrl, Class type) throws IOException { - return _retrieve(tailApiUrl, type, "GET", true); - } - - /*package*/ T retrieveWithAuth(String tailApiUrl, Class type, String method) throws IOException { - return _retrieve(tailApiUrl, type, method, true); - } - - private T _retrieve(String tailApiUrl, Class type, String method, boolean withAuth) throws IOException { - while (true) {// loop while API rate limit is hit - HttpURLConnection uc = setupConnection(method, withAuth, getApiURL(tailApiUrl)); - try { - return parse(uc,type); - } catch (IOException e) { - handleApiError(e,uc); - } - } - } - - /** - * Loads pagenated resources. - * - * Every iterator call reports a new batch. - */ - /*package*/ Iterator retrievePaged(final String tailApiUrl, final Class type, final boolean withAuth) { - return new Iterator() { - /** - * The next batch to be returned from {@link #next()}. - */ - T next; - /** - * URL of the next resource to be retrieved, or null if no more data is available. - */ - URL url; - - { - try { - url = getApiURL(tailApiUrl); - } catch (IOException e) { - throw new Error(e); - } - } - - public boolean hasNext() { - fetch(); - return next!=null; - } - - public T next() { - fetch(); - T r = next; - if (r==null) throw new NoSuchElementException(); - next = null; - return r; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - private void fetch() { - if (next!=null) return; // already fetched - if (url==null) return; // no more data to fetch - - try { - while (true) {// loop while API rate limit is hit - HttpURLConnection uc = setupConnection("GET", withAuth, url); - try { - next = parse(uc,type); - assert next!=null; - findNextURL(uc); - return; - } catch (IOException e) { - handleApiError(e,uc); - } - } - } catch (IOException e) { - throw new Error(e); - } - } - - /** - * Locate the next page from the pagination "Link" tag. - */ - private void findNextURL(HttpURLConnection uc) throws MalformedURLException { - url = null; // start defensively - String link = uc.getHeaderField("Link"); - if (link==null) return; - - for (String token : link.split(", ")) { - if (token.endsWith("rel=\"next\"")) { - // found the next page. This should look something like - // ; rel="next" - int idx = token.indexOf('>'); - url = new URL(token.substring(1,idx)); - return; - } - } - - // no more "next" link. we are done. - } - }; + if (tailApiUrl.startsWith("/")) + return new URL("https://api."+githubServer+tailApiUrl); + else + return new URL(tailApiUrl); } - private HttpURLConnection setupConnection(String method, boolean withAuth, URL url) throws IOException { - HttpURLConnection uc = (HttpURLConnection) url.openConnection(); - - // if the authentication is needed but no credential is given, try it anyway (so that some calls - // that do work with anonymous access in the reduced form should still work.) - // if OAuth token is present, it'll be set in the URL, so need to set the Authorization header - if (withAuth && encodedAuthorization!=null && this.oauthAccessToken == null) - uc.setRequestProperty("Authorization", "Basic " + encodedAuthorization); - - uc.setRequestMethod(method); - uc.setRequestProperty("Accept-Encoding", "gzip"); - if (method.equals("PUT")) { - uc.setDoOutput(true); - uc.setRequestProperty("Content-Length","0"); - uc.getOutputStream().close(); - } - return uc; - } - - private T parse(HttpURLConnection uc, Class type) throws IOException { - InputStreamReader r = null; - try { - r = new InputStreamReader(wrapStream(uc, uc.getInputStream()), "UTF-8"); - if (type==null) { - String data = IOUtils.toString(r); - return null; - } - return MAPPER.readValue(r,type); - } finally { - IOUtils.closeQuietly(r); - } - } - - /** - * Handles the "Content-Encoding" header. - */ - private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException { - String encoding = uc.getContentEncoding(); - if (encoding==null || in==null) return in; - if (encoding.equals("gzip")) return new GZIPInputStream(in); - - throw new UnsupportedOperationException("Unexpected Content-Encoding: "+encoding); - } - - /** - * If the error is because of the API limit, wait 10 sec and return normally. - * Otherwise throw an exception reporting an error. - */ - /*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException { - if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) { - // API limit reached. wait 10 secs and return normally - try { - Thread.sleep(10000); - return; - } catch (InterruptedException _) { - throw (InterruptedIOException)new InterruptedIOException().initCause(e); - } - } - - if (e instanceof FileNotFoundException) - throw e; // pass through 404 Not Found to allow the caller to handle it intelligently - - InputStream es = wrapStream(uc, uc.getErrorStream()); - try { - if (es!=null) - throw (IOException)new IOException(IOUtils.toString(es,"UTF-8")).initCause(e); - else - throw e; - } finally { - IOUtils.closeQuietly(es); - } + /*package*/ Requester retrieve() { + return new Requester(this).method("GET"); } /** * Gets the current rate limit. */ public GHRateLimit getRateLimit() throws IOException { - return retrieveWithAuth("/rate_limit", JsonRateLimit.class).rate; + return retrieve().withCredential().to("/rate_limit", JsonRateLimit.class).rate; } /** @@ -362,7 +177,7 @@ public GHRateLimit getRateLimit() throws IOException { public GHMyself getMyself() throws IOException { requireCredential(); - GHMyself u = retrieveWithAuth("/user", GHMyself.class); + GHMyself u = retrieve().withCredential().to("/user", GHMyself.class); u.root = this; users.put(u.getLogin(), u); @@ -376,7 +191,7 @@ public GHMyself getMyself() throws IOException { public GHUser getUser(String login) throws IOException { GHUser u = users.get(login); if (u == null) { - u = retrieve("/users/" + login, GHUser.class); + u = retrieve().to("/users/" + login, GHUser.class); u.root = this; users.put(u.getLogin(), u); } @@ -399,7 +214,7 @@ protected GHUser getUser(GHUser orig) throws IOException { public GHOrganization getOrganization(String name) throws IOException { GHOrganization o = orgs.get(name); if (o==null) { - o = retrieve("/orgs/" + name, GHOrganization.class).wrapUp(this); + o = retrieve().to("/orgs/" + name, GHOrganization.class).wrapUp(this); orgs.put(name,o); } return o; @@ -422,7 +237,7 @@ public GHRepository getRepository(String name) throws IOException { * TODO: make this automatic. */ public Map getMyOrganizations() throws IOException { - GHOrganization[] orgs = retrieveWithAuth("/user/orgs", GHOrganization[].class); + GHOrganization[] orgs = retrieve().withCredential().to("/user/orgs", GHOrganization[].class); Map r = new HashMap(); for (GHOrganization o : orgs) { // don't put 'o' into orgs because they are shallow @@ -436,7 +251,7 @@ public Map getMyOrganizations() throws IOException { */ public List getEvents() throws IOException { // TODO: pagenation - GHEventInfo[] events = retrieve("/events", GHEventInfo[].class); + GHEventInfo[] events = retrieve().to("/events", GHEventInfo[].class); for (GHEventInfo e : events) e.wrapUp(this); return Arrays.asList(events); @@ -462,9 +277,10 @@ public T parseEventPayload(Reader r, Class type) t * Newly created repository. */ public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException { - return new Poster(this).withCredential() + Requester requester = new Requester(this).withCredential() .with("name", name).with("description", description).with("homepage", homepage) - .with("public", isPublic ? 1 : 0).to("/user/repos", GHRepository.class,"POST").wrap(this); + .with("public", isPublic ? 1 : 0); + return requester.method("POST").to("/user/repos", GHRepository.class).wrap(this); } /** @@ -472,7 +288,7 @@ public GHRepository createRepository(String name, String description, String hom */ public boolean isCredentialValid() throws IOException { try { - retrieveWithAuth("/user", GHUser.class); + retrieve().withCredential().to("/user", GHUser.class); return true; } catch (IOException e) { return false; diff --git a/src/main/java/org/kohsuke/github/Poster.java b/src/main/java/org/kohsuke/github/Poster.java deleted file mode 100644 index d18c054b62..0000000000 --- a/src/main/java/org/kohsuke/github/Poster.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.kohsuke.github; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Field; -import java.net.HttpURLConnection; -import java.net.ProtocolException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.kohsuke.github.GitHub.*; - -/** - * Handles HTTP POST. - * @author Kohsuke Kawaguchi - */ -class Poster { - private final GitHub root; - private final List args = new ArrayList(); - private boolean authenticate; - - private static class Entry { - String key; - Object value; - - private Entry(String key, Object value) { - this.key = key; - this.value = value; - } - } - - Poster(GitHub root) { - this.root = root; - } - - public Poster withCredential() { - root.requireCredential(); - authenticate = true; - return this; - } - - public Poster with(String key, int value) { - return _with(key, value); - } - - public Poster with(String key, Integer value) { - if (value!=null) - _with(key, value.intValue()); - return this; - } - - public Poster with(String key, boolean value) { - return _with(key, value); - } - - public Poster with(String key, String value) { - return _with(key, value); - } - - public Poster with(String key, Collection value) { - return _with(key, value); - } - - public Poster _with(String key, Object value) { - if (value!=null) { - args.add(new Entry(key,value)); - } - return this; - } - - public void to(String tailApiUrl) throws IOException { - to(tailApiUrl,null); - } - - /** - * POSTs the form to the specified URL. - * - * @throws IOException - * if the server returns 4xx/5xx responses. - * @return - * {@link Reader} that reads the response. - */ - public T to(String tailApiUrl, Class type) throws IOException { - return to(tailApiUrl,type,"POST"); - } - - public T to(String tailApiUrl, Class type, String method) throws IOException { - while (true) {// loop while API rate limit is hit - HttpURLConnection uc = (HttpURLConnection) root.getApiURL(tailApiUrl).openConnection(); - - uc.setDoOutput(true); - uc.setRequestProperty("Content-type","application/x-www-form-urlencoded"); - if (authenticate) { - if (root.oauthAccessToken!=null) { - uc.setRequestProperty("Authorization", "token " + root.oauthAccessToken); - } else { - if (root.password==null) - throw new IllegalArgumentException("V3 API doesn't support API token"); - uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization); - } - } - try { - uc.setRequestMethod(method); - } catch (ProtocolException e) { - // JDK only allows one of the fixed set of verbs. Try to override that - try { - Field $method = HttpURLConnection.class.getDeclaredField("method"); - $method.setAccessible(true); - $method.set(uc,method); - } catch (Exception x) { - throw (IOException)new IOException("Failed to set the custom verb").initCause(x); - } - } - - - Map json = new HashMap(); - for (Entry e : args) { - json.put(e.key, e.value); - } - MAPPER.writeValue(uc.getOutputStream(),json); - - try { - InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8"); - String data = IOUtils.toString(r); - if (type==null) { - return null; - } - return MAPPER.readValue(data,type); - } catch (IOException e) { - root.handleApiError(e,uc); - } - } - } -} diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java new file mode 100644 index 0000000000..be8bb3ffd6 --- /dev/null +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -0,0 +1,345 @@ +/* + * The MIT License + * + * Copyright (c) 2010, Kohsuke Kawaguchi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.kohsuke.github; + +import org.apache.commons.io.IOUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.Reader; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.zip.GZIPInputStream; + +import static org.kohsuke.github.GitHub.*; + +/** + * A builder pattern for making HTTP call and parsing its output. + * + * @author Kohsuke Kawaguchi + */ +class Requester { + private final GitHub root; + private final List args = new ArrayList(); + private boolean authenticate; + + /** + * Request method. + */ + private String method = "POST"; + + private static class Entry { + String key; + Object value; + + private Entry(String key, Object value) { + this.key = key; + this.value = value; + } + } + + Requester(GitHub root) { + this.root = root; + } + + /** + * Makes a request with authentication credential. + */ + public Requester withCredential() { + // keeping it inline with retrieveWithAuth not to enforce the check + // root.requireCredential(); + authenticate = true; + return this; + } + + public Requester with(String key, int value) { + return _with(key, value); + } + + public Requester with(String key, Integer value) { + if (value!=null) + _with(key, value.intValue()); + return this; + } + + public Requester with(String key, boolean value) { + return _with(key, value); + } + + public Requester with(String key, String value) { + return _with(key, value); + } + + public Requester with(String key, Collection value) { + return _with(key, value); + } + + public Requester _with(String key, Object value) { + if (value!=null) { + args.add(new Entry(key,value)); + } + return this; + } + + public Requester method(String method) { + this.method = method; + return this; + } + + public void to(String tailApiUrl) throws IOException { + to(tailApiUrl,null); + } + + /** + * Sends a request to the specified URL, and parses the response into the given type via databinding. + * + * @throws IOException + * if the server returns 4xx/5xx responses. + * @return + * {@link Reader} that reads the response. + */ + public T to(String tailApiUrl, Class type) throws IOException { + return _to(tailApiUrl, type, null); + } + + /** + * Like {@link #to(String, Class)} but updates an existing object instead of creating a new instance. + */ + public T to(String tailApiUrl, T existingInstance) throws IOException { + return _to(tailApiUrl, null, existingInstance); + } + + /** + * Short for {@code method(method).to(tailApiUrl,type)} + */ + @Deprecated + public T to(String tailApiUrl, Class type, String method) throws IOException { + return method(method).to(tailApiUrl,type); + } + + private T _to(String tailApiUrl, Class type, T instance) throws IOException { + while (true) {// loop while API rate limit is hit + HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl)); + + if (!method.equals("GET")) { + uc.setDoOutput(true); + uc.setRequestProperty("Content-type","application/x-www-form-urlencoded"); + + Map json = new HashMap(); + for (Entry e : args) { + json.put(e.key, e.value); + } + MAPPER.writeValue(uc.getOutputStream(),json); + } + + try { + return parse(uc,type,instance); + } catch (IOException e) { + handleApiError(e,uc); + } + } + } + + /** + * Loads pagenated resources. + * + * Every iterator call reports a new batch. + */ + /*package*/ Iterator asIterator(final String tailApiUrl, final Class type) { + method("GET"); + if (!args.isEmpty()) throw new IllegalStateException(); + + return new Iterator() { + /** + * The next batch to be returned from {@link #next()}. + */ + T next; + /** + * URL of the next resource to be retrieved, or null if no more data is available. + */ + URL url; + + { + try { + url = root.getApiURL(tailApiUrl); + } catch (IOException e) { + throw new Error(e); + } + } + + public boolean hasNext() { + fetch(); + return next!=null; + } + + public T next() { + fetch(); + T r = next; + if (r==null) throw new NoSuchElementException(); + next = null; + return r; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + private void fetch() { + if (next!=null) return; // already fetched + if (url==null) return; // no more data to fetch + + try { + while (true) {// loop while API rate limit is hit + HttpURLConnection uc = setupConnection(url); + try { + next = parse(uc,type,null); + assert next!=null; + findNextURL(uc); + return; + } catch (IOException e) { + handleApiError(e,uc); + } + } + } catch (IOException e) { + throw new Error(e); + } + } + + /** + * Locate the next page from the pagination "Link" tag. + */ + private void findNextURL(HttpURLConnection uc) throws MalformedURLException { + url = null; // start defensively + String link = uc.getHeaderField("Link"); + if (link==null) return; + + for (String token : link.split(", ")) { + if (token.endsWith("rel=\"next\"")) { + // found the next page. This should look something like + // ; rel="next" + int idx = token.indexOf('>'); + url = new URL(token.substring(1,idx)); + return; + } + } + + // no more "next" link. we are done. + } + }; + } + + + private HttpURLConnection setupConnection(URL url) throws IOException { + HttpURLConnection uc = (HttpURLConnection) url.openConnection(); + + // if the authentication is needed but no credential is given, try it anyway (so that some calls + // that do work with anonymous access in the reduced form should still work.) + // if OAuth token is present, it'll be set in the URL, so need to set the Authorization header + if (authenticate && root.encodedAuthorization!=null && root.oauthAccessToken == null) + uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization); + + try { + uc.setRequestMethod(method); + } catch (ProtocolException e) { + // JDK only allows one of the fixed set of verbs. Try to override that + try { + Field $method = HttpURLConnection.class.getDeclaredField("method"); + $method.setAccessible(true); + $method.set(uc,method); + } catch (Exception x) { + throw (IOException)new IOException("Failed to set the custom verb").initCause(x); + } + } + uc.setRequestProperty("Accept-Encoding", "gzip"); + return uc; + } + + private T parse(HttpURLConnection uc, Class type, T instance) throws IOException { + InputStreamReader r = null; + try { + r = new InputStreamReader(wrapStream(uc, uc.getInputStream()), "UTF-8"); + String data = IOUtils.toString(r); + if (type!=null) + return MAPPER.readValue(data,type); + if (instance!=null) + return MAPPER.readerForUpdating(instance).readValue(data); + return null; + } finally { + IOUtils.closeQuietly(r); + } + } + + /** + * Handles the "Content-Encoding" header. + */ + private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException { + String encoding = uc.getContentEncoding(); + if (encoding==null || in==null) return in; + if (encoding.equals("gzip")) return new GZIPInputStream(in); + + throw new UnsupportedOperationException("Unexpected Content-Encoding: "+encoding); + } + + /** + * If the error is because of the API limit, wait 10 sec and return normally. + * Otherwise throw an exception reporting an error. + */ + /*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException { + if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) { + // API limit reached. wait 10 secs and return normally + try { + Thread.sleep(10000); + return; + } catch (InterruptedException _) { + throw (InterruptedIOException)new InterruptedIOException().initCause(e); + } + } + + if (e instanceof FileNotFoundException) + throw e; // pass through 404 Not Found to allow the caller to handle it intelligently + + InputStream es = wrapStream(uc, uc.getErrorStream()); + try { + if (es!=null) + throw (IOException)new IOException(IOUtils.toString(es,"UTF-8")).initCause(e); + else + throw e; + } finally { + IOUtils.closeQuietly(es); + } + } +} diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java index 1b93cee1d1..ba4a4a4804 100644 --- a/src/test/java/org/kohsuke/AppTest.java +++ b/src/test/java/org/kohsuke/AppTest.java @@ -4,11 +4,14 @@ import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHCommit.File; import org.kohsuke.github.GHCommitComment; +import org.kohsuke.github.GHCommitState; +import org.kohsuke.github.GHCommitStatus; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHEventInfo; import org.kohsuke.github.GHEventPayload; import org.kohsuke.github.GHHook; import org.kohsuke.github.GHBranch; +import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHIssueState; import org.kohsuke.github.GHKey; import org.kohsuke.github.GHMyself; @@ -45,7 +48,7 @@ public void testRepoCRUD() throws Exception { public void testCredentialValid() throws IOException { assertTrue(GitHub.connect().isCredentialValid()); - assertFalse(GitHub.connect("totally","bogus").isCredentialValid()); + assertFalse(GitHub.connect("totally", "bogus").isCredentialValid()); } public void testRateLimit() throws IOException { @@ -85,7 +88,7 @@ public void testRepoPermissions() throws Exception { assertFalse(r.hasAdminAccess()); } - public void tryGetMyself() throws Exception { + public void testGetMyself() throws Exception { GitHub hub = GitHub.connect(); GHMyself me = hub.getMyself(); System.out.println(me); @@ -308,4 +311,25 @@ public void testOrganization() throws IOException { // t.add(labs.getRepository("xyz")); } + + public void testCommitStatus() throws Exception { + GitHub gitHub = GitHub.connect(); + GHRepository r = gitHub.getUser("kohsuke").getRepository("test"); + GHCommitStatus state; +// state = r.createCommitStatus("edacdd76b06c5f3f0697a22ca75803169f25f296", GHCommitState.FAILURE, "http://jenkins-ci.org/", "oops!"); + + List lst = r.listCommitStatuses("edacdd76b06c5f3f0697a22ca75803169f25f296").asList(); + state = lst.get(0); + System.out.println(state); + assertEquals("oops!",state.getDescription()); + assertEquals("http://jenkins-ci.org/",state.getTargetUrl()); + } + + public void testPullRequestPopulate() throws Exception { + GitHub gitHub = GitHub.connect(); + GHRepository r = gitHub.getUser("kohsuke").getRepository("github-api"); + GHPullRequest p = r.getPullRequest(17); + GHUser u = p.getUser(); + assertNotNull(u.getName()); + } }