Skip to content

Commit

Permalink
Fix open-metadata#2866: Add support for PATCH to feed API to be able …
Browse files Browse the repository at this point in the history
…to resolve a thread
  • Loading branch information
vivekratnavel committed Mar 1, 2022
1 parent 68d09a8 commit ea5f3e9
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 0 deletions.
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());
}
}

0 comments on commit ea5f3e9

Please sign in to comment.