diff --git a/pom.xml b/pom.xml index de5ce58c4f..fec4cefb66 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.kohsuke github-api jar - 1.2 + 1.3 GitHub API for Java http://kohsuke.org/github-api/ GitHub API for Java @@ -75,6 +75,18 @@ + + org.jvnet.hudson + htmlunit + 2.6-hudson-2 + + + + xml-apis + xml-apis + + + junit junit diff --git a/src/main/java/org/kohsuke/github/GHOrganization.java b/src/main/java/org/kohsuke/github/GHOrganization.java new file mode 100644 index 0000000000..24405373b6 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHOrganization.java @@ -0,0 +1,46 @@ +package org.kohsuke.github; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import java.io.IOException; +import java.util.Map; + +/** + * @author Kohsuke Kawaguchi + */ +public class GHOrganization extends GHPerson { + /** + * Creates a new repository. + * + * @return + * Newly created repository. + */ + public GHRepository createRepository(String name, String description, String homepage, String team, boolean isPublic) throws IOException { + // such API doesn't exist, so fall back to HTML scraping + WebClient wc = root.createWebClient(); + HtmlPage pg = (HtmlPage)wc.getPage("https://github.com/organizations/"+login+"/repositories/new"); + HtmlForm f = pg.getForms().get(1); + f.getInputByName("repository[name]").setValueAttribute(name); + f.getInputByName("repository[description]").setValueAttribute(description); + f.getInputByName("repository[homepage]").setValueAttribute(homepage); + f.getSelectByName("team_id").getOptionByText(team).setSelected(true); + f.submit(f.getButtonByCaption("Create Repository")); + + return root.getUser(login).getRepository(name); + +// GHRepository r = new Poster(root).withCredential() +// .with("name", name).with("description", description).with("homepage", homepage) +// .with("public", isPublic ? 1 : 0).to(root.getApiURL("/organizations/"+login+"/repos/create"), JsonRepository.class).repository; +// r.root = root; +// return r; + } + + /** + * Teams by their names. + */ + public Map getTeams() throws IOException { + return root.retrieveWithAuth(root.getApiURL("/organizations/"+login+"/teams"),JsonTeams.class).toMap(this); + } +} diff --git a/src/main/java/org/kohsuke/github/GHPerson.java b/src/main/java/org/kohsuke/github/GHPerson.java new file mode 100644 index 0000000000..db87442484 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPerson.java @@ -0,0 +1,58 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import static org.kohsuke.github.GitHub.*; + +/** + * Common part of {@link GHUser} and {@link GHOrganization}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class GHPerson { + /*package almost final*/ GitHub root; + + protected String gravatar_id,login; + + protected int public_gist_count,public_repo_count,following_count,id; + + /** + * Repositories that this user owns. + */ + private transient Map repositories; + + /** + * Gets the repositories this user owns. + */ + public Map getRepositories() throws IOException { + if (repositories==null) { + repositories = new TreeMap(); + URL url = new URL("http://github.com/api/v2/json/repos/show/" + login); + for (GHRepository r : MAPPER.readValue(url, JsonRepositories.class).repositories) { + r.root = root; + repositories.put(r.getName(),r); + } + } + + return Collections.unmodifiableMap(repositories); + } + + public GHRepository getRepository(String name) throws IOException { + return getRepositories().get(name); + } + + /** + * Gravatar ID of this user, like 0cb9832a01c22c083390f3c5dcb64105 + */ + public String getGravatarId() { + return gravatar_id; + } + + public String getLogin() { + return login; + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 69f7711a60..116b454b3e 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -64,6 +64,10 @@ public GHUser getOwner() throws IOException { return root.getUser(owner); } + protected String getOwnerName() { + return owner; + } + public boolean hasIssues() { return has_issues; } @@ -142,9 +146,7 @@ public void delete() throws IOException { * Forks this repository. */ public GHRepository fork() throws IOException { - GHRepository r = new Poster(root).withCredential().to(root.getApiURL("/repos/fork/" + owner + "/" + name), JsonRepository.class).repository; - r.root = root; - return r; + return new Poster(root).withCredential().to(root.getApiURL("/repos/fork/" + owner + "/" + name), JsonRepository.class).wrap(root); } private void verifyMine() throws IOException { diff --git a/src/main/java/org/kohsuke/github/GHTeam.java b/src/main/java/org/kohsuke/github/GHTeam.java new file mode 100644 index 0000000000..84d5efc789 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHTeam.java @@ -0,0 +1,62 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.net.URL; +import java.util.Set; + +/** + * A team in GitHub organization. + * + * @author Kohsuke Kawaguchi + */ +public class GHTeam { + private String name,permission; + private int id; + + protected /*final*/ GHOrganization org; + + public String getName() { + return name; + } + + public String getPermission() { + return permission; + } + + public int getId() { + return id; + } + + /** + * Retrieves the current members. + */ + public Set getMembers() throws IOException { + return org.root.retrieveWithAuth(getApiURL("/members"),JsonUsersWithDetails.class).toSet(org.root); + } + + /** + * Adds a member to the team. + */ + public void add(GHUser u) throws IOException { + org.root.retrieveWithAuth(getApiURL("/members?name="+u.getLogin()),null, "POST"); + } + + /** + * Removes a member to the team. + */ + public void remove(GHUser u) throws IOException { + org.root.retrieveWithAuth(getApiURL("/members?name="+u.getLogin()),null, "DELETE"); + } + + public void add(GHRepository r) throws IOException { + org.root.retrieveWithAuth(getApiURL("/repositories?name="+r.getOwnerName()+'/'+r.getName()),null, "POST"); + } + + public void remove(GHRepository r) throws IOException { + org.root.retrieveWithAuth(getApiURL("/repositories?name="+r.getOwnerName()+'/'+r.getName()),null, "DELETE"); + } + + private URL getApiURL(String tail) throws IOException { + return org.root.getApiURL("/organizations/"+org.getLogin()+"/teams/"+id+tail); + } +} diff --git a/src/main/java/org/kohsuke/github/GHUser.java b/src/main/java/org/kohsuke/github/GHUser.java index b439ca4ee8..205b466c0e 100644 --- a/src/main/java/org/kohsuke/github/GHUser.java +++ b/src/main/java/org/kohsuke/github/GHUser.java @@ -24,36 +24,16 @@ package org.kohsuke.github; import java.io.IOException; -import java.net.URL; -import java.util.Collections; -import java.util.Map; import java.util.Set; -import java.util.TreeMap; - -import static org.kohsuke.github.GitHub.MAPPER; /** * Represents an user of GitHub. * * @author Kohsuke Kawaguchi */ -public class GHUser { - /*package almost final*/ GitHub root; - - private String gravatar_id,name,company,location,created_at,blog,login,email; - private int public_gist_count,public_repo_count,following_count,id,followers_count; - - /** - * Repositories that this user owns. - */ - private transient Map repositories; - - /** - * Gravatar ID of this user, like 0cb9832a01c22c083390f3c5dcb64105 - */ - public String getGravatarId() { - return gravatar_id; - } +public class GHUser extends GHPerson { + private String name,company,location,created_at,blog,email; + private int followers_count; /** * Gets the human-readable name of the user, like "Kohsuke Kawaguchi" @@ -124,26 +104,6 @@ public int getFollowersCount() { return followers_count; } - /** - * Gets the repositories this user owns. - */ - public Map getRepositories() throws IOException { - if (repositories==null) { - repositories = new TreeMap(); - URL url = new URL("http://github.com/api/v2/json/repos/show/" + login); - for (GHRepository r : MAPPER.readValue(url, JsonRepositories.class).repositories) { - r.root = root; - repositories.put(r.getName(),r); - } - } - - return Collections.unmodifiableMap(repositories); - } - - public GHRepository getRepository(String name) throws IOException { - return getRepositories().get(name); - } - /** * Follow this user. */ diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index cd3a1ed8f8..616c123908 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -23,15 +23,22 @@ */ package org.kohsuke.github; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; 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.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -48,12 +55,15 @@ public class GitHub { /*package*/ final String login; /*package*/ final String token; + /*package*/ final String password; private final Map users = new HashMap(); + private final Map orgs = new HashMap(); - private GitHub(String login, String apiToken) { + private GitHub(String login, String apiToken, String password) { this.login = login; this.token = apiToken; + this.password = password; } /** @@ -68,11 +78,11 @@ public static GitHub connect() throws IOException { } finally { IOUtils.closeQuietly(in); } - return new GitHub(props.getProperty("login"),props.getProperty("token")); + return new GitHub(props.getProperty("login"),props.getProperty("token"),props.getProperty("password")); } public static GitHub connect(String login, String apiToken) throws IOException { - return new GitHub(login,apiToken); + return new GitHub(login,apiToken,null); } /** @@ -81,7 +91,7 @@ public static GitHub connect(String login, String apiToken) throws IOException { * All operations that requires authentication will fail. */ public static GitHub connectAnonymously() { - return new GitHub(null,null); + return new GitHub(null,null,null); } /*package*/ void requireCredential() { @@ -97,6 +107,30 @@ public static GitHub connectAnonymously() { return MAPPER.readValue(getApiURL(tail),type); } + /*package*/ T retrieveWithAuth(URL url, Class type) throws IOException { + return retrieveWithAuth(url,type,"GET"); + } + /*package*/ T retrieveWithAuth(URL url, Class type, String method) throws IOException { + HttpURLConnection uc = (HttpURLConnection) url.openConnection(); + + BASE64Encoder enc = new sun.misc.BASE64Encoder(); + String userpassword = login + "/token" + ":" + token; + String encodedAuthorization = enc.encode(userpassword.getBytes()); + uc.setRequestProperty("Authorization", "Basic " + encodedAuthorization); + uc.setRequestMethod(method); + + try { + InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8"); + if (type==null) { + String data = IOUtils.toString(r); + return null; + } + return MAPPER.readValue(r,type); + } catch (IOException e) { + throw (IOException)new IOException(IOUtils.toString(uc.getErrorStream(),"UTF-8")).initCause(e); + } + } + /** * Obtains the object that represents the named user. */ @@ -110,6 +144,29 @@ public GHUser getUser(String login) throws IOException { return u; } + /** + * Interns the given {@link GHUser}. + */ + protected GHUser getUser(GHUser orig) throws IOException { + GHUser u = users.get(orig.getLogin()); + if (u==null) { + orig.root = this; + users.put(login,orig); + return orig; + } + return u; + } + + public GHOrganization getOrganization(String name) throws IOException { + GHOrganization o = orgs.get(name); + if (o==null) { + o = MAPPER.readValue(getApiURL("/organizations/"+name), JsonOrganization.class).organization; + o.root = this; + orgs.put(name,o); + } + return o; + } + /** * Gets the {@link GHUser} that represents yourself. */ @@ -125,11 +182,21 @@ public GHUser getMyself() throws IOException { * Newly created repository. */ public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException { - GHRepository r = new Poster(this).withCredential() + return new Poster(this).withCredential() .with("name", name).with("description", description).with("homepage", homepage) - .with("public", isPublic ? 1 : 0).to(getApiURL("/repos/create"), JsonRepository.class).repository; - r.root = this; - return r; + .with("public", isPublic ? 1 : 0).to(getApiURL("/repos/create"), JsonRepository.class).wrap(this); + } + + WebClient createWebClient() throws IOException { + WebClient wc = new WebClient(); + wc.setJavaScriptEnabled(false); + wc.setCssEnabled(false); + HtmlPage pg = (HtmlPage)wc.getPage("https://github.com/login"); + HtmlForm f = pg.getForms().get(0); + f.getInputByName("login").setValueAttribute(login); + f.getInputByName("password").setValueAttribute(password); + f.submit(); + return wc; } diff --git a/src/main/java/org/kohsuke/github/JsonOrganization.java b/src/main/java/org/kohsuke/github/JsonOrganization.java new file mode 100644 index 0000000000..02c7479b84 --- /dev/null +++ b/src/main/java/org/kohsuke/github/JsonOrganization.java @@ -0,0 +1,8 @@ +package org.kohsuke.github; + +/** + * @author Kohsuke Kawaguchi + */ +class JsonOrganization { + public GHOrganization organization; +} diff --git a/src/main/java/org/kohsuke/github/JsonRepository.java b/src/main/java/org/kohsuke/github/JsonRepository.java index 1f13a196b3..eb8eadd315 100644 --- a/src/main/java/org/kohsuke/github/JsonRepository.java +++ b/src/main/java/org/kohsuke/github/JsonRepository.java @@ -28,4 +28,9 @@ */ class JsonRepository { public GHRepository repository; + + public GHRepository wrap(GitHub root) { + repository.root = root; + return repository; + } } diff --git a/src/main/java/org/kohsuke/github/JsonTeams.java b/src/main/java/org/kohsuke/github/JsonTeams.java new file mode 100644 index 0000000000..1f1db72ba7 --- /dev/null +++ b/src/main/java/org/kohsuke/github/JsonTeams.java @@ -0,0 +1,21 @@ +package org.kohsuke.github; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author Kohsuke Kawaguchi + */ +class JsonTeams { + public List teams; + + Map toMap(GHOrganization org) { + Map r = new TreeMap(); + for (GHTeam t : teams) { + t.org = org; + r.put(t.getName(),t); + } + return r; + } +} diff --git a/src/main/java/org/kohsuke/github/JsonUsersWithDetails.java b/src/main/java/org/kohsuke/github/JsonUsersWithDetails.java new file mode 100644 index 0000000000..424e53c9da --- /dev/null +++ b/src/main/java/org/kohsuke/github/JsonUsersWithDetails.java @@ -0,0 +1,20 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Kohsuke Kawaguchi + */ +class JsonUsersWithDetails { + public List users; + + public Set toSet(GitHub root) throws IOException { + Set r = new HashSet(); + for (GHUser u : users) + r.add(root.getUser(u)); + return r; + } +} diff --git a/src/main/java/org/kohsuke/github/Poster.java b/src/main/java/org/kohsuke/github/Poster.java index a6eafdf823..0f4a939358 100644 --- a/src/main/java/org/kohsuke/github/Poster.java +++ b/src/main/java/org/kohsuke/github/Poster.java @@ -78,11 +78,15 @@ public void to(URL url) throws IOException { * {@link Reader} that reads the response. */ public T to(URL url, Class type) throws IOException { + return to(url,type,"POST"); + } + + public T to(URL url, Class type, String method) throws IOException { HttpURLConnection uc = (HttpURLConnection) url.openConnection(); uc.setDoOutput(true); uc.setRequestProperty("Content-type","application/x-www-form-urlencoded"); - uc.setRequestMethod("POST"); + uc.setRequestMethod(method); StringBuilder body = new StringBuilder(); @@ -100,7 +104,10 @@ public T to(URL url, Class type) throws IOException { try { InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8"); - if (type==null) return null; + if (type==null) { + String data = IOUtils.toString(r); + return null; + } return MAPPER.readValue(r,type); } catch (IOException e) { throw (IOException)new IOException(IOUtils.toString(uc.getErrorStream(),"UTF-8")).initCause(e); diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java index ebbce47e3a..84ba0d6d8d 100644 --- a/src/test/java/org/kohsuke/AppTest.java +++ b/src/test/java/org/kohsuke/AppTest.java @@ -3,6 +3,9 @@ import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; +import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHTeam; import org.kohsuke.github.GitHub; import java.io.IOException; @@ -12,10 +15,24 @@ */ public class AppTest extends TestCase { public void testApp() throws IOException { - GitHub hub = GitHub.connectAnonymously(); -// hub.createRepository("test","test repository",null,true); -// hub.getUser("kohsuke").getRepository("test").delete(); + GitHub gitHub = GitHub.connect(); + GHOrganization labs = gitHub.getOrganization("HudsonLabs"); + GHTeam t = labs.getTeams().get("Core Developers"); - System.out.println(hub.getUser("kohsuke").getRepository("hudson").getCollaborators()); + t.add(labs.getRepository("xyz")); + +// t.add(gitHub.getMyself()); +// System.out.println(t.getMembers()); +// t.remove(gitHub.getMyself()); +// System.out.println(t.getMembers()); + +// GHRepository r = GitHub.connect().getOrganization("HudsonLabs").createRepository("auto-test", "some description", "http://kohsuke.org/", "Plugin Developers", true); + +// r. +// GitHub hub = GitHub.connectAnonymously(); +//// hub.createRepository("test","test repository",null,true); +//// hub.getUser("kohsuke").getRepository("test").delete(); +// +// System.out.println(hub.getUser("kohsuke").getRepository("hudson").getCollaborators()); } }