Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #2866: Add support for PATCH to feed API to be able to resolve a thread #3027

Merged
merged 1 commit into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@
import static org.openmetadata.catalog.Entity.helper;
import static org.openmetadata.catalog.util.EntityUtil.toBoolean;

import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.feed.EntityLinkThreadCount;
import org.openmetadata.catalog.api.feed.ThreadCount;
import org.openmetadata.catalog.entity.feed.Thread;
import org.openmetadata.catalog.resources.feeds.FeedResource;
import org.openmetadata.catalog.resources.feeds.FeedUtil;
import org.openmetadata.catalog.resources.feeds.MessageParser;
import org.openmetadata.catalog.resources.feeds.MessageParser.EntityLink;
Expand All @@ -39,6 +45,8 @@
import org.openmetadata.catalog.type.Relationship;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.RestUtil;
import org.openmetadata.catalog.util.RestUtil.PatchResponse;

public class FeedRepository {
private final CollectionDAO dao;
Expand Down Expand Up @@ -230,6 +238,47 @@ public List<Thread> listThreads(String link, int limitPosts) throws IOException
return limitPostsInThreads(threads, limitPosts);
}

@Transaction
public final PatchResponse<Thread> patch(UriInfo uriInfo, UUID id, String user, JsonPatch patch)
throws IOException, ParseException {
// Get all the fields in the original thread that can be updated during PATCH operation
Thread original = get(id.toString());

// Apply JSON patch to the original thread to get the updated thread
Thread updated = JsonUtils.applyPatch(original, patch, Thread.class);
// update the "updatedBy" and "updatedAt" fields
updated.withUpdatedAt(System.currentTimeMillis()).withUpdatedBy(user);

restorePatchAttributes(original, updated);

// Update the attributes
String change = patchUpdate(original, updated) ? RestUtil.ENTITY_UPDATED : RestUtil.ENTITY_NO_CHANGE;
Thread updatedHref = FeedResource.addHref(uriInfo, updated);
return new PatchResponse<>(Status.OK, updatedHref, change);
}

private void restorePatchAttributes(Thread original, Thread updated) {
// Patch can't make changes to following fields. Ignore the changes
updated.withId(original.getId()).withAbout(original.getAbout());
}

private boolean patchUpdate(Thread original, Thread updated) throws JsonProcessingException {
updated.setId(original.getId());

// store the updated thread
// if there is no change, there is no need to apply patch
if (fieldsChanged(original, updated)) {
dao.feedDAO().update(updated.getId().toString(), JsonUtils.pojoToJson(updated));
return true;
}
return false;
}

private boolean fieldsChanged(Thread original, Thread updated) {
// Patch supports only isResolved and message for now
return original.getResolved() != updated.getResolved() || !original.getMessage().equals(updated.getMessage());
}

private List<Thread> limitPostsInThreads(List<Thread> threads, int limitPosts) {
for (Thread t : threads) {
List<Post> posts = t.getPosts();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,33 @@

package org.openmetadata.catalog.resources.feeds;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
Expand All @@ -51,6 +59,7 @@
import org.openmetadata.catalog.security.Authorizer;
import org.openmetadata.catalog.type.Post;
import org.openmetadata.catalog.util.RestUtil;
import org.openmetadata.catalog.util.RestUtil.PatchResponse;
import org.openmetadata.catalog.util.ResultList;

@Path("/v1/feed")
Expand All @@ -61,8 +70,15 @@
public class FeedResource {
// TODO add /v1/feed?user=userid
public static final String COLLECTION_PATH = "/v1/feed/";
public static final List<String> ALLOWED_FIELDS = getAllowedFields();

private final FeedRepository dao;

private static List<String> getAllowedFields() {
JsonPropertyOrder propertyOrder = Thread.class.getAnnotation(JsonPropertyOrder.class);
return new ArrayList<>(Arrays.asList(propertyOrder.value()));
}

public static List<Thread> addHref(UriInfo uriInfo, List<Thread> threads) {
threads.forEach(t -> addHref(uriInfo, t));
return threads;
Expand Down Expand Up @@ -148,6 +164,33 @@ public Thread get(@Context UriInfo uriInfo, @PathParam("id") String id) throws I
return addHref(uriInfo, dao.get(id));
}

@PATCH
@Path("/{id}")
@Operation(
summary = "Update a thread by `id`.",
tags = "feeds",
description = "Update an existing thread using JsonPatch.",
externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902"))
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
public Response updateThread(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@PathParam("id") String id,
@RequestBody(
description = "JsonPatch with array of operations",
content =
@Content(
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
examples = {
@ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]")
}))
JsonPatch patch)
throws IOException, ParseException {
PatchResponse<Thread> response =
dao.patch(uriInfo, UUID.fromString(id), securityContext.getUserPrincipal().getName(), patch);
return response.toResponse();
}

@GET
@Path("/count")
@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
import static org.openmetadata.catalog.security.SecurityUtil.authHeaders;
import static org.openmetadata.catalog.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.catalog.util.TestUtils.NON_EXISTENT_ENTITY;
import static org.openmetadata.catalog.util.TestUtils.assertListNotNull;
import static org.openmetadata.catalog.util.TestUtils.assertResponse;
import static org.openmetadata.catalog.util.TestUtils.assertResponseContains;

import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.ws.rs.client.WebTarget;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -55,6 +58,7 @@
import org.openmetadata.catalog.type.Column;
import org.openmetadata.catalog.type.ColumnDataType;
import org.openmetadata.catalog.type.Post;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;

@Slf4j
Expand Down Expand Up @@ -266,6 +270,38 @@ void post_validAddPost_200() throws HttpResponseException {
assertEquals(POST_COUNT, postList.getData().size());
}

@Test
void patch_thread_200() throws IOException {
// create a thread
CreateThread create = create().withMessage("message");
Thread thread = createAndCheck(create, ADMIN_AUTH_HEADERS);

String originalJson = JsonUtils.pojoToJson(thread);

// update message and resolved state
Thread updated = thread.withMessage("updated message").withResolved(true);

patchThreadAndCheck(updated, originalJson, ADMIN_AUTH_HEADERS);
}

@Test
void patch_thread_not_allowed_fields() throws IOException {
// create a thread
CreateThread create = create().withMessage("message");
Thread thread = createAndCheck(create, ADMIN_AUTH_HEADERS);

String originalJson = JsonUtils.pojoToJson(thread);

// update the About of the thread
String originalAbout = thread.getAbout();
Thread updated = thread.withAbout("<#E/user>");

patchThread(updated.getId(), originalJson, updated, ADMIN_AUTH_HEADERS);
updated = getThread(thread.getId(), ADMIN_AUTH_HEADERS);
// verify that the "About" is not changed
validateThread(updated, thread.getMessage(), thread.getCreatedBy(), originalAbout);
}

@Test
void list_threadsWithPostsLimit() throws HttpResponseException {
Thread thread = createAndCheck(create(), AUTH_HEADERS);
Expand Down Expand Up @@ -402,4 +438,33 @@ private int getThreadCount(String entityLink, Map<String, String> authHeaders) t
linkThreadCount.stream().filter(l -> l.getEntityLink().equals(entityLink)).findFirst().orElseThrow();
return threadCount.getCount();
}

protected final Thread patchThreadAndCheck(Thread updated, String originalJson, Map<String, String> authHeaders)
throws IOException {

// Validate information returned in patch response has the updates
Thread returned = patchThread(updated.getId(), originalJson, updated, authHeaders);

compareEntities(updated, returned, authHeaders);

// GET the entity and Validate information returned
Thread getEntity = getThread(updated.getId(), authHeaders);
compareEntities(updated, getEntity, authHeaders);

return returned;
}

public final Thread patchThread(UUID id, String originalJson, Thread updated, Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
String updatedThreadJson = JsonUtils.pojoToJson(updated);
JsonPatch patch = JsonUtils.getJsonPatch(originalJson, updatedThreadJson);
return TestUtils.patch(getResource(String.format("feed/%s", id)), patch, Thread.class, authHeaders);
}

public void compareEntities(Thread expected, Thread patched, Map<String, String> authHeaders) {
assertListNotNull(patched.getId(), patched.getHref(), patched.getAbout());
assertEquals(expected.getMessage(), patched.getMessage());
assertEquals(expected.getResolved(), patched.getResolved());
assertEquals(TestUtils.getPrincipal(authHeaders), patched.getUpdatedBy());
}
}