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 Strolch
* Reports
*
* @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 orderingParams;
protected Map filterCriteriaParams;
protected Set directCriteria;
protected boolean parallel;
protected boolean descending;
protected boolean allowMissingColumns;
protected boolean filterMissingValuesAsTrue;
protected List columnIds;
protected StringParameter dateRangeSelP;
protected DateRange dateRange;
protected Map> filtersByPolicy;
protected MapOfSets 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 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 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 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 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 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