forked from hub4j/github-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented initial notification API support.
Fixes issue hub4j#119
- Loading branch information
Showing
6 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
193 changes: 193 additions & 0 deletions
193
src/main/java/org/kohsuke/github/GHNotificationStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
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. | ||
* | ||
* <p> | ||
* This class supports two modes of retrieving notifications that can | ||
* be controlled via {@link #nonBlocking(boolean)}. | ||
* | ||
* <p> | ||
* 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. | ||
* | ||
* <p> | ||
* 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<GHThread> { | ||
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<GHThread> 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<GHThread>() { | ||
/** | ||
* 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; | ||
} | ||
} | ||
|
||
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(); | ||
} | ||
}; | ||
} | ||
|
||
private static final GHThread[] EMPTY_ARRAY = new GHThread[0]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package org.kohsuke.github; | ||
|
||
import java.util.Date; | ||
|
||
/** | ||
* A conversation in the notification API. | ||
* | ||
* | ||
* @see <a href="https://developer.github.com/v3/activity/notifications/">documentation</a> | ||
* @author Kohsuke Kawaguchi | ||
*/ | ||
public class GHThread extends GHObject { | ||
private GHRepository repository; | ||
private Subject subject; | ||
private String reason; | ||
private boolean unread; | ||
private String last_read_at; | ||
|
||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters