1163 lines
38 KiB
Java
1163 lines
38 KiB
Java
package li.strolch.report.policy;
|
|
|
|
import static java.util.Comparator.comparing;
|
|
import static java.util.Comparator.comparingInt;
|
|
import static java.util.stream.Collectors.toList;
|
|
import static li.strolch.model.StrolchModelConstants.*;
|
|
import static li.strolch.report.ReportConstants.*;
|
|
import static li.strolch.utils.helper.StringHelper.EMPTY;
|
|
|
|
import java.time.ZonedDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Stream;
|
|
|
|
import com.google.gson.JsonObject;
|
|
import li.strolch.model.*;
|
|
import li.strolch.model.parameter.AbstractParameter;
|
|
import li.strolch.model.parameter.DateParameter;
|
|
import li.strolch.model.parameter.Parameter;
|
|
import li.strolch.model.parameter.StringParameter;
|
|
import li.strolch.model.policy.PolicyDef;
|
|
import li.strolch.model.visitor.ElementStateVisitor;
|
|
import li.strolch.model.visitor.ElementZdtDateVisitor;
|
|
import li.strolch.persistence.api.StrolchTransaction;
|
|
import li.strolch.policy.PolicyHandler;
|
|
import li.strolch.report.ReportConstants;
|
|
import li.strolch.report.ReportElement;
|
|
import li.strolch.utils.ObjectHelper;
|
|
import li.strolch.utils.collections.DateRange;
|
|
import li.strolch.utils.collections.MapOfLists;
|
|
import li.strolch.utils.collections.MapOfSets;
|
|
import li.strolch.utils.collections.TypedTuple;
|
|
import li.strolch.utils.dbc.DBC;
|
|
import li.strolch.utils.iso8601.ISO8601;
|
|
|
|
/**
|
|
* A Generic Report defines a report as is described at <a href="https://strolch.li/documentation-reports.html">Strolch
|
|
* Reports</a>
|
|
*
|
|
* @author Robert von Burg <eitch@eitchnet.ch>
|
|
*/
|
|
public class GenericReport extends ReportPolicy {
|
|
|
|
public static final int MAX_FACET_VALUE_LIMIT = 100;
|
|
protected Resource reportRes;
|
|
|
|
protected ParameterBag columnsBag;
|
|
protected List<StringParameter> orderingParams;
|
|
protected Map<String, StringParameter> filterCriteriaParams;
|
|
protected Set<String> directCriteria;
|
|
protected boolean parallel;
|
|
protected boolean descending;
|
|
protected boolean allowMissingColumns;
|
|
protected boolean filterMissingValuesAsTrue;
|
|
protected List<String> columnIds;
|
|
protected StringParameter dateRangeSelP;
|
|
|
|
protected DateRange dateRange;
|
|
protected Map<ReportFilterPolicy, TypedTuple<StringParameter, StringParameter>> filtersByPolicy;
|
|
protected MapOfSets<String, String> filtersById;
|
|
|
|
protected long counter;
|
|
protected boolean withPage;
|
|
protected int offset = -1;
|
|
protected int limit = -1;
|
|
|
|
protected JsonObject i18nData;
|
|
|
|
public GenericReport(StrolchTransaction tx) {
|
|
super(tx);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the {@code Resource} with the given ID, and initializes this instance with the data specified on the
|
|
* report
|
|
*
|
|
* @param reportId
|
|
* the report to use
|
|
*/
|
|
@Override
|
|
public void initialize(String reportId) {
|
|
|
|
// get the reportRes
|
|
this.reportRes = tx().getResourceBy(TYPE_REPORT, reportId, true);
|
|
|
|
StringParameter objectTypeP = this.reportRes.getStringP(PARAM_OBJECT_TYPE);
|
|
String objectType = objectTypeP.getValue();
|
|
|
|
this.columnsBag = this.reportRes.getParameterBag(BAG_COLUMNS, true);
|
|
|
|
this.columnIds = this.columnsBag.getParameters().stream() //
|
|
.sorted(comparingInt(Parameter::getIndex)) //
|
|
.map(StrolchElement::getId) //
|
|
.collect(toList());
|
|
|
|
this.parallel = this.reportRes.getBoolean(PARAM_PARALLEL);
|
|
this.descending = this.reportRes.getBoolean(PARAM_DESCENDING);
|
|
this.allowMissingColumns = this.reportRes.getBoolean(PARAM_ALLOW_MISSING_COLUMNS);
|
|
this.filterMissingValuesAsTrue = this.reportRes.getBoolean(PARAM_FILTER_MISSING_VALUES_AS_TRUE);
|
|
this.dateRangeSelP = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_DATE_RANGE_SEL);
|
|
|
|
// evaluate filter criteria params
|
|
this.filterCriteriaParams = new HashMap<>();
|
|
StringParameter objectTypeFilterCriteriaP = objectTypeP.getClone();
|
|
objectTypeFilterCriteriaP.setId(objectType);
|
|
if (objectTypeFilterCriteriaP.getUom().equals(UOM_NONE))
|
|
throw new IllegalStateException(
|
|
"Join UOM " + objectTypeFilterCriteriaP.getUom() + " invalid: " + objectTypeFilterCriteriaP.getId()
|
|
+ " for " + objectTypeFilterCriteriaP.getLocator());
|
|
this.filterCriteriaParams.put(objectType, objectTypeFilterCriteriaP);
|
|
if (this.reportRes.hasParameterBag(BAG_JOINS)) {
|
|
ParameterBag joinBag = this.reportRes.getParameterBag(BAG_JOINS);
|
|
joinBag.getParameters().forEach(parameter -> {
|
|
StringParameter joinP = (StringParameter) parameter;
|
|
if (joinP.getUom().equals(UOM_NONE))
|
|
throw new IllegalStateException(
|
|
"Join UOM " + joinP.getUom() + " invalid: " + joinP.getId() + " for " + joinP.getLocator());
|
|
this.filterCriteriaParams.put(parameter.getId(), joinP);
|
|
});
|
|
}
|
|
if (this.reportRes.hasParameterBag(BAG_ADDITIONAL_TYPE)) {
|
|
ParameterBag additionalTypeBag = this.reportRes.getParameterBag(BAG_ADDITIONAL_TYPE);
|
|
StringParameter additionalTypeP = additionalTypeBag.getParameter(PARAM_OBJECT_TYPE, true);
|
|
if (additionalTypeP.getUom().equals(UOM_NONE))
|
|
throw new IllegalStateException(
|
|
"Additional Type UOM " + additionalTypeP.getUom() + " invalid: " + additionalTypeP.getId()
|
|
+ " for " + additionalTypeP.getLocator());
|
|
this.filterCriteriaParams.put(additionalTypeP.getValue(), additionalTypeP);
|
|
}
|
|
if (this.reportRes.hasParameterBag(BAG_ADDITIONAL_JOINS)) {
|
|
ParameterBag joinBag = this.reportRes.getParameterBag(BAG_ADDITIONAL_JOINS);
|
|
joinBag.getParameters().forEach(parameter -> {
|
|
StringParameter joinP = (StringParameter) parameter;
|
|
if (joinP.getUom().equals(UOM_NONE))
|
|
throw new IllegalStateException(
|
|
"Additional Join UOM " + joinP.getUom() + " invalid: " + joinP.getId() + " for "
|
|
+ joinP.getLocator());
|
|
this.filterCriteriaParams.put(parameter.getId(), joinP);
|
|
});
|
|
}
|
|
|
|
// evaluate ordering params
|
|
if (this.reportRes.hasParameterBag(BAG_ORDERING)) {
|
|
ParameterBag orderingBag = this.reportRes.getParameterBag(BAG_ORDERING, true);
|
|
if (orderingBag.hasParameters()) {
|
|
this.orderingParams = orderingBag.getParameters()
|
|
.stream()
|
|
.map(e -> (StringParameter) e)
|
|
.collect(toList());
|
|
this.orderingParams.sort(comparingInt(AbstractParameter::getIndex));
|
|
}
|
|
}
|
|
|
|
// evaluate filters
|
|
this.filtersByPolicy = new HashMap<>();
|
|
List<ParameterBag> filterBags = this.reportRes.getParameterBagsByType(TYPE_FILTER);
|
|
for (ParameterBag filterBag : filterBags) {
|
|
|
|
if (filterBag.hasParameter(PARAM_FIELD_REF) && (filterBag.hasParameter(PARAM_FIELD_REF1)
|
|
|| filterBag.hasParameter(PARAM_FIELD_REF2))) {
|
|
throw new IllegalArgumentException(
|
|
"Filter " + filterBag.getLocator() + " can not have combination of " + PARAM_FIELD_REF
|
|
+ " and any of " + PARAM_FIELD_REF1 + ", " + PARAM_FIELD_REF2);
|
|
} else if ((filterBag.hasParameter(PARAM_FIELD_REF1) && !filterBag.hasParameter(PARAM_FIELD_REF2)) || (
|
|
!filterBag.hasParameter(PARAM_FIELD_REF1) && filterBag.hasParameter(PARAM_FIELD_REF2))) {
|
|
throw new IllegalArgumentException(
|
|
"Filter " + filterBag.getLocator() + " must have both " + PARAM_FIELD_REF1 + " and "
|
|
+ PARAM_FIELD_REF2);
|
|
} else if (!filterBag.hasParameter(PARAM_FIELD_REF) && (!filterBag.hasParameter(PARAM_FIELD_REF1)
|
|
|| !filterBag.hasParameter(PARAM_FIELD_REF2))) {
|
|
throw new IllegalArgumentException(
|
|
"Filter " + filterBag.getLocator() + " is missing the " + PARAM_FIELD_REF + " or "
|
|
+ PARAM_FIELD_REF1 + ", " + PARAM_FIELD_REF2 + " combination!");
|
|
}
|
|
|
|
// prepare filter function policy
|
|
StringParameter functionP = filterBag.getParameter(PARAM_POLICY);
|
|
PolicyHandler policyHandler = getContainer().getComponent(PolicyHandler.class);
|
|
PolicyDef policyDef = PolicyDef.valueOf(functionP.getInterpretation(), functionP.getUom());
|
|
ReportFilterPolicy filterFunction = policyHandler.getPolicy(policyDef, tx());
|
|
filterFunction.init(functionP.getValue());
|
|
|
|
TypedTuple<StringParameter, StringParameter> refTuple = new TypedTuple<>();
|
|
if (filterBag.hasParameter(PARAM_FIELD_REF)) {
|
|
refTuple.setFirst(filterBag.getParameter(PARAM_FIELD_REF));
|
|
} else {
|
|
refTuple.setFirst(filterBag.getParameter(PARAM_FIELD_REF1));
|
|
refTuple.setSecond(filterBag.getParameter(PARAM_FIELD_REF2));
|
|
}
|
|
|
|
this.filtersByPolicy.put(filterFunction, refTuple);
|
|
}
|
|
|
|
// get the list of types of criteria to query directly, not over the element stream
|
|
this.directCriteria = new HashSet<>(this.reportRes.getStringList(PARAM_DIRECT_CRITERIA));
|
|
}
|
|
|
|
@Override
|
|
public Resource getReportResource() {
|
|
return this.reportRes;
|
|
}
|
|
|
|
public boolean isDescending() {
|
|
return this.descending;
|
|
}
|
|
|
|
@Override
|
|
public boolean isParallel() {
|
|
return this.parallel;
|
|
}
|
|
|
|
@Override
|
|
public void setI18nData(JsonObject i18nData) {
|
|
this.i18nData = i18nData;
|
|
}
|
|
|
|
@Override
|
|
public boolean withPage() {
|
|
return withPage;
|
|
}
|
|
|
|
@Override
|
|
public int getOffset() {
|
|
return this.offset;
|
|
}
|
|
|
|
@Override
|
|
public int getLimit() {
|
|
return this.limit;
|
|
}
|
|
|
|
@Override
|
|
public long getCounter() {
|
|
return this.counter;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the report has a date range selector specified
|
|
*
|
|
* @return true if the report has a date range selector specified
|
|
*/
|
|
@Override
|
|
public boolean hasDateRangeSelector() {
|
|
return this.dateRangeSelP != null;
|
|
}
|
|
|
|
/**
|
|
* Sets the given date range
|
|
*
|
|
* @param dateRange
|
|
* the date range to set
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public GenericReport dateRange(DateRange dateRange) {
|
|
this.dateRange = dateRange;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the currently set {@link DateRange} or null if not set
|
|
*
|
|
* @return the date range, or null if not set
|
|
*/
|
|
public DateRange getDateRange() {
|
|
return this.dateRange;
|
|
}
|
|
|
|
/**
|
|
* The keys for the header of this report, as is defined on the {@link ReportConstants#BAG_COLUMNS} parameter bag
|
|
*
|
|
* @return the keys for the header
|
|
*/
|
|
@Override
|
|
public List<String> getColumnKeys() {
|
|
return this.columnIds;
|
|
}
|
|
|
|
/**
|
|
* Applies the given filter for the given element type
|
|
*
|
|
* @param type
|
|
* the type of element to filter
|
|
* @param ids
|
|
* the IDs of the elements to filter to
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public GenericReport filter(String type, String... ids) {
|
|
if (this.filtersById == null)
|
|
this.filtersById = new MapOfSets<>();
|
|
for (String id : ids) {
|
|
this.filtersById.addElement(type, id);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Applies the given filter for the given element type
|
|
*
|
|
* @param type
|
|
* the type of element to filter
|
|
* @param ids
|
|
* the IDs of the elements to filter to
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public GenericReport filter(String type, List<String> ids) {
|
|
if (this.filtersById == null)
|
|
this.filtersById = new MapOfSets<>();
|
|
for (String id : ids) {
|
|
this.filtersById.addElement(type, id);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Applies the given filter for the given element type
|
|
*
|
|
* @param type
|
|
* the type of element to filter
|
|
* @param ids
|
|
* the IDs of the elements to filter to
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public GenericReport filter(String type, Set<String> ids) {
|
|
if (this.filtersById == null)
|
|
this.filtersById = new MapOfSets<>();
|
|
for (String id : ids) {
|
|
this.filtersById.addElement(type, id);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
protected synchronized void incrementCounter() {
|
|
this.counter++;
|
|
}
|
|
|
|
/**
|
|
* Builds the stream of rows on which further transformations can be performed. Each row is a {@link Map} for where
|
|
* the key is an element type, and the value is the associated element
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public Stream<Map<String, StrolchRootElement>> buildStream() {
|
|
return buildStream(true);
|
|
}
|
|
|
|
/**
|
|
* Builds the stream of rows on which further transformations can be performed. Each row is a {@link Map} for where
|
|
* the key is an element type, and the value is the associated element
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public Stream<Map<String, StrolchRootElement>> buildStream(boolean withOrdering) {
|
|
|
|
Stream<Map<String, StrolchRootElement>> stream;
|
|
|
|
// query the main objects and return a stream
|
|
stream = queryRows() //
|
|
|
|
// transform each element into a map of Type,Value pairs
|
|
.map(this::evaluateRow);
|
|
|
|
stream = handleAdditionalTypes(stream);
|
|
|
|
stream = flatMap(stream);
|
|
|
|
if (hasFilter())
|
|
stream = stream.filter(this::filter);
|
|
|
|
stream = stream.peek(e -> incrementCounter());
|
|
|
|
if (withOrdering && hasOrdering())
|
|
stream = stream.sorted(this::sort);
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Allows sub classes to extend this stream, i.e. flat map an object to extend the stream where necessary
|
|
*
|
|
* @param stream
|
|
* the stream to extend
|
|
*
|
|
* @return the stream
|
|
*/
|
|
public Stream<Map<String, StrolchRootElement>> flatMap(Stream<Map<String, StrolchRootElement>> stream) {
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Handles additional joining, so that we can join on arbitrary elements, matching on a {@link StringParameter}
|
|
*
|
|
* <p>
|
|
* the element we want to join is defined by the objectType parameter
|
|
* </p>
|
|
*
|
|
* <code>
|
|
* <Parameter Id="objectType" Hidden="true" Name="Object Type" Type="String" Interpretation="Order-Ref"
|
|
* Uom="Order" Value="Order"/>
|
|
* </code>
|
|
*
|
|
* <p>
|
|
* the joining is defined by two parameters, the joinParam defines the parameter for the additional join type to
|
|
* match
|
|
* </p>
|
|
*
|
|
* <code>
|
|
* <Parameter Id="joinParam" Name="Join Param" Type="String" Value="Bags/relations/product"/>
|
|
* </code>
|
|
*
|
|
* <p>
|
|
* and the joinWith parameter defines which already joined type and param to match on
|
|
* </p>
|
|
*
|
|
* <code>
|
|
* <Parameter Id="joinWith" Name="Join With" Type="String" Interpretation="Resource-Ref" Uom="Slot"
|
|
* Value="Bags/relations/product"/>
|
|
* </code>
|
|
*
|
|
* @param stream
|
|
* the current stream of rows
|
|
*
|
|
* @return the new stream of rows, which iterates over the additionally joined elements, thus creating a cartesian
|
|
* product stream
|
|
*/
|
|
protected Stream<Map<String, StrolchRootElement>> handleAdditionalTypes(
|
|
Stream<Map<String, StrolchRootElement>> stream) {
|
|
|
|
// see if we need to do additional type joining
|
|
ParameterBag additionalTypeBag = this.reportRes.getParameterBag(BAG_ADDITIONAL_TYPE);
|
|
if (additionalTypeBag == null)
|
|
return stream;
|
|
|
|
StringParameter objectTypeP = additionalTypeBag.getStringP(PARAM_OBJECT_TYPE);
|
|
|
|
StringParameter joinParamP = additionalTypeBag.getStringP(PARAM_JOIN_PARAM);
|
|
String[] locatorParts = joinParamP.getValue().split(Locator.PATH_SEPARATOR);
|
|
if (locatorParts.length != 3)
|
|
throw new IllegalStateException(
|
|
"Parameter reference (" + joinParamP.getValue() + ") is invalid as it does not have 3 parts for "
|
|
+ joinParamP.getLocator());
|
|
String bagKey = locatorParts[1];
|
|
String paramKey = locatorParts[2];
|
|
|
|
MapOfLists<String, StrolchRootElement> joinElements = getStreamFor(objectTypeP).collect(MapOfLists::new,
|
|
(mapOfLists, e) -> {
|
|
StringParameter joinP = e.getParameter(bagKey, paramKey, true);
|
|
mapOfLists.addElement(joinP.getValue(), e);
|
|
}, MapOfLists::addAll);
|
|
|
|
StringParameter joinWithP = additionalTypeBag.getStringP(PARAM_JOIN_WITH);
|
|
return stream.flatMap(row -> {
|
|
|
|
StrolchRootElement joinElement = row.get(joinWithP.getUom());
|
|
if (joinElement == null)
|
|
throw new IllegalStateException(
|
|
"Additional join type " + joinWithP.getUom() + " is not available on row for "
|
|
+ joinWithP.getLocator());
|
|
|
|
Optional<Parameter<?>> refP = lookupParameter(joinWithP, joinElement, false);
|
|
if (refP.isEmpty()) {
|
|
throw new IllegalStateException(
|
|
"Parameter reference (" + joinWithP.getValue() + ") for " + joinWithP.getLocator()
|
|
+ " not found on " + joinElement.getLocator());
|
|
}
|
|
|
|
StringParameter joinP = (StringParameter) refP.get();
|
|
if (joinP.isEmpty())
|
|
return Stream.of(row);
|
|
List<StrolchRootElement> elements = joinElements.getList(joinP.getValue());
|
|
if (elements == null)
|
|
return Stream.of(row);
|
|
|
|
return elements.stream().map(additionalElement -> {
|
|
Map<String, StrolchRootElement> additionalRow = new HashMap<>(row);
|
|
additionalRow.put(additionalElement.getType(), additionalElement);
|
|
handleJoins(additionalRow, BAG_ADDITIONAL_JOINS);
|
|
return additionalRow;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Performs the report, returning a stream of {@link ReportElement}
|
|
*
|
|
* @return this for chaining
|
|
*/
|
|
@Override
|
|
public Stream<ReportElement> doReport() {
|
|
return buildStream().map(e -> new ReportElement(this.columnIds, columnId -> formatColumn(e, columnId)));
|
|
}
|
|
|
|
protected String formatColumn(Map<String, StrolchRootElement> row, String columnId) {
|
|
StringParameter columnDefP = this.columnsBag.getParameter(columnId, true);
|
|
Object value = evaluateColumnValue(columnDefP, row, false);
|
|
if (value instanceof ZonedDateTime) {
|
|
return ISO8601.toString((ZonedDateTime) value);
|
|
} else if (value instanceof Date) {
|
|
return ISO8601.toString((Date) value);
|
|
} else if (value instanceof Parameter) {
|
|
return formatColumn((Parameter<?>) value);
|
|
} else
|
|
return value.toString();
|
|
}
|
|
|
|
protected String formatColumn(Parameter<?> param) {
|
|
return param.getValueAsString();
|
|
}
|
|
|
|
@Override
|
|
public Stream<ReportElement> doReportWithPage(int offset, int limit) {
|
|
DBC.PRE.assertTrue("offset must >= 0", offset >= 0);
|
|
DBC.PRE.assertTrue("limit must > 0", limit > 0);
|
|
this.limit = limit;
|
|
this.offset = offset;
|
|
this.withPage = true;
|
|
|
|
return doReport().skip(this.offset).limit(this.limit);
|
|
}
|
|
|
|
/**
|
|
* <p>Check to see if the given element is to be filtered, i.e. is to be kept in the stream</p>
|
|
*
|
|
* <p>This implementation checks if a join parameters is set as hidden, including if the parameter {@link
|
|
* ReportConstants#PARAM_OBJECT_TYPE} is defined as hidden</p>
|
|
*
|
|
* <p>This method can be overridden for further filtering</p>
|
|
*
|
|
* @param type
|
|
* the type of element to filter
|
|
*
|
|
* @return true if the element is to be kept, false if not
|
|
*/
|
|
protected boolean filterCriteriaAllowed(String type) {
|
|
return !this.filterCriteriaParams.get(type).isHidden();
|
|
}
|
|
|
|
/**
|
|
* Generates the filter criteria for this report, i.e. it returns a {@link MapOfSets} which defines the type of
|
|
* elements on which a filter can be set and the {@link Set} of IDs which can be used for filtering.
|
|
*
|
|
* @param limit
|
|
* the max number of values per filter criteria to return
|
|
*
|
|
* @return the filter criteria as a map of sets
|
|
*/
|
|
@Override
|
|
public MapOfSets<String, StrolchRootElement> generateFilterCriteria(int limit) {
|
|
|
|
if (limit <= 0 || limit >= MAX_FACET_VALUE_LIMIT) {
|
|
logger.warn("Overriding invalid limit " + limit + " with " + MAX_FACET_VALUE_LIMIT);
|
|
limit = 100;
|
|
}
|
|
|
|
int maxFacetValues;
|
|
int reportMaxFacetValues = this.reportRes.getInteger(PARAM_MAX_FACET_VALUES);
|
|
if (reportMaxFacetValues != 0 && reportMaxFacetValues != limit) {
|
|
logger.warn("Report " + this.reportRes.getId() + " has " + PARAM_MAX_FACET_VALUES + " defined as "
|
|
+ reportMaxFacetValues + ". Ignoring requested limit " + limit);
|
|
maxFacetValues = reportMaxFacetValues;
|
|
} else {
|
|
maxFacetValues = limit;
|
|
}
|
|
|
|
MapOfSets<String, StrolchRootElement> result = new MapOfSets<>(true);
|
|
|
|
// we need the list of possible element types, which designate the criteria
|
|
List<String> criteria = this.filterCriteriaParams.values().stream() //
|
|
.filter(p -> {
|
|
if (p.getUom().equals(UOM_NONE))
|
|
throw new IllegalStateException(
|
|
"Join UOM " + p.getUom() + " invalid: " + p.getId() + " for " + p.getLocator());
|
|
if (p.getId().equals(PARAM_OBJECT_TYPE))
|
|
return filterCriteriaAllowed(p.getUom());
|
|
return filterCriteriaAllowed(p.getId());
|
|
}) //
|
|
.sorted(comparing(StringParameter::getIndex)) //
|
|
.map(StringParameter::getUom) //
|
|
.collect(toList());
|
|
|
|
int maxRowsForFacetGeneration = this.reportRes.getInteger(PARAM_MAX_ROWS_FOR_FACET_GENERATION);
|
|
|
|
if (!this.directCriteria.isEmpty()) {
|
|
criteria.forEach(type -> {
|
|
if (!this.directCriteria.contains(type))
|
|
return;
|
|
StringParameter filterCriteriaP = this.filterCriteriaParams.get(type);
|
|
Stream<? extends StrolchRootElement> stream = switch (filterCriteriaP.getInterpretation()) {
|
|
case INTERPRETATION_RESOURCE_REF -> tx().streamResources(filterCriteriaP.getUom());
|
|
case INTERPRETATION_ORDER_REF -> tx().streamOrders(filterCriteriaP.getUom());
|
|
case INTERPRETATION_ACTIVITY_REF -> tx().streamActivities(filterCriteriaP.getUom());
|
|
default -> throw new IllegalArgumentException(
|
|
"Unhandled filter criteria interpretation " + filterCriteriaP.getInterpretation() + " for "
|
|
+ filterCriteriaP.getLocator());
|
|
};
|
|
|
|
stream = stream.map(this::mapFilterCriteria).filter(this::filterDirectCriteria);
|
|
|
|
if (hasOrdering())
|
|
stream = stream.sorted(this::sortDirectCriteria);
|
|
|
|
if (maxFacetValues > 0)
|
|
stream = stream.limit(maxFacetValues);
|
|
|
|
stream.forEachOrdered(e -> result.addElement(e.getType(), e));
|
|
});
|
|
|
|
criteria.removeAll(this.directCriteria);
|
|
}
|
|
|
|
Stream<Map<String, StrolchRootElement>> stream = buildStream(false);
|
|
if (maxRowsForFacetGeneration > 0)
|
|
stream = stream.limit(maxRowsForFacetGeneration);
|
|
|
|
Iterator<Map<String, StrolchRootElement>> iter = stream.iterator();
|
|
|
|
while (iter.hasNext()) {
|
|
Map<String, StrolchRootElement> row = iter.next();
|
|
|
|
for (String criterion : criteria) {
|
|
if (row.containsKey(criterion) && result.size(criterion) < maxFacetValues)
|
|
result.addElement(criterion, row.get(criterion));
|
|
}
|
|
|
|
// stop if we have enough data
|
|
if (result.stream()
|
|
.filter(e -> !this.directCriteria.contains(e.getKey()))
|
|
.mapToInt(e -> e.getValue().size())
|
|
.allMatch(v -> v >= maxFacetValues))
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
protected StrolchRootElement mapFilterCriteria(StrolchRootElement element) {
|
|
return element;
|
|
}
|
|
|
|
@Override
|
|
public Stream<StrolchRootElement> generateFilterCriteria(String type) {
|
|
return buildStream().filter(row -> row.containsKey(type)).map(row -> row.get(type)).distinct();
|
|
}
|
|
|
|
/**
|
|
* Returns true if an ordering is defined by means of {@link ReportConstants#BAG_ORDERING}
|
|
*
|
|
* @return true if an ordering is defined
|
|
*/
|
|
protected boolean hasOrdering() {
|
|
return this.orderingParams != null && !this.orderingParams.isEmpty();
|
|
}
|
|
|
|
protected int sortDirectCriteria(StrolchRootElement column1, StrolchRootElement column2) {
|
|
if (column1 == null && column2 == null)
|
|
return 0;
|
|
if (column1 == null)
|
|
return -1;
|
|
if (column2 == null)
|
|
return 1;
|
|
|
|
for (StringParameter fieldRefP : this.orderingParams) {
|
|
String type = fieldRefP.getUom();
|
|
|
|
if (!column1.getType().equals(type))
|
|
continue;
|
|
|
|
int sortVal;
|
|
if (fieldRefP.getValue().startsWith("$")) {
|
|
Object columnValue1 = evaluateColumnValue(fieldRefP, Map.of(column1.getType(), column1), false);
|
|
Object columnValue2 = evaluateColumnValue(fieldRefP, Map.of(column2.getType(), column2), false);
|
|
|
|
if (this.descending) {
|
|
sortVal = ObjectHelper.compare(columnValue2, columnValue1, true);
|
|
} else {
|
|
sortVal = ObjectHelper.compare(columnValue1, columnValue2, true);
|
|
}
|
|
|
|
} else {
|
|
Optional<Parameter<?>> param1 = lookupParameter(fieldRefP, column1, false);
|
|
Optional<Parameter<?>> param2 = lookupParameter(fieldRefP, column2, false);
|
|
|
|
if (param1.isEmpty() && param2.isEmpty())
|
|
continue;
|
|
|
|
if (param1.isPresent() && param2.isEmpty())
|
|
return 1;
|
|
else if (param1.isEmpty())
|
|
return -1;
|
|
|
|
if (this.descending)
|
|
sortVal = param2.get().compareTo(param1.get());
|
|
else
|
|
sortVal = param1.get().compareTo(param2.get());
|
|
}
|
|
|
|
if (sortVal != 0)
|
|
return sortVal;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Implements a sorting of the given two rows. This implementation using the ordering as is defined in
|
|
* {@link ReportConstants#BAG_ORDERING}
|
|
*
|
|
* @param row1
|
|
* the left side
|
|
* @param row2
|
|
* the right side
|
|
*
|
|
* @return the value {@code -1}, {@code 0} or {@code 1}, depending on the defined ordering
|
|
*/
|
|
protected int sort(Map<String, StrolchRootElement> row1, Map<String, StrolchRootElement> row2) {
|
|
|
|
for (StringParameter fieldRefP : this.orderingParams) {
|
|
|
|
String type = fieldRefP.getUom();
|
|
|
|
StrolchRootElement column1 = row1.get(type);
|
|
StrolchRootElement column2 = row2.get(type);
|
|
if (column1 == null && column2 == null)
|
|
continue;
|
|
if (column1 == null)
|
|
return -1;
|
|
if (column2 == null)
|
|
return 1;
|
|
|
|
int sortVal;
|
|
if (fieldRefP.getValue().startsWith("$")) {
|
|
Object columnValue1 = evaluateColumnValue(fieldRefP, row1, false);
|
|
Object columnValue2 = evaluateColumnValue(fieldRefP, row2, false);
|
|
|
|
if (this.descending) {
|
|
sortVal = ObjectHelper.compare(columnValue2, columnValue1, true);
|
|
} else {
|
|
sortVal = ObjectHelper.compare(columnValue1, columnValue2, true);
|
|
}
|
|
|
|
} else {
|
|
Optional<Parameter<?>> param1 = lookupParameter(fieldRefP, column1, false);
|
|
Optional<Parameter<?>> param2 = lookupParameter(fieldRefP, column2, false);
|
|
|
|
if (param1.isEmpty() && param2.isEmpty())
|
|
continue;
|
|
|
|
if (param1.isPresent() && param2.isEmpty())
|
|
return 1;
|
|
else if (param1.isEmpty())
|
|
return -1;
|
|
|
|
if (this.descending)
|
|
sortVal = param2.get().compareTo(param1.get());
|
|
else
|
|
sortVal = param1.get().compareTo(param2.get());
|
|
}
|
|
|
|
if (sortVal != 0)
|
|
return sortVal;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns true if a filter is defined, i.e. {@link ParameterBag ParameterBags} of type
|
|
* {@link ReportConstants#TYPE_FILTER}, a date range
|
|
*
|
|
* @return true if a filter is defined
|
|
*/
|
|
protected boolean hasFilter() {
|
|
return !this.filtersByPolicy.isEmpty() || this.dateRange != null || (this.filtersById != null
|
|
&& !this.filtersById.isEmpty());
|
|
}
|
|
|
|
protected boolean filterDirectCriteria(StrolchRootElement element) {
|
|
|
|
// do filtering by policies
|
|
for (ReportFilterPolicy filterPolicy : this.filtersByPolicy.keySet()) {
|
|
TypedTuple<StringParameter, StringParameter> refTuple = this.filtersByPolicy.get(filterPolicy);
|
|
if (refTuple.hasBoth()) {
|
|
// not applicable for direct criteria
|
|
continue;
|
|
}
|
|
|
|
// not for this element
|
|
if (!refTuple.getFirst().getUom().equals(element.getType()))
|
|
continue;
|
|
|
|
Object value = evaluateColumnValue(refTuple.getFirst(), Map.of(element.getType(), element), true);
|
|
if (this.filterMissingValuesAsTrue && value == null)
|
|
continue;
|
|
if (value == null || !filterPolicy.filter(value))
|
|
return false;
|
|
}
|
|
|
|
// then we do a filter by criteria
|
|
if (this.filtersById != null && !this.filtersById.isEmpty() && this.filtersById.containsSet(
|
|
element.getType())) {
|
|
|
|
if (!this.filtersById.getSet(element.getType()).contains(element.getId()))
|
|
return false;
|
|
}
|
|
|
|
// otherwise we want to keep this row
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the element is filtered, i.e. is to be kep, false if it should not be kept in the stream
|
|
*
|
|
* @param row
|
|
* the row to check if it is filtered
|
|
*
|
|
* @return if the element is filtered
|
|
*/
|
|
protected boolean filter(Map<String, StrolchRootElement> row) {
|
|
|
|
// do filtering by policies
|
|
for (ReportFilterPolicy filterPolicy : this.filtersByPolicy.keySet()) {
|
|
TypedTuple<StringParameter, StringParameter> refTuple = this.filtersByPolicy.get(filterPolicy);
|
|
|
|
if (refTuple.hasBoth()) {
|
|
Object value1 = evaluateColumnValue(refTuple.getFirst(), row, true);
|
|
Object value2 = evaluateColumnValue(refTuple.getSecond(), row, true);
|
|
|
|
if (this.filterMissingValuesAsTrue && (value1 == null || value2 == null))
|
|
continue;
|
|
if (value1 == null || value2 == null || !filterPolicy.filter(value1, value2))
|
|
return false;
|
|
|
|
} else {
|
|
Object value = evaluateColumnValue(refTuple.getFirst(), row, true);
|
|
if (this.filterMissingValuesAsTrue && value == null)
|
|
continue;
|
|
if (value == null || !filterPolicy.filter(value))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// do a date range selection, if required
|
|
if (this.dateRange != null) {
|
|
if (this.dateRangeSelP == null)
|
|
throw new IllegalStateException(
|
|
"DateRange defined, but reportRes does not defined a date range selector!");
|
|
|
|
String type = this.dateRangeSelP.getUom();
|
|
StrolchRootElement element = row.get(type);
|
|
if (element == null)
|
|
return false;
|
|
|
|
String dateRangeSel = this.dateRangeSelP.getValue();
|
|
|
|
ZonedDateTime date;
|
|
if (dateRangeSel.equals(COL_DATE)) {
|
|
date = element.accept(new ElementZdtDateVisitor());
|
|
} else {
|
|
Optional<Parameter<?>> param = lookupParameter(this.dateRangeSelP, element, false);
|
|
if (param.isEmpty() || param.get().getValueType() != StrolchValueType.DATE)
|
|
throw new IllegalStateException(
|
|
"Date Range selector is invalid, as referenced parameter is not a Date but "
|
|
+ (param.isPresent() ? param.get().getValueType() : "null"));
|
|
|
|
date = ((DateParameter) param.get()).getValueZdt();
|
|
}
|
|
|
|
if (!this.dateRange.contains(date))
|
|
return false;
|
|
}
|
|
|
|
// then we do a filter by criteria
|
|
if (this.filtersById != null && !this.filtersById.isEmpty()) {
|
|
for (String type : this.filtersById.keySet()) {
|
|
StrolchRootElement element = row.get(type);
|
|
if (element == null)
|
|
return false;
|
|
|
|
if (!this.filtersById.getSet(type).contains(element.getId()))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// otherwise we want to keep this row
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Evaluates the column value from the given column definition and row
|
|
*
|
|
* @param columnDefP
|
|
* the column definition
|
|
* @param row
|
|
* the row
|
|
* @param allowNull
|
|
* handles the return value if the lookup fails. If true, then null is returned, else the empty string is
|
|
* returned
|
|
*
|
|
* @return the column value
|
|
*/
|
|
protected Object evaluateColumnValue(StringParameter columnDefP, Map<String, StrolchRootElement> row,
|
|
boolean allowNull) {
|
|
|
|
String columnDef = columnDefP.getValue();
|
|
String refType = columnDefP.getUom();
|
|
|
|
// get the referenced object
|
|
StrolchRootElement column = row.get(refType);
|
|
|
|
Object columnValue;
|
|
|
|
if (column == null) {
|
|
columnValue = allowNull ? null : EMPTY;
|
|
} else if (columnDef.equals(COL_OBJECT)) {
|
|
columnValue = column;
|
|
} else if (columnDef.equals(COL_ID)) {
|
|
columnValue = column.getId();
|
|
} else if (columnDef.equals(COL_NAME)) {
|
|
columnValue = column.getName();
|
|
} else if (columnDef.equals(COL_TYPE)) {
|
|
columnValue = column.getType();
|
|
} else if (columnDef.equals(COL_STATE)) {
|
|
columnValue = column.accept(new ElementStateVisitor());
|
|
} else if (columnDef.equals(COL_DATE)) {
|
|
columnValue = column.accept(new ElementZdtDateVisitor());
|
|
} else if (columnDef.startsWith(COL_SEARCH)) {
|
|
Parameter<?> parameter = findParameter(columnDefP, column);
|
|
if (parameter == null)
|
|
columnValue = EMPTY;
|
|
else
|
|
columnValue = parameter;
|
|
} else {
|
|
columnValue = lookupParameter(columnDefP, column, allowNull) //
|
|
.orElseGet(() -> allowNull ? null : new StringParameter(columnDefP.getValue(), columnDef, ""));
|
|
}
|
|
|
|
return columnValue;
|
|
}
|
|
|
|
/**
|
|
* Finds a parameter given the column definition
|
|
*
|
|
* @param columnDefP
|
|
* the column definition
|
|
* @param column
|
|
* the element from which the parameter is to be retrieved
|
|
*
|
|
* @return the parameter, or null if it does not exist
|
|
*/
|
|
protected Parameter<Object> findParameter(StringParameter columnDefP, StrolchRootElement column) {
|
|
|
|
String columnDef = columnDefP.getValue();
|
|
|
|
String[] searchParts = columnDef.split(SEARCH_SEPARATOR);
|
|
if (searchParts.length != 3)
|
|
throw new IllegalStateException(
|
|
"Parameter search reference (" + columnDef + ") is invalid as it does not have 3 parts for "
|
|
+ columnDefP.getLocator());
|
|
|
|
String parentParamId = searchParts[1];
|
|
String paramRef = searchParts[2];
|
|
|
|
String[] locatorParts = paramRef.split(Locator.PATH_SEPARATOR);
|
|
if (locatorParts.length != 3)
|
|
throw new IllegalStateException(
|
|
"Parameter search reference (" + paramRef + ") is invalid as it does not have 3 parts for "
|
|
+ columnDefP.getLocator());
|
|
|
|
String bagKey = locatorParts[1];
|
|
String paramKey = locatorParts[2];
|
|
|
|
Optional<Parameter<Object>> parameter = tx().findParameterOnHierarchy(column, parentParamId, bagKey, paramKey);
|
|
return parameter.orElse(null);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the given parameter with the given parameter reference from the given column
|
|
*
|
|
* @param paramRefP
|
|
* the parameter reference
|
|
* @param element
|
|
* the element
|
|
*
|
|
* @return the {@link Optional} with the parameter
|
|
*/
|
|
protected Optional<Parameter<?>> lookupParameter(StringParameter paramRefP, StrolchRootElement element,
|
|
boolean overrideAllowMissingColumns) {
|
|
String paramRef = paramRefP.getValue();
|
|
|
|
String[] locatorParts = paramRef.split(Locator.PATH_SEPARATOR);
|
|
if (locatorParts.length != 3)
|
|
throw new IllegalStateException(
|
|
"Parameter reference (" + paramRef + ") is invalid as it does not have 3 parts for "
|
|
+ paramRefP.getLocator());
|
|
|
|
String bagKey = locatorParts[1];
|
|
String paramKey = locatorParts[2];
|
|
|
|
Parameter<?> param = element.getParameter(bagKey, paramKey);
|
|
if (!overrideAllowMissingColumns && !this.allowMissingColumns && param == null)
|
|
throw new IllegalStateException(
|
|
"Parameter reference (" + paramRef + ") for " + paramRefP.getLocator() + " not found on "
|
|
+ element.getLocator());
|
|
|
|
return Optional.ofNullable(param);
|
|
}
|
|
|
|
/**
|
|
* Returns a stream of {@link StrolchRootElement} which denote the rows of the report. This implementation uses
|
|
* {@link ReportConstants#PARAM_OBJECT_TYPE} to stream the initial rows
|
|
*
|
|
* @return the stream of {@link StrolchRootElement StrolchRootElement}
|
|
*/
|
|
protected Stream<? extends StrolchRootElement> queryRows() {
|
|
Stream<? extends StrolchRootElement> stream = getStreamFor(getObjectTypeParam());
|
|
return isParallel() ? stream.parallel() : stream;
|
|
}
|
|
|
|
protected StringParameter getObjectTypeParam() {
|
|
return this.reportRes.getStringP(BAG_PARAMETERS, PARAM_OBJECT_TYPE);
|
|
}
|
|
|
|
protected String getObjectType() {
|
|
return getObjectTypeParam().getValue();
|
|
}
|
|
|
|
protected boolean hasJoinOnType(String type) {
|
|
return (this.reportRes.hasParameterBag(BAG_JOINS) //
|
|
&& this.reportRes.getParameterBag(BAG_JOINS).hasParameter(type)) //
|
|
|| (this.reportRes.hasParameterBag(BAG_ADDITIONAL_TYPE) //
|
|
&& this.reportRes.getParameterBag(BAG_ADDITIONAL_TYPE).getString(PARAM_OBJECT_TYPE).equals(type)) //
|
|
|| (this.reportRes.hasParameterBag(BAG_ADDITIONAL_JOINS) //
|
|
&& this.reportRes.getParameterBag(BAG_ADDITIONAL_JOINS).hasParameter(type));
|
|
}
|
|
|
|
protected Stream<? extends StrolchRootElement> getStreamFor(StringParameter objectTypeP) {
|
|
switch (objectTypeP.getInterpretation()) {
|
|
case INTERPRETATION_RESOURCE_REF:
|
|
return tx().streamResources(objectTypeP.getUom());
|
|
case INTERPRETATION_ORDER_REF:
|
|
return tx().streamOrders(objectTypeP.getUom());
|
|
case INTERPRETATION_ACTIVITY_REF:
|
|
return tx().streamActivities(objectTypeP.getUom());
|
|
default:
|
|
throw new IllegalArgumentException("Unhandled element type " + objectTypeP.getInterpretation());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evaluates the row for the given element. The resulting {@link Map} contains the joins on all elements and the
|
|
* keys are the type of elements and values are the actual elements
|
|
*
|
|
* @param element
|
|
* the element from which the row is evaluated
|
|
*
|
|
* @return the {@link Map} of elements denoting the row for the given element
|
|
*/
|
|
protected Map<String, StrolchRootElement> evaluateRow(StrolchRootElement element) {
|
|
|
|
// interpretation -> Resource-Ref, etc.
|
|
// uom -> object type
|
|
// value -> element type where relation is defined for this join
|
|
|
|
// create the refs element
|
|
Map<String, StrolchRootElement> refs = new HashMap<>();
|
|
// and add the starting point
|
|
refs.put(element.getType(), element);
|
|
|
|
// now add all the joins
|
|
handleJoins(refs, BAG_JOINS);
|
|
|
|
return refs;
|
|
}
|
|
|
|
protected void handleJoins(Map<String, StrolchRootElement> refs, String joinBagId) {
|
|
ParameterBag joinBag = this.reportRes.getParameterBag(joinBagId);
|
|
if (joinBag != null && joinBag.hasParameters()) {
|
|
for (String paramId : joinBag.getParameterKeySet()) {
|
|
StringParameter joinP = joinBag.getParameter(paramId);
|
|
addColumnJoin(refs, joinBag, joinP, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the join with the given elements
|
|
*
|
|
* @param refs
|
|
* the current row, with any already retrieved joins
|
|
* @param joinBag
|
|
* the {@link ReportConstants#BAG_JOINS} {@link ParameterBag}
|
|
* @param joinP
|
|
* the join definition
|
|
* @param optional
|
|
* a boolean defining if the join my be missing
|
|
*
|
|
* @return the joined element, or null if it does not exist and {@code optional} is false
|
|
*/
|
|
protected StrolchRootElement addColumnJoin(Map<String, StrolchRootElement> refs, ParameterBag joinBag,
|
|
StringParameter joinP, boolean optional) {
|
|
|
|
String interpretation = joinP.getInterpretation();
|
|
String elementType = interpretation.substring(0, interpretation.indexOf(SUFFIX_REF));
|
|
String joinType = joinP.getUom();
|
|
String dependencyType = joinP.getValue();
|
|
|
|
// get dependency
|
|
StrolchRootElement dependency;
|
|
if (refs.containsKey(dependencyType)) {
|
|
dependency = refs.get(dependencyType);
|
|
} else {
|
|
// recursively find the dependency
|
|
StringParameter dependencyP = joinBag.getParameter(dependencyType);
|
|
if (dependencyP == null)
|
|
throw new IllegalStateException(
|
|
"The defined join dependency " + dependencyType + " does not exist for " + joinP.getLocator());
|
|
dependency = addColumnJoin(refs, joinBag, dependencyP, false);
|
|
if (dependency == null)
|
|
return null;
|
|
}
|
|
|
|
ParameterBag relationsBag = dependency.getParameterBag(BAG_RELATIONS);
|
|
if (relationsBag == null)
|
|
throw new IllegalStateException(
|
|
"Invalid join definition value: " + joinP.getValue() + " on: " + joinP.getLocator() + " as "
|
|
+ dependency.getLocator() + " has no ParameterBag " + BAG_RELATIONS);
|
|
|
|
List<Parameter<?>> relationParams = relationsBag.getParametersByInterpretationAndUom(interpretation, joinType)
|
|
.stream()
|
|
.filter(p -> p.getValueType() == StrolchValueType.STRING)
|
|
.collect(toList());
|
|
|
|
if (relationParams.isEmpty())
|
|
throw new IllegalStateException("Found no relation parameters with UOM " + joinType + " of type "
|
|
+ StrolchValueType.STRING.getType() + " on dependency " + dependency.getLocator());
|
|
if (relationParams.size() > 1)
|
|
throw new IllegalStateException(
|
|
"Found multiple possible relation parameters for UOM " + joinType + " on dependency "
|
|
+ dependency.getLocator());
|
|
|
|
Parameter<?> relationParam = relationParams.get(0);
|
|
StringParameter relationP = (StringParameter) relationParam;
|
|
if (relationP.getValue().isEmpty() && optional)
|
|
return null;
|
|
|
|
Locator locator = Locator.valueOf(elementType, joinType, relationP.getValue());
|
|
StrolchRootElement joinElem = tx().findElement(locator, true);
|
|
if (joinElem == null)
|
|
return null;
|
|
|
|
refs.put(joinType, joinElem);
|
|
return joinElem;
|
|
}
|
|
}
|