Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.openmetadata.service.resources.feeds.FeedResource Maven / Gradle / Ivy
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmetadata.service.resources.feeds;
import static org.openmetadata.schema.type.EventType.POST_CREATED;
import static org.openmetadata.schema.type.EventType.THREAD_CREATED;
import static org.openmetadata.service.jdbi3.RoleRepository.DOMAIN_ONLY_ACCESS_ROLE;
import static org.openmetadata.service.security.DefaultAuthorizer.getSubjectContext;
import static org.openmetadata.service.util.RestUtil.CHANGE_CUSTOM_HEADER;
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 io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.openmetadata.schema.api.CreateTaskDetails;
import org.openmetadata.schema.api.feed.CloseTask;
import org.openmetadata.schema.api.feed.CreatePost;
import org.openmetadata.schema.api.feed.CreateThread;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.api.feed.ThreadCount;
import org.openmetadata.schema.entity.feed.Thread;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.schema.type.Post;
import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.schema.type.TaskStatus;
import org.openmetadata.schema.type.ThreadType;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.FeedFilter;
import org.openmetadata.service.jdbi3.FeedRepository;
import org.openmetadata.service.jdbi3.FeedRepository.FilterType;
import org.openmetadata.service.jdbi3.FeedRepository.PaginationType;
import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.security.policyevaluator.PostResourceContext;
import org.openmetadata.service.security.policyevaluator.ResourceContextInterface;
import org.openmetadata.service.security.policyevaluator.SubjectContext;
import org.openmetadata.service.security.policyevaluator.ThreadResourceContext;
import org.openmetadata.service.util.RestUtil;
import org.openmetadata.service.util.RestUtil.PatchResponse;
import org.openmetadata.service.util.ResultList;
@Path("/v1/feed")
@Tag(
name = "Feeds",
description = "Feeds API supports `Activity Feeds` and `Conversation Threads`.")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "feeds")
public class FeedResource {
public static final String COLLECTION_PATH = "/v1/feed/";
private final FeedRepository dao;
private final Authorizer authorizer;
public static void addHref(UriInfo uriInfo, List threads) {
if (uriInfo != null) {
threads.forEach(t -> addHref(uriInfo, t));
}
}
public static Thread addHref(UriInfo uriInfo, Thread thread) {
if (uriInfo != null) {
thread.setHref(RestUtil.getHref(uriInfo, COLLECTION_PATH, thread.getId()));
}
return thread;
}
public FeedResource(Authorizer authorizer) {
this.dao = Entity.getFeedRepository();
this.authorizer = authorizer;
}
public static class ThreadList extends ResultList {
/* Required for serde */
}
public static class PostList extends ResultList {
/* Required for serde */
}
public static class ThreadCountList extends ResultList {
/* Required for serde */
}
@GET
@Operation(
operationId = "listThreads",
summary = "List threads",
description = "Get a list of threads, optionally filtered by `entityLink`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "List of threads",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ThreadList.class)))
})
public ResultList list(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(
description =
"Limit the number of posts sorted by chronological order (1 to 1000000, default = 3)",
schema = @Schema(type = "integer"))
@Min(0)
@Max(1000000)
@DefaultValue("3")
@QueryParam("limitPosts")
int limitPosts,
@Parameter(description = "Limit the number of threads returned. (1 to 1000000, default = 10)")
@DefaultValue("10")
@Min(1)
@Max(1000000)
@QueryParam("limit")
int limitParam,
@Parameter(
description = "Returns list of threads before this cursor",
schema = @Schema(type = "string"))
@QueryParam("before")
String before,
@Parameter(
description = "Returns list of threads after this cursor",
schema = @Schema(type = "string"))
@QueryParam("after")
String after,
@Parameter(
description =
"Filter threads by entity link of entity about which this thread is created",
schema =
@Schema(type = "string", example = ""))
@QueryParam("entityLink")
String entityLink,
@Parameter(
description =
"Filter threads by user id. This filter requires a 'filterType' query param. The default filter type is 'OWNER'. This filter cannot be combined with the entityLink filter.",
schema = @Schema(type = "string"))
@QueryParam("userId")
UUID userId,
@Parameter(
description =
"Filter type definition for the user filter. It can take one of 'OWNER', 'FOLLOWS', 'MENTIONS'. This must be used with the 'user' query param",
schema = @Schema(implementation = FilterType.class))
@QueryParam("filterType")
FilterType filterType,
@Parameter(
description =
"Filter threads by whether they are resolved or not. By default resolved is false")
@DefaultValue("false")
@QueryParam("resolved")
boolean resolved,
@Parameter(
description =
"The type of thread to filter the results. It can take one of 'Conversation', 'Task', 'Announcement'",
schema = @Schema(implementation = ThreadType.class))
@QueryParam("type")
ThreadType threadType,
@Parameter(
description =
"The status of tasks to filter the results. It can take one of 'Open', 'Closed'. This filter will take effect only when type is set to Task",
schema = @Schema(implementation = TaskStatus.class))
@QueryParam("taskStatus")
TaskStatus taskStatus,
@Parameter(
description =
"Whether to filter results by announcements that are currently active. This filter will take effect only when type is set to Announcement",
schema = @Schema(type = "boolean"))
@QueryParam("activeAnnouncement")
Boolean activeAnnouncement) {
SubjectContext subjectContext = getSubjectContext(securityContext);
RestUtil.validateCursors(before, after);
FeedFilter filter =
FeedFilter.builder()
.threadType(threadType)
.taskStatus(taskStatus)
.activeAnnouncement(activeAnnouncement)
.resolved(resolved)
.filterType(filterType)
.paginationType(before != null ? PaginationType.BEFORE : PaginationType.AFTER)
.before(before)
.after(after)
.applyDomainFilter(
!subjectContext.isAdmin() && subjectContext.hasAnyRole(DOMAIN_ONLY_ACCESS_ROLE))
.domains(
getSubjectContext(securityContext).getUserDomains().stream()
.map(EntityReference::getId)
.toList())
.build();
ResultList threads = dao.list(filter, entityLink, limitPosts, userId, limitParam);
addHref(uriInfo, threads.getData());
return threads;
}
@GET
@Path("/{id}")
@Operation(
operationId = "getThreadByID",
summary = "Get a thread by Id",
description = "Get a thread by `Id`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The thread",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "404", description = "Thread for instance {id} is not found")
})
public Thread get(
@Context UriInfo uriInfo,
@Parameter(description = "Id of the Thread", schema = @Schema(type = "string"))
@PathParam("id")
UUID id,
@Parameter(description = "Type of the Entity", schema = @Schema(type = "string"))
@PathParam("entityType")
String entityType) {
return addHref(uriInfo, dao.get(id));
}
@GET
@Path("/tasks/{id}")
@Operation(
operationId = "getTaskByID",
summary = "Get a task thread by task Id",
description = "Get a task thread by `task Id`.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The task thread",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "404", description = "Task for instance {id} is not found")
})
public Thread getTask(
@Context UriInfo uriInfo,
@Parameter(description = "Id of the task thread", schema = @Schema(type = "string"))
@PathParam("id")
String id) {
return addHref(uriInfo, dao.getTask(Integer.parseInt(id)));
}
@PUT
@Path("/tasks/{id}/resolve")
@Operation(
operationId = "resolveTask",
summary = "Resolve a task",
description = "Resolve a task.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The task thread",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response resolveTask(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the task thread", schema = @Schema(type = "string"))
@PathParam("id")
String id,
@Valid ResolveTask resolveTask) {
Thread task = dao.getTask(Integer.parseInt(id));
dao.checkPermissionsForResolveTask(authorizer, task, false, securityContext);
return dao.resolveTask(uriInfo, task, securityContext.getUserPrincipal().getName(), resolveTask)
.toResponse();
}
@PUT
@Path("/tasks/{id}/close")
@Operation(
operationId = "closeTask",
summary = "Close a task",
description = "Close a task without making any changes to the entity.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The task thread.",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response closeTask(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the task thread", schema = @Schema(type = "string"))
@PathParam("id")
String id,
@Valid CloseTask closeTask) {
Thread task = dao.getTask(Integer.parseInt(id));
dao.checkPermissionsForResolveTask(authorizer, task, true, securityContext);
return dao.closeTask(uriInfo, task, securityContext.getUserPrincipal().getName(), closeTask)
.toResponse();
}
@PATCH
@Path("/{id}")
@Operation(
operationId = "patchThread",
summary = "Update a thread by `Id`.",
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,
@Parameter(description = "Id of the thread", schema = @Schema(type = "string"))
@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) {
PatchResponse response =
dao.patchThread(
uriInfo, UUID.fromString(id), securityContext.getUserPrincipal().getName(), patch);
return response.toResponse();
}
@GET
@Path("/count")
@Operation(
operationId = "countThreads",
summary = "Count of threads",
description =
"Get a count of threads, optionally filtered by `entityLink` for each of the entities.",
responses = {
@ApiResponse(
responseCode = "200",
description = "Count of threads",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ThreadCountList.class)))
})
public ResultList getThreadCount(
@Context UriInfo uriInfo,
@Parameter(
description = "Filter threads by entity link",
schema =
@Schema(type = "string", example = ""))
@QueryParam("entityLink")
String entityLink) {
return new ResultList<>(dao.getThreadsCount(entityLink));
}
@POST
@Operation(
operationId = "createThread",
summary = "Create a thread",
description =
"Create a new thread. A thread is created about a data asset when a user posts the first post.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The thread",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createThread(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid CreateThread create) {
Thread thread = getThread(securityContext, create);
addHref(uriInfo, dao.create(thread));
return Response.created(thread.getHref())
.entity(thread)
.header(CHANGE_CUSTOM_HEADER, THREAD_CREATED)
.build();
}
@POST
@Path("/{id}/posts")
@Operation(
operationId = "addPostToThread",
summary = "Add post to a thread",
description = "Add a post to an existing thread.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The post",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Thread.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response addPost(
@Context SecurityContext securityContext,
@Context UriInfo uriInfo,
@Parameter(description = "Id of the thread", schema = @Schema(type = "string"))
@PathParam("id")
UUID id,
@Valid CreatePost createPost) {
Post post = getPost(createPost);
Thread thread =
addHref(
uriInfo, dao.addPostToThread(id, post, securityContext.getUserPrincipal().getName()));
return Response.created(thread.getHref())
.header(CHANGE_CUSTOM_HEADER, POST_CREATED)
.entity(thread)
.build();
}
@PATCH
@Path("/{threadId}/posts/{postId}")
@Operation(
operationId = "patchPostOfThread",
summary = "Update post of a thread by `Id`.",
description = "Update a post of an existing thread using JsonPatch.",
externalDocs =
@ExternalDocumentation(
description = "JsonPatch RFC",
url = "https://tools.ietf.org/html/rfc6902"),
responses = {
@ApiResponse(responseCode = "400", description = "Bad request"),
@ApiResponse(responseCode = "404", description = "post with {postId} is not found")
})
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
public Response patchPost(
@Context SecurityContext securityContext,
@Context UriInfo uriInfo,
@Parameter(description = "Id of the thread", schema = @Schema(type = "string"))
@PathParam("threadId")
UUID threadId,
@Parameter(description = "Id of the post", schema = @Schema(type = "string"))
@PathParam("postId")
UUID postId,
@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) {
// validate and get thread & post
Thread thread = dao.get(threadId);
Post post = dao.getPostById(thread, postId);
PatchResponse response =
dao.patchPost(thread, post, securityContext.getUserPrincipal().getName(), patch);
return response.toResponse();
}
@DELETE
@Path("/{threadId}")
@Operation(
operationId = "deleteThread",
summary = "Delete a thread by Id",
description = "Delete an existing thread and all its relationships.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "thread with {threadId} is not found"),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response deleteThread(
@Context SecurityContext securityContext,
@Parameter(
description = "ThreadId of the thread to be deleted",
schema = @Schema(type = "string"))
@PathParam("threadId")
UUID threadId) {
// validate and get the thread
Thread thread = dao.get(threadId);
// delete thread only if the admin/bot/author tries to delete it
OperationContext operationContext =
new OperationContext(Entity.THREAD, MetadataOperation.DELETE);
ResourceContextInterface resourceContext = new ThreadResourceContext(thread.getCreatedBy());
authorizer.authorize(securityContext, operationContext, resourceContext);
return dao.deleteThread(thread, securityContext.getUserPrincipal().getName()).toResponse();
}
@DELETE
@Path("/{threadId}/posts/{postId}")
@Operation(
operationId = "deletePostFromThread",
summary = "Delete a post from its thread",
description = "Delete a post from an existing thread.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "post with {postId} is not found"),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response deletePost(
@Context SecurityContext securityContext,
@Parameter(
description = "ThreadId of the post to be deleted",
schema = @Schema(type = "string"))
@PathParam("threadId")
UUID threadId,
@Parameter(
description = "PostId of the post to be deleted",
schema = @Schema(type = "string"))
@PathParam("postId")
UUID postId) {
// validate and get thread & post
Thread thread = dao.get(threadId);
Post post = dao.getPostById(thread, postId);
// delete post only if the admin/bot/author tries to delete it
OperationContext operationContext =
new OperationContext(Entity.THREAD, MetadataOperation.DELETE);
ResourceContextInterface resourceContext = new PostResourceContext(post.getFrom());
authorizer.authorize(securityContext, operationContext, resourceContext);
return dao.deletePost(thread, post, securityContext.getUserPrincipal().getName()).toResponse();
}
@GET
@Path("/{id}/posts")
@Operation(
operationId = "getAllPostOfThread",
summary = "Get all the posts of a thread",
description = "Get all the posts of an existing thread.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The posts of the given thread.",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = PostList.class))),
})
public ResultList getPosts(
@Context UriInfo uriInfo,
@Parameter(description = "Id of the thread", schema = @Schema(type = "string"))
@PathParam("id")
UUID id) {
return new ResultList<>(dao.listPosts(id));
}
private Thread getThread(SecurityContext securityContext, CreateThread create) {
UUID randomUUID = UUID.randomUUID();
return new Thread()
.withId(randomUUID)
.withThreadTs(System.currentTimeMillis())
.withMessage(create.getMessage())
.withCreatedBy(create.getFrom())
.withAbout(create.getAbout())
.withAddressedTo(create.getAddressedTo())
.withReactions(Collections.emptyList())
.withType(create.getType())
.withTask(getTaskDetails(create.getTaskDetails()))
.withAnnouncement(create.getAnnouncementDetails())
.withChatbot(create.getChatbotDetails())
.withUpdatedBy(securityContext.getUserPrincipal().getName())
.withUpdatedAt(System.currentTimeMillis())
.withEntityRef(new EntityReference().withId(randomUUID).withType(Entity.THREAD))
.withGeneratedBy(Thread.GeneratedBy.USER);
}
private Post getPost(CreatePost create) {
return new Post()
.withId(UUID.randomUUID())
.withMessage(create.getMessage())
.withFrom(create.getFrom())
.withReactions(Collections.emptyList())
.withPostTs(System.currentTimeMillis());
}
private TaskDetails getTaskDetails(CreateTaskDetails create) {
if (create != null) {
return new TaskDetails()
.withAssignees(formatAssignees(create.getAssignees()))
.withType(create.getType())
.withStatus(TaskStatus.Open)
.withOldValue(create.getOldValue())
.withSuggestion(create.getSuggestion());
}
return null;
}
public static List formatAssignees(List assignees) {
List result = new ArrayList<>();
assignees.forEach(
assignee ->
result.add(
new EntityReference().withId(assignee.getId()).withType(assignee.getType())));
return result;
}
}