Skip to content

Commit

Permalink
Add createTimestamp to workItem queries (MID-4733)
Browse files Browse the repository at this point in the history
It is now possible not only to sort on createTimestamp but also use
LT, GT and EQ filters on it; provided they are part of a conjunction.
(Note that LTEQ/GTEQ is not supported.)

Contains necessary enhancements in query/filter factoring process.

(cherry picked from commit 6ad8e0c)
  • Loading branch information
mederly committed Aug 20, 2018
1 parent 465f083 commit fe098ad
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 38 deletions.
Expand Up @@ -17,6 +17,8 @@
package com.evolveum.midpoint.schema.util;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.xml.namespace.QName;

Expand All @@ -25,7 +27,7 @@
import com.evolveum.midpoint.prism.query.Visitor;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.prism.query.builder.S_AtomicFilterExit;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.mutable.MutableBoolean;

Expand All @@ -43,6 +45,7 @@
import com.evolveum.prism.xml.ns._public.types_3.PolyStringType;
import org.jetbrains.annotations.NotNull;

import static java.util.Collections.singletonList;
import static org.apache.commons.collections4.CollectionUtils.isEmpty;

public class ObjectQueryUtil {
Expand Down Expand Up @@ -515,63 +518,125 @@ public static FilterComponents factorOutQuery(ObjectQuery query, QName... names)
}

public static FilterComponents factorOutQuery(ObjectQuery query, ItemPath... paths) {
return factorOutFilter(query != null ? query.getFilter() : null, paths);
return factorOutQuery(query, DEFAULT_EXTRACTORS, paths);
}

public static FilterComponents factorOutQuery(ObjectQuery query, List<FilterExtractor> extractors, ItemPath... paths) {
return factorOutFilter(query != null ? query.getFilter() : null, extractors, paths);
}

@SuppressWarnings("unused")
public static FilterComponents factorOutFilter(ObjectFilter filter, ItemPath... paths) {
return factorOutFilter(filter, DEFAULT_EXTRACTORS, paths);
}

public static FilterComponents factorOutFilter(ObjectFilter filter, List<FilterExtractor> extractors, ItemPath... paths) {
FilterComponents components = new FilterComponents();
factorOutFilter(components, simplify(filter), Arrays.asList(paths), true);
factorOutFilter(components, simplify(filter), extractors, Arrays.asList(paths), true);
return components;
}

// TODO better API
@SuppressWarnings("unused")
public static FilterComponents factorOutOrFilter(ObjectFilter filter, ItemPath... paths) {
FilterComponents components = new FilterComponents();
factorOutFilter(components, simplify(filter), Arrays.asList(paths), false);
factorOutFilter(components, simplify(filter), DEFAULT_EXTRACTORS, Arrays.asList(paths), false);
return components;
}

private static void factorOutFilter(FilterComponents filterComponents, ObjectFilter filter, List<ItemPath> paths, boolean connectedByAnd) {
if (filter instanceof EqualFilter) {
EqualFilter equalFilter = (EqualFilter) filter;
if (ItemPath.containsEquivalent(paths, equalFilter.getPath())) {
filterComponents.addToKnown(equalFilter.getPath(), equalFilter.getValues());
} else {
filterComponents.addToRemainder(equalFilter);
}
} else if (filter instanceof RefFilter) {
RefFilter refFilter = (RefFilter) filter;
if (ItemPath.containsEquivalent(paths, refFilter.getPath())) {
filterComponents.addToKnown(refFilter.getPath(), refFilter.getValues());
} else {
filterComponents.addToRemainder(refFilter);
}
} else if (connectedByAnd && filter instanceof AndFilter) {
/**
* Describes how to treat a filter when factoring out a query/filter.
*/
public static class FilterExtractor {
@NotNull private final Predicate<ObjectFilter> selector; // does this extractor apply?
@NotNull private final Function<ObjectFilter, ItemPath> pathExtractor; // give me the item path!
@NotNull private final Function<ObjectFilter, List<? extends PrismValue>> valueExtractor; // give me values! (optional)
public FilterExtractor(@NotNull Predicate<ObjectFilter> selector,
@NotNull Function<ObjectFilter, ItemPath> pathExtractor,
@NotNull Function<ObjectFilter, List<? extends PrismValue>> valueExtractor) {
this.selector = selector;
this.pathExtractor = pathExtractor;
this.valueExtractor = valueExtractor;
}
}

public static final FilterExtractor EQUAL_EXTRACTOR = new FilterExtractor(
filter -> filter instanceof EqualFilter,
filter -> ((EqualFilter<?>) filter).getPath(),
filter -> ((EqualFilter<?>) filter).getValues());

public static final FilterExtractor REF_EXTRACTOR = new FilterExtractor(
filter -> filter instanceof RefFilter,
filter -> ((RefFilter) filter).getPath(),
filter -> ((RefFilter) filter).getValues());

public static final List<FilterExtractor> DEFAULT_EXTRACTORS = Arrays.asList(EQUAL_EXTRACTOR, REF_EXTRACTOR);

private static void factorOutFilter(FilterComponents filterComponents, ObjectFilter filter, @NotNull List<FilterExtractor> extractors,
List<ItemPath> paths, boolean connectedByAnd) {

if (connectedByAnd && filter instanceof AndFilter) {
for (ObjectFilter condition : ((AndFilter) filter).getConditions()) {
factorOutFilter(filterComponents, condition, paths, true);
factorOutFilter(filterComponents, condition, extractors, paths, true);
}
} else if (!connectedByAnd && filter instanceof OrFilter) {
for (ObjectFilter condition : ((OrFilter) filter).getConditions()) {
factorOutFilter(filterComponents, condition, paths, false);
factorOutFilter(filterComponents, condition, extractors, paths, false);
}
} else if (filter instanceof TypeFilter) {
// this is a bit questionable...
factorOutFilter(filterComponents, ((TypeFilter) filter).getFilter(), paths, connectedByAnd);
} else if (filter != null) {
filterComponents.addToRemainder(filter);
factorOutFilter(filterComponents, ((TypeFilter) filter).getFilter(), extractors, paths, connectedByAnd);
} else {
// nothing to do with a null filter
boolean found = false;
for (FilterExtractor extractor : extractors) {
if (extractor.selector.test(filter)) {
ItemPath filterPath = extractor.pathExtractor.apply(filter);
if (ItemPath.containsEquivalent(paths, filterPath)) {
filterComponents.addToKnown(filterPath, extractor.valueExtractor.apply(filter), filter);
found = true;
break;
}
}
}
if (!found) {
if (filter != null) {
filterComponents.addToRemainder(filter);
} else {
// nothing to do with a null filter
}
}
}
}

/**
* Result of the query/filter factorization.
*/
public static class FilterComponents {
/**
* "Value" components: intersection of values found. Useful for equality-type filters.
* Usually ignored for other kinds of filters.
*/
private Map<ItemPath,Collection<? extends PrismValue>> knownComponents = new HashMap<>();
/**
* "Filter" components: collection of all related filters found. Useful e.g. for GT/LT-type filters.
*/
private Map<ItemPath, Collection<ObjectFilter>> knownComponentFilters = new HashMap<>();
/**
* All the rest.
*/
private List<ObjectFilter> remainderClauses = new ArrayList<>();

@SuppressWarnings("unused")
public Map<ItemPath, Collection<? extends PrismValue>> getKnownComponents() {
return knownComponents;
}

@SuppressWarnings("unused")
public Map<ItemPath, Collection<ObjectFilter>> getKnownComponentFilters() {
return knownComponentFilters;
}

@SuppressWarnings("unused")
public ObjectFilter getRemainder() {
if (remainderClauses.size() == 0) {
return null;
Expand All @@ -582,13 +647,19 @@ public ObjectFilter getRemainder() {
}
}

public void addToKnown(ItemPath path, List values) {
void addToKnown(ItemPath path, List<? extends PrismValue> values, ObjectFilter filter) {
Map.Entry<ItemPath, Collection<? extends PrismValue>> entry = getKnownComponent(path);
if (entry != null) {
entry.setValue(CollectionUtils.intersection(entry.getValue(), values));
} else {
knownComponents.put(path, values);
}
Map.Entry<ItemPath, Collection<ObjectFilter>> entryFilter = getKnownComponentFilter(path);
if (entryFilter != null) {
entryFilter.getValue().add(filter);
} else {
knownComponentFilters.put(path, new ArrayList<>(singletonList(filter)));
}
}

public Map.Entry<ItemPath, Collection<? extends PrismValue>> getKnownComponent(ItemPath path) {
Expand All @@ -600,17 +671,26 @@ public Map.Entry<ItemPath, Collection<? extends PrismValue>> getKnownComponent(I
return null;
}

public Map.Entry<ItemPath, Collection<ObjectFilter>> getKnownComponentFilter(ItemPath path) {
for (Map.Entry<ItemPath, Collection<ObjectFilter>> entry : knownComponentFilters.entrySet()) {
if (path.equivalent(entry.getKey())) {
return entry;
}
}
return null;
}

public void addToRemainder(ObjectFilter filter) {
remainderClauses.add(filter);
}

@SuppressWarnings("unused")
public boolean hasRemainder() {
return !remainderClauses.isEmpty();
}

public List<ObjectFilter> getRemainderClauses() {
return remainderClauses;
}

}
}
Expand Up @@ -21,15 +21,14 @@
import com.evolveum.midpoint.prism.PrismReferenceValue;
import com.evolveum.midpoint.prism.PrismValue;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.query.ObjectFilter;
import com.evolveum.midpoint.prism.query.ObjectPaging;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.*;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.SearchResultList;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.constants.ObjectTypes;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ObjectQueryUtil;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.schema.util.WfContextUtil;
import com.evolveum.midpoint.task.api.TaskManager;
Expand Down Expand Up @@ -59,6 +58,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.xml.datatype.XMLGregorianCalendar;
import java.util.*;

import static com.evolveum.midpoint.schema.constants.ObjectTypes.TASK;
Expand Down Expand Up @@ -118,20 +118,28 @@ public SearchResultList<WorkItemType> searchWorkItems(ObjectQuery query, Collect
// primitive 'query interpreter'
// returns null if no results should be returned
private TaskQuery createTaskQuery(ObjectQuery query, boolean includeVariables, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult result) throws SchemaException {
FilterComponents components = factorOutQuery(query, F_ASSIGNEE_REF, F_CANDIDATE_REF, F_EXTERNAL_ID);
List<ObjectFilter> remainingClauses = components.getRemainderClauses();
if (!remainingClauses.isEmpty()) {
throw new SchemaException("Unsupported clause(s) in search filter: " + remainingClauses);
}

final ItemPath WORK_ITEM_ID_PATH = new ItemPath(F_EXTERNAL_ID);
final ItemPath ASSIGNEE_PATH = new ItemPath(F_ASSIGNEE_REF);
final ItemPath CANDIDATE_PATH = new ItemPath(F_CANDIDATE_REF);
final ItemPath CREATED_PATH = new ItemPath(WorkItemType.F_CREATE_TIMESTAMP);

final Map.Entry<ItemPath, Collection<? extends PrismValue>> workItemIdFilter = components.getKnownComponent(WORK_ITEM_ID_PATH);
final ObjectQueryUtil.FilterExtractor CREATED_LT_GT_EXTRACTOR = new ObjectQueryUtil.FilterExtractor(
filter -> filter instanceof ComparativeFilter && !((ComparativeFilter) filter).isEquals(),
filter -> ((ComparativeFilter) filter).getPath(),
filter -> new ArrayList<>());
final List<ObjectQueryUtil.FilterExtractor> EXTRACTORS = new ArrayList<>(ObjectQueryUtil.DEFAULT_EXTRACTORS);
EXTRACTORS.add(CREATED_LT_GT_EXTRACTOR);

FilterComponents components = factorOutQuery(query, EXTRACTORS, ASSIGNEE_PATH, CANDIDATE_PATH, WORK_ITEM_ID_PATH, CREATED_PATH);
List<ObjectFilter> remainingClauses = components.getRemainderClauses();
if (!remainingClauses.isEmpty()) {
throw new SchemaException("Unsupported clause(s) in search filter: " + remainingClauses);
}

final Map.Entry<ItemPath, Collection<? extends PrismValue>> workItemIdFilter = components.getKnownComponent(WORK_ITEM_ID_PATH);
final Map.Entry<ItemPath, Collection<? extends PrismValue>> assigneeFilter = components.getKnownComponent(ASSIGNEE_PATH);
final Map.Entry<ItemPath, Collection<? extends PrismValue>> candidateRolesFilter = components.getKnownComponent(CANDIDATE_PATH);
final Map.Entry<ItemPath, Collection<ObjectFilter>> createdFilters = components.getKnownComponentFilter(CREATED_PATH);

TaskQuery taskQuery = activitiEngine.getTaskService().createTaskQuery();

Expand All @@ -157,6 +165,29 @@ private TaskQuery createTaskQuery(ObjectQuery query, boolean includeVariables, C
}
}

if (createdFilters != null) {
for (ObjectFilter filter : createdFilters.getValue()) {
PrismPropertyValue value = ((PropertyValueFilter<?>) filter).getSingleValue();
if (value == null) {
throw new SchemaException("'createdTimestamp' filter contains a null value: " + filter);
}
Object realValue = value.getRealValue();
if (!(realValue instanceof XMLGregorianCalendar)) {
throw new SchemaException("'createdTimestamp' filter contains a value other than XMLGregorianCalendar: " + realValue + " in " + filter);
}
Date date = XmlTypeConverter.toDate((XMLGregorianCalendar) realValue);
if (filter instanceof GreaterFilter) {
taskQuery = taskQuery.taskCreatedAfter(date);
} else if (filter instanceof LessFilter) {
taskQuery = taskQuery.taskCreatedBefore(date);
} else if (filter instanceof EqualFilter) {
taskQuery = taskQuery.taskCreatedOn(date);
} else {
throw new IllegalStateException("Unexpected filter: " + filter);
}
}
}

if (query != null && query.getPaging() != null) {
ObjectPaging paging = query.getPaging();
if (paging.getOrderingInstructions().size() > 1) {
Expand Down
Expand Up @@ -20,6 +20,7 @@
import com.evolveum.midpoint.prism.PrismReferenceValue;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.SearchResultList;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.Task;
Expand All @@ -39,6 +40,7 @@
import org.springframework.test.context.ContextConfiguration;
import org.testng.annotations.Test;

import javax.xml.datatype.XMLGregorianCalendar;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -100,6 +102,62 @@ public void test100SearchByMoreAssignees() throws Exception {
}
}

@Test
public void test110SearchByCreateTimestamp() throws Exception {
final String TEST_NAME = "test110SearchByCreateTimestamp";
TestUtil.displayTestTitle(this, TEST_NAME);
login(userAdministrator);

Task task = createTask(TEST_NAME);
OperationResult result = task.getResult();

{
SearchResultList<WorkItemType> itemsAll = modelService.searchContainers(WorkItemType.class, null, null, task, result);
display("itemsAll", itemsAll);
assertEquals("Wrong # of total work items", 1, itemsAll.size());
}

XMLGregorianCalendar created;
{
ObjectQuery query2 = QueryBuilder.queryFor(WorkItemType.class, prismContext)
.item(WorkItemType.F_CREATE_TIMESTAMP).lt(XmlTypeConverter.createXMLGregorianCalendar(System.currentTimeMillis()))
.and().item(WorkItemType.F_CREATE_TIMESTAMP).gt(XmlTypeConverter.createXMLGregorianCalendar(System.currentTimeMillis()-300000))
.build();
SearchResultList<WorkItemType> items2 = modelService.searchContainers(WorkItemType.class, query2, null, task, result);
display("items2", items2);
assertEquals("Wrong # of work items found using 'create timestamp' query", 1, items2.size());
created = items2.get(0).getCreateTimestamp();
}

{
ObjectQuery query3 = QueryBuilder.queryFor(WorkItemType.class, prismContext)
.item(WorkItemType.F_CREATE_TIMESTAMP).gt(XmlTypeConverter.createXMLGregorianCalendar(System.currentTimeMillis()))
.build();
SearchResultList<WorkItemType> items3 = modelService.searchContainers(WorkItemType.class, query3, null, task, result);
display("items3", items3);
assertEquals("Wrong # of work items found using 'create timestamp' query (in future)", 0, items3.size());
}

{
ObjectQuery query4 = QueryBuilder.queryFor(WorkItemType.class, prismContext)
.item(WorkItemType.F_CREATE_TIMESTAMP).eq(XmlTypeConverter.createXMLGregorianCalendar(System.currentTimeMillis()))
.build();
SearchResultList<WorkItemType> items4 = modelService.searchContainers(WorkItemType.class, query4, null, task, result);
display("items4", items4);
assertEquals("Wrong # of work items found using 'create timestamp' query (in future)", 0, items4.size());
}

{
// hopefully the DBMS will match this!
ObjectQuery query5 = QueryBuilder.queryFor(WorkItemType.class, prismContext)
.item(WorkItemType.F_CREATE_TIMESTAMP).eq(created)
.build();
SearchResultList<WorkItemType> items5 = modelService.searchContainers(WorkItemType.class, query5, null, task, result);
display("items5", items5);
assertEquals("Wrong # of work items found using 'create timestamp' query", 1, items5.size());
}
}

/**
* Actually, this mechanism is not used anymore. We use identity links to store information about assignees.
* But keeping this test - just in case something like that would be needed later.
Expand Down

0 comments on commit fe098ad

Please sign in to comment.