package li.strolch.report.policy; import static java.util.Comparator.comparingInt; import static li.strolch.report.ReportConstants.*; import static li.strolch.utils.helper.StringHelper.EMPTY; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import li.strolch.agent.api.ComponentContainer; import li.strolch.model.*; import li.strolch.model.parameter.*; import li.strolch.model.policy.PolicyDef; import li.strolch.model.visitor.ElementDateVisitor; import li.strolch.model.visitor.ElementStateVisitor; import li.strolch.persistence.api.StrolchTransaction; import li.strolch.policy.PolicyHandler; import li.strolch.report.ReportElement; import li.strolch.runtime.StrolchConstants; import li.strolch.utils.ObjectHelper; import li.strolch.utils.collections.DateRange; import li.strolch.utils.collections.MapOfSets; import li.strolch.utils.iso8601.ISO8601FormatFactory; /** * @author Robert von Burg <eitch@eitchnet.ch> */ public class GenericReport extends ReportPolicy { private Resource reportRes; private boolean hideObjectTypeFromFilterCriteria; private String objectType; private ParameterBag columnsBag; private List orderingParams; private boolean descending; private boolean allowMissingColumns; private List columnIds; private StringParameter dateRangeSelP; private DateRange dateRange; private Map filtersByPolicy; private MapOfSets filtersById; public GenericReport(ComponentContainer container, StrolchTransaction tx) { super(container, tx); } public void initialize(String reportId) { // get the reportRes this.reportRes = tx().getResourceBy(TYPE_REPORT, reportId, true); BooleanParameter hideObjectTypeFromFilterCriteriaP = this.reportRes .getParameter(BAG_PARAMETERS, PARAM_HIDE_OBJECT_TYPE_FROM_FILTER_CRITERIA); this.hideObjectTypeFromFilterCriteria = hideObjectTypeFromFilterCriteriaP != null && hideObjectTypeFromFilterCriteriaP.getValue(); if (this.hideObjectTypeFromFilterCriteria) { this.objectType = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_OBJECT_TYPE).getValue(); } this.columnsBag = this.reportRes.getParameterBag(BAG_COLUMNS, true); this.columnIds = this.columnsBag.getParameters().stream() // .sorted(comparingInt(Parameter::getIndex)) // .map(StrolchElement::getId) // .collect(Collectors.toList()); if (this.reportRes.hasParameter(BAG_PARAMETERS, PARAM_DESCENDING)) this.descending = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_DESCENDING).getValue(); if (this.reportRes.hasParameter(BAG_PARAMETERS, PARAM_ALLOW_MISSING_COLUMNS)) this.allowMissingColumns = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_ALLOW_MISSING_COLUMNS) .getValue(); this.dateRangeSelP = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_DATE_RANGE_SEL); // evaluate ordering params if (this.reportRes.hasParameterBag(BAG_ORDERING)) { ParameterBag orderingBag = this.reportRes.getParameterBag(BAG_ORDERING); if (orderingBag.hasParameters()) { this.orderingParams = orderingBag.getParameters().stream().map(e -> (StringParameter) e) .collect(Collectors.toList()); this.orderingParams.sort(comparingInt(AbstractParameter::getIndex)); } } // evaluate filters this.filtersByPolicy = new HashMap<>(); List filterBags = this.reportRes.getParameterBagsByType(TYPE_FILTER); for (ParameterBag filterBag : filterBags) { // 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()); StringParameter fieldRefP = filterBag.getParameter(PARAM_FIELD_REF); this.filtersByPolicy.put(filterFunction, fieldRefP); } } public boolean hasDateRangeSelector() { return this.dateRangeSelP != null; } public GenericReport dateRange(DateRange dateRange) { this.dateRange = dateRange; return this; } public List getColumnKeys() { return this.columnIds; } 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; } 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; } 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; } public Stream> buildStream() { // query the main objects and return a stream Stream> stream = queryRows().map(this::evaluateRow); if (hasFilter()) stream = stream.filter(this::filter); if (hasOrdering()) stream = stream.sorted(this::sort); return stream; } public Stream doReport() { return buildStream().map(e -> new ReportElement(this.columnIds, columnId -> { StringParameter columnDefP = this.columnsBag.getParameter(columnId, true); Object value = evaluateColumnValue(columnDefP, e); if (value instanceof Date) return ISO8601FormatFactory.getInstance().formatDate((Date) value); if (value instanceof Parameter) return ((Parameter) value).getValueAsString(); return value.toString(); })); } protected boolean filterCriteria(StrolchRootElement element) { if (this.hideObjectTypeFromFilterCriteria) return !element.getType().equals(this.objectType); return true; } public MapOfSets generateFilterCriteria() { return buildStream() // .flatMap(e -> e.values().stream().filter(this::filterCriteria)) // .collect( // Collector.of( // (Supplier>) MapOfSets::new, // (m, e) -> m.addElement(e.getType(), e), // MapOfSets::addAll, // m -> m)); } protected boolean hasOrdering() { return this.orderingParams != null && !this.orderingParams.isEmpty(); } protected int sort(Map row1, Map 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); Object columnValue2 = evaluateColumnValue(fieldRefP, row2); if (this.descending) { sortVal = ObjectHelper.compare(columnValue1, columnValue2, true); } else { sortVal = ObjectHelper.compare(columnValue2, columnValue1, true); } } else { Optional> param1 = lookupParameter(fieldRefP, column1); Optional> param2 = lookupParameter(fieldRefP, column2); if (!param1.isPresent() && !param2.isPresent()) continue; if (param1.isPresent() && !param2.isPresent()) return 1; else if (!param1.isPresent()) return -1; if (this.descending) sortVal = param1.get().compareTo(param2.get()); else sortVal = param2.get().compareTo(param1.get()); } if (sortVal != 0) return sortVal; } return 0; } protected boolean hasFilter() { return !this.filtersByPolicy.isEmpty() || this.dateRange != null || (this.filtersById != null && !this.filtersById.isEmpty()); } protected boolean filter(Map row) { // do filtering by policies for (ReportFilterPolicy filterPolicy : this.filtersByPolicy.keySet()) { StringParameter fieldRefP = this.filtersByPolicy.get(filterPolicy); String type = fieldRefP.getUom(); StrolchRootElement column = row.get(type); // if column is null, then don't include in result if (column == null) return false; if (fieldRefP.getValue().startsWith("$")) { Object value = evaluateColumnValue(fieldRefP, row); if (!filterPolicy.filter(value)) return false; } else { Optional> param = lookupParameter(fieldRefP, column); if (param.isPresent() && !filterPolicy.filter(param.get())) 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(); Date date; if (dateRangeSel.equals(COL_DATE)) { date = element.accept(new ElementDateVisitor()); } else { Optional> param = lookupParameter(this.dateRangeSelP, element); if (!param.isPresent() || StrolchValueType.parse(param.get().getType()) != StrolchValueType.DATE) throw new IllegalStateException( "Date Range selector is invalid, as referenced parameter is not a Date but " + (param .isPresent() ? param.get().getType() : "null")); date = ((DateParameter) param.get()).getValue(); } 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; } protected Object evaluateColumnValue(StringParameter columnDefP, Map row) { String columnDef = columnDefP.getValue(); String refType = columnDefP.getUom(); // get the referenced object StrolchRootElement column = row.get(refType); Object columnValue; if (column == null) { columnValue = 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 ElementDateVisitor()); } else if (columnDef.startsWith(COL_SEARCH)) { Parameter parameter = findParameter(columnDefP, column); if (parameter == null) columnValue = EMPTY; else columnValue = parameter; } else { columnValue = lookupParameter(columnDefP, column).orElseGet(StringParameter::new); } return columnValue; } protected Parameter 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]; return findParameter(column, parentParamId, bagKey, paramKey); } protected Parameter findParameter(StrolchRootElement element, String parentParamId, String bagKey, String paramKey) { Parameter parameter = element.getParameter(bagKey, paramKey); if (parameter != null) return parameter; StringParameter parentRefP = element.getParameter(BAG_RELATIONS, parentParamId); if (parentRefP == null) return null; Resource parent = tx().getResourceBy(parentRefP); if (parent == null) return null; return findParameter(parent, parentParamId, bagKey, paramKey); } protected Optional> lookupParameter(StringParameter paramRefP, StrolchRootElement column) { 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 = column.getParameter(bagKey, paramKey); if (!allowMissingColumns && param == null) throw new IllegalStateException( "Parameter reference (" + paramRef + ") for " + paramRefP.getLocator() + " not found on " + column .getLocator()); return Optional.ofNullable(param); } protected Stream queryRows() { StringParameter objectTypeP = this.reportRes.getParameter(BAG_PARAMETERS, PARAM_OBJECT_TYPE); // find the type of object for which the reportRes is created switch (objectTypeP.getInterpretation()) { case StrolchConstants.INTERPRETATION_RESOURCE_REF: return tx().streamResources(objectTypeP.getUom()); case StrolchConstants.INTERPRETATION_ORDER_REF: return tx().streamOrders(objectTypeP.getUom()); case StrolchConstants.INTERPRETATION_ACTIVITY_REF: return tx().streamActivities(objectTypeP.getUom()); default: throw new IllegalArgumentException("Unhandled element type " + objectTypeP.getInterpretation()); } } protected Map evaluateRow(StrolchRootElement resource) { // interpretation -> Resource-Ref, etc. // uom -> object type // value -> element type where relation is defined for this join // create the refs element HashMap refs = new HashMap<>(); // and add the starting point refs.put(resource.getType(), resource); if (!this.reportRes.hasParameterBag(BAG_JOINS)) return refs; ParameterBag joinBag = this.reportRes.getParameterBag(BAG_JOINS); for (String paramId : joinBag.getParameterKeySet()) { StringParameter joinP = joinBag.getParameter(paramId); addColumnJoin(refs, joinBag, joinP, true); } return refs; } protected StrolchRootElement addColumnJoin(Map 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); 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> relationParams = relationsBag.getParametersByInterpretationAndUom(interpretation, joinType) .stream().filter(p -> p.getValueType() == StrolchValueType.STRING).collect(Collectors.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; } }