Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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;
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 static final Logger logger = LoggerFactory.getLogger(ReferenceFinder.class);

private final EntityManager entityManager;
private final ObjectHookRegistry objectHookRegistry;

public ReferenceFinder(EntityManager entityManager, ObjectHookRegistry objectHookRegistry) {
this.entityManager = entityManager;
this.objectHookRegistry = objectHookRegistry;
}

public List<FindReferencesResponse> 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<FindReferencesResponse> 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<Function> functionStream = functionAccessor.streamLazy()) {
functionStream.forEach(function -> {
try {
List<Object> 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHouldn't we rethrow this or add the error to the response?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how relevant that is. The implementation will browse all plans and all composite keywords to see if they use the searched reference, if an exception is raised because the context could not be rebuilt for one of them, I don't think this should break the operation, adding it as a warning would be doable (with quite some effort) but I also don't think this warning would be relevant for the user doing the request.

}
});
}

// Find plans containing usages
try (Stream<Plan> stream = (request.includeHiddenPlans) ? planAccessor.streamLazy() : planAccessor.getVisiblePlans()) {
stream.forEach(plan -> {
try {
List<Object> 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem

}
});
}

// Sort the results by name
results.sort(Comparator.comparing(f -> f.name));
return results;
}

private List<Object> 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))
.collect(Collectors.toList());
}

// returns a (generic) set of objects referenced by a plan
private Set<Object> getReferencedObjects(String entityType, AbstractOrganizableObject object) throws Exception {
Set<Object> 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)

// 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);

return referencedObjects;
}

private boolean doesRequestMatch(FindReferencesRequest req, Object o) {
if (o instanceof Plan) {
Plan p = (Plan) o;
switch (req.searchType) {
case PLAN_NAME:
return req.searchValue.equals(p.getAttribute(AbstractOrganizableObject.NAME));
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 req.searchValue.equals(f.getAttribute(AbstractOrganizableObject.NAME));
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 req.searchValue.equals(r.getAttribute(AbstractOrganizableObject.NAME));
case RESOURCE_ID:
return r.getId().toString().equals(req.searchValue);
default:
return false;
}
} else {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,182 +1,36 @@
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;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Singleton
@Path("references")
@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(), getContext().require(ObjectHookRegistry.class));
}


// 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<FindReferencesResponse> findReferencesTest() {
List<FindReferencesResponse> 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<FindReferencesResponse> 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<FindReferencesResponse> 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<Function> functionStream = functionAccessor.streamLazy()) {
functionStream.forEach(function -> {
List<Object> matchingObjects = getReferencedObjectsMatchingRequest(EntityConstants.functions, function, request);
if (!matchingObjects.isEmpty()) {
results.add(new FindReferencesResponse(function));
}
});
}

// Find plans containing usages
try (Stream<Plan> stream = (request.includeHiddenPlans) ? planAccessor.streamLazy() : planAccessor.getVisiblePlans()) {
stream.forEach(plan -> {
List<Object> 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<Object> getReferencedObjectsMatchingRequest(String entityType, AbstractOrganizableObject object, FindReferencesRequest request) {
List<Object> 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<Object> getReferencedObjects(String entityType, AbstractOrganizableObject object) {
Set<Object> 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();
}


}
Loading