-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
build user task search endpoint that indexes tasks and allows to run …
…queries for tasks assigned to users - supported for file system, mongodb and db
- Loading branch information
1 parent
60ec166
commit 9eecca2
Showing
59 changed files
with
2,776 additions
and
73 deletions.
There are no files selected for viewing
42 changes: 42 additions & 0 deletions
42
addons/user-tasks/automatiko-user-tasks-index-db-addon/pom.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>io.automatiko.addons</groupId> | ||
<artifactId>user-tasks</artifactId> | ||
<version>0.0.0-SNAPSHOT</version> | ||
</parent> | ||
<artifactId>automatiko-user-tasks-index-db-addon</artifactId> | ||
<name>Automatiko Engine :: Add-Ons :: User Tasks :: Index :: DB</name> | ||
<description>User task index based on DB AddOn for Automatiko Engine</description> | ||
<properties> | ||
<java.module.name>io.automatiko.addons.usertasks.index.db</java.module.name> | ||
</properties> | ||
<dependencies> | ||
<dependency> | ||
<groupId>io.automatiko.engine</groupId> | ||
<artifactId>automatiko-engine-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.automatiko.engine</groupId> | ||
<artifactId>automatiko-engine-common</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.automatiko.workflow</groupId> | ||
<artifactId>automatiko-workflow-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.automatiko.addons</groupId> | ||
<artifactId>automatiko-user-tasks-index</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.quarkus</groupId> | ||
<artifactId>quarkus-resteasy-reactive-jackson</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.quarkus</groupId> | ||
<artifactId>quarkus-hibernate-orm-panache</artifactId> | ||
</dependency> | ||
</dependencies> | ||
</project> |
94 changes: 94 additions & 0 deletions
94
...addon/src/main/java/io/automatiko/addons/usertasks/index/db/DBUserTaskEventPublisher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package io.automatiko.addons.usertasks.index.db; | ||
|
||
import java.util.Collection; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import org.eclipse.microprofile.config.inject.ConfigProperty; | ||
|
||
import io.automatiko.addon.usertasks.index.UserTask; | ||
import io.automatiko.engine.api.event.DataEvent; | ||
import io.automatiko.engine.api.event.EventPublisher; | ||
import io.automatiko.engine.services.event.UserTaskInstanceDataEvent; | ||
import io.automatiko.engine.services.event.impl.UserTaskInstanceEventBody; | ||
import io.automatiko.engine.workflow.base.instance.impl.humantask.phases.Claim; | ||
import io.automatiko.engine.workflow.base.instance.impl.humantask.phases.Release; | ||
import io.automatiko.engine.workflow.base.instance.impl.workitem.Active; | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.inject.Inject; | ||
import jakarta.transaction.Transactional; | ||
|
||
@ApplicationScoped | ||
public class DBUserTaskEventPublisher implements EventPublisher { | ||
|
||
private String serviceUrl; | ||
|
||
private boolean keepCompleted; | ||
|
||
@Inject | ||
public DBUserTaskEventPublisher( | ||
@ConfigProperty(name = "quarkus.automatiko.service-url") Optional<String> serviceUrl, | ||
@ConfigProperty(name = "quarkus.automatiko.on-instance-end") Optional<String> onInstanceEnd) { | ||
this.serviceUrl = serviceUrl.orElse(""); | ||
this.keepCompleted = onInstanceEnd.orElse("remove").equalsIgnoreCase("keep"); | ||
} | ||
|
||
@Override | ||
@Transactional | ||
public void publish(DataEvent<?> event) { | ||
|
||
if (event instanceof UserTaskInstanceDataEvent) { | ||
UserTaskInstanceDataEvent uevent = (UserTaskInstanceDataEvent) event; | ||
UserTaskInstanceEventBody data = uevent.getData(); | ||
|
||
UserTaskInfoEntity task = new UserTaskInfoEntity(); | ||
|
||
task.setId(data.getId()); | ||
task.setTaskName(data.getTaskName()); | ||
task.setTaskDescription(data.getTaskDescription()); | ||
task.setPotentialUsers(nullIfEmpty(data.getPotentialUsers())); | ||
task.setPotentialGroups(nullIfEmpty(data.getPotentialGroups())); | ||
task.setExcludedUsers(nullIfEmpty(data.getExcludedUsers())); | ||
task.setTaskPriority(data.getTaskPriority()); | ||
task.setState(data.getState()); | ||
task.setActualOwner(data.getActualOwner()); | ||
task.setCompleteDate(data.getCompleteDate()); | ||
task.setFormLink(this.serviceUrl + data.getFormLink()); | ||
task.setProcessId(data.getProcessId()); | ||
task.setProcessInstanceId(data.getProcessInstanceId()); | ||
task.setRootProcessId(data.getRootProcessId()); | ||
task.setRootProcessInstanceId(data.getRootProcessInstanceId()); | ||
task.setReferenceId(data.getReferenceId()); | ||
task.setReferenceName(data.getReferenceName()); | ||
task.setStartDate(data.getStartDate()); | ||
|
||
if (keepCompleted || isActive(task)) { | ||
UserTaskInfoEntity.persist(task); | ||
} else { | ||
UserTaskInfoEntity.deleteById(task.getId()); | ||
} | ||
} | ||
|
||
} | ||
|
||
@Override | ||
public void publish(Collection<DataEvent<?>> events) { | ||
for (DataEvent<?> event : events) { | ||
publish(event); | ||
} | ||
} | ||
|
||
private Set<String> nullIfEmpty(Set<String> set) { | ||
if (set == null || set.isEmpty()) { | ||
return null; | ||
} | ||
|
||
return set; | ||
} | ||
|
||
private boolean isActive(UserTask task) { | ||
return Active.STATUS.equalsIgnoreCase(task.getState()) || Claim.STATUS.equalsIgnoreCase(task.getState()) | ||
|| Release.STATUS.equalsIgnoreCase(task.getState()); | ||
} | ||
|
||
} |
199 changes: 199 additions & 0 deletions
199
...-addon/src/main/java/io/automatiko/addons/usertasks/index/db/DBUserTaskIndexResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
package io.automatiko.addons.usertasks.index.db; | ||
|
||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import io.automatiko.addon.usertasks.index.UserTask; | ||
import io.automatiko.addon.usertasks.index.UserTaskIndexResource; | ||
import io.automatiko.engine.api.auth.IdentityProvider; | ||
import io.automatiko.engine.api.auth.IdentitySupplier; | ||
import io.automatiko.engine.api.workflow.workitem.NotAuthorizedException; | ||
import io.quarkus.arc.All; | ||
import io.quarkus.panache.common.Sort; | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.inject.Inject; | ||
import jakarta.ws.rs.NotFoundException; | ||
import jakarta.ws.rs.core.UriInfo; | ||
|
||
@ApplicationScoped | ||
public class DBUserTaskIndexResource implements UserTaskIndexResource { | ||
|
||
private IdentitySupplier identitySupplier; | ||
|
||
private Map<String, DbCustomQueryBuilder> customQueries = new HashMap<>(); | ||
|
||
@Inject | ||
public DBUserTaskIndexResource(IdentitySupplier identitySupplier, | ||
@All List<DbCustomQueryBuilder> queries) { | ||
this.identitySupplier = identitySupplier; | ||
|
||
queries.stream().forEach(q -> customQueries.put(q.id(), q)); | ||
} | ||
|
||
@Override | ||
public Collection<? extends UserTask> findTasks(String name, String description, String state, | ||
String priority, int page, int size, String sortBy, boolean sortAsc, String user, List<String> groups) { | ||
|
||
IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups); | ||
try { | ||
Sort sort = null; | ||
if (sortBy != null) { | ||
sort = Sort.by(sortBy(sortBy), sortAsc ? Sort.Direction.Ascending : Sort.Direction.Descending); | ||
} | ||
Map<String, Object> parameters = new HashMap<>(); | ||
StringBuilder builder = new StringBuilder(authFilter(identityProvider, parameters)); | ||
|
||
if (name != null) { | ||
builder.append(" t.taskName like :taskName and"); | ||
parameters.put("taskName", "%" + name + " %"); | ||
} | ||
if (description != null) { | ||
builder.append(" t.taskDescription like :taskDescription and"); | ||
parameters.put("taskDescription", "%" + description + " %"); | ||
} | ||
if (state != null) { | ||
builder.append(" t.state = :state and"); | ||
parameters.put("state", state); | ||
} | ||
if (priority != null) { | ||
builder.append(" t.taskPriority like :taskPriority and"); | ||
parameters.put("taskPriority", priority); | ||
} | ||
|
||
String query = builder.toString(); | ||
// remove the last and | ||
query = query.substring(0, query.length() - 4); | ||
|
||
return UserTaskInfoEntity.find(query, sort, parameters).page(calculatePage(page, size), size).list(); | ||
} finally { | ||
IdentityProvider.set(null); | ||
} | ||
} | ||
|
||
@Override | ||
public UserTask findTask(String id, String user, List<String> groups) { | ||
IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups); | ||
try { | ||
|
||
UserTaskInfoEntity entity = UserTaskInfoEntity.findById(id); | ||
|
||
try { | ||
enforceAuthorization(entity, identityProvider); | ||
return entity; | ||
} catch (NotAuthorizedException e) { | ||
return null; | ||
} | ||
|
||
} finally { | ||
IdentityProvider.set(null); | ||
} | ||
} | ||
|
||
@Override | ||
public Collection<? extends UserTask> queryTasks(UriInfo uriInfo, String name, int page, int size, String sortBy, | ||
boolean sortAsc, String user, List<String> groups) { | ||
|
||
DbCustomQueryBuilder customQuery = customQueries.get(name); | ||
|
||
if (customQuery == null) { | ||
throw new NotFoundException("Query with id '" + name + "' was not registered"); | ||
} | ||
|
||
IdentityProvider identityProvider = identitySupplier.buildIdentityProvider(user, groups); | ||
try { | ||
Sort sort = null; | ||
if (sortBy != null) { | ||
sort = Sort.by(sortBy(sortBy), sortAsc ? Sort.Direction.Ascending : Sort.Direction.Descending); | ||
} | ||
Map<String, Object> parameters = new HashMap<>(); | ||
StringBuilder builder = new StringBuilder(authFilter(identityProvider, parameters)); | ||
|
||
DbQueryFilter extraFilter = customQuery.build(uriInfo.getQueryParameters()); | ||
|
||
String query = builder.append(" " + extraFilter.queryFilter()).toString(); | ||
parameters.putAll(extraFilter.parameters()); | ||
|
||
return UserTaskInfoEntity.find(query, sort, parameters).page(calculatePage(page, size), size).list(); | ||
} finally { | ||
IdentityProvider.set(null); | ||
} | ||
} | ||
|
||
protected String authFilter(IdentityProvider identityProvider, Map<String, Object> parameters) { | ||
|
||
parameters.put("user", identityProvider.getName()); | ||
parameters.put("groups", identityProvider.getRoles()); | ||
|
||
String authFilter = "from UserTaskInfoEntity t left join t.potentialUsers pu left join t.potentialGroups pg where (:user not member of t.excludedUsers) and (:user member of t.potentialUsers or pg in (:groups) or t.actualOwner = :user or (size(pg) < 1 and size(pu) < 1)) and "; | ||
|
||
return authFilter; | ||
|
||
} | ||
|
||
protected void enforceAuthorization(UserTaskInfoEntity entity, IdentityProvider identity) { | ||
|
||
if (identity != null) { | ||
// in case identity/auth info is given enforce security restrictions | ||
String user = identity.getName(); | ||
String currentOwner = entity.getActualOwner(); | ||
// if actual owner is already set always enforce same user | ||
if (currentOwner != null && !currentOwner.trim().isEmpty() && !user.equals(currentOwner)) { | ||
|
||
throw new NotAuthorizedException( | ||
"User " + user + " is not authorized to access task instance with id " + entity.getId()); | ||
} | ||
|
||
checkAssignedOwners(entity, user, identity); | ||
} | ||
} | ||
|
||
protected void checkAssignedOwners(UserTaskInfoEntity entity, String user, IdentityProvider identity) { | ||
// is not in the excluded users | ||
if (entity.getExcludedUsers().contains(user)) { | ||
throw new NotAuthorizedException( | ||
"User " + user + " is not authorized to access task instance with id " + entity.getId()); | ||
} | ||
|
||
// if there are no assignments means open to everyone | ||
if (entity.getPotentialUsers().isEmpty() && entity.getPotentialGroups().isEmpty()) { | ||
return; | ||
} | ||
// check if user is in potential users or groups | ||
if (!entity.getPotentialUsers().contains(user) && entity.getPotentialGroups().stream().noneMatch(identity::hasRole)) { | ||
throw new NotAuthorizedException( | ||
"User " + user + " is not authorized to access task instance with id " + entity.getId()); | ||
} | ||
} | ||
|
||
protected int calculatePage(int page, int size) { | ||
if (page <= 1) { | ||
return 0; | ||
} | ||
|
||
return (page - 1) * size; | ||
} | ||
|
||
protected String sortBy(String sortBy) { | ||
|
||
String fieldName = sortBy; | ||
|
||
switch (sortBy) { | ||
case "name": | ||
fieldName = "taskName"; | ||
break; | ||
case "description": | ||
fieldName = "taskDescription"; | ||
break; | ||
case "priority": | ||
fieldName = "taskPriority"; | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
return "t." + fieldName; | ||
} | ||
|
||
} |
7 changes: 7 additions & 0 deletions
7
...-db-addon/src/main/java/io/automatiko/addons/usertasks/index/db/DbCustomQueryBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package io.automatiko.addons.usertasks.index.db; | ||
|
||
import io.automatiko.addon.usertasks.index.CustomQueryBuilder; | ||
|
||
public abstract class DbCustomQueryBuilder implements CustomQueryBuilder<DbQueryFilter> { | ||
|
||
} |
24 changes: 24 additions & 0 deletions
24
...s-index-db-addon/src/main/java/io/automatiko/addons/usertasks/index/db/DbQueryFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package io.automatiko.addons.usertasks.index.db; | ||
|
||
import java.util.Map; | ||
|
||
public class DbQueryFilter { | ||
|
||
private final String queryFilter; | ||
|
||
private final Map<String, Object> parameters; | ||
|
||
public DbQueryFilter(String queryFilter, Map<String, Object> parameters) { | ||
this.queryFilter = queryFilter; | ||
this.parameters = parameters; | ||
} | ||
|
||
public String queryFilter() { | ||
return queryFilter; | ||
} | ||
|
||
public Map<String, Object> parameters() { | ||
return parameters; | ||
} | ||
|
||
} |
Oops, something went wrong.