Skip to content

Commit

Permalink
Merge pull request #560 from andy-may-at/issue/559/implement_content_…
Browse files Browse the repository at this point in the history
…tags_resource
  • Loading branch information
PierreBtz committed Apr 3, 2023
2 parents 3a92607 + e7976c8 commit c0283d3
Show file tree
Hide file tree
Showing 7 changed files with 419 additions and 1 deletion.
105 changes: 104 additions & 1 deletion src/main/java/org/zendesk/client/v2/Zendesk.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.zendesk.client.v2.model.hc.Article;
import org.zendesk.client.v2.model.hc.ArticleAttachments;
import org.zendesk.client.v2.model.hc.Category;
import org.zendesk.client.v2.model.hc.ContentTag;
import org.zendesk.client.v2.model.hc.Locales;
import org.zendesk.client.v2.model.hc.PermissionGroup;
import org.zendesk.client.v2.model.hc.Section;
Expand Down Expand Up @@ -98,7 +99,7 @@
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.function.Function;

/**
* @author stephenc
Expand Down Expand Up @@ -2534,6 +2535,59 @@ public Iterable<Holiday> getHolidaysForSchedule(Long scheduleId) {
handleList(Holiday.class, "holidays")));
}

public ContentTag getContentTag(String contentTagId) {
return complete(submit(req("GET", tmpl("/guide/content_tags/{id}").set("id", contentTagId)),
handle(ContentTag.class, "content_tag")));
}

public ContentTag createContentTag(ContentTag contentTag) {
checkHasName(contentTag);
return complete(submit(req("POST", cnst("/guide/content_tags"),
JSON, json(Collections.singletonMap("content_tag", contentTag))),
handle(ContentTag.class, "content_tag")));
}

public ContentTag updateContentTag(ContentTag contentTag) {
checkHasId(contentTag);
checkHasName(contentTag);
return complete(submit(req("PUT", tmpl("/guide/content_tags/{id}").set("id", contentTag.getId()),
JSON, json(Collections.singletonMap("content_tag", contentTag))),
handle(ContentTag.class, "content_tag")));
}

public void deleteContentTag(ContentTag contentTag) {
checkHasId(contentTag);
complete(submit(req("DELETE", tmpl("/guide/content_tags/{id}").set("id", contentTag.getId())),
handleStatus()));
}

public Iterable<ContentTag> getContentTags() {
int defaultPageSize = 10;
return getContentTags(defaultPageSize, null);
}

public Iterable<ContentTag> getContentTags(int pageSize) {
return getContentTags(pageSize, null);
}

public Iterable<ContentTag> getContentTags(int pageSize, String namePrefix) {
Function<String, Uri> afterCursorUriBuilder = (String afterCursor) -> buildContentTagsSearchUrl(pageSize, namePrefix, afterCursor);
return new PagedIterable<>(afterCursorUriBuilder.apply(null),
handleListWithAfterCursorButNoLinks(ContentTag.class, afterCursorUriBuilder, "records"));
}

private Uri buildContentTagsSearchUrl(int pageSize, String namePrefixFilter, String afterCursor) {
final StringBuilder uriBuilder = new StringBuilder("/guide/content_tags?page[size]=").append(pageSize);

if (namePrefixFilter != null) {
uriBuilder.append("&filter[name_prefix]=").append(encodeUrl(namePrefixFilter));
}
if (afterCursor != null) {
uriBuilder.append("&page[after]=").append(encodeUrl(afterCursor));
}
return cnst(uriBuilder.toString());
}

//////////////////////////////////////////////////////////////////////
// Helper methods
//////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -2687,6 +2741,7 @@ public JobStatus onCompleted(Response response) throws Exception {
private static final String COUNT = "count";
private static final int INCREMENTAL_EXPORT_MAX_COUNT_BY_REQUEST = 1000;


private abstract class PagedAsyncCompletionHandler<T> extends ZendeskAsyncCompletionHandler<T> {
private String nextPage;

Expand Down Expand Up @@ -2871,6 +2926,42 @@ public List<ArticleAttachments> onCompleted(Response response) throws Exception
};
}

/**
* For a resource (e.g. ContentTag) which supports cursor based pagination for multiple results,
* but where the response does not have a `links.next` node (which would hold the URL of the next page)
* So we need to build the next page URL from the original URL and the meta.after_cursor node value
*
* @param <T> The class of the resource
* @param afterCursorUriBuilder a function to build the URL for the next page `fn(after_cursor_value) => URL_of_next_page`
* @param name the name of the Json node that contains the resources entities (e.g. 'records' for ContentTag)
*/
private <T> PagedAsyncCompletionHandler<List<T>> handleListWithAfterCursorButNoLinks(
Class<T> clazz, Function<String, Uri> afterCursorUriBuilder, String name) {

return new PagedAsyncListCompletionHandler<T>(clazz, name) {
@Override
public void setPagedProperties(JsonNode responseNode, Class<?> clazz) {
JsonNode metaNode = responseNode.get("meta");
String nextPage = null;
if (metaNode == null) {
if (logger.isDebugEnabled()) {
logger.debug("meta" + " property not found, pagination not supported" +
(clazz != null ? " for " + clazz.getName() : ""));
}
} else {
JsonNode afterCursorNode = metaNode.get("after_cursor");
if (afterCursorNode != null) {
JsonNode hasMoreNode = metaNode.get("has_more");
if (hasMoreNode != null && hasMoreNode.asBoolean()) {
nextPage = afterCursorUriBuilder.apply(afterCursorNode.asText()).toString();
}
}
}
setNextPage(nextPage);
}
};
}

private TemplateUri tmpl(String template) {
return new TemplateUri(url + template);
}
Expand Down Expand Up @@ -3132,6 +3223,18 @@ private static void checkHasId(UserSegment userSegment) {
}
}

private static void checkHasId(ContentTag contentTag) {
if (contentTag.getId() == null) {
throw new IllegalArgumentException("Content Tag requires id");
}
}

private static void checkHasName(ContentTag contentTag) {
if (contentTag.getName() == null || contentTag.getName().trim().isEmpty()) {
throw new IllegalArgumentException("Content Tag requires name");
}
}

private static void checkHasToken(Attachment.Upload upload) {
if (upload.getToken() == null) {
throw new IllegalArgumentException("Upload requires token");
Expand Down
94 changes: 94 additions & 0 deletions src/main/java/org/zendesk/client/v2/model/hc/ContentTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.zendesk.client.v2.model.hc;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Date;
import java.util.Objects;

/**
* You can assign a content tag to posts and articles to loosely group them together.
* For more information, see <a href="https://support.zendesk.com/hc/en-us/articles/4848925672730">About Content tags</a>
* in Zendesk help.
*/
public class ContentTag {

/** Automatically assigned when the content tag is created.
* N.B. unlike many other entities, the id field is a String, not a Long */
private String id;

/** The name of the content tag */
private String name;

/** The time the content tag was created */
@JsonProperty("created_at")
private Date createdAt;

/** The time the content tag was last updated */
@JsonProperty("updated_at")
private Date updatedAt;

public ContentTag() {
}

public ContentTag(String id, String name, Date createdAt, Date updatedAt) {
this.id = id;
this.name = name;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Date getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}

public Date getUpdatedAt() {
return updatedAt;
}

public void setUpdatedAt(Date updatedAt) {
this.updatedAt = updatedAt;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ContentTag that = (ContentTag) o;
return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
}

@Override
public int hashCode() {
return Objects.hash(id, name, createdAt, updatedAt);
}

@Override
public String toString() {
return "ContentTag{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}
117 changes: 117 additions & 0 deletions src/test/java/org/zendesk/client/v2/ContentTagsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.zendesk.client.v2;

import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import org.apache.commons.text.RandomStringGenerator;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.zendesk.client.v2.model.hc.ContentTag;

import java.text.SimpleDateFormat;
import java.util.TimeZone;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;


public class ContentTagsTest {

private static final String MOCK_URL_FORMATTED_STRING = "http://localhost:%d";
public static final RandomStringGenerator RANDOM_STRING_GENERATOR =
new RandomStringGenerator.Builder().withinRange('a', 'z').build();
private static final String MOCK_API_TOKEN = RANDOM_STRING_GENERATOR.generate(15);
private static final String MOCK_USERNAME = RANDOM_STRING_GENERATOR.generate(10).toLowerCase() + "@cloudbees.com";

@ClassRule
public static WireMockClassRule zendeskApiClass = new WireMockClassRule(options()
.dynamicPort()
.dynamicHttpsPort()
.usingFilesUnderClasspath("wiremock")
);

@Rule
public WireMockClassRule zendeskApiMock = zendeskApiClass;

private Zendesk client;

@Before
public void setUp() throws Exception {
int ephemeralPort = zendeskApiMock.port();

String hostname = String.format(MOCK_URL_FORMATTED_STRING, ephemeralPort);

client = new Zendesk.Builder(hostname)
.setUsername(MOCK_USERNAME)
.setToken(MOCK_API_TOKEN)
.build();
}

@After
public void closeClient() {
if (client != null) {
client.close();
}
client = null;
}

@Test
public void getContentTags_willPageOverMultiplePages() throws Exception {
zendeskApiMock.stubFor(
get(
urlPathEqualTo("/api/v2/guide/content_tags"))
.withQueryParam("page%5Bsize%5D", equalTo("2"))
.willReturn(ok()
.withBodyFile("content_tags/content_tag_search_first_page.json")
)
);
zendeskApiMock.stubFor(
get(
urlPathEqualTo("/api/v2/guide/content_tags"))
.withQueryParam("page%5Bsize%5D", equalTo("2"))
.withQueryParam("page%5Bafter%5D", equalTo("first_after_cursor"))
.willReturn(ok()
.withBodyFile("content_tags/content_tag_search_second_page.json")
)
);
zendeskApiMock.stubFor(
get(
urlPathEqualTo("/api/v2/guide/content_tags"))
.withQueryParam("page%5Bsize%5D", equalTo("2"))
.withQueryParam("page%5Bafter%5D", equalTo("second_after_cursor"))
.willReturn(ok()
.withBodyFile("content_tags/content_tag_search_third_page.json")
)
);

Iterable<ContentTag> actualResults = client.getContentTags(2);

SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));

assertThat(actualResults).containsExactly(
new ContentTag("11111111111111111111111111", "first name",
df.parse("2023-03-13 10:01:00"),
df.parse("2023-03-13 10:01:01")
),
new ContentTag("22222222222222222222222222", "second name",
df.parse("2023-03-13 10:02:00"),
df.parse("2023-03-13 10:02:02")
),
new ContentTag("33333333333333333333333333", "third name",
df.parse("2023-03-13 10:03:00"),
df.parse("2023-03-13 10:03:03")
),
new ContentTag("44444444444444444444444444", "fourth name",
df.parse("2023-03-13 10:04:00"),
df.parse("2023-03-13 10:04:04")
),
new ContentTag("55555555555555555555555555", "fifth name",
df.parse("2023-03-13 10:05:00"),
df.parse("2023-03-13 10:05:05")
)
);
}
}
Loading

0 comments on commit c0283d3

Please sign in to comment.