Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4587 add incident id in filter to historic process instance query #4590

Merged
Merged
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
Expand Up @@ -123,6 +123,12 @@
"desc": "Only include process instances which have a root incident. Value may only be `true`, as `false` is the default behavior."
},

"incidentIdIn": {
"type": "array",
"itemType": "string",
"desc": "Restrict to instances that have an incident with one of the given ids. ${listTypeDescription}"
},

"incidentType": {
"type": "string",
"desc": "Filter by the incident type. See the [User Guide](${docsUrl}/user-guide/process-engine/incidents/#incident-types) for a list of incident types."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public class HistoricProcessInstanceQueryDto extends AbstractQueryDto<HistoricPr
private Boolean completed;
private Boolean externallyTerminated;
private Boolean internallyTerminated;
private List<String> incidentIds;

private List<VariableQueryParameterDto> variables;

Expand Down Expand Up @@ -306,6 +307,11 @@ public void setIncidentType(String incidentType) {
this.incidentType = incidentType;
}

@CamundaQueryParam(value = "incidentIdIn", converter = StringListConverter.class)
public void setIncidentIdIn(List<String> incidentIds) {
this.incidentIds = incidentIds;
}

@CamundaQueryParam(value = "tenantIdIn", converter = StringListConverter.class)
public void setTenantIdIn(List<String> tenantIds) {
this.tenantIds = tenantIds;
Expand Down Expand Up @@ -443,6 +449,9 @@ protected void applyFilters(HistoricProcessInstanceQuery query) {
if (withRootIncidents != null && withRootIncidents) {
query.withRootIncidents();
}
if (incidentIds != null && !incidentIds.isEmpty()) {
query.incidentIdIn(incidentIds.toArray(new String[0]));
}
if (incidentStatus != null) {
query.incidentStatus(incidentStatus);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class HistoricProcessInstanceRestServiceQueryTest extends AbstractRestSer
protected static final String QUERY_PARAM_EXECUTED_ACTIVITY_AFTER = "executedActivityAfter";
protected static final String QUERY_PARAM_EXECUTED_ACTIVITY_IDS = "executedActivityIdIn";
protected static final String QUERY_PARAM_ACTIVE_ACTIVITY_IDS = "activeActivityIdIn";
protected static final String QUERY_PARAM_INCIDENT_IDS = "incidentIdIn";

@ClassRule
public static TestContainerRule rule = new TestContainerRule();
Expand Down Expand Up @@ -2317,5 +2318,32 @@ public void testQueryByRootProcessInstancesAsPost() {

verify(mockedQuery).rootProcessInstances();
}
@Test
public void testQueryByIncidentIdIn() {
given()
.queryParam(QUERY_PARAM_INCIDENT_IDS, "1,2")
.then().expect()
.statusCode(Status.OK.getStatusCode())
.when()
.get(HISTORIC_PROCESS_INSTANCE_RESOURCE_URL);

verify(mockedQuery).incidentIdIn("1", "2");
}

@Test
public void testQueryByIncidentIdInAsPost() {
Map<String, List<String>> parameters = new HashMap<String, List<String>>();
parameters.put(QUERY_PARAM_INCIDENT_IDS, Arrays.asList("1", "2"));

given()
.contentType(POST_JSON_CONTENT_TYPE)
.body(parameters)
.then().expect()
.statusCode(Status.OK.getStatusCode())
.when()
.post(HISTORIC_PROCESS_INSTANCE_RESOURCE_URL);

verify(mockedQuery).incidentIdIn("1", "2");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ public interface HistoricProcessInstanceQuery extends Query<HistoricProcessInsta
*/
HistoricProcessInstanceQuery withRootIncidents();

/**
* Only select historic process instances that have incidents with given ids.
*/
HistoricProcessInstanceQuery incidentIdIn(String... incidentIds);

/**
* Only select historic process instances with incident status either 'open' or 'resolved'.
* To get all process instances with incidents, use {@link HistoricProcessInstanceQuery#withIncidents()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class HistoricProcessInstanceQueryImpl extends AbstractVariableQueryImpl<
protected String[] executedActivityIds;
protected String[] activeActivityIds;
protected String state;
protected String[] incidentIds;

protected String caseInstanceId;

Expand Down Expand Up @@ -179,6 +180,12 @@ public HistoricProcessInstanceQuery withRootIncidents() {
this.withRootIncidents = true;
return this;
}

public HistoricProcessInstanceQuery incidentIdIn(String... incidentIds) {
ensureNotNull("incidentIds", (Object[]) incidentIds);
this.incidentIds = incidentIds;
return this;
}

public HistoricProcessInstanceQuery incidentType(String incidentType) {
ensureNotNull("incident type", incidentType);
Expand Down Expand Up @@ -730,6 +737,10 @@ public String[] getTenantIds() {
return tenantIds;
}

public String[] getIncidentIds() {
return incidentIds;
}

@Override
public HistoricProcessInstanceQuery executedActivityAfter(Date date) {
this.executedActivityAfter = date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@
<bind name="JOIN_TYPE" value="'left join'" />
</if>

<if test="query != null &amp;&amp; (query.withIncidents || query.withRootIncidents || query.incidentStatus != null || query.incidentMessage != null || query.incidentMessageLike != null || query.incidentType != null)">
<if test="query != null &amp;&amp; (query.withIncidents || query.withRootIncidents || query.incidentStatus != null || query.incidentMessage != null || query.incidentMessageLike != null || query.incidentType != null || (incidentIds != null &amp;&amp; incidentIds.length > 0))">
Copy link
Member

Choose a reason for hiding this comment

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

❓ Do we need to extend to docs that incidentIds are excluded for Or queries here: https://docs.camunda.org/manual/develop/user-guide/process-engine/process-engine-api/#or-queries

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

❓ Are they excluded? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can just add a test to verify this.

<bind name="INC_JOIN" value="true" />
</if>

Expand Down Expand Up @@ -553,6 +553,14 @@
<if test="query.incidentType != null">
${queryType} INC.INCIDENT_TYPE_ = #{query.incidentType}
</if>

<if test="query.incidentIds != null &amp;&amp; query.incidentIds.length > 0">
${queryType} INC.ID_ IN
<foreach item="incidentId" index="index" collection="query.incidentIds" open="(" separator="," close=")">
#{incidentId}
</foreach>
</if>

<if test="query.incidentMessage != null">
${queryType} INC.INCIDENT_MSG_ = #{query.incidentMessage}
</if>
Expand Down
Copy link
Member

Choose a reason for hiding this comment

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

✍️ the test case here is 2 incidents we query, we fetch two instances. Test cases that we need to consider covering:

  • query for incidents in instances that don't have incidents => 0
  • mix query for 2 incidents in instances with one incident => 1
  • null input => exception

Copy link
Member Author

@venetrius venetrius Sep 19, 2024

Choose a reason for hiding this comment

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

  • In this test we already have 3 incidents but only 2 of them are queried, it might not be apparent because for the third the returned process instance is ignored.
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess");
    ProcessInstance processInstance2 = runtimeService.startProcessInstanceByKey("failingProcess");
    runtimeService.startProcessInstanceByKey("failingProcess");
  • I added a test for the case when: null input => exception

  • I am adding a test where only some instances have incidents, Work in Progress

Copy link
Member Author

Choose a reason for hiding this comment

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

Added test when not all process instances have incidents.

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.List;
import java.util.Map;

import java.util.stream.Collectors;
import org.apache.commons.lang3.time.DateUtils;
import org.camunda.bpm.engine.BadUserRequestException;
import org.camunda.bpm.engine.CaseService;
Expand All @@ -59,6 +60,7 @@
import org.camunda.bpm.engine.impl.history.event.HistoricProcessInstanceEventEntity;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.camunda.bpm.engine.repository.ProcessDefinition;
import org.camunda.bpm.engine.runtime.Incident;
import org.camunda.bpm.engine.runtime.Job;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
Expand Down Expand Up @@ -1693,6 +1695,100 @@ public void testQueryByMultipleInvalidProcessDefinitionKeyIn() {
}
}

@Test
@RequiredHistoryLevel(ProcessEngineConfiguration.HISTORY_FULL)
@Deployment(resources={"org/camunda/bpm/engine/test/api/runtime/failingProcessCreateOneIncident.bpmn20.xml"})
public void shouldQueryProcessInstancesWithIncidentIdIn() {
// GIVEN
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingProcess");
ProcessInstance processInstance2 = runtimeService.startProcessInstanceByKey("failingProcess");
runtimeService.startProcessInstanceByKey("failingProcess");
List<String> queriedProcessInstances = Arrays.asList(processInstance.getId(), processInstance2.getId());

testHelper.executeAvailableJobs();
Incident incident = runtimeService.createIncidentQuery().processInstanceId(queriedProcessInstances.get(0)).singleResult();
Incident incident2 = runtimeService.createIncidentQuery().processInstanceId(queriedProcessInstances.get(1)).singleResult();

// WHEN
List<HistoricProcessInstance> processInstanceList =
historyService.createHistoricProcessInstanceQuery().incidentIdIn(incident.getId(), incident2.getId()).list();

// THEN
assertEquals(2, processInstanceList.size());
assertThat(queriedProcessInstances)
.containsExactlyInAnyOrderElementsOf(
processInstanceList.stream()
.map(HistoricProcessInstance::getId)
.collect(Collectors.toList()));
}

@Test
@RequiredHistoryLevel(ProcessEngineConfiguration.HISTORY_FULL)
@Deployment(resources={"org/camunda/bpm/engine/test/api/runtime/failingProcessAfterUserTaskCreateOneIncident.bpmn20.xml"})
public void shouldOnlyQueryProcessInstancesWithIncidentIdIn() {
// GIVEN
ProcessInstance processWithIncident1 = runtimeService.startProcessInstanceByKey("failingProcess");
ProcessInstance processWithIncident2 = runtimeService.startProcessInstanceByKey("failingProcess");

List<Task> tasks = taskService.createTaskQuery().list();
assertEquals(2, tasks.size());
taskService.complete(tasks.get(0).getId());
taskService.complete(tasks.get(1).getId());

ProcessInstance processWithoutIncident = runtimeService.startProcessInstanceByKey("failingProcess");

List<String> queriedProcessInstances = Arrays.asList(processWithIncident1.getId(), processWithIncident2.getId());

testHelper.executeAvailableJobs();
Incident incident = runtimeService.createIncidentQuery().processInstanceId(queriedProcessInstances.get(0)).singleResult();
Incident incident2 = runtimeService.createIncidentQuery().processInstanceId(queriedProcessInstances.get(1)).singleResult();

// WHEN
List<HistoricProcessInstance> processInstanceList =
historyService.createHistoricProcessInstanceQuery().incidentIdIn(incident.getId(), incident2.getId()).list();

// THEN
assertEquals(2, processInstanceList.size());
assertThat(queriedProcessInstances)
.containsExactlyInAnyOrderElementsOf(
processInstanceList.stream()
.map(HistoricProcessInstance::getId)
.collect(Collectors.toList()));
}

@Test
public void shouldFailWhenQueryWithNullIncidentIdIn() {
try {
historyService.createHistoricProcessInstanceQuery().incidentIdIn(null).list();
fail("incidentMessage with null value is not allowed");
} catch( NullValueException nex ) {
// expected
}
}

@Test
@RequiredHistoryLevel(ProcessEngineConfiguration.HISTORY_FULL)
@Deployment(resources={"org/camunda/bpm/engine/test/api/runtime/failingSubProcessCreateOneIncident.bpmn20.xml"})
public void shouldQueryByIncidentIdInSubProcess() {
// given
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("failingSubProcess");

testHelper.executeAvailableJobs();

List<Incident> incidentList = runtimeService.createIncidentQuery().list();
assertEquals(1, incidentList.size());

Incident incident = runtimeService.createIncidentQuery().processInstanceId(processInstance.getId()).singleResult();

// when
HistoricProcessInstance historicPI =
historyService.createHistoricProcessInstanceQuery().incidentIdIn(incident.getId()).singleResult();

// then
assertThat(historicPI).isNotNull();
assertThat(historicPI.getId()).isEqualTo(processInstance.getId());
}

@Test
@Deployment(resources = {"org/camunda/bpm/engine/test/history/oneAsyncTaskProcess.bpmn20.xml"})
public void testShouldStoreHistoricProcessInstanceVariableOnAsyncBefore() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>

<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:camunda="http://camunda.org/schema/1.0/bpmn"
targetNamespace="Examples">

<process id="failingProcess" name="Failing Process" isExecutable="true">

<startEvent id="start" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="userTask" />
<userTask id="userTask" />
<sequenceFlow id="flow2" sourceRef="userTask" targetRef="serviceTask" />
<serviceTask id="serviceTask" camunda:async="true"
camunda:class="org.camunda.bpm.engine.test.api.runtime.FailingDelegate" name="Service Task"/>
<sequenceFlow id="flow3" sourceRef="serviceTask" targetRef="end" />
<endEvent id="end" />

</process>

</definitions>