From 174c63878cf80a1b5a8196709b8f7e8d6366e9c6 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Thu, 8 Jan 2026 14:00:19 +0100 Subject: [PATCH 1/4] SED-4340 find-usages-of-keyword-and-plan-referenced-by-attribute-is-broken --- .../step/core/references/ReferenceFinder.java | 124 ++++++++++++++ .../references/ReferenceFinderServices.java | 141 +--------------- .../java/step/core/GlobalContextBuilder.java | 28 ++-- .../core/references/ReferenceFinderTest.java | 151 ++++++++++++++++++ .../entities/EntityDependencyTreeVisitor.java | 32 ++-- .../step/core/entities/EntityManager.java | 3 +- .../EntityDependencyTreeVisitorTest.java | 2 +- .../packages/FunctionPackageEntity.java | 7 +- .../artefacts/handlers/FunctionLocator.java | 73 ++++++--- .../artefacts/handlers/LocatorHelper.java | 4 +- .../step/artefacts/handlers/PlanLocator.java | 59 ++++++- .../main/java/step/core/plans/PlanEntity.java | 26 ++- .../functions/accessor/FunctionEntity.java | 28 +++- .../step/core/scheduler/ScheduleEntity.java | 2 +- 14 files changed, 471 insertions(+), 209 deletions(-) create mode 100644 step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java create mode 100644 step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java new file mode 100644 index 0000000000..52f84fe645 --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java @@ -0,0 +1,124 @@ +package step.core.references; + +import step.core.accessors.AbstractOrganizableObject; +import step.core.entities.EntityConstants; +import step.core.entities.EntityDependencyTreeVisitor; +import step.core.entities.EntityManager; +import step.core.plans.Plan; +import step.core.plans.PlanAccessor; +import step.functions.Function; +import step.functions.accessor.FunctionAccessor; +import step.resources.Resource; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ReferenceFinder { + + private final EntityManager entityManager; + + public ReferenceFinder(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public List findReferences(FindReferencesRequest request) { + if (request.searchType == null) { + throw new IllegalArgumentException("A valid searchType must be provided"); + } + if (request.searchValue == null || request.searchValue.trim().isEmpty()) { + throw new IllegalArgumentException("A non-empty searchValue must be provided"); + } + + List results = new ArrayList<>(); + + PlanAccessor planAccessor = (PlanAccessor) entityManager.getEntityByName(EntityConstants.plans).getAccessor(); + + // Find composite keywords containing requested usages; composite KWs are really just plans in disguise :-) + FunctionAccessor functionAccessor = (FunctionAccessor) entityManager.getEntityByName(EntityConstants.functions).getAccessor(); + + try (Stream functionStream = functionAccessor.streamLazy()) { + functionStream.forEach(function -> { + List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.functions, function, request); + if (!matchingObjects.isEmpty()) { + results.add(new FindReferencesResponse(function)); + } + }); + } + + // Find plans containing usages + try (Stream stream = (request.includeHiddenPlans) ? planAccessor.streamLazy() : planAccessor.getVisiblePlans()) { + stream.forEach(plan -> { + List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.plans, plan, request); + if (!matchingObjects.isEmpty()) { + results.add(new FindReferencesResponse(plan)); + } + }); + } + + // Sort the results by name + results.sort(Comparator.comparing(f -> f.name)); + return results; + } + + private List getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) { + List referencedObjects = getReferencedObjects(entityType, object).stream().filter(o -> (o != null && !o.equals(object))).collect(Collectors.toList()); + //System.err.println("objects referenced from plan: " + planToString(plan) + ": "+ referencedObjects.stream().map(ReferenceFinderServices::objectToString).collect(Collectors.toList())); + return referencedObjects.stream().filter(o -> doesRequestMatch(request, o)).collect(Collectors.toList()); + } + + // returns a (generic) set of objects referenced by a plan + private Set getReferencedObjects(String entityType, AbstractOrganizableObject object) { + Set referencedObjects = new HashSet<>(); + + // The references can be filled in three different ways due to the implementation: + // 1. through the predicate (just below) + // 2. by (actual object) reference in the tree visitor (onResolvedEntity) + + // No context predicate is used by the reference finder, since we want to find all entities (i.e. if we search the usages of a Keyword from the Common project, we should be able + // to find plans using it in other projects. + // This unfortunately can return incorrect results, i.e. a keyword "MyKeyword" is created in ProjectA and ProjectB, A PlanA is created in ProjectA and is using the KW of the same project. + // Searching usage of "MyKeyword" in projectB will return the planA from projectA + EntityDependencyTreeVisitor entityDependencyTreeVisitor = new EntityDependencyTreeVisitor(entityManager, o -> true); + FindReferencesTreeVisitor entityTreeVisitor = new FindReferencesTreeVisitor(entityManager, referencedObjects); + entityDependencyTreeVisitor.visitEntityDependencyTree(entityType, object.getId().toString(), entityTreeVisitor, EntityDependencyTreeVisitor.VISIT_MODE.RESOLVE_ALL); + + return referencedObjects; + } + + private boolean doesRequestMatch(FindReferencesRequest req, Object o) { + if (o instanceof Plan) { + Plan p = (Plan) o; + switch (req.searchType) { + case PLAN_NAME: + return p.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + case PLAN_ID: + return p.getId().toString().equals(req.searchValue); + default: + return false; + } + } else if (o instanceof Function) { + Function f = (Function) o; + switch (req.searchType) { + case KEYWORD_NAME: + return f.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + case KEYWORD_ID: + return f.getId().toString().equals(req.searchValue); + default: + return false; + } + } else if (o instanceof Resource) { + Resource r = (Resource) o; + switch (req.searchType) { + case RESOURCE_NAME: + return r.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + case RESOURCE_ID: + return r.getId().toString().equals(req.searchValue); + default: + return false; + } + } else { + return false; + } + } +} diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java index 910960109e..b6110c07c9 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java @@ -28,155 +28,20 @@ @Tag(name = "References") public class ReferenceFinderServices extends AbstractStepServices { - private EntityManager entityManager; + private ReferenceFinder referenceFinder; @PostConstruct public void init() throws Exception { super.init(); - entityManager = getContext().getEntityManager(); + referenceFinder = new ReferenceFinder(getContext().getEntityManager()); } - - // Uncomment for easier debugging (poor man's Unit Test), URL will be http://localhost:8080/rest/references/findReferencesDebug - /* - @GET - @Path("/findReferencesDebug") - @Produces(MediaType.APPLICATION_JSON) - public List findReferencesTest() { - List result = new ArrayList<>(); - result.addAll(findReferences(new FindReferencesRequest(PLAN_NAME, "TestXXX"))); -// result.addAll(findReferences(new FindReferencesRequest(PLAN_ID, "6195001c0a98d92da8a57830"))); - result.addAll(findReferences(new FindReferencesRequest(KEYWORD_NAME, "UnitTest"))); -// result.addAll(findReferences(new FindReferencesRequest(KEYWORD_ID, "60cca3488b81b227a5fe92d9"))); - return result; - } - //*/ - @Path("/findReferences") @POST @Secured @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public List findReferences(FindReferencesRequest request) { - if (request.searchType == null) { - throw new IllegalArgumentException("A valid searchType must be provided"); - } - if (request.searchValue == null || request.searchValue.trim().isEmpty()) { - throw new IllegalArgumentException("A non-empty searchValue must be provided"); - } - - List results = new ArrayList<>(); - - PlanAccessor planAccessor = (PlanAccessor) entityManager.getEntityByName(EntityConstants.plans).getAccessor(); - - // Find composite keywords containing requested usages; composite KWs are really just plans in disguise :-) - FunctionAccessor functionAccessor = (FunctionAccessor) entityManager.getEntityByName(EntityConstants.functions).getAccessor(); - - try (Stream functionStream = functionAccessor.streamLazy()) { - functionStream.forEach(function -> { - List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.functions, function, request); - if (!matchingObjects.isEmpty()) { - results.add(new FindReferencesResponse(function)); - } - }); - } - - // Find plans containing usages - try (Stream stream = (request.includeHiddenPlans) ? planAccessor.streamLazy() : planAccessor.getVisiblePlans()) { - stream.forEach(plan -> { - List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.plans, plan, request); - if (!matchingObjects.isEmpty()) { - results.add(new FindReferencesResponse(plan)); - } - }); - } - - // Sort the results by name - results.sort(Comparator.comparing(f -> f.name)); - return results; - } - - private List getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) { - List referencedObjects = getReferencedObjects(entityType, object).stream().filter(o -> (o != null && !o.equals(object))).collect(Collectors.toList()); - //System.err.println("objects referenced from plan: " + planToString(plan) + ": "+ referencedObjects.stream().map(ReferenceFinderServices::objectToString).collect(Collectors.toList())); - return referencedObjects.stream().filter(o -> doesRequestMatch(request, o)).collect(Collectors.toList()); + return referenceFinder.findReferences(request); } - - // returns a (generic) set of objects referenced by a plan - private Set getReferencedObjects(String entityType, AbstractOrganizableObject object) { - Set referencedObjects = new HashSet<>(); - - // The references can be filled in three different ways due to the implementation: - // 1. through the predicate (just below) - // 2. by (actual object) reference in the tree visitor (onResolvedEntity) - // 3. by object ID in the tree visitor (onResolvedEntityId) - - ObjectPredicate visitedObjectPredicate = visitedObject -> { - referencedObjects.add(visitedObject); - return true; - }; - - EntityDependencyTreeVisitor entityDependencyTreeVisitor = new EntityDependencyTreeVisitor(entityManager, visitedObjectPredicate); - FindReferencesTreeVisitor entityTreeVisitor = new FindReferencesTreeVisitor(entityManager, referencedObjects); - entityDependencyTreeVisitor.visitEntityDependencyTree(entityType, object.getId().toString(), entityTreeVisitor, false); - - return referencedObjects; - } - - private boolean doesRequestMatch(FindReferencesRequest req, Object o) { - if (o instanceof Plan) { - Plan p = (Plan) o; - switch (req.searchType) { - case PLAN_NAME: - return p.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); - case PLAN_ID: - return p.getId().toString().equals(req.searchValue); - default: - return false; - } - } else if (o instanceof Function) { - Function f = (Function) o; - switch (req.searchType) { - case KEYWORD_NAME: - return f.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); - case KEYWORD_ID: - return f.getId().toString().equals(req.searchValue); - default: - return false; - } - } else if (o instanceof Resource) { - Resource r = (Resource) o; - switch (req.searchType) { - case RESOURCE_NAME: - return r.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); - case RESOURCE_ID: - return r.getId().toString().equals(req.searchValue); - default: - return false; - } - } else { - return false; - } - } - - // the following functions are only needed for debugging - private static String objectToString(Object o) { - if (o instanceof Plan) { - return planToString((Plan) o); - } else if (o instanceof Function) { - return functionToString((Function) o); - } else { - return o.getClass() + " " + o.toString(); - } - } - - private static String planToString(Plan plan) { - return "PLAN: " + plan.getAttributes().toString() + " id=" + plan.getId().toString(); - } - - private static String functionToString(Function function) { - return "FUNCTION: " + function.getAttributes().toString() + " id=" + function.getId().toString(); - } - - } diff --git a/step-controller/step-controller-server/src/test/java/step/core/GlobalContextBuilder.java b/step-controller/step-controller-server/src/test/java/step/core/GlobalContextBuilder.java index f20b27a981..06cf7e72c5 100644 --- a/step-controller/step-controller-server/src/test/java/step/core/GlobalContextBuilder.java +++ b/step-controller/step-controller-server/src/test/java/step/core/GlobalContextBuilder.java @@ -22,6 +22,9 @@ import ch.exense.commons.io.FileHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import step.artefacts.handlers.FunctionLocator; +import step.artefacts.handlers.PlanLocator; +import step.artefacts.handlers.SelectorHelper; import step.core.access.InMemoryUserAccessor; import step.core.access.User; import step.core.access.UserAccessor; @@ -30,6 +33,8 @@ import step.core.artefacts.reports.ReportNode; import step.core.artefacts.reports.ReportNodeAccessor; import step.core.dynamicbeans.DynamicBeanResolver; +import step.core.dynamicbeans.DynamicJsonObjectResolver; +import step.core.dynamicbeans.DynamicJsonValueResolver; import step.core.dynamicbeans.DynamicValueResolver; import step.core.entities.Entity; import step.core.entities.EntityConstants; @@ -40,17 +45,19 @@ import step.core.plans.InMemoryPlanAccessor; import step.core.plans.Plan; import step.core.plans.PlanAccessor; +import step.core.plans.PlanEntity; import step.core.plugins.ControllerPluginManager; import step.core.plugins.PluginManager.Builder.CircularDependencyException; import step.core.repositories.RepositoryObjectManager; import step.core.scheduler.ExecutionTaskAccessor; import step.core.scheduler.ExecutiontTaskParameters; import step.core.scheduler.InMemoryExecutionTaskAccessor; +import step.core.scheduler.ScheduleEntity; import step.expressions.ExpressionHandler; import step.framework.server.ServerPluginManager; import step.framework.server.tables.TableRegistry; -import step.functions.Function; import step.functions.accessor.FunctionAccessor; +import step.functions.accessor.FunctionEntity; import step.functions.accessor.InMemoryFunctionAccessorImpl; import step.resources.*; @@ -101,20 +108,23 @@ public static GlobalContext createGlobalContext() throws CircularDependencyExcep logger.error("Unable to create temp folder for the resource manager", e); } - context.setEntityManager(new EntityManager()); + DynamicJsonObjectResolver dynamicJsonObjectResolver = new DynamicJsonObjectResolver(new DynamicJsonValueResolver(context.getExpressionHandler())); + SelectorHelper selectorHelper = new SelectorHelper(dynamicJsonObjectResolver); + PlanLocator planLocator = new PlanLocator(context.getPlanAccessor(), selectorHelper); + FunctionLocator functionLocator = new FunctionLocator(functionAccessor, selectorHelper); + + EntityManager entityManager = new EntityManager(); + context.setEntityManager(entityManager); context.getEntityManager() .register(new Entity(EntityConstants.executions, context.getExecutionAccessor(), Execution.class)) - .register(new Entity(EntityConstants.plans, context.getPlanAccessor(), Plan.class)) + .register(new PlanEntity(context.getPlanAccessor(), planLocator, entityManager)) .register(new Entity(EntityConstants.reports, context.getReportAccessor(), ReportNode.class)) - .register(new Entity(EntityConstants.tasks, - context.getScheduleAccessor(), ExecutiontTaskParameters.class)) + .register(new ScheduleEntity(context.getScheduleAccessor(), ExecutiontTaskParameters.class, entityManager)) .register(new Entity(EntityConstants.users, context.getUserAccessor(), User.class)) - .register(new Entity(EntityConstants.functions, - (FunctionAccessor) functionAccessor, Function.class)) - .register(new Entity(EntityConstants.resources, resourceAccessor, - Resource.class)) + .register(new FunctionEntity(functionAccessor, functionLocator, entityManager)) + .register(new ResourceEntity(resourceAccessor, entityManager)) .register(new Entity(EntityConstants.resourceRevisions, resourceRevisionAccessor, ResourceRevision.class)); diff --git a/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java b/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java new file mode 100644 index 0000000000..5f2e5fcfa1 --- /dev/null +++ b/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java @@ -0,0 +1,151 @@ +package step.core.references; + +import org.junit.Before; +import org.junit.Test; +import step.artefacts.CallFunction; +import step.artefacts.CallPlan; +import step.artefacts.ForEachBlock; +import step.attachments.FileResolver; +import step.core.GlobalContext; +import step.core.accessors.AbstractOrganizableObject; +import step.core.dynamicbeans.DynamicValue; +import step.core.plans.Plan; +import step.core.plans.builder.PlanBuilder; +import step.core.plugins.PluginManager; +import step.datapool.file.CSVDataPool; +import step.functions.Function; +import step.functions.accessor.FunctionAccessor; +import step.planbuilder.BaseArtefacts; +import step.planbuilder.FunctionArtefacts; +import step.plugins.functions.types.CompositeFunction; +import step.resources.Resource; + +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.*; +import static step.core.GlobalContextBuilder.createGlobalContext; +import static step.resources.ResourceManager.RESOURCE_TYPE_DATASOURCE; + +public class ReferenceFinderTest { + + public static final String CALLING_PLAN_BY_ID = "CALLING PLAN BY ID"; + public static final String CALLING_PLAN_BY_NAME = "CALLING PLAN BY NAME"; + public static final String CALLED_PLAN_NAME = "CALLED PLAN"; + public static final String CALLED_FUNCTION_NAME = "CALLED FUNCTION"; + public static final String COMPOSITE_KEYWORD = "COMPOSITE_KEYWORD"; + public static final String CSV_FILE = "CSV FILE"; + public static final String PLAN_USING_RESOURCE = "PLAN_USING_RESOURCE"; + private GlobalContext context; + private ReferenceFinder referenceFinder; + private FunctionAccessor functionAccessor; + + @Before + public void setup() throws ClassNotFoundException, PluginManager.Builder.CircularDependencyException, InstantiationException, IllegalAccessException { + context = createGlobalContext(); + referenceFinder = new ReferenceFinder(context.getEntityManager()); + functionAccessor = context.require(FunctionAccessor.class); + } + + @Test + public void findReferencesForPlansAndKeyword() { + Plan calledPlan = PlanBuilder.create().startBlock(BaseArtefacts.sequence()).endBlock().build(); + calledPlan.addAttribute(AbstractOrganizableObject.NAME, CALLED_PLAN_NAME); + context.getPlanAccessor().save(calledPlan); + + Function function = new Function(); + function.addAttribute(AbstractOrganizableObject.NAME, CALLED_FUNCTION_NAME); + functionAccessor.save(function); + CallFunction callFunction = FunctionArtefacts.keyword(function.getAttribute(AbstractOrganizableObject.NAME), "{\"key1\":\"val1\"}"); + + //Plan calling another plan by ID + CallPlan callPlanById = new CallPlan(); + String calledPlanId = calledPlan.getId().toString(); + callPlanById.setPlanId(calledPlanId); + Plan planCallingPlanById = PlanBuilder.create().startBlock(BaseArtefacts.sequence()).add(callPlanById).add(callFunction).endBlock().build(); + planCallingPlanById.addAttribute(AbstractOrganizableObject.NAME, CALLING_PLAN_BY_ID); + context.getPlanAccessor().save(planCallingPlanById); + + //Plan calling another plan by attributes + CallPlan callPlanByName = new CallPlan(); + callPlanByName.setSelectionAttributes(new DynamicValue("{\"name\":\"" + CALLED_PLAN_NAME + "\"}")); + Plan planCallingPlanByName = PlanBuilder.create().startBlock(BaseArtefacts.sequence()).add(callPlanByName).add(callFunction).endBlock().build(); + planCallingPlanByName.addAttribute(AbstractOrganizableObject.NAME, CALLING_PLAN_BY_NAME); + context.getPlanAccessor().save(planCallingPlanByName); + + //Composite Keyword calling plan and keyword by name + CompositeFunction compositeFunction = new CompositeFunction(); + compositeFunction.addAttribute(AbstractOrganizableObject.NAME, COMPOSITE_KEYWORD); + compositeFunction.setPlan(planCallingPlanByName); + functionAccessor.save(compositeFunction); + + //Search usage by Plan ID + List findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.PLAN_ID, calledPlanId)); + assertPlansAndCompositesAreFound(findReferencesResponse, planCallingPlanById, planCallingPlanByName, compositeFunction); + + //Search usage by Plan name + findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.PLAN_NAME, CALLED_PLAN_NAME)); + assertPlansAndCompositesAreFound(findReferencesResponse, planCallingPlanById, planCallingPlanByName, compositeFunction); + + //Search Keyword By ID + findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.KEYWORD_ID, function.getId().toString())); + assertPlansAndCompositesAreFound(findReferencesResponse, planCallingPlanById, planCallingPlanByName, compositeFunction); + + //Search Keyword By name + findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.KEYWORD_NAME, CALLED_FUNCTION_NAME)); + assertPlansAndCompositesAreFound(findReferencesResponse, planCallingPlanById, planCallingPlanByName, compositeFunction); + + } + + private static void assertPlansAndCompositesAreFound(List findReferencesResponse, Plan planCallingPlanById, + Plan planCallingPlanByName, CompositeFunction compositeFunction) { + assertFirstResponseReference(3, findReferencesResponse, planCallingPlanById, CALLING_PLAN_BY_ID); + FindReferencesResponse findReferencesResponse2 = findReferencesResponse.get(1); + assertEquals(planCallingPlanByName.getId().toString(), findReferencesResponse2.id); + assertEquals(CALLING_PLAN_BY_NAME, findReferencesResponse2.name); + FindReferencesResponse findReferencesResponse3 = findReferencesResponse.get(2); + assertEquals(compositeFunction.getId().toString(), findReferencesResponse3.id); + assertEquals(COMPOSITE_KEYWORD, findReferencesResponse3.name); + } + + @Test + public void findReferencesForResources() throws IOException { + Resource resource = new Resource(); + resource.setResourceName(CSV_FILE); + resource.setResourceType(RESOURCE_TYPE_DATASOURCE); + context.getResourceManager().saveResource(resource); + + ForEachBlock f = new ForEachBlock(); + CSVDataPool p = new CSVDataPool(); + p.setFile(new DynamicValue(FileResolver.createPathForResourceId(resource.getId().toString()))); + f.setDataSource(p); + f.setDataSourceType("excel"); + + Plan planUsingResource = PlanBuilder.create().startBlock(BaseArtefacts.sequence()).add(f).endBlock().build(); + planUsingResource.addAttribute(AbstractOrganizableObject.NAME, PLAN_USING_RESOURCE); + context.getPlanAccessor().save(planUsingResource); + + List findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.RESOURCE_ID, resource.getId().toString())); + assertFirstResponseReference(1, findReferencesResponse, planUsingResource, PLAN_USING_RESOURCE); + + + findReferencesResponse = + referenceFinder.findReferences(new FindReferencesRequest(FindReferencesRequest.Type.RESOURCE_NAME, CSV_FILE)); + assertFirstResponseReference(1, findReferencesResponse, planUsingResource, PLAN_USING_RESOURCE); + + + } + + private static void assertFirstResponseReference(int expected, List findReferencesResponse, Plan planUsingResource, String planUsingResource1) { + assertEquals(expected, findReferencesResponse.size()); + FindReferencesResponse findReferencesResponse1 = findReferencesResponse.get(0); + assertEquals(planUsingResource.getId().toString(), findReferencesResponse1.id); + assertEquals(planUsingResource1, findReferencesResponse1.name); + } + +} \ No newline at end of file diff --git a/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java b/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java index 236b4550d1..ad6bac8762 100644 --- a/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java +++ b/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java @@ -28,6 +28,12 @@ public class EntityDependencyTreeVisitor { // TODO declare it as non-static to avoid potential leaks private static final Map, BeanInfo> beanInfoCache = new ConcurrentHashMap<>(); + public enum VISIT_MODE { + SINGLE, + RECURSIVE, + RESOLVE_ALL + } + public EntityDependencyTreeVisitor(EntityManager entityManager, ObjectPredicate objectPredicate) { super(); this.entityManager = entityManager; @@ -35,27 +41,27 @@ public EntityDependencyTreeVisitor(EntityManager entityManager, ObjectPredicate } public void visitEntityDependencyTree(String entityName, String entityId, EntityTreeVisitor visitor, - boolean recursive) { - EntityTreeVisitorContext context = new EntityTreeVisitorContext(objectPredicate, recursive, visitor); + VISIT_MODE visitMode) { + EntityTreeVisitorContext context = new EntityTreeVisitorContext(objectPredicate, visitMode, visitor); visitEntity(entityName, entityId, context); } public void visitSingleObject(Object object, EntityTreeVisitor visitor, Set messageCollector) { - EntityTreeVisitorContext context = new EntityTreeVisitorContext(objectPredicate, false, visitor); + EntityTreeVisitorContext context = new EntityTreeVisitorContext(objectPredicate, VISIT_MODE.SINGLE, visitor); resolveEntityDependencies(object, context); } public class EntityTreeVisitorContext { - private final boolean recursive; + private final VISIT_MODE visitMode; private final ObjectPredicate objectPredicate; private final EntityTreeVisitor visitor; private final Map stack = new HashMap<>(); - public EntityTreeVisitorContext(ObjectPredicate objectPredicate, boolean recursive, EntityTreeVisitor visitor) { + public EntityTreeVisitorContext(ObjectPredicate objectPredicate, VISIT_MODE visitMode, EntityTreeVisitor visitor) { super(); this.objectPredicate = objectPredicate; - this.recursive = recursive; + this.visitMode = visitMode; this.visitor = visitor; } @@ -64,11 +70,15 @@ public ObjectPredicate getObjectPredicate() { } public void visitEntity(String entityName, String entityId) { - if (recursive) { + if (VISIT_MODE.RECURSIVE.equals(visitMode)) { EntityDependencyTreeVisitor.this.visitEntity(entityName, entityId, this); } } - + + public void onResolvedEntity(String entityName, String entityId, Object entity) { + visitor.onResolvedEntity(entityName, entityId, entity); + } + public String resolvedEntityId(String entityName, String entityId) { return visitor.onResolvedEntityId(entityName, entityId); } @@ -77,8 +87,8 @@ public EntityTreeVisitor getVisitor() { return visitor; } - public boolean isRecursive() { - return recursive; + public VISIT_MODE getVisitMode() { + return visitMode; } protected Map getStack() { @@ -249,7 +259,7 @@ private String resolveEntityIdAndVisitResolvedEntity(String entityName, Object a } // Visit the resolved entity - if (resolvedEntityId != null && visitorContext.isRecursive()) { + if (resolvedEntityId != null && VISIT_MODE.RECURSIVE.equals(visitorContext.getVisitMode())) { visitEntity(entityName, resolvedEntityId, visitorContext); } diff --git a/step-core/src/main/java/step/core/entities/EntityManager.java b/step-core/src/main/java/step/core/entities/EntityManager.java index c6c98933a5..d3a01d559a 100644 --- a/step-core/src/main/java/step/core/entities/EntityManager.java +++ b/step-core/src/main/java/step/core/entities/EntityManager.java @@ -98,6 +98,7 @@ public void getEntitiesReferences(String entityType, ObjectPredicate objectPredi */ public void getEntitiesReferences(String entityName, String entityId, ObjectPredicate objectPredicate, EntityReferencesMap references, boolean recursive) { EntityDependencyTreeVisitor entityDependencyTreeVisitor = new EntityDependencyTreeVisitor(this, objectPredicate); + EntityDependencyTreeVisitor.VISIT_MODE visitMode = recursive ? EntityDependencyTreeVisitor.VISIT_MODE.RECURSIVE : EntityDependencyTreeVisitor.VISIT_MODE.SINGLE; entityDependencyTreeVisitor.visitEntityDependencyTree(entityName, entityId, new EntityTreeVisitor() { @Override @@ -114,7 +115,7 @@ public void onResolvedEntity(String entityName, String entityId, Object entity) public String onResolvedEntityId(String entityName, String resolvedEntityId) { return null; } - }, recursive); + }, visitMode); } public void updateReferences(Object entity, Map references, ObjectPredicate objectPredicate, Set messageCollector) { diff --git a/step-core/src/test/java/step/core/entities/EntityDependencyTreeVisitorTest.java b/step-core/src/test/java/step/core/entities/EntityDependencyTreeVisitorTest.java index df664803bc..96798523fa 100644 --- a/step-core/src/test/java/step/core/entities/EntityDependencyTreeVisitorTest.java +++ b/step-core/src/test/java/step/core/entities/EntityDependencyTreeVisitorTest.java @@ -73,7 +73,7 @@ public void onResolvedEntity(String entityName, String entityId, Object entity) public String onResolvedEntityId(String entityName, String resolvedEntityId) { return null; } - }, true); + }, EntityDependencyTreeVisitor.VISIT_MODE.RECURSIVE); assertEquals(9, entityIds.size()); diff --git a/step-functions/step-functions-package/src/main/java/step/functions/packages/FunctionPackageEntity.java b/step-functions/step-functions-package/src/main/java/step/functions/packages/FunctionPackageEntity.java index 8bdc174fef..6dd1de9b4f 100644 --- a/step-functions/step-functions-package/src/main/java/step/functions/packages/FunctionPackageEntity.java +++ b/step-functions/step-functions-package/src/main/java/step/functions/packages/FunctionPackageEntity.java @@ -1,10 +1,7 @@ package step.functions.packages; -import step.core.entities.Entity; +import step.core.entities.*; import step.core.entities.EntityDependencyTreeVisitor.EntityTreeVisitorContext; -import step.core.entities.EntityManager; -import step.core.entities.DependencyTreeVisitorHook; -import step.core.entities.EntityManagerSupplier; import step.functions.Function; public class FunctionPackageEntity extends Entity { @@ -28,7 +25,7 @@ public void onVisitEntity(Object o, EntityTreeVisitorContext context) { Function f = (Function) o; String id = (String) f.getCustomField(FUNCTION_PACKAGE_ID); if (id != null) { - if(context.isRecursive()) { + if(EntityDependencyTreeVisitor.VISIT_MODE.RECURSIVE.equals(context.getVisitMode())) { context.visitEntity(entityName, id); } diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java index bb209d1717..1a5483e074 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java @@ -45,56 +45,91 @@ public FunctionLocator(FunctionAccessor functionAccessor, SelectorHelper selecto /** * Resolve a {@link CallFunction} artefact to the underlying {@link Function} * - * @param callFunctionArtefact the CallFunction artefact + * @param callFunctionArtefact the CallFunction artifact to be resolved * @param objectPredicate the predicate to be used to filter the results out * @param bindings the bindings to be used for the evaluation of dynamic expressions (can be null) - * @return the {@link Function} referenced by this artefact + * @return the {@link Function} referenced by this artifact */ public Function getFunction(CallFunction callFunctionArtefact, ObjectPredicate objectPredicate, Map bindings) { + return selectAllFunctionsByPriority(callFunctionArtefact, objectPredicate, bindings, true).get(0); + } + + /** + * Resolve a {@link CallFunction} artefact to the underlying list of matching {@link Function} + * @param callFunctionArtefact the {@link CallFunction} artifact to resolve + * @param objectPredicate to filter out results + * @param bindings to be used for evaluation of selection criteria + * @param strictMode whether selection is strict and must find a result or we can ignore unresolvable dynamic selection criteria and bypass activation expression + * @return the list of resolved Keywords, can be empty when strictMode is false + */ + public List selectAllFunctionsByPriority(CallFunction callFunctionArtefact, ObjectPredicate objectPredicate, Map bindings, boolean strictMode) { Objects.requireNonNull(callFunctionArtefact, "The artefact must not be null"); Objects.requireNonNull(objectPredicate, "The object predicate must not be null"); - Function function; + String selectionAttributesJson = callFunctionArtefact.getFunction().get(); Map attributes; try { attributes = selectorHelper.buildSelectionAttributesMap(selectionAttributesJson, bindings); } catch (Exception e) { - throw new NoSuchElementException("Unable to find keyword with attributes "+selectionAttributesJson + ". Cause: " + e.getMessage()); + //We only throw exception for missing bindings when strictMode is ON + if (strictMode) { + throw new NoSuchElementException("Unable to find keyword with attributes " + selectionAttributesJson + ". Cause: " + e.getMessage()); + } else { + return List.of(); + } } - if(attributes.size()>0) { - + if (!attributes.isEmpty()) { Stream stream = StreamSupport.stream(functionAccessor.findManyByAttributes(attributes), false); stream = stream.filter(objectPredicate); List functionsMatchingByAttributes = stream.collect(Collectors.toList()); // reorder matching functions: the function from current AP has a priority - List orderedFunctions = LocatorHelper.prioritizeAndFilterApEntities(functionsMatchingByAttributes, bindings); + List orderedFunctions = LocatorHelper.prioritizeAndFilterApEntities(functionsMatchingByAttributes, bindings, !strictMode); + // In strict mode at least one match is required + if (strictMode && orderedFunctions.isEmpty()) { + throw new NoSuchElementException("Unable to find keyword with attributes " + selectionAttributesJson); + } - // after prioritization, we check the chosen active keyword version + // after prioritization, we either select only the one matching the active version whenever provided or returned all matching ones Set activeKeywordVersions = getActiveKeywordVersions(bindings); - if (activeKeywordVersions != null && activeKeywordVersions.size() > 0) { - // First try to find a function matching one of the active versions - function = orderedFunctions.stream().filter(f -> { + if (activeKeywordVersions != null && !activeKeywordVersions.isEmpty()) { + // First try to find the functions matching one of the active versions + List activeVersions = orderedFunctions.stream().filter(f -> { String version = f.getAttributes().get(AbstractOrganizableObject.VERSION); return version != null && activeKeywordVersions.contains(version); - }).findFirst().orElse(null); - // if no function has been found with one of the active versions, return the first function WITHOUT version - if (function == null) { - function = orderedFunctions.stream().filter(f -> { + }).collect(Collectors.toList()); + // if no function has been found with one of the active versions, return the functions WITHOUT versions + if (activeVersions.isEmpty()) { + activeVersions = orderedFunctions.stream().filter(f -> { String version = f.getAttributes().get(AbstractOrganizableObject.VERSION); return version == null || version.trim().isEmpty(); - }).findFirst().orElseThrow(() -> new NoSuchElementException("Unable to find keyword with attributes " + selectionAttributesJson + " matching on of the versions: " + activeKeywordVersions)); + }).collect(Collectors.toList()); + } + if (activeVersions.isEmpty() && strictMode) { + throw new NoSuchElementException("Unable to find keyword with attributes " + selectionAttributesJson + " matching on of the versions: " + activeKeywordVersions); + } else { + return activeVersions; } } else { - // No active versions defined. Return the first function - function = orderedFunctions.stream().findFirst().orElseThrow(() -> new NoSuchElementException("Unable to find keyword with attributes " + selectionAttributesJson)); + //No version defined with simply return the ordered function by priorities + return orderedFunctions; } - return function; } else { throw new NoSuchElementException("No selection attribute defined"); } + } + /** + * Resolve a {@link CallFunction} artefact to the underlying list of matching {@link Function} + * + * @param callFunctionArtefact the CallFunction artifact + * @param objectPredicate the predicate to be used to filter the results out + * @param bindings the bindings to be used for the evaluation of dynamic expressions (can be null) + * @return the {@link Function} list referenced by this artifact + */ + public List getMatchingFunctions(CallFunction callFunctionArtefact, ObjectPredicate objectPredicate, Map bindings) { + return selectAllFunctionsByPriority(callFunctionArtefact, objectPredicate, bindings, false); } private Set getActiveKeywordVersions(Map bindings) { diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/LocatorHelper.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/LocatorHelper.java index 973301eed1..24484628eb 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/LocatorHelper.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/LocatorHelper.java @@ -17,7 +17,7 @@ public class LocatorHelper { /** * Reorders and filters entities according to the current automation package and activation expression */ - public static List prioritizeAndFilterApEntities(List entities, Map bindings) { + public static List prioritizeAndFilterApEntities(List entities, Map bindings, boolean bypassActivation) { // reorder entities: entities from current AP have a priority List entitiesFromSameAP = new ArrayList<>(); List entitiesActivatedExplicitly = new ArrayList<>(); @@ -34,7 +34,7 @@ public static List prioritizeAndFilterA break; } } - if (evaluationExpressionIsDefined(entity)) { + if (!bypassActivation && evaluationExpressionIsDefined(entity)) { if (isActivated(bindings, entity)) { entitiesActivatedExplicitly.add(entity); } diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/PlanLocator.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/PlanLocator.java index 71165ae51b..49ffa00937 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/PlanLocator.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/PlanLocator.java @@ -44,7 +44,7 @@ public PlanLocator(PlanAccessor accessor, SelectorHelper selectorHelper) { } /** - * Resolve a {@link CallPlan} artefact to the underlying {@link Plan}. Returns null if plan is not resolved by ID + * Resolve a {@link CallPlan} artefact to the underlying {@link Plan}. If multiple plans are resolved, the first one (ordered by priority) is returned) * * @param artefact the {@link CallPlan} artefact * @param objectPredicate the predicate to be used to filter the results out @@ -52,24 +52,69 @@ public PlanLocator(PlanAccessor accessor, SelectorHelper selectorHelper) { * @return the {@link Plan} referenced by the provided artefact */ public Plan selectPlan(CallPlan artefact, ObjectPredicate objectPredicate, Map bindings) { + return selectAlPlansByAttributesAndPriority(artefact, objectPredicate, bindings, true).get(0); + } + + /** + * Resolve a {@link CallPlan} artefact to the list of underlying matching {@link Plan}. + * @param artefact the {@link CallPlan} artefact + * @param objectPredicate the predicate to be used to filter the results out + * @param bindings the bindings to be used for the evaluation of dynamic expressions (can be null) + * @param strictMode whether selection is strict and must find a result or we can ignore unresolvable dynamic selection criteria and bypass activation expression + * @return the list of resolved Plan, can be empty when strictMode is false + */ + public List selectAlPlansByAttributesAndPriority(CallPlan artefact, ObjectPredicate objectPredicate, Map bindings, boolean strictMode) { Objects.requireNonNull(artefact, "The artefact must not be null"); Objects.requireNonNull(objectPredicate, "The object predicate must not be null"); - Plan a; + // Handle CallPlan with plan ID reference if(artefact.getPlanId()!=null) { - a = Optional.ofNullable(accessor.get(artefact.getPlanId())).orElseThrow(() -> new NoSuchElementException("Unable to find plan with id: " + artefact.getPlanId())); + Plan plan = accessor.get(artefact.getPlanId()); + if (plan != null && objectPredicate.test(plan)) { + return List.of(plan); + } else if (strictMode) { + throw new NoSuchElementException("Unable to find plan with id: " + artefact.getPlanId()); + } else { + return List.of(); + } } else { - Map selectionAttributes = selectorHelper.buildSelectionAttributesMap(artefact.getSelectionAttributes().get(), bindings); + // Handle Call Plan with call by attributes + String selectionAttributesJson = artefact.getSelectionAttributes().get(); + Map selectionAttributes; + try { + selectionAttributes = selectorHelper.buildSelectionAttributesMap(selectionAttributesJson, bindings); + } catch (Exception e) { + //In case bindings are missing, we only throw an exception in strict Mode (used in execution context) + if (strictMode) { + throw e; + } else { + return List.of(); + } + } Stream stream = StreamSupport.stream(accessor.findManyByAttributes(selectionAttributes), false); stream = stream.filter(objectPredicate); List matchingPlans = stream.collect(Collectors.toList()); // The same logic as for functions - plans from current automation package have priority in 'CallPlan' // We use prioritization by current automation package and filtering by activation expressions - List orderedPlans = LocatorHelper.prioritizeAndFilterApEntities(matchingPlans, bindings); - a = orderedPlans.stream().findFirst().orElseThrow(()->new NoSuchElementException("Unable to find plan with attributes: "+selectionAttributes.toString())); + List orderedPlans = LocatorHelper.prioritizeAndFilterApEntities(matchingPlans, bindings, !strictMode); + if (strictMode && orderedPlans.isEmpty()) { + throw new NoSuchElementException("Unable to find plan with attributes: "+ selectionAttributesJson); + } + return orderedPlans; } - return a; + } + + /** + * Resolve a {@link CallPlan} artefact to the list of underlying matching {@link Plan}. + * + * @param artefact the {@link CallPlan} artefact + * @param objectPredicate the predicate to be used to filter the results out + * @param bindings the bindings to be used for the evaluation of dynamic expressions (can be null) + * @return the list of {@link Plan} referenced by this artifact + */ + public List getMatchingPlans(CallPlan artefact, ObjectPredicate objectPredicate, Map bindings) { + return selectAlPlansByAttributesAndPriority(artefact, objectPredicate, bindings, false); } /** diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/core/plans/PlanEntity.java b/step-plans/step-plans-base-artefacts/src/main/java/step/core/plans/PlanEntity.java index a72d62db68..73e8ddb9d1 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/core/plans/PlanEntity.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/core/plans/PlanEntity.java @@ -12,13 +12,25 @@ public class PlanEntity extends Entity> { public PlanEntity(Accessor accessor, PlanLocator planLocator, EntityManager entityManager) { super(EntityConstants.plans, accessor, Plan.class); entityManager.addDependencyTreeVisitorHook((entity, context) -> { - //This is only required to recursively visit the plans referenced by callPlan artefacts - if (entity instanceof CallPlan && context.isRecursive()) { - try { - Plan plan = planLocator.selectPlanNotNull((CallPlan) entity, context.getObjectPredicate(), null); - context.visitEntity(EntityConstants.plans, plan.getId().toString()); - } catch (PlanLocator.PlanLocatorException ex) { - context.getVisitor().onWarning(ex.getMessage()); + // Only apply the logic for CallPlan artifacts + if (entity instanceof CallPlan) { + CallPlan callPlan = (CallPlan) entity; + switch (context.getVisitMode()) { + case RECURSIVE: + // In recursive mode, we recursively visit the resolved plan. If multiple plans match, the one with the highest priority is chosen + try { + Plan plan = planLocator.selectPlanNotNull(callPlan, context.getObjectPredicate(), null); + context.visitEntity(EntityConstants.plans, plan.getId().toString()); + } catch (PlanLocator.PlanLocatorException ex) { + context.getVisitor().onWarning(ex.getMessage()); + } + break; + case RESOLVE_ALL: + // In resolve ALL mode, we resolve all matching plans but do not visit recursively + planLocator.getMatchingPlans(callPlan, context.getObjectPredicate(), null).forEach(p -> { + context.onResolvedEntity(EntityConstants.plans, p.getId().toHexString(), p); + }); + break; } } }); diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/functions/accessor/FunctionEntity.java b/step-plans/step-plans-base-artefacts/src/main/java/step/functions/accessor/FunctionEntity.java index 646e152043..d1cb4996e8 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/functions/accessor/FunctionEntity.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/functions/accessor/FunctionEntity.java @@ -20,14 +20,26 @@ public class FunctionEntity extends Entity> { public FunctionEntity(Accessor accessor, FunctionLocator functionLocator, EntityManager entityManager) { super(EntityConstants.functions, accessor, Function.class); entityManager.addDependencyTreeVisitorHook((t, context) -> { - //This is only required to recursively visit the function referenced by callFunction artefacts - if (t instanceof CallFunction && context.isRecursive()) { - try { - Function function = functionLocator.getFunction((CallFunction) t, context.getObjectPredicate(), - null); - context.visitEntity(EntityConstants.functions, function.getId().toString()); - } catch (NoSuchElementException e) { - context.getVisitor().onWarning("The keyword referenced by the call keyword artefact '" + ((CallFunction) t).getAttribute(AbstractOrganizableObject.NAME) + "' could not be found"); + //Only apply logic is the entity is a CallFunction + if (t instanceof CallFunction) { + CallFunction callFunction = (CallFunction) t; + switch (context.getVisitMode()) { + case RECURSIVE: + //In recursive mode we visit the resolved entity recursively (the highest priority is chosen if multiple entity matches) + try { + Function function = functionLocator.getFunction(callFunction, context.getObjectPredicate(), + null); + context.visitEntity(EntityConstants.functions, function.getId().toString()); + } catch (NoSuchElementException e) { + context.getVisitor().onWarning("The keyword referenced by the call keyword artefact '" + (callFunction).getAttribute(AbstractOrganizableObject.NAME) + "' could not be found"); + } + break; + case RESOLVE_ALL: + //In resolve All mode we resolve all matching entities but do not visit recursively + functionLocator.getMatchingFunctions(callFunction, context.getObjectPredicate(), null).forEach(f -> { + context.onResolvedEntity(EntityConstants.functions, f.getId().toHexString(), f); + }); + break; } } }); diff --git a/step-plans/step-plans-core/src/main/java/step/core/scheduler/ScheduleEntity.java b/step-plans/step-plans-core/src/main/java/step/core/scheduler/ScheduleEntity.java index 8afcf62cc1..fb78a5f81c 100644 --- a/step-plans/step-plans-core/src/main/java/step/core/scheduler/ScheduleEntity.java +++ b/step-plans/step-plans-core/src/main/java/step/core/scheduler/ScheduleEntity.java @@ -24,7 +24,7 @@ public void onVisitEntity(Object entity, EntityDependencyTreeVisitor.EntityTreeV if (repositoryObject != null && repositoryObject.getRepositoryID() != null && repositoryObject.getRepositoryID().equals(LOCAL_REPOSITORY_ID)) { String localPlanId = repositoryObject.getRepositoryParameters().get(RepositoryObjectReference.PLAN_ID); if (localPlanId != null) { - if (context.isRecursive()) { + if (EntityDependencyTreeVisitor.VISIT_MODE.RECURSIVE.equals(context.getVisitMode())) { context.visitEntity(EntityConstants.plans, localPlanId); } String newEntityId = context.resolvedEntityId(EntityConstants.plans, localPlanId); From 54984471ab80da198dcc087566130fb8a8af5ecd Mon Sep 17 00:00:00 2001 From: David Stephan Date: Thu, 8 Jan 2026 14:45:37 +0100 Subject: [PATCH 2/4] SED-4340 PR feedbacks --- .../java/step/core/references/ReferenceFinder.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java index 52f84fe645..d4ca7e1398 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java @@ -62,9 +62,10 @@ public List findReferences(FindReferencesRequest request } private List getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) { - List referencedObjects = getReferencedObjects(entityType, object).stream().filter(o -> (o != null && !o.equals(object))).collect(Collectors.toList()); - //System.err.println("objects referenced from plan: " + planToString(plan) + ": "+ referencedObjects.stream().map(ReferenceFinderServices::objectToString).collect(Collectors.toList())); - return referencedObjects.stream().filter(o -> doesRequestMatch(request, o)).collect(Collectors.toList()); + return getReferencedObjects(entityType, object).stream() + .filter(o -> (o != null && !o.equals(object))) + .filter(o -> doesRequestMatch(request, o)) + .collect(Collectors.toList()); } // returns a (generic) set of objects referenced by a plan @@ -91,7 +92,7 @@ private boolean doesRequestMatch(FindReferencesRequest req, Object o) { Plan p = (Plan) o; switch (req.searchType) { case PLAN_NAME: - return p.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + return req.searchValue.equals(p.getAttribute(AbstractOrganizableObject.NAME)); case PLAN_ID: return p.getId().toString().equals(req.searchValue); default: @@ -101,7 +102,7 @@ private boolean doesRequestMatch(FindReferencesRequest req, Object o) { Function f = (Function) o; switch (req.searchType) { case KEYWORD_NAME: - return f.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + return req.searchValue.equals(f.getAttribute(AbstractOrganizableObject.NAME)); case KEYWORD_ID: return f.getId().toString().equals(req.searchValue); default: @@ -111,7 +112,7 @@ private boolean doesRequestMatch(FindReferencesRequest req, Object o) { Resource r = (Resource) o; switch (req.searchType) { case RESOURCE_NAME: - return r.getAttribute(AbstractOrganizableObject.NAME).equals(req.searchValue); + return req.searchValue.equals(r.getAttribute(AbstractOrganizableObject.NAME)); case RESOURCE_ID: return r.getId().toString().equals(req.searchValue); default: From a431643e726cfd1d1f9af0cdb6a1e9507f5a067b Mon Sep 17 00:00:00 2001 From: David Stephan Date: Fri, 9 Jan 2026 10:50:21 +0100 Subject: [PATCH 3/4] SED-4340 PR feedbacks --- .../main/java/step/core/references/ReferenceFinder.java | 6 +++--- .../step/core/entities/EntityDependencyTreeVisitor.java | 9 +++++++++ .../java/step/artefacts/handlers/FunctionLocator.java | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java index d4ca7e1398..d39c5fd6ab 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java @@ -72,9 +72,9 @@ private List getReferencedObjectsMatchingRequest(String entityType, Abst private Set getReferencedObjects(String entityType, AbstractOrganizableObject object) { Set referencedObjects = new HashSet<>(); - // The references can be filled in three different ways due to the implementation: - // 1. through the predicate (just below) - // 2. by (actual object) reference in the tree visitor (onResolvedEntity) + // The references can be filled in two different ways due to the implementation: + // 1. by (actual object) reference in the tree visitor (onResolvedEntity) + // 2. by object ID in the tree visitor (onResolvedEntityId) // No context predicate is used by the reference finder, since we want to find all entities (i.e. if we search the usages of a Keyword from the Common project, we should be able // to find plans using it in other projects. diff --git a/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java b/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java index ad6bac8762..29db0dc1cf 100644 --- a/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java +++ b/step-core/src/main/java/step/core/entities/EntityDependencyTreeVisitor.java @@ -29,8 +29,17 @@ public class EntityDependencyTreeVisitor { private static final Map, BeanInfo> beanInfoCache = new ConcurrentHashMap<>(); public enum VISIT_MODE { + /** + * Visit one entity resolving its references. One reference is resolved to at most one entity. Resolved entities are NOT visited recursively. + */ SINGLE, + /** + * Visit one entity resolving its references. One reference is resolved to at most one entity. Resolved entities are visited recursively. + */ RECURSIVE, + /** + * Visit one entity resolving its references. One reference can be resolved to multiple entities. Resolved entities are NOT visited recursively. + */ RESOLVE_ALL } diff --git a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java index 1a5483e074..eae8c43ca7 100644 --- a/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java +++ b/step-plans/step-plans-base-artefacts/src/main/java/step/artefacts/handlers/FunctionLocator.java @@ -112,7 +112,7 @@ public List selectAllFunctionsByPriority(CallFunction callFunctionArte return activeVersions; } } else { - //No version defined with simply return the ordered function by priorities + //No active version provided, we simply return the ordered function by priorities return orderedFunctions; } } else { From e0c4c97fffed6353c957f9658165f7cf120bd33a Mon Sep 17 00:00:00 2001 From: David Stephan Date: Fri, 9 Jan 2026 11:31:40 +0100 Subject: [PATCH 4/4] SED-4340 fixing incorrect context and predicate usage --- .../step/core/references/ReferenceFinder.java | 49 +++++++++++++------ .../references/ReferenceFinderServices.java | 15 +----- .../core/references/ReferenceFinderTest.java | 3 +- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java index d39c5fd6ab..2e0c19bccd 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinder.java @@ -1,9 +1,15 @@ package step.core.references; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import step.core.AbstractContext; import step.core.accessors.AbstractOrganizableObject; import step.core.entities.EntityConstants; import step.core.entities.EntityDependencyTreeVisitor; import step.core.entities.EntityManager; +import step.core.objectenricher.EnricheableObject; +import step.core.objectenricher.ObjectHookRegistry; +import step.core.objectenricher.ObjectPredicate; import step.core.plans.Plan; import step.core.plans.PlanAccessor; import step.functions.Function; @@ -16,10 +22,14 @@ public class ReferenceFinder { + private static final Logger logger = LoggerFactory.getLogger(ReferenceFinder.class); + private final EntityManager entityManager; + private final ObjectHookRegistry objectHookRegistry; - public ReferenceFinder(EntityManager entityManager) { + public ReferenceFinder(EntityManager entityManager, ObjectHookRegistry objectHookRegistry) { this.entityManager = entityManager; + this.objectHookRegistry = objectHookRegistry; } public List findReferences(FindReferencesRequest request) { @@ -39,9 +49,13 @@ public List findReferences(FindReferencesRequest request try (Stream functionStream = functionAccessor.streamLazy()) { functionStream.forEach(function -> { - List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.functions, function, request); - if (!matchingObjects.isEmpty()) { - results.add(new FindReferencesResponse(function)); + try { + List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.functions, function, request); + if (!matchingObjects.isEmpty()) { + results.add(new FindReferencesResponse(function)); + } + } catch (Exception e) { + logger.error("Unable to find references for function {}", function.getId(), e); } }); } @@ -49,9 +63,13 @@ public List findReferences(FindReferencesRequest request // Find plans containing usages try (Stream stream = (request.includeHiddenPlans) ? planAccessor.streamLazy() : planAccessor.getVisiblePlans()) { stream.forEach(plan -> { - List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.plans, plan, request); - if (!matchingObjects.isEmpty()) { - results.add(new FindReferencesResponse(plan)); + try { + List matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.plans, plan, request); + if (!matchingObjects.isEmpty()) { + results.add(new FindReferencesResponse(plan)); + } + } catch (Exception e) { + logger.error("Unable to find references for plan {}", plan.getId(), e); } }); } @@ -61,7 +79,7 @@ public List findReferences(FindReferencesRequest request return results; } - private List getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) { + private List getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) throws Exception { return getReferencedObjects(entityType, object).stream() .filter(o -> (o != null && !o.equals(object))) .filter(o -> doesRequestMatch(request, o)) @@ -69,18 +87,21 @@ private List getReferencedObjectsMatchingRequest(String entityType, Abst } // returns a (generic) set of objects referenced by a plan - private Set getReferencedObjects(String entityType, AbstractOrganizableObject object) { + private Set getReferencedObjects(String entityType, AbstractOrganizableObject object) throws Exception { Set referencedObjects = new HashSet<>(); // The references can be filled in two different ways due to the implementation: // 1. by (actual object) reference in the tree visitor (onResolvedEntity) // 2. by object ID in the tree visitor (onResolvedEntityId) - // No context predicate is used by the reference finder, since we want to find all entities (i.e. if we search the usages of a Keyword from the Common project, we should be able - // to find plans using it in other projects. - // This unfortunately can return incorrect results, i.e. a keyword "MyKeyword" is created in ProjectA and ProjectB, A PlanA is created in ProjectA and is using the KW of the same project. - // Searching usage of "MyKeyword" in projectB will return the planA from projectA - EntityDependencyTreeVisitor entityDependencyTreeVisitor = new EntityDependencyTreeVisitor(entityManager, o -> true); + // When searching the references of a give entity we must apply the predicate as if we were in the context of this entity + ObjectPredicate predicate = o -> true; //default value for non enricheable objects + if (object instanceof EnricheableObject) { + AbstractContext context = new AbstractContext() {}; + objectHookRegistry.rebuildContext(context, (EnricheableObject) object); + predicate = objectHookRegistry.getObjectPredicate(context); + } + EntityDependencyTreeVisitor entityDependencyTreeVisitor = new EntityDependencyTreeVisitor(entityManager, predicate); FindReferencesTreeVisitor entityTreeVisitor = new FindReferencesTreeVisitor(entityManager, referencedObjects); entityDependencyTreeVisitor.visitEntityDependencyTree(entityType, object.getId().toString(), entityTreeVisitor, EntityDependencyTreeVisitor.VISIT_MODE.RESOLVE_ALL); diff --git a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java index b6110c07c9..8ad874cbe0 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/references/ReferenceFinderServices.java @@ -1,18 +1,9 @@ package step.core.references; import io.swagger.v3.oas.annotations.tags.Tag; -import step.core.accessors.AbstractOrganizableObject; import step.core.deployment.AbstractStepServices; -import step.core.entities.EntityConstants; +import step.core.objectenricher.ObjectHookRegistry; import step.framework.server.security.Secured; -import step.core.entities.EntityDependencyTreeVisitor; -import step.core.entities.EntityManager; -import step.core.objectenricher.ObjectPredicate; -import step.core.plans.Plan; -import step.core.plans.PlanAccessor; -import step.functions.Function; -import step.functions.accessor.FunctionAccessor; -import step.resources.Resource; import jakarta.annotation.PostConstruct; import jakarta.inject.Singleton; @@ -20,8 +11,6 @@ import jakarta.ws.rs.core.MediaType; import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; @Singleton @Path("references") @@ -33,7 +22,7 @@ public class ReferenceFinderServices extends AbstractStepServices { @PostConstruct public void init() throws Exception { super.init(); - referenceFinder = new ReferenceFinder(getContext().getEntityManager()); + referenceFinder = new ReferenceFinder(getContext().getEntityManager(), getContext().require(ObjectHookRegistry.class)); } @Path("/findReferences") diff --git a/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java b/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java index 5f2e5fcfa1..ca1ae42dff 100644 --- a/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java +++ b/step-controller/step-controller-server/src/test/java/step/core/references/ReferenceFinderTest.java @@ -9,6 +9,7 @@ import step.core.GlobalContext; import step.core.accessors.AbstractOrganizableObject; import step.core.dynamicbeans.DynamicValue; +import step.core.objectenricher.ObjectHookRegistry; import step.core.plans.Plan; import step.core.plans.builder.PlanBuilder; import step.core.plugins.PluginManager; @@ -43,7 +44,7 @@ public class ReferenceFinderTest { @Before public void setup() throws ClassNotFoundException, PluginManager.Builder.CircularDependencyException, InstantiationException, IllegalAccessException { context = createGlobalContext(); - referenceFinder = new ReferenceFinder(context.getEntityManager()); + referenceFinder = new ReferenceFinder(context.getEntityManager(), new ObjectHookRegistry()); functionAccessor = context.require(FunctionAccessor.class); }