(adapt(req.asIterator("/search/issues", IssueSearchResult.class))) {
- protected void wrapUp(GHIssue[] page) {
- for (GHIssue c : page)
- c.wrap(root);
- }
- };
- }
- };
+ @Override
+ protected String getApiUrl() {
+ return "/search/issues";
}
}
diff --git a/src/main/java/org/kohsuke/github/GHNotificationStream.java b/src/main/java/org/kohsuke/github/GHNotificationStream.java
new file mode 100644
index 0000000000..5ea6fb69cb
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHNotificationStream.java
@@ -0,0 +1,207 @@
+package org.kohsuke.github;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Listens to GitHub notification stream.
+ *
+ *
+ * This class supports two modes of retrieving notifications that can
+ * be controlled via {@link #nonBlocking(boolean)}.
+ *
+ *
+ * In the blocking mode, which is the default, iterator will be infinite.
+ * The call to {@link Iterator#next()} will block until a new notification
+ * arrives. This is useful for application that runs perpetually and reacts
+ * to notifications.
+ *
+ *
+ * In the non-blocking mode, the iterator will only report the set of
+ * notifications initially retrieved from GitHub, then quit. This is useful
+ * for a batch application to process the current set of notifications.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GitHub#listNotifications()
+ * @see GHRepository#listNotifications()
+ */
+public class GHNotificationStream implements Iterable {
+ private final GitHub root;
+
+ private Boolean all, participating;
+ private String since;
+ private String apiUrl;
+ private boolean nonBlocking = false;
+
+ /*package*/ GHNotificationStream(GitHub root, String apiUrl) {
+ this.root = root;
+ this.apiUrl = apiUrl;
+ }
+
+ /**
+ * Should the stream include notifications that are already read?
+ */
+ public GHNotificationStream read(boolean v) {
+ all = v;
+ return this;
+ }
+
+ /**
+ * Should the stream be restricted to notifications in which the user
+ * is directly participating or mentioned?
+ */
+ public GHNotificationStream participating(boolean v) {
+ participating = v;
+ return this;
+ }
+
+ public GHNotificationStream since(long timestamp) {
+ return since(new Date(timestamp));
+ }
+
+ public GHNotificationStream since(Date dt) {
+ since = GitHub.printDate(dt);
+ return this;
+ }
+
+ /**
+ * If set to true, {@link #iterator()} will stop iterating instead of blocking and
+ * waiting for the updates to arrive.
+ */
+ public GHNotificationStream nonBlocking(boolean v) {
+ this.nonBlocking = v;
+ return this;
+ }
+
+ /**
+ * Returns an infinite blocking {@link Iterator} that returns
+ * {@link GHThread} as notifications arrive.
+ */
+ public Iterator iterator() {
+ // capture the configuration setting here
+ final Requester req = new Requester(root).method("GET")
+ .with("all", all).with("participating", participating).with("since", since);
+
+ return new Iterator() {
+ /**
+ * Stuff we've fetched but haven't returned to the caller.
+ * Newer ones first.
+ */
+ private GHThread[] threads = EMPTY_ARRAY;
+
+ /**
+ * Next element in {@link #threads} to return. This counts down.
+ */
+ private int idx=-1;
+
+ /**
+ * threads whose updated_at is older than this should be ignored.
+ */
+ private long lastUpdated = -1;
+
+ /**
+ * Next request should have "If-Modified-Since" header with this value.
+ */
+ private String lastModified;
+
+ /**
+ * When is the next polling allowed?
+ */
+ private long nextCheckTime = -1;
+
+ private GHThread next;
+
+ public GHThread next() {
+ if (next==null) {
+ next = fetch();
+ if (next==null)
+ throw new NoSuchElementException();
+ }
+
+ GHThread r = next;
+ next = null;
+ return r;
+ }
+
+ public boolean hasNext() {
+ if (next==null)
+ next = fetch();
+ return next!=null;
+ }
+
+ GHThread fetch() {
+ try {
+ while (true) {// loop until we get new threads to return
+
+ // if we have fetched un-returned threads, use them first
+ while (idx>=0) {
+ GHThread n = threads[idx--];
+ long nt = n.getUpdatedAt().getTime();
+ if (nt >= lastUpdated) {
+ lastUpdated = nt;
+ return n.wrap(root);
+ }
+ }
+
+ if (nonBlocking && nextCheckTime>=0)
+ return null; // nothing more to report, and we aren't blocking
+
+ // observe the polling interval before making the call
+ while (true) {
+ long now = System.currentTimeMillis();
+ if (nextCheckTime < now) break;
+ long waitTime = Math.max(Math.min(nextCheckTime - now, 1000), 60 * 1000);
+ Thread.sleep(waitTime);
+ }
+
+ req.setHeader("If-Modified-Since", lastModified);
+
+ threads = req.to(apiUrl, GHThread[].class);
+ if (threads==null) {
+ threads = EMPTY_ARRAY; // if unmodified, we get empty array
+ } else {
+ // we get a new batch, but we want to ignore the ones that we've seen
+ lastUpdated++;
+ }
+ idx = threads.length-1;
+
+ nextCheckTime = calcNextCheckTime();
+ lastModified = req.getResponseHeader("Last-Modified");
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private long calcNextCheckTime() {
+ String v = req.getResponseHeader("X-Poll-Interval");
+ if (v==null) v="60";
+ return System.currentTimeMillis()+Integer.parseInt(v)*1000;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public void markAsRead() throws IOException {
+ markAsRead(-1);
+ }
+
+ /**
+ * Marks all the notifications as read.
+ */
+ public void markAsRead(long timestamp) throws IOException {
+ final Requester req = new Requester(root).method("PUT");
+ if (timestamp>=0)
+ req.with("last_read_at", GitHub.printDate(new Date(timestamp)));
+ req.asHttpStatusCode(apiUrl);
+ }
+
+ private static final GHThread[] EMPTY_ARRAY = new GHThread[0];
+}
diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java
index 1c9fcc7b34..ff1539892d 100644
--- a/src/main/java/org/kohsuke/github/GHRepository.java
+++ b/src/main/java/org/kohsuke/github/GHRepository.java
@@ -1166,11 +1166,17 @@ public Reader renderMarkdown(String text, MarkdownMode mode) throws IOException
.with("text", text)
.with("mode",mode==null?null:mode.toString())
.with("context", getFullName())
- .read("/markdown"),
+ .asStream("/markdown"),
"UTF-8");
}
-
+ /**
+ * List all the notifications in a repository for the current user.
+ */
+ public GHNotificationStream listNotifications() {
+ return new GHNotificationStream(root,getApiTailUrl("/notifications"));
+ }
+
@Override
public String toString() {
diff --git a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java
new file mode 100644
index 0000000000..cbcbb9e19d
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java
@@ -0,0 +1,82 @@
+package org.kohsuke.github;
+
+import java.util.Locale;
+
+/**
+ * Search repositories.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GitHub#searchRepositories()
+ */
+public class GHRepositorySearchBuilder extends GHSearchBuilder {
+ /*package*/ GHRepositorySearchBuilder(GitHub root) {
+ super(root,RepositorySearchResult.class);
+ }
+
+ /**
+ * Search terms.
+ */
+ public GHRepositorySearchBuilder q(String term) {
+ super.q(term);
+ return this;
+ }
+
+ public GHRepositorySearchBuilder in(String v) {
+ return q("in:"+v);
+ }
+
+ public GHRepositorySearchBuilder size(String v) {
+ return q("size:"+v);
+ }
+
+ public GHRepositorySearchBuilder forks(String v) {
+ return q("forks:"+v);
+ }
+
+ public GHRepositorySearchBuilder created(String v) {
+ return q("created:"+v);
+ }
+
+ public GHRepositorySearchBuilder pushed(String v) {
+ return q("pushed:"+v);
+ }
+
+ public GHRepositorySearchBuilder user(String v) {
+ return q("user:"+v);
+ }
+
+ public GHRepositorySearchBuilder repo(String v) {
+ return q("repo:"+v);
+ }
+
+ public GHRepositorySearchBuilder language(String v) {
+ return q("language:"+v);
+ }
+
+ public GHRepositorySearchBuilder stars(String v) {
+ return q("stars:"+v);
+ }
+
+ public GHRepositorySearchBuilder sort(Sort sort) {
+ req.with("sort",sort.toString().toLowerCase(Locale.ENGLISH));
+ return this;
+ }
+
+ public enum Sort { STARS, FORKS, UPDATED }
+
+ private static class RepositorySearchResult extends SearchResult {
+ private GHRepository[] items;
+
+ @Override
+ /*package*/ GHRepository[] getItems(GitHub root) {
+ for (GHRepository item : items)
+ item.wrap(root);
+ return items;
+ }
+ }
+
+ @Override
+ protected String getApiUrl() {
+ return "/search/repositories";
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GHSearchBuilder.java b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
new file mode 100644
index 0000000000..766c6a319f
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
@@ -0,0 +1,54 @@
+package org.kohsuke.github;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class for various search builders.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public abstract class GHSearchBuilder {
+ protected final GitHub root;
+ protected final Requester req;
+ protected final List terms = new ArrayList();
+
+ /**
+ * Data transfer object that receives the result of search.
+ */
+ private final Class extends SearchResult> receiverType;
+
+ /*package*/ GHSearchBuilder(GitHub root, Class extends SearchResult> receiverType) {
+ this.root = root;
+ this.req = root.retrieve();
+ this.receiverType = receiverType;
+ }
+
+ /**
+ * Search terms.
+ */
+ public GHSearchBuilder q(String term) {
+ terms.add(term);
+ return this;
+ }
+
+ /**
+ * Performs the search.
+ */
+ public PagedSearchIterable list() {
+ return new PagedSearchIterable(root) {
+ public PagedIterator iterator() {
+ req.set("q", StringUtils.join(terms, " "));
+ return new PagedIterator(adapt(req.asIterator(getApiUrl(), receiverType))) {
+ protected void wrapUp(T[] page) {
+ // SearchResult.getItems() should do it
+ }
+ };
+ }
+ };
+ }
+
+ protected abstract String getApiUrl();
+}
diff --git a/src/main/java/org/kohsuke/github/GHSubscription.java b/src/main/java/org/kohsuke/github/GHSubscription.java
index bc5dcb92e4..16acbed45e 100644
--- a/src/main/java/org/kohsuke/github/GHSubscription.java
+++ b/src/main/java/org/kohsuke/github/GHSubscription.java
@@ -4,9 +4,11 @@
import java.util.Date;
/**
- * Represents your subscribing to a repository.
+ * Represents your subscribing to a repository / conversation thread..
*
* @author Kohsuke Kawaguchi
+ * @see GHRepository#getSubscription()
+ * @see GHThread#getSubscription()
*/
public class GHSubscription {
private String created_at, url, repository_url, reason;
diff --git a/src/main/java/org/kohsuke/github/GHThread.java b/src/main/java/org/kohsuke/github/GHThread.java
new file mode 100644
index 0000000000..e1f749a9f1
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHThread.java
@@ -0,0 +1,98 @@
+package org.kohsuke.github;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ * A conversation in the notification API.
+ *
+ * @see documentation
+ * @see GHNotificationStream
+ * @author Kohsuke Kawaguchi
+ */
+public class GHThread extends GHObject {
+ private GitHub root;
+ private GHRepository repository;
+ private Subject subject;
+ private String reason;
+ private boolean unread;
+ private String last_read_at;
+ private String url,subscription_url;
+
+ static class Subject {
+ String title;
+ String url;
+ String latest_comment_url;
+ String type;
+ }
+
+ private GHThread() {// no external construction allowed
+ }
+
+ /**
+ * Returns null if the entire thread has never been read.
+ */
+ public Date getLastReadAt() {
+ return GitHub.parseDate(last_read_at);
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public GHRepository getRepository() {
+ return repository;
+ }
+
+ // TODO: how to expose the subject?
+
+ public boolean isRead() {
+ return !unread;
+ }
+
+ public String getTitle() {
+ return subject.title;
+ }
+
+ public String getType() {
+ return subject.type;
+ }
+
+ /*package*/ GHThread wrap(GitHub root) {
+ this.root = root;
+ if (this.repository!=null)
+ this.repository.wrap(root);
+ return this;
+ }
+
+ /**
+ * Marks this thread as read.
+ */
+ public void markAsRead() throws IOException {
+ new Requester(root).method("PATCH").to(url);
+ }
+
+ /**
+ * Subscribes to this conversation to get notifications.
+ */
+ public GHSubscription subscribe(boolean subscribed, boolean ignored) throws IOException {
+ return new Requester(root)
+ .with("subscribed", subscribed)
+ .with("ignored", ignored)
+ .method("PUT").to(subscription_url, GHSubscription.class).wrapUp(root);
+ }
+
+ /**
+ * Returns the current subscription for this thread.
+ *
+ * @return null if no subscription exists.
+ */
+ public GHSubscription getSubscription() throws IOException {
+ try {
+ return new Requester(root).to(subscription_url, GHSubscription.class).wrapUp(root);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
new file mode 100644
index 0000000000..ee0c5f62a0
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
@@ -0,0 +1,72 @@
+package org.kohsuke.github;
+
+import java.util.Locale;
+
+/**
+ * Search users.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GitHub#searchUsers()
+ */
+public class GHUserSearchBuilder extends GHSearchBuilder {
+ /*package*/ GHUserSearchBuilder(GitHub root) {
+ super(root,UserSearchResult.class);
+ }
+
+ /**
+ * Search terms.
+ */
+ public GHUserSearchBuilder q(String term) {
+ super.q(term);
+ return this;
+ }
+
+ public GHUserSearchBuilder type(String v) {
+ return q("type:"+v);
+ }
+
+ public GHUserSearchBuilder in(String v) {
+ return q("in:"+v);
+ }
+
+ public GHUserSearchBuilder repos(String v) {
+ return q("repos:"+v);
+ }
+
+ public GHUserSearchBuilder location(String v) {
+ return q("location:"+v);
+ }
+
+ public GHUserSearchBuilder language(String v) {
+ return q("language:"+v);
+ }
+
+ public GHUserSearchBuilder created(String v) {
+ return q("created:"+v);
+ }
+
+ public GHUserSearchBuilder followers(String v) {
+ return q("followers:"+v);
+ }
+
+ public GHUserSearchBuilder sort(Sort sort) {
+ req.with("sort",sort.toString().toLowerCase(Locale.ENGLISH));
+ return this;
+ }
+
+ public enum Sort { FOLLOWERS, REPOSITORIES, JOINED }
+
+ private static class UserSearchResult extends SearchResult {
+ private GHUser[] items;
+
+ @Override
+ /*package*/ GHUser[] getItems(GitHub root) {
+ return GHUser.wrap(items,root);
+ }
+ }
+
+ @Override
+ protected String getApiUrl() {
+ return "/search/users";
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index ec1a2a46d5..33601668ff 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -443,6 +443,34 @@ public GHIssueSearchBuilder searchIssues() {
return new GHIssueSearchBuilder(this);
}
+ /**
+ * Search users.
+ */
+ public GHUserSearchBuilder searchUsers() {
+ return new GHUserSearchBuilder(this);
+ }
+
+ /**
+ * Search repositories.
+ */
+ public GHRepositorySearchBuilder searchRepositories() {
+ return new GHRepositorySearchBuilder(this);
+ }
+
+ /**
+ * Search content.
+ */
+ public GHContentSearchBuilder searchContent() {
+ return new GHContentSearchBuilder(this);
+ }
+
+ /**
+ * List all the notifications.
+ */
+ public GHNotificationStream listNotifications() {
+ return new GHNotificationStream(this,"/notifications");
+ }
+
/**
* This provides a dump of every public repository, in the order that they were created.
* @see documentation
@@ -487,7 +515,7 @@ public Reader renderMarkdown(String text) throws IOException {
new Requester(this)
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
.contentType("text/plain;charset=UTF-8")
- .read("/markdown/raw"),
+ .asStream("/markdown/raw"),
"UTF-8");
}
diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
index 2adb23381f..65b6c7490d 100644
--- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
@@ -8,11 +8,17 @@
* @author Kohsuke Kawaguchi
*/
public abstract class PagedSearchIterable extends PagedIterable {
+ private final GitHub root;
+
/**
* As soon as we have any result fetched, it's set here so that we can report the total count.
*/
private SearchResult result;
+ /*package*/ PagedSearchIterable(GitHub root) {
+ this.root = root;
+ }
+
/**
* Returns the total number of hit, including the results that's not yet fetched.
*/
@@ -43,7 +49,7 @@ public boolean hasNext() {
public T[] next() {
SearchResult v = base.next();
if (result==null) result = v;
- return v.getItems();
+ return v.getItems(root);
}
public void remove() {
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index 7f34e72144..cbab7b82fb 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -23,7 +23,8 @@
*/
package org.kohsuke.github;
-import static org.kohsuke.github.GitHub.MAPPER;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import org.apache.commons.io.IOUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -43,6 +44,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -51,8 +53,7 @@
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import org.apache.commons.io.IOUtils;
+import static org.kohsuke.github.GitHub.*;
/**
* A builder pattern for making HTTP call and parsing its output.
@@ -62,6 +63,7 @@
class Requester {
private final GitHub root;
private final List args = new ArrayList();
+ private final Map headers = new LinkedHashMap();
/**
* Request method.
@@ -70,6 +72,11 @@ class Requester {
private String contentType = "application/x-www-form-urlencoded";
private InputStream body;
+ /**
+ * Current connection.
+ */
+ private HttpURLConnection uc;
+
private static class Entry {
String key;
Object value;
@@ -84,6 +91,15 @@ private Entry(String key, Object value) {
this.root = root;
}
+ /**
+ * Sets the request HTTP header.
+ *
+ * If a header of the same name is already set, this method overrides it.
+ */
+ public void setHeader(String name, String value) {
+ headers.put(name,value);
+ }
+
/**
* Makes a request with authentication credential.
*/
@@ -107,6 +123,10 @@ public Requester with(String key, Integer value) {
public Requester with(String key, boolean value) {
return _with(key, value);
}
+ public Requester with(String key, Boolean value) {
+ return _with(key, value);
+ }
+
public Requester with(String key, String value) {
return _with(key, value);
@@ -196,12 +216,12 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti
}
tailApiUrl += qs.toString();
}
- HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
+ setupConnection(root.getApiURL(tailApiUrl));
- buildRequest(uc);
+ buildRequest();
try {
- T result = parse(uc, type, instance);
+ T result = parse(type, instance);
if (type != null && type.isArray()) { // we might have to loop for pagination - done through recursion
final String links = uc.getHeaderField("link");
if (links != null && links.contains("rel=\"next\"")) {
@@ -222,7 +242,7 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti
}
return result;
} catch (IOException e) {
- handleApiError(e,uc);
+ handleApiError(e);
}
}
}
@@ -232,33 +252,41 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti
*/
public int asHttpStatusCode(String tailApiUrl) throws IOException {
while (true) {// loop while API rate limit is hit
- HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
+ setupConnection(root.getApiURL(tailApiUrl));
- buildRequest(uc);
+ buildRequest();
try {
return uc.getResponseCode();
} catch (IOException e) {
- handleApiError(e,uc);
+ handleApiError(e);
}
}
}
- public InputStream read(String tailApiUrl) throws IOException {
+ public InputStream asStream(String tailApiUrl) throws IOException {
while (true) {// loop while API rate limit is hit
- HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
+ setupConnection(root.getApiURL(tailApiUrl));
- buildRequest(uc);
+ buildRequest();
try {
- return wrapStream(uc,uc.getInputStream());
+ return wrapStream(uc.getInputStream());
} catch (IOException e) {
- handleApiError(e,uc);
+ handleApiError(e);
}
}
}
- private void buildRequest(HttpURLConnection uc) throws IOException {
+ public String getResponseHeader(String header) {
+ return uc.getHeaderField(header);
+ }
+
+
+ /**
+ * Set up the request parameters or POST payload.
+ */
+ private void buildRequest() throws IOException {
if (!method.equals("GET")) {
uc.setDoOutput(true);
uc.setRequestProperty("Content-type", contentType);
@@ -347,14 +375,14 @@ private void fetch() {
try {
while (true) {// loop while API rate limit is hit
- HttpURLConnection uc = setupConnection(url);
+ setupConnection(url);
try {
- next = parse(uc,type,null);
+ next = parse(type,null);
assert next!=null;
- findNextURL(uc);
+ findNextURL();
return;
} catch (IOException e) {
- handleApiError(e,uc);
+ handleApiError(e);
}
}
} catch (IOException e) {
@@ -365,7 +393,7 @@ private void fetch() {
/**
* Locate the next page from the pagination "Link" tag.
*/
- private void findNextURL(HttpURLConnection uc) throws MalformedURLException {
+ private void findNextURL() throws MalformedURLException {
url = null; // start defensively
String link = uc.getHeaderField("Link");
if (link==null) return;
@@ -386,14 +414,20 @@ private void findNextURL(HttpURLConnection uc) throws MalformedURLException {
}
- private HttpURLConnection setupConnection(URL url) throws IOException {
- HttpURLConnection uc = root.getConnector().connect(url);
+ private void setupConnection(URL url) throws IOException {
+ uc = root.getConnector().connect(url);
// 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 (root.encodedAuthorization!=null)
uc.setRequestProperty("Authorization", root.encodedAuthorization);
+ for (Map.Entry e : headers.entrySet()) {
+ String v = e.getValue();
+ if (v!=null)
+ uc.setRequestProperty(e.getKey(), v);
+ }
+
try {
uc.setRequestMethod(method);
} catch (ProtocolException e) {
@@ -407,13 +441,14 @@ private HttpURLConnection setupConnection(URL url) throws IOException {
}
}
uc.setRequestProperty("Accept-Encoding", "gzip");
- return uc;
}
- private T parse(HttpURLConnection uc, Class type, T instance) throws IOException {
+ private T parse(Class type, T instance) throws IOException {
+ if (uc.getResponseCode()==304)
+ return null; // special case handling for 304 unmodified, as the content will be ""
InputStreamReader r = null;
try {
- r = new InputStreamReader(wrapStream(uc, uc.getInputStream()), "UTF-8");
+ r = new InputStreamReader(wrapStream(uc.getInputStream()), "UTF-8");
String data = IOUtils.toString(r);
if (type!=null)
try {
@@ -432,7 +467,7 @@ private T parse(HttpURLConnection uc, Class type, T instance) throws IOEx
/**
* Handles the "Content-Encoding" header.
*/
- private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException {
+ private InputStream wrapStream(InputStream in) throws IOException {
String encoding = uc.getContentEncoding();
if (encoding==null || in==null) return in;
if (encoding.equals("gzip")) return new GZIPInputStream(in);
@@ -443,19 +478,20 @@ private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOEx
/**
* Handle API error by either throwing it or by returning normally to retry.
*/
- /*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException {
+ /*package*/ void handleApiError(IOException e) throws IOException {
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
root.rateLimitHandler.onError(e,uc);
}
- 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());
+ InputStream es = wrapStream(uc.getErrorStream());
try {
- if (es!=null)
- throw (IOException)new IOException(IOUtils.toString(es,"UTF-8")).initCause(e);
- else
+ if (es!=null) {
+ if (e instanceof FileNotFoundException) {
+ // pass through 404 Not Found to allow the caller to handle it intelligently
+ throw (IOException) new FileNotFoundException(IOUtils.toString(es, "UTF-8")).initCause(e);
+ } else
+ throw (IOException) new IOException(IOUtils.toString(es, "UTF-8")).initCause(e);
+ } else
throw e;
} finally {
IOUtils.closeQuietly(es);
diff --git a/src/main/java/org/kohsuke/github/SearchResult.java b/src/main/java/org/kohsuke/github/SearchResult.java
index 26651510c6..a8a7583a4c 100644
--- a/src/main/java/org/kohsuke/github/SearchResult.java
+++ b/src/main/java/org/kohsuke/github/SearchResult.java
@@ -9,5 +9,8 @@ abstract class SearchResult {
int total_count;
boolean incomplete_results;
- public abstract T[] getItems();
+ /**
+ * Wraps up the retrieved object and return them. Only called once.
+ */
+ /*package*/ abstract T[] getItems(GitHub root);
}
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 3c2af01970..ccd0c9cf38 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -41,4 +41,11 @@ This library comes with a pluggable connector to use different HTTP client imple
through `HttpConnector`. In particular, this means you can use [OkHttp](http://square.github.io/okhttp/),
so we can make use of it's HTTP response cache.
Making a conditional request against the GitHub API and receiving a 304 response
-[does not count against the rate limit](http://developer.github.com/v3/#conditional-requests).
\ No newline at end of file
+[does not count against the rate limit](http://developer.github.com/v3/#conditional-requests).
+
+The following code shows an example of how to set up persistent cache on the disk:
+
+ Cache cache = new Cache(cacheDirectory, 10 * 1024 * 1024); // 10MB cache
+ GitHub gitHub = GitHubBuilder.fromCredentials()
+ .withConnector(new OkHttpConnector(new OkUrlFactory(new OkHttpClient().setCache(cache))))
+ .build();
diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java
index 6e56bbff89..b5d7c517a7 100755
--- a/src/test/java/org/kohsuke/github/AppTest.java
+++ b/src/test/java/org/kohsuke/github/AppTest.java
@@ -777,6 +777,56 @@ public void markDown() throws Exception {
assertTrue(actual.contains("to fix issue"));
}
+ @Test
+ public void searchUsers() throws Exception {
+ PagedSearchIterable r = gitHub.searchUsers().q("tom").repos(">42").followers(">1000").list();
+ GHUser u = r.iterator().next();
+ System.out.println(u.getName());
+ assertNotNull(u.getId());
+ assertTrue(r.getTotalCount() > 0);
+ }
+
+ @Test
+ public void searchRepositories() throws Exception {
+ PagedSearchIterable r = gitHub.searchRepositories().q("tetris").language("assembly").sort(GHRepositorySearchBuilder.Sort.STARS).list();
+ GHRepository u = r.iterator().next();
+ System.out.println(u.getName());
+ assertNotNull(u.getId());
+ assertEquals("Assembly", u.getLanguage());
+ assertTrue(r.getTotalCount() > 0);
+ }
+
+ @Test
+ public void searchContent() throws Exception {
+ PagedSearchIterable r = gitHub.searchContent().q("addClass").in("file").language("js").repo("jquery/jquery").list();
+ GHContent c = r.iterator().next();
+ System.out.println(c.getName());
+ assertNotNull(c.getDownloadUrl());
+ assertNotNull(c.getOwner());
+ assertEquals("jquery/jquery",c.getOwner().getFullName());
+ assertTrue(r.getTotalCount() > 0);
+ }
+
+ @Test
+ public void notifications() throws Exception {
+ boolean found=false;
+ for (GHThread t : gitHub.listNotifications().nonBlocking(true).read(true)) {
+ if (!found) {
+ found = true;
+ t.markAsRead(); // test this by calling it once on old nofication
+ }
+ assertNotNull(t.getTitle());
+ assertNotNull(t.getReason());
+
+ System.out.println(t.getTitle());
+ System.out.println(t.getLastReadAt());
+ System.out.println(t.getType());
+ System.out.println();
+ }
+ assertTrue(found);
+ gitHub.listNotifications().markAsRead();
+ }
+
private void kohsuke() {
String login = getUser().getLogin();
Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));