diff --git a/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiConfiguration.java new file mode 100644 index 0000000000..eed32fcfd8 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiConfiguration.java @@ -0,0 +1,16 @@ +package com.netgrif.application.engine.actions; + +import com.netgrif.application.engine.adapter.spring.actions.ActionApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ActionApiConfiguration { + + @Bean + @ConditionalOnMissingBean(ActionApi.class) + public ActionApi actionApi() { + return new ActionApiImpl(); + } +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java new file mode 100644 index 0000000000..53ff7d0686 --- /dev/null +++ b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java @@ -0,0 +1,187 @@ +package com.netgrif.application.engine.actions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.netgrif.application.engine.adapter.spring.actions.ActionApi; +import com.netgrif.application.engine.auth.service.UserService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; +import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; +import com.netgrif.application.engine.elastic.web.requestbodies.CaseSearchRequest; +import com.netgrif.application.engine.elastic.web.requestbodies.ElasticTaskSearchRequest; +import com.netgrif.application.engine.objects.auth.domain.AbstractUser; +import com.netgrif.application.engine.objects.auth.domain.ActorTransformer; +import com.netgrif.application.engine.objects.auth.domain.LoggedUser; +import com.netgrif.application.engine.objects.auth.domain.User; +import com.netgrif.application.engine.objects.auth.dto.AuthPrincipalDto; +import com.netgrif.application.engine.objects.petrinet.domain.throwable.TransitionNotExecutableException; +import com.netgrif.application.engine.objects.workflow.domain.Case; +import com.netgrif.application.engine.objects.workflow.domain.Task; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.DeleteCaseEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.dataoutcomes.GetDataEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.AssignTaskEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.CancelTaskEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.FinishTaskEventOutcome; +import com.netgrif.application.engine.workflow.service.interfaces.IDataService; +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.querydsl.core.types.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.*; + +@Slf4j +public class ActionApiImpl implements ActionApi { + + private UserService userService; + + private IDataService dataService; + + private ITaskService taskService; + + private IWorkflowService workflowService; + + private IElasticCaseService elasticCaseService; + + private IElasticTaskService elasticTaskService; + + private ObjectMapper objectMapper; + + @Autowired + public void setDataService(IDataService dataService) { + this.dataService = dataService; + } + + @Autowired + public void setTaskService(ITaskService taskService) { + this.taskService = taskService; + } + + @Autowired + public void setWorkflowService(IWorkflowService workflowService) { + this.workflowService = workflowService; + } + + @Autowired + public void setElasticCaseService(IElasticCaseService elasticCaseService) { + this.elasticCaseService = elasticCaseService; + } + + @Autowired + public void setUserService(UserService userService) { + this.userService = userService; + } + + @Autowired + public void setElasticTaskService(IElasticTaskService elasticTaskService) { + this.elasticTaskService = elasticTaskService; + } + + @Autowired + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public GetDataEventOutcome getData(String taskId, Map params) { + return dataService.getData(taskId, params); + } + + @Override + public SetDataEventOutcome setData(String taskId, Map> dataSet, Map params) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(dataSet); + ObjectNode values = (ObjectNode) mapper.readTree(json); + return dataService.setData(taskId, values, params); + } + + @Override + public Page searchCases(String processIdentifier, Predicate predicate, Pageable pageable) { + return workflowService.search(predicate, pageable); + } + + @Override + public Page searchCases(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection) { + boolean intersect = Boolean.TRUE.equals(isIntersection); + List caseSearchRequests = elasticStringQueries.stream().map(query -> CaseSearchRequest.builder().query(query).build()).toList(); + LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); + Locale locale = LocaleContextHolder.getLocale(); + return elasticCaseService.search(caseSearchRequests, loggedUser, pageable, locale, intersect); + } + + @Override + public CreateCaseEventOutcome createCaseByIdentifier(String identifier, String title, String color, AuthPrincipalDto authPrincipalDto, Map params) { + LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); + Locale locale = LocaleContextHolder.getLocale(); + return workflowService.createCaseByIdentifier(identifier, title, color, loggedUser, locale, params); + } + + @Override + public DeleteCaseEventOutcome deleteCase(String caseId, Map params) { + return workflowService.deleteCase(caseId, params); + } + + @Override + public Page searchTasks(String processIdentifier, Predicate predicate, Pageable pageable) { + return taskService.search(predicate, pageable); + } + + @Override + public Page searchTasks(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection) { + boolean intersect = Boolean.TRUE.equals(isIntersection); + List taskSearchRequests = elasticStringQueries.stream().map(query -> ElasticTaskSearchRequest.builder().query(query).build()).toList(); + LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); + Locale locale = LocaleContextHolder.getLocale(); + return elasticTaskService.search(taskSearchRequests, loggedUser, pageable, locale, intersect); + } + + @Override + public AssignTaskEventOutcome assignTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException { + Task task = taskService.findOne(taskId); + AbstractUser user = resolveAbstractUser(authPrincipalDto); + return taskService.assignTask(task, user, params); + } + + @Override + public CancelTaskEventOutcome cancelTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) { + Task task = taskService.findOne(taskId); + AbstractUser user = resolveAbstractUser(authPrincipalDto); + return taskService.cancelTask(task, user, params); + } + + @Override + public FinishTaskEventOutcome finishTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException { + Task task = taskService.findOne(taskId); + AbstractUser user = resolveAbstractUser(authPrincipalDto); + return taskService.finishTask(task, user, params); + } + + @Override + public Case findCase(String caseId) { + return workflowService.findOne(caseId); + } + + @Override + public Task findTask(String taskId) { + return taskService.findOne(taskId); + } + + @Override + public Page searchUsers(Predicate predicate, Pageable pageable, String realmId) { + return userService.search(predicate, pageable, realmId); + } + + private AbstractUser resolveAbstractUser(AuthPrincipalDto authPrincipalDto) { + if (authPrincipalDto == null) { + throw new IllegalArgumentException("AuthPrincipalDto cannot be null."); + } + Optional userOptional = userService.findUserByUsername(authPrincipalDto.getUsername(), authPrincipalDto.getRealmId()); + return userOptional.orElseThrow(() -> new IllegalArgumentException("User with username [%s] and realm ID [%s] not found".formatted(authPrincipalDto.getUsername(), authPrincipalDto.getRealmId()))); + } +} diff --git a/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java b/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java index a01e639ae7..f30bd91155 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/elastic/web/requestbodies/ElasticTaskSearchRequest.java @@ -4,12 +4,14 @@ import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.PetriNet; import com.netgrif.application.engine.workflow.web.requestbodies.taskSearch.TaskSearchCaseRequest; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +@Builder @NoArgsConstructor @AllArgsConstructor public class ElasticTaskSearchRequest extends TaskSearchRequest { diff --git a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/ProcessRoleService.java b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/ProcessRoleService.java index 1c94340d3a..1da1afa0b0 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/ProcessRoleService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/petrinet/service/ProcessRoleService.java @@ -15,8 +15,6 @@ import com.netgrif.application.engine.objects.importer.model.EventPhaseType; import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; import com.netgrif.application.engine.objects.petrinet.domain.dataset.logic.action.Action; -import com.netgrif.application.engine.objects.workflow.domain.Case; -import com.netgrif.application.engine.objects.workflow.domain.QCase; import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.context.RoleContext; import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.runner.RoleActionsRunner; import com.netgrif.application.engine.objects.petrinet.domain.events.Event; @@ -102,8 +100,8 @@ public Optional get(ProcessResourceId processResourceId) { } @Override - public void delete(String s) { - Optional processRole = processRoleRepository.findByCompositeId(s); + public void delete(String roleId) { + Optional processRole = processRoleRepository.findByCompositeId(roleId); processRole.ifPresent(processRoleRepository::delete); } diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/auth/dto/AuthPrincipalDto.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/auth/dto/AuthPrincipalDto.java new file mode 100644 index 0000000000..3333122aae --- /dev/null +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/auth/dto/AuthPrincipalDto.java @@ -0,0 +1,25 @@ +package com.netgrif.application.engine.objects.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class AuthPrincipalDto implements Serializable { + + @Serial + private static final long serialVersionUID = 6725518942728316525L; + + private String username; + + private String realmId; + + @ToString.Exclude + @EqualsAndHashCode.Exclude + @JsonIgnore + private String sessionId; +} diff --git a/nae-spring-core-adapter/pom.xml b/nae-spring-core-adapter/pom.xml index baa4218272..e3edd06578 100644 --- a/nae-spring-core-adapter/pom.xml +++ b/nae-spring-core-adapter/pom.xml @@ -98,14 +98,19 @@ jackson-module-jsonSchema ${jackson.version} + + org.apache.commons + commons-lang3 + com.querydsl querydsl-core ${querydsl.version} - org.apache.commons - commons-lang3 + com.querydsl + querydsl-apt + ${querydsl.version} @@ -131,6 +136,30 @@ + + com.mysema.maven + apt-maven-plugin + 1.1.3 + + + + process + + + target/generated-sources/java + + + com.querydsl.apt.QuerydslAnnotationProcessor + + + + groovy.lang.MetaClass + true + + + + + diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java new file mode 100644 index 0000000000..dc9f5dd5a4 --- /dev/null +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java @@ -0,0 +1,175 @@ +package com.netgrif.application.engine.adapter.spring.actions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.netgrif.application.engine.objects.auth.domain.User; +import com.netgrif.application.engine.objects.auth.dto.AuthPrincipalDto; +import com.netgrif.application.engine.objects.petrinet.domain.throwable.TransitionNotExecutableException; +import com.netgrif.application.engine.objects.workflow.domain.Case; +import com.netgrif.application.engine.objects.workflow.domain.Task; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.caseoutcomes.DeleteCaseEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.dataoutcomes.GetDataEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.AssignTaskEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.CancelTaskEventOutcome; +import com.netgrif.application.engine.objects.workflow.domain.eventoutcomes.taskoutcomes.FinishTaskEventOutcome; +import com.querydsl.core.types.Predicate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Map; +import java.util.List; + + +/** + * ActionApi provides methods for performing operations and actions on workflow-related entities such as cases, + * tasks, and users. It facilitates data retrieval, task management, and realm-specific searches within the system. + */ +public interface ActionApi { + + /** + * Retrieves data associated with a specific task. + * + * @param taskId the ID of the task for which data is to be retrieved + * @param params additional parameters for data retrieval + * @return the outcome of the data retrieval operation + */ + GetDataEventOutcome getData(String taskId, Map params); + + /** + * Sets or updates data for a specific task. + * + * @param taskId the ID of the task to use to update data - task does not have to contain the data that is updated + * @param dataSet the data to be set, organized as a map + * @param params additional parameters for the operation + * @return the outcome of the set data operation + * @throws JsonProcessingException if there is an error processing JSON data + */ + SetDataEventOutcome setData(String taskId, Map> dataSet, Map params) throws JsonProcessingException; + + /** + * Finds a specific case by its ID. + * + * @param caseId the ID of the case to find + * @return the found case + * @throws IllegalArgumentException if not found + */ + Case findCase(String caseId); + + /** + * Searches for cases matching the given predicate. + * + * @param processIdentifier reserved for interface compatibility; this implementation + * does not filter by process identifier and returns cases + * from all processes. Use the predicate parameter to filter + * by specific process(es) if needed. + * @param predicate the criteria for filtering cases + * @param pageable the pagination information + * @return a page of cases matching the criteria + */ + Page searchCases(String processIdentifier, Predicate predicate, Pageable pageable); + + /** + * Searches for cases using elastic search queries. + * + * @param elasticStringQueries a list of queries for filtering cases + * @param authPrincipalDto the authorization principal used for the search + * @param pageable the pagination information + * @param isIntersection true to intersect results of all queries; false for union + * @return a page of cases matching the criteria + */ + Page searchCases(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection); + + /** + * Creates a new case identified by the given process identifier. + * + * @param identifier the process identifier for creating the case + * @param title the title of the new case + * @param color the color associated with the case + * @param authPrincipalDto the authorization principal for creating the case + * @param params additional parameters for the operation + * @return the outcome of the case creation operation + */ + CreateCaseEventOutcome createCaseByIdentifier(String identifier, String title, String color, AuthPrincipalDto authPrincipalDto, Map params); + + /** + * Deletes a specific case by its ID. + * + * @param caseId the ID of the case to delete + * @param params additional parameters for the operation + * @return the outcome of the delete operation + */ + DeleteCaseEventOutcome deleteCase(String caseId, Map params); + + /** + * Finds a specific task by its ID. + * + * @param taskId the ID of the task to find + * @return the found task + * @throws IllegalArgumentException if not found + */ + Task findTask(String taskId); + + /** + * Searches for tasks matching the given process identifier and predicate. + * + * @param processIdentifier the identifier of the process + * @param predicate the criteria for filtering tasks + * @param pageable the pagination information + * @return a page of tasks matching the criteria + */ + Page searchTasks(String processIdentifier, Predicate predicate, Pageable pageable); + + /** + * Searches for tasks using elastic search queries. + * + * @param elasticStringQueries a list of queries for filtering tasks + * @param authPrincipalDto the authorization principal used for the search + * @param pageable the pagination information + * @param isIntersection true to intersect results of all queries; false for union + * @return a page of tasks matching the criteria + */ + Page searchTasks(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection); + + /** + * Assigns a specific task to a user. + * + * @param taskId the ID of the task to assign + * @param authPrincipalDto the authorization principal used for the operation + * @param params additional parameters for the operation + * @return the outcome of the task assignment operation + * @throws TransitionNotExecutableException if the task's transition cannot be executed + */ + AssignTaskEventOutcome assignTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException; + + /** + * Cancels a specific task. + * + * @param taskId the ID of the task to cancel + * @param authPrincipalDto the authorization principal used for the operation + * @param params additional parameters for the operation + * @return the outcome of the task cancellation operation + */ + CancelTaskEventOutcome cancelTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params); + + /** + * Marks a specific task as finished. + * + * @param taskId the ID of the task to finish + * @param authPrincipalDto the authorization principal used for the operation + * @param params additional parameters for the operation + * @return the outcome of the task completion operation + * @throws TransitionNotExecutableException if the task's transition cannot be executed + */ + FinishTaskEventOutcome finishTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException; + + /** + * Searches for users in a specific realm based on a predicate. + * + * @param predicate the criteria for filtering users + * @param pageable the pagination information + * @param realmId the ID of the realm to search + * @return a page of users matching the criteria + */ + Page searchUsers(Predicate predicate, Pageable pageable, String realmId); +} diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java new file mode 100644 index 0000000000..ed4a19d075 --- /dev/null +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java @@ -0,0 +1,38 @@ +package com.netgrif.application.engine.adapter.spring.actions; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum ActionApiMethods { + + GET_DATA("getData"), + SET_DATA("setData"), + FIND_CASE("findCase"), + SEARCH_CASES("searchCases"), + CREATE_CASE_BY_IDENTIFIER("createCaseByIdentifier"), + DELETE_CASE("deleteCase"), + FIND_TASK("findTask"), + SEARCH_TASKS("searchTasks"), + ASSIGN_TASK("assignTask"), + CANCEL_TASK("cancelTask"), + FINISH_TASK("finishTask"), + SEARCH_USERS("searchUsers"); + + private String methodName; + + private static final Map METHODS_BY_NAME_MAP = Arrays.stream(values()).collect(Collectors.toUnmodifiableMap(ActionApiMethods::getMethodName, e -> e)); + + ActionApiMethods(String methodName) { + this.methodName = methodName; + } + + public String getMethodName() { + return methodName; + } + + + public static ActionApiMethods fromMethodName(String methodName) { + return METHODS_BY_NAME_MAP.get(methodName); + } +} diff --git a/nae-user-common/src/main/java/com/netgrif/application/engine/auth/package-info.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/package-info.java similarity index 94% rename from nae-user-common/src/main/java/com/netgrif/application/engine/auth/package-info.java rename to nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/package-info.java index eb189e2a25..742273d634 100644 --- a/nae-user-common/src/main/java/com/netgrif/application/engine/auth/package-info.java +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/package-info.java @@ -10,7 +10,7 @@ Group.class, User.class }) -package com.netgrif.application.engine.auth; +package com.netgrif.application.engine.adapter.spring; import com.netgrif.application.engine.adapter.spring.petrinet.domain.PetriNet; import com.netgrif.application.engine.adapter.spring.petrinet.domain.roles.ProcessRole; diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/NaeReflectionUtils.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/NaeReflectionUtils.java index dea75bc483..eadac8c7e9 100644 --- a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/NaeReflectionUtils.java +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/NaeReflectionUtils.java @@ -1,7 +1,10 @@ package com.netgrif.application.engine.adapter.spring.utils; +import com.netgrif.application.engine.adapter.spring.utils.exceptions.AmbiguousMethodCallException; import org.springframework.aop.framework.AopProxyUtils; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -54,4 +57,85 @@ public static int indexOfClass(List list, Class clazz) { } + /** + * Finds and returns the {@code Method} object for the specified method name and parameter types on the given bean. + * This method first attempts to locate the method on the target class of the bean. + * If not found, it tries to locate a method using superclasses of the parameter types. + * + * @param bean the bean instance on which the method is to be located + * @param methodToExecute the name of the method to be invoked + * @param requestParamTypes the types of the parameters expected by the method + * @return the {@code Method} object corresponding to the specified name and parameters + * @throws NoSuchMethodException if the method cannot be found + * @throws AmbiguousMethodCallException if multiple methods match the specified criteria, resulting in ambiguity + */ + public static Method findMethod(Object bean, String methodToExecute, Class[] requestParamTypes) + throws NoSuchMethodException, AmbiguousMethodCallException { + Objects.requireNonNull(bean, "bean must not be null"); + Objects.requireNonNull(methodToExecute, "methodToExecute must not be null"); + final Class[] paramTypes = (requestParamTypes == null) ? new Class[0] : requestParamTypes; + try { + return NaeReflectionUtils.resolveClass(bean).getMethod(methodToExecute, paramTypes); + } catch (NoSuchMethodException e) { + return findMethodWithSuperClassParams(bean, methodToExecute, requestParamTypes, e); + } + } + + /** + * Attempts to find a method on the target class of the given bean using superclass parameter types when an exact match is not found. + * If a method with the specified name is located, and the parameters can be assigned from the provided parameter types, + * the method is returned. If multiple methods match the criteria, an {@code AmbiguousMethodCallException} is thrown. + * + * @param bean the bean instance on which the method is to be located + * @param methodToExecute the name of the method to be invoked + * @param requestParamTypes the types of the parameters expected by the method + * @param caughtException the exception caught from a previous attempt to find the method (used for re-throw if no method is found) + * @return the {@code Method} object that matches the specified criteria + * @throws NoSuchMethodException if no suitable method is found + * @throws AmbiguousMethodCallException if multiple methods match the specified name and parameters, causing ambiguity + */ + private static Method findMethodWithSuperClassParams(Object bean, String methodToExecute, Class[] requestParamTypes, + NoSuchMethodException caughtException) + throws NoSuchMethodException, AmbiguousMethodCallException { + Class cls = NaeReflectionUtils.resolveClass(bean); + Method[] methods = Arrays.stream(cls.getMethods()) + .filter(m -> !m.isBridge() && !m.isSynthetic()) + .toArray(Method[]::new); + Method methodToInvoke = null; + outerLoop: + for (Method method : methods) { + if (!methodToExecute.equals(method.getName())) { + continue; + } + + Class[] paramTypes = method.getParameterTypes(); + int requestParamsLen = requestParamTypes.length; + int paramsLen = paramTypes.length; + + if (requestParamsLen == 0 && paramsLen == 0) { + methodToInvoke = method; + break; + } else if (paramsLen == 0 || paramsLen != requestParamsLen) { + continue; + } + + for (int i = 0; i < requestParamTypes.length; ++i) { + if (!paramTypes[i].isAssignableFrom(requestParamTypes[i])) { + continue outerLoop; + } + } + + if (methodToInvoke != null) { + throw new AmbiguousMethodCallException(String.format("Method %s is ambiguous for the param types %s", + methodToExecute, Arrays.toString(requestParamTypes))); + } + methodToInvoke = method; + } + + if (methodToInvoke == null) { + throw caughtException; + } else { + return methodToInvoke; + } + } } diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/exceptions/AmbiguousMethodCallException.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/exceptions/AmbiguousMethodCallException.java new file mode 100644 index 0000000000..32ffaf8363 --- /dev/null +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/utils/exceptions/AmbiguousMethodCallException.java @@ -0,0 +1,32 @@ +package com.netgrif.application.engine.adapter.spring.utils.exceptions; + +import java.io.Serial; + +/** + * Exception thrown when a method call cannot be resolved unambiguously + * due to multiple possible matches. + */ +public final class AmbiguousMethodCallException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with the specified detail message. + * + * @param message a description of the ambiguity that caused the exception + */ + public AmbiguousMethodCallException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message a description of the ambiguity that caused the exception + * @param throwable the cause of the exception + */ + public AmbiguousMethodCallException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/nae-user-ce/src/main/java/com/netgrif/application/engine/auth/service/UserServiceImpl.java b/nae-user-ce/src/main/java/com/netgrif/application/engine/auth/service/UserServiceImpl.java index 9820e6296b..217c9e8c33 100644 --- a/nae-user-ce/src/main/java/com/netgrif/application/engine/auth/service/UserServiceImpl.java +++ b/nae-user-ce/src/main/java/com/netgrif/application/engine/auth/service/UserServiceImpl.java @@ -12,6 +12,7 @@ import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; import com.netgrif.application.engine.objects.petrinet.domain.roles.ProcessRole; import com.netgrif.application.engine.objects.workflow.domain.ProcessResourceId; +import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -588,6 +589,21 @@ public void updateAdminWithRoles(Collection roles) { log.debug("Admin [{}] now has [{}] process roles", admin.getUsername(), admin.getProcessRoles().size()); } + + /** + * Searches for users in the specified realm based on the provided predicate and pagination parameters. + * + * @param predicate the query conditions to filter users + * @param pageable the pagination parameters for the search results + * @param realmId the name of the realm, used to determine which collection to query + * @return a paginated list of users matching the predicate within the specified realm + */ + @Override + public Page search(Predicate predicate, Pageable pageable, String realmId) { + String collectionName = collectionNameProvider.getCollectionNameForRealm(realmId); + return userRepository.findAllByQuery(predicate, pageable, mongoTemplate, collectionName); + } + protected User initializeNewUser(String username, String email, String firstName, String lastName, String password, String realmId) { log.trace("Initializing new user [{}] in realm [{}]", username, realmId); User user = new User(); diff --git a/nae-user-common/pom.xml b/nae-user-common/pom.xml index c4e756a301..6c38baf881 100644 --- a/nae-user-common/pom.xml +++ b/nae-user-common/pom.xml @@ -117,17 +117,6 @@ ${mapstruct.version} provided - - - com.querydsl - querydsl-core - ${querydsl.version} - - - com.querydsl - querydsl-apt - ${querydsl.version} - com.querydsl querydsl-mongodb @@ -164,30 +153,6 @@ - - com.mysema.maven - apt-maven-plugin - 1.1.3 - - - - process - - - target/generated-sources/java - - - com.querydsl.apt.QuerydslAnnotationProcessor - - - - groovy.lang.MetaClass - true - - - - - diff --git a/nae-user-common/src/main/java/com/netgrif/application/engine/auth/service/UserService.java b/nae-user-common/src/main/java/com/netgrif/application/engine/auth/service/UserService.java index 63e0111c82..9490c21308 100644 --- a/nae-user-common/src/main/java/com/netgrif/application/engine/auth/service/UserService.java +++ b/nae-user-common/src/main/java/com/netgrif/application/engine/auth/service/UserService.java @@ -5,6 +5,7 @@ import com.netgrif.application.engine.objects.petrinet.domain.PetriNet; import com.netgrif.application.engine.objects.petrinet.domain.roles.ProcessRole; import com.netgrif.application.engine.objects.workflow.domain.ProcessResourceId; +import com.querydsl.core.types.Predicate; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.core.query.Query; @@ -424,4 +425,14 @@ Page searchAllCoMembers(String query, Collection search(Predicate predicate, Pageable pageable, String realmId); }