Skip to content

Commit c0283d3

Browse files
authored
Merge pull request #560 from andy-may-at/issue/559/implement_content_tags_resource
2 parents 3a92607 + e7976c8 commit c0283d3

File tree

7 files changed

+419
-1
lines changed

7 files changed

+419
-1
lines changed

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.zendesk.client.v2.model.hc.Article;
6464
import org.zendesk.client.v2.model.hc.ArticleAttachments;
6565
import org.zendesk.client.v2.model.hc.Category;
66+
import org.zendesk.client.v2.model.hc.ContentTag;
6667
import org.zendesk.client.v2.model.hc.Locales;
6768
import org.zendesk.client.v2.model.hc.PermissionGroup;
6869
import org.zendesk.client.v2.model.hc.Section;
@@ -98,7 +99,7 @@
9899
import java.util.Optional;
99100
import java.util.concurrent.ExecutionException;
100101
import java.util.concurrent.TimeUnit;
101-
import java.util.regex.Pattern;
102+
import java.util.function.Function;
102103

103104
/**
104105
* @author stephenc
@@ -2534,6 +2535,59 @@ public Iterable<Holiday> getHolidaysForSchedule(Long scheduleId) {
25342535
handleList(Holiday.class, "holidays")));
25352536
}
25362537

2538+
public ContentTag getContentTag(String contentTagId) {
2539+
return complete(submit(req("GET", tmpl("/guide/content_tags/{id}").set("id", contentTagId)),
2540+
handle(ContentTag.class, "content_tag")));
2541+
}
2542+
2543+
public ContentTag createContentTag(ContentTag contentTag) {
2544+
checkHasName(contentTag);
2545+
return complete(submit(req("POST", cnst("/guide/content_tags"),
2546+
JSON, json(Collections.singletonMap("content_tag", contentTag))),
2547+
handle(ContentTag.class, "content_tag")));
2548+
}
2549+
2550+
public ContentTag updateContentTag(ContentTag contentTag) {
2551+
checkHasId(contentTag);
2552+
checkHasName(contentTag);
2553+
return complete(submit(req("PUT", tmpl("/guide/content_tags/{id}").set("id", contentTag.getId()),
2554+
JSON, json(Collections.singletonMap("content_tag", contentTag))),
2555+
handle(ContentTag.class, "content_tag")));
2556+
}
2557+
2558+
public void deleteContentTag(ContentTag contentTag) {
2559+
checkHasId(contentTag);
2560+
complete(submit(req("DELETE", tmpl("/guide/content_tags/{id}").set("id", contentTag.getId())),
2561+
handleStatus()));
2562+
}
2563+
2564+
public Iterable<ContentTag> getContentTags() {
2565+
int defaultPageSize = 10;
2566+
return getContentTags(defaultPageSize, null);
2567+
}
2568+
2569+
public Iterable<ContentTag> getContentTags(int pageSize) {
2570+
return getContentTags(pageSize, null);
2571+
}
2572+
2573+
public Iterable<ContentTag> getContentTags(int pageSize, String namePrefix) {
2574+
Function<String, Uri> afterCursorUriBuilder = (String afterCursor) -> buildContentTagsSearchUrl(pageSize, namePrefix, afterCursor);
2575+
return new PagedIterable<>(afterCursorUriBuilder.apply(null),
2576+
handleListWithAfterCursorButNoLinks(ContentTag.class, afterCursorUriBuilder, "records"));
2577+
}
2578+
2579+
private Uri buildContentTagsSearchUrl(int pageSize, String namePrefixFilter, String afterCursor) {
2580+
final StringBuilder uriBuilder = new StringBuilder("/guide/content_tags?page[size]=").append(pageSize);
2581+
2582+
if (namePrefixFilter != null) {
2583+
uriBuilder.append("&filter[name_prefix]=").append(encodeUrl(namePrefixFilter));
2584+
}
2585+
if (afterCursor != null) {
2586+
uriBuilder.append("&page[after]=").append(encodeUrl(afterCursor));
2587+
}
2588+
return cnst(uriBuilder.toString());
2589+
}
2590+
25372591
//////////////////////////////////////////////////////////////////////
25382592
// Helper methods
25392593
//////////////////////////////////////////////////////////////////////
@@ -2687,6 +2741,7 @@ public JobStatus onCompleted(Response response) throws Exception {
26872741
private static final String COUNT = "count";
26882742
private static final int INCREMENTAL_EXPORT_MAX_COUNT_BY_REQUEST = 1000;
26892743

2744+
26902745
private abstract class PagedAsyncCompletionHandler<T> extends ZendeskAsyncCompletionHandler<T> {
26912746
private String nextPage;
26922747

@@ -2871,6 +2926,42 @@ public List<ArticleAttachments> onCompleted(Response response) throws Exception
28712926
};
28722927
}
28732928

2929+
/**
2930+
* For a resource (e.g. ContentTag) which supports cursor based pagination for multiple results,
2931+
* but where the response does not have a `links.next` node (which would hold the URL of the next page)
2932+
* So we need to build the next page URL from the original URL and the meta.after_cursor node value
2933+
*
2934+
* @param <T> The class of the resource
2935+
* @param afterCursorUriBuilder a function to build the URL for the next page `fn(after_cursor_value) => URL_of_next_page`
2936+
* @param name the name of the Json node that contains the resources entities (e.g. 'records' for ContentTag)
2937+
*/
2938+
private <T> PagedAsyncCompletionHandler<List<T>> handleListWithAfterCursorButNoLinks(
2939+
Class<T> clazz, Function<String, Uri> afterCursorUriBuilder, String name) {
2940+
2941+
return new PagedAsyncListCompletionHandler<T>(clazz, name) {
2942+
@Override
2943+
public void setPagedProperties(JsonNode responseNode, Class<?> clazz) {
2944+
JsonNode metaNode = responseNode.get("meta");
2945+
String nextPage = null;
2946+
if (metaNode == null) {
2947+
if (logger.isDebugEnabled()) {
2948+
logger.debug("meta" + " property not found, pagination not supported" +
2949+
(clazz != null ? " for " + clazz.getName() : ""));
2950+
}
2951+
} else {
2952+
JsonNode afterCursorNode = metaNode.get("after_cursor");
2953+
if (afterCursorNode != null) {
2954+
JsonNode hasMoreNode = metaNode.get("has_more");
2955+
if (hasMoreNode != null && hasMoreNode.asBoolean()) {
2956+
nextPage = afterCursorUriBuilder.apply(afterCursorNode.asText()).toString();
2957+
}
2958+
}
2959+
}
2960+
setNextPage(nextPage);
2961+
}
2962+
};
2963+
}
2964+
28742965
private TemplateUri tmpl(String template) {
28752966
return new TemplateUri(url + template);
28762967
}
@@ -3132,6 +3223,18 @@ private static void checkHasId(UserSegment userSegment) {
31323223
}
31333224
}
31343225

3226+
private static void checkHasId(ContentTag contentTag) {
3227+
if (contentTag.getId() == null) {
3228+
throw new IllegalArgumentException("Content Tag requires id");
3229+
}
3230+
}
3231+
3232+
private static void checkHasName(ContentTag contentTag) {
3233+
if (contentTag.getName() == null || contentTag.getName().trim().isEmpty()) {
3234+
throw new IllegalArgumentException("Content Tag requires name");
3235+
}
3236+
}
3237+
31353238
private static void checkHasToken(Attachment.Upload upload) {
31363239
if (upload.getToken() == null) {
31373240
throw new IllegalArgumentException("Upload requires token");
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.zendesk.client.v2.model.hc;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
5+
import java.util.Date;
6+
import java.util.Objects;
7+
8+
/**
9+
* You can assign a content tag to posts and articles to loosely group them together.
10+
* For more information, see <a href="https://support.zendesk.com/hc/en-us/articles/4848925672730">About Content tags</a>
11+
* in Zendesk help.
12+
*/
13+
public class ContentTag {
14+
15+
/** Automatically assigned when the content tag is created.
16+
* N.B. unlike many other entities, the id field is a String, not a Long */
17+
private String id;
18+
19+
/** The name of the content tag */
20+
private String name;
21+
22+
/** The time the content tag was created */
23+
@JsonProperty("created_at")
24+
private Date createdAt;
25+
26+
/** The time the content tag was last updated */
27+
@JsonProperty("updated_at")
28+
private Date updatedAt;
29+
30+
public ContentTag() {
31+
}
32+
33+
public ContentTag(String id, String name, Date createdAt, Date updatedAt) {
34+
this.id = id;
35+
this.name = name;
36+
this.createdAt = createdAt;
37+
this.updatedAt = updatedAt;
38+
}
39+
40+
public String getId() {
41+
return id;
42+
}
43+
44+
public void setId(String id) {
45+
this.id = id;
46+
}
47+
48+
public String getName() {
49+
return name;
50+
}
51+
52+
public void setName(String name) {
53+
this.name = name;
54+
}
55+
56+
public Date getCreatedAt() {
57+
return createdAt;
58+
}
59+
60+
public void setCreatedAt(Date createdAt) {
61+
this.createdAt = createdAt;
62+
}
63+
64+
public Date getUpdatedAt() {
65+
return updatedAt;
66+
}
67+
68+
public void setUpdatedAt(Date updatedAt) {
69+
this.updatedAt = updatedAt;
70+
}
71+
72+
@Override
73+
public boolean equals(Object o) {
74+
if (this == o) return true;
75+
if (o == null || getClass() != o.getClass()) return false;
76+
ContentTag that = (ContentTag) o;
77+
return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
78+
}
79+
80+
@Override
81+
public int hashCode() {
82+
return Objects.hash(id, name, createdAt, updatedAt);
83+
}
84+
85+
@Override
86+
public String toString() {
87+
return "ContentTag{" +
88+
"id='" + id + '\'' +
89+
", name='" + name + '\'' +
90+
", createdAt=" + createdAt +
91+
", updatedAt=" + updatedAt +
92+
'}';
93+
}
94+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package org.zendesk.client.v2;
2+
3+
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
4+
import org.apache.commons.text.RandomStringGenerator;
5+
import org.junit.After;
6+
import org.junit.Before;
7+
import org.junit.ClassRule;
8+
import org.junit.Rule;
9+
import org.junit.Test;
10+
import org.zendesk.client.v2.model.hc.ContentTag;
11+
12+
import java.text.SimpleDateFormat;
13+
import java.util.TimeZone;
14+
15+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
16+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
20+
public class ContentTagsTest {
21+
22+
private static final String MOCK_URL_FORMATTED_STRING = "http://localhost:%d";
23+
public static final RandomStringGenerator RANDOM_STRING_GENERATOR =
24+
new RandomStringGenerator.Builder().withinRange('a', 'z').build();
25+
private static final String MOCK_API_TOKEN = RANDOM_STRING_GENERATOR.generate(15);
26+
private static final String MOCK_USERNAME = RANDOM_STRING_GENERATOR.generate(10).toLowerCase() + "@cloudbees.com";
27+
28+
@ClassRule
29+
public static WireMockClassRule zendeskApiClass = new WireMockClassRule(options()
30+
.dynamicPort()
31+
.dynamicHttpsPort()
32+
.usingFilesUnderClasspath("wiremock")
33+
);
34+
35+
@Rule
36+
public WireMockClassRule zendeskApiMock = zendeskApiClass;
37+
38+
private Zendesk client;
39+
40+
@Before
41+
public void setUp() throws Exception {
42+
int ephemeralPort = zendeskApiMock.port();
43+
44+
String hostname = String.format(MOCK_URL_FORMATTED_STRING, ephemeralPort);
45+
46+
client = new Zendesk.Builder(hostname)
47+
.setUsername(MOCK_USERNAME)
48+
.setToken(MOCK_API_TOKEN)
49+
.build();
50+
}
51+
52+
@After
53+
public void closeClient() {
54+
if (client != null) {
55+
client.close();
56+
}
57+
client = null;
58+
}
59+
60+
@Test
61+
public void getContentTags_willPageOverMultiplePages() throws Exception {
62+
zendeskApiMock.stubFor(
63+
get(
64+
urlPathEqualTo("/api/v2/guide/content_tags"))
65+
.withQueryParam("page%5Bsize%5D", equalTo("2"))
66+
.willReturn(ok()
67+
.withBodyFile("content_tags/content_tag_search_first_page.json")
68+
)
69+
);
70+
zendeskApiMock.stubFor(
71+
get(
72+
urlPathEqualTo("/api/v2/guide/content_tags"))
73+
.withQueryParam("page%5Bsize%5D", equalTo("2"))
74+
.withQueryParam("page%5Bafter%5D", equalTo("first_after_cursor"))
75+
.willReturn(ok()
76+
.withBodyFile("content_tags/content_tag_search_second_page.json")
77+
)
78+
);
79+
zendeskApiMock.stubFor(
80+
get(
81+
urlPathEqualTo("/api/v2/guide/content_tags"))
82+
.withQueryParam("page%5Bsize%5D", equalTo("2"))
83+
.withQueryParam("page%5Bafter%5D", equalTo("second_after_cursor"))
84+
.willReturn(ok()
85+
.withBodyFile("content_tags/content_tag_search_third_page.json")
86+
)
87+
);
88+
89+
Iterable<ContentTag> actualResults = client.getContentTags(2);
90+
91+
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
92+
df.setTimeZone(TimeZone.getTimeZone("UTC"));
93+
94+
assertThat(actualResults).containsExactly(
95+
new ContentTag("11111111111111111111111111", "first name",
96+
df.parse("2023-03-13 10:01:00"),
97+
df.parse("2023-03-13 10:01:01")
98+
),
99+
new ContentTag("22222222222222222222222222", "second name",
100+
df.parse("2023-03-13 10:02:00"),
101+
df.parse("2023-03-13 10:02:02")
102+
),
103+
new ContentTag("33333333333333333333333333", "third name",
104+
df.parse("2023-03-13 10:03:00"),
105+
df.parse("2023-03-13 10:03:03")
106+
),
107+
new ContentTag("44444444444444444444444444", "fourth name",
108+
df.parse("2023-03-13 10:04:00"),
109+
df.parse("2023-03-13 10:04:04")
110+
),
111+
new ContentTag("55555555555555555555555555", "fifth name",
112+
df.parse("2023-03-13 10:05:00"),
113+
df.parse("2023-03-13 10:05:05")
114+
)
115+
);
116+
}
117+
}

0 commit comments

Comments
 (0)