Skip to content

Commit 9aff8a1

Browse files
committed
Merge pull request cloudbees-oss#30 from tetrapods/master
Support for Triggers, Targets, Twickets, and Fixes infinite looping bug in PagedIterable
2 parents 58b37c9 + 9fa6173 commit 9aff8a1

File tree

14 files changed

+1071
-52
lines changed

14 files changed

+1071
-52
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Here is the status of the various API components:
6868
* [Satisfaction Ratings](http://developer.zendesk.com/documentation/rest_api/satisfaction_ratings.html)
6969
* [Sharing Agreements](http://developer.zendesk.com/documentation/rest_api/sharing_agreements.html)
7070
* [Suspended Tickets](http://developer.zendesk.com/documentation/rest_api/suspended_tickets.html)
71-
* [Triggers](http://developer.zendesk.com/documentation/rest_api/triggers.html)
71+
* [Triggers](http://developer.zendesk.com/documentation/rest_api/triggers.html)
7272

7373
History
7474
-------

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
<parent>
2121
<groupId>com.cloudbees</groupId>
2222
<artifactId>cloudbees-oss-parent</artifactId>
23-
<version>6</version>
23+
<version>5</version>
2424
</parent>
2525

2626
<groupId>com.cloudbees.thirdparty</groupId>
2727
<artifactId>zendesk-java-client</artifactId>
28-
<version>0.2.5-SNAPSHOT</version>
28+
<version>0.2.1-SNAPSHOT</version>
2929

3030
<name>zendesk-java-client</name>
3131
<description>Java client for the Zendesk API</description>

src/main/java/org/zendesk/client/v2/Zendesk.java

Lines changed: 179 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package org.zendesk.client.v2;
2+
3+
import java.util.concurrent.ExecutionException;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
import org.zendesk.client.v2.model.*;
7+
import org.zendesk.client.v2.model.targets.*;
28

39
import com.fasterxml.jackson.annotation.JsonInclude;
410
import com.fasterxml.jackson.core.JsonProcessingException;
5-
import com.fasterxml.jackson.databind.DeserializationFeature;
6-
import com.fasterxml.jackson.databind.JsonNode;
7-
import com.fasterxml.jackson.databind.ObjectMapper;
8-
import com.fasterxml.jackson.databind.SerializationFeature;
9-
import com.ning.http.client.AsyncCompletionHandler;
10-
import com.ning.http.client.AsyncHttpClient;
11-
import com.ning.http.client.ListenableFuture;
12-
import com.ning.http.client.Realm;
11+
import com.fasterxml.jackson.databind.*;
12+
import com.ning.http.client.*;
1313
import com.ning.http.client.Request;
14+
1415
import com.ning.http.client.RequestBuilder;
1516
import com.ning.http.client.Response;
1617

@@ -43,9 +44,8 @@
4344
import java.util.Iterator;
4445
import java.util.List;
4546
import java.util.Map;
46-
import java.util.NoSuchElementException;
47-
import java.util.concurrent.ExecutionException;
48-
47+
import java.util.NoSuchElementException;
48+
4949

5050
/**
5151
* @author stephenc
@@ -59,22 +59,41 @@ public class Zendesk implements Closeable {
5959
private final String url;
6060
private final String oauthToken;
6161
private final ObjectMapper mapper;
62-
private final Logger logger;
62+
private final Logger logger;
6363
private boolean closed = false;
6464
private static final Map<String, Class<? extends SearchResultEntity>> searchResultTypes = searchResultTypes();
65+
private static final Map<String, Class<? extends Target>> targetTypes = targetTypes();
6566

6667
private static Map<String, Class<? extends SearchResultEntity>> searchResultTypes() {
67-
Map<String, Class<? extends SearchResultEntity>> result = new HashMap<String, Class<? extends
68-
SearchResultEntity>>();
69-
result.put("ticket", Ticket.class);
70-
result.put("user", User.class);
71-
result.put("group", Group.class);
72-
result.put("organization", Organization.class);
73-
result.put("topic", Topic.class);
74-
return Collections.unmodifiableMap(result);
68+
Map<String, Class<? extends SearchResultEntity>> result = new HashMap<String, Class<? extends
69+
SearchResultEntity>>();
70+
result.put("ticket", Ticket.class);
71+
result.put("user", User.class);
72+
result.put("group", Group.class);
73+
result.put("organization", Organization.class);
74+
result.put("topic", Topic.class);
75+
return Collections.unmodifiableMap(result);
7576
}
76-
77-
private Zendesk(AsyncHttpClient client, String url, String username, String password) {
77+
78+
private static Map<String, Class<? extends Target>> targetTypes() {
79+
Map<String, Class<? extends Target>> result = new HashMap<String, Class<? extends Target>>();
80+
result.put("url_target", UrlTarget.class);
81+
result.put("email_target",EmailTarget.class);
82+
result.put("basecamp_target", BasecampTarget.class);
83+
result.put("campfire_target", CampfireTarget.class);
84+
result.put("pivotal_target", PivotalTarget.class);
85+
result.put("twitter_target", TwitterTarget.class);
86+
87+
// TODO: Implement other Target types
88+
//result.put("clickatell_target", ClickatellTarget.class);
89+
//result.put("flowdock_target", FlowdockTarget.class);
90+
//result.put("get_satisfaction_target", GetSatisfactionTarget.class);
91+
//result.put("yammer_target", YammerTarget.class);
92+
93+
return Collections.unmodifiableMap(result);
94+
}
95+
96+
private Zendesk(AsyncHttpClient client, String url, String username, String password) {
7897
this.logger = LoggerFactory.getLogger(Zendesk.class);
7998
this.closeClient = client == null;
8099
this.oauthToken = null;
@@ -92,7 +111,7 @@ private Zendesk(AsyncHttpClient client, String url, String username, String pass
92111
throw new IllegalStateException("Cannot specify token or password without specifying username");
93112
}
94113
this.realm = null;
95-
}
114+
}
96115
this.mapper = createMapper();
97116
}
98117

@@ -360,6 +379,47 @@ public void deleteAttachment(long id) {
360379
complete(submit(req("DELETE", tmpl("/attachments/{id}.json").set("id", id)), handleStatus()));
361380
}
362381

382+
public Iterable<Target> getTargets() {
383+
return new PagedIterable<Target>(cnst("/targets.json"), handleTargetList("targets"));
384+
}
385+
386+
public Target getTarget(long id) {
387+
return complete(submit(req("GET", tmpl("/targets/{id}.json").set("id", id)), handle(Target.class, "target")));
388+
}
389+
390+
public Target createTarget(Target target) {
391+
return complete(submit(req("POST", cnst("/targets.json"), JSON, json(Collections.singletonMap("target", target))),
392+
handle(Target.class, "target")));
393+
}
394+
395+
public void deleteTarget(long targetId) {
396+
complete(submit(req("DELETE", tmpl("/targets/{id}.json").set("id", targetId)), handleStatus()));
397+
}
398+
399+
public Iterable<Trigger> getTriggers() {
400+
return new PagedIterable<Trigger>(cnst("/triggers.json"), handleList(Trigger.class, "triggers"));
401+
}
402+
403+
public Trigger getTrigger(long id) {
404+
return complete(submit(req("GET", tmpl("/triggers/{id}.json").set("id", id)), handle(Trigger.class, "trigger")));
405+
}
406+
407+
public Trigger createTrigger(Trigger trigger) {
408+
return complete(submit(req("POST", cnst("/triggers.json"), JSON, json(Collections.singletonMap("trigger", trigger))),
409+
handle(Trigger.class, "trigger")));
410+
}
411+
412+
public void deleteTrigger(long triggerId) {
413+
complete(submit(req("DELETE", tmpl("/triggers/{id}.json").set("id", triggerId)), handleStatus()));
414+
}
415+
416+
417+
public Iterable<TwitterMonitor> getTwitterMonitors() {
418+
return new PagedIterable<TwitterMonitor>(cnst("/channels/twitter/monitored_twitter_handles.json"),
419+
handleList(TwitterMonitor.class, "monitored_twitter_handles"));
420+
}
421+
422+
363423
public Iterable<User> getUsers() {
364424
return new PagedIterable<User>(cnst("/users.json"), handleList(User.class, "users"));
365425
}
@@ -626,6 +686,24 @@ public Comment getRequestComment(long requestId, long commentId) {
626686
handle(Comment.class, "comment")));
627687
}
628688

689+
public Ticket createComment(long ticketId, Comment comment) {
690+
Ticket ticket = new Ticket();
691+
ticket.setComment(comment);
692+
return complete(submit(req("PUT", tmpl("/tickets/{id}.json").set("id", ticketId), JSON,
693+
json(Collections.singletonMap("ticket", ticket))),
694+
handle(Ticket.class, "ticket")));
695+
}
696+
697+
public Ticket createTicketFromTweet(long tweetId, long monitorId) {
698+
Map<String,Object> map = new HashMap<String,Object>();
699+
map.put("twitter_status_message_id", tweetId);
700+
map.put("monitored_twitter_handle_id", monitorId);
701+
702+
return complete(submit(req("POST", cnst("/channels/twitter/tickets.json"), JSON,
703+
json(Collections.singletonMap("ticket", map ))),
704+
handle(Ticket.class, "ticket")));
705+
}
706+
629707
public Iterable<Organization> getOrganizations() {
630708
return new PagedIterable<Organization>(cnst("/organizations.json"),
631709
handleList(Organization.class, "organizations"));
@@ -912,6 +990,14 @@ public <T extends SearchResultEntity> Iterable<T> getSearchResults(Class<T> type
912990
.set("params", params),
913991
handleList(type, "results"));
914992
}
993+
994+
public void notifyApp(String json) {
995+
complete(submit(req("POST", cnst("/apps/notify.json"), JSON, json.getBytes()), handleStatus()));
996+
}
997+
998+
public void updateInstallation(int id, String json) {
999+
complete(submit(req("PUT", tmpl("/apps/installations/{id}.json").set("id", id), JSON, json.getBytes()), handleStatus()));
1000+
}
9151001

9161002
// TODO search with sort order
9171003
// TODO search with query building API
@@ -950,7 +1036,7 @@ private Request req(String method, Uri template) {
9501036
builder.setRealm(realm);
9511037
} else {
9521038
builder.addHeader("Authorization", "Bearer " + oauthToken);
953-
}
1039+
}
9541040
builder.setUrl(template.toString());
9551041
return builder.build();
9561042
}
@@ -961,7 +1047,7 @@ private Request req(String method, Uri template, String contentType, byte[] body
9611047
builder.setRealm(realm);
9621048
} else {
9631049
builder.addHeader("Authorization", "Bearer " + oauthToken);
964-
}
1050+
}
9651051
builder.setUrl(template.toString());
9661052
builder.addHeader("Content-type", contentType);
9671053
builder.setBody(body);
@@ -979,6 +1065,8 @@ private Request req(String method, Uri template, int page) {
9791065
builder.setUrl(template.toString().replace("%2B", "+")); //replace out %2B with + due to API restriction
9801066
return builder.build();
9811067
}
1068+
1069+
9821070

9831071
protected AsyncCompletionHandler<Void> handleStatus() {
9841072
return new AsyncCompletionHandler<Void>() {
@@ -1041,25 +1129,33 @@ public List<T> onCompleted(Response response) throws Exception {
10411129
throw new ZendeskResponseException(response);
10421130
}
10431131
};
1044-
}
1045-
1132+
}
1133+
10461134
protected <T> AsyncCompletionHandler<List<T>> handleList(final Class<T> clazz, final String name) {
1047-
return new AsyncCompletionHandler<List<T>>() {
1048-
@Override
1049-
public List<T> onCompleted(Response response) throws Exception {
1050-
logResponse(response);
1051-
if (isStatus2xx(response)) {
1052-
List<T> values = new ArrayList<T>();
1053-
for (JsonNode node : mapper.readTree(response.getResponseBodyAsStream()).get(name)) {
1054-
values.add(mapper.convertValue(node, clazz));
1055-
}
1056-
return values;
1057-
}
1058-
throw new ZendeskResponseException(response);
1059-
}
1060-
};
1061-
}
1062-
1135+
final AtomicInteger readCount = new AtomicInteger(0);
1136+
return new AsyncCompletionHandler<List<T>>() {
1137+
@Override
1138+
public List<T> onCompleted(Response response) throws Exception {
1139+
logResponse(response);
1140+
if (isStatus2xx(response)) {
1141+
JsonNode responseNode = mapper.readTree(response.getResponseBodyAsBytes());
1142+
int count = responseNode.get("count").asInt();
1143+
if (count > readCount.get()) {
1144+
List<T> values = new ArrayList<T>();
1145+
for (JsonNode node : responseNode.get(name)) {
1146+
values.add(mapper.convertValue(node, clazz));
1147+
readCount.incrementAndGet();
1148+
}
1149+
return values;
1150+
} else {
1151+
return null;
1152+
}
1153+
}
1154+
throw new ZendeskResponseException(response);
1155+
}
1156+
};
1157+
}
1158+
10631159
protected AsyncCompletionHandler<List<SearchResultEntity>> handleSearchList(final String name) {
10641160
return new AsyncCompletionHandler<List<SearchResultEntity>>() {
10651161
@Override
@@ -1080,6 +1176,35 @@ public List<SearchResultEntity> onCompleted(Response response) throws Exception
10801176
};
10811177
}
10821178

1179+
protected AsyncCompletionHandler<List<Target>> handleTargetList(final String name) {
1180+
final AtomicInteger readCount = new AtomicInteger(0);
1181+
return new AsyncCompletionHandler<List<Target>>() {
1182+
@Override
1183+
public List<Target> onCompleted(Response response) throws Exception {
1184+
logResponse(response);
1185+
if (isStatus2xx(response)) {
1186+
JsonNode responseNode = mapper.readTree(response.getResponseBodyAsBytes());
1187+
int count = responseNode.get("count").asInt();
1188+
if (count > readCount.get()) {
1189+
List<Target> values = new ArrayList<Target>();
1190+
for (JsonNode node : responseNode.get(name)) {
1191+
Class<? extends Target> clazz = targetTypes.get(node.get("type").asText());
1192+
if (clazz != null) {
1193+
values.add(mapper.convertValue(node, clazz));
1194+
}
1195+
readCount.incrementAndGet();
1196+
}
1197+
return values;
1198+
} else {
1199+
return null;
1200+
}
1201+
}
1202+
throw new ZendeskResponseException(response);
1203+
}
1204+
};
1205+
}
1206+
1207+
10831208
private TemplateUri tmpl(String template) {
10841209
return new TemplateUri(url + template);
10851210
}
@@ -1223,7 +1348,7 @@ public static ObjectMapper createMapper() {
12231348
mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
12241349
mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
12251350
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
1226-
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
1351+
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
12271352
return mapper;
12281353
}
12291354

@@ -1236,6 +1361,7 @@ private class PagedIterable<T> implements Iterable<T> {
12361361
private final Uri url;
12371362
private final AsyncCompletionHandler<List<T>> handler;
12381363
private final int initialPage;
1364+
private int size = 0;
12391365

12401366
private PagedIterable(Uri url, AsyncCompletionHandler<List<T>> handler) {
12411367
this(url, handler, 1);
@@ -1262,12 +1388,16 @@ private PagedIterator(int page) {
12621388

12631389
public boolean hasNext() {
12641390
if (current == null || !current.hasNext()) {
1265-
if (page > 0) {
1391+
if (page > 0) {
12661392
List<T> values = complete(submit(req("GET", url, page++), handler));
1267-
if (values.isEmpty()) {
1393+
if (values == null || values.isEmpty()) {
12681394
page = -1;
1395+
} else {
1396+
synchronized (this) {
1397+
size += values.size();
1398+
}
1399+
current = values.iterator();
12691400
}
1270-
current = values.iterator();
12711401
} else {
12721402
return false;
12731403
}
@@ -1329,6 +1459,7 @@ public Builder setToken(String token) {
13291459
return this;
13301460
}
13311461

1462+
13321463
public Builder setOauthToken(String oauthToken) {
13331464
this.oauthToken = oauthToken;
13341465
if (oauthToken != null) {
@@ -1352,4 +1483,5 @@ public Zendesk build() {
13521483
return new Zendesk(client, url, username, password);
13531484
}
13541485
}
1486+
13551487
}

0 commit comments

Comments
 (0)