diff --git a/src/de/memtext/util/ServletHelper.java b/src/de/memtext/util/ServletHelper.java
index c3d53db..0058f00 100644
--- a/src/de/memtext/util/ServletHelper.java
+++ b/src/de/memtext/util/ServletHelper.java
@@ -1,78 +1,75 @@
-package de.memtext.util;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.xml.transform.ErrorListener;
-import javax.xml.transform.TransformerException;
-
-import de.superx.servlet.ServletBasics;
-
-/**
- * Abstrakte Basisklasse für ServletHelper-Objekt, nimmt request und response
- * und den Logger auf. Objekte, die erweitern müssen (wie bei Thread) run()
- * aufrufen. Dann wird die Authentifizierung geprüft und wenn die OK ist, wird
- * die perform()-Methode des eigentlichen Objekts aufgerufen (die z.B. Maske
- * aufbaut oder Tabelle holt). wenn eine Exception auftritt sorgt run() dafür,
- * dass der Fehler gemeldet wird. Als Variablen enthält diese Klasse bereits den
- * StringBuffer returnText mit dem geplanten Rückgabetext. Und userid mit der
- * UserId (wenn Authentifizierung OK)
- */
-public abstract class ServletHelper extends ServletBasics{
-
-
- public ServletHelper(HttpServletRequest request,
- HttpServletResponse response, String sessiontype)
- throws IOException {
- super(request,response,sessiontype);
- }
-
- /**
- * Prüft falls gewünscht die Authentifizierung (existiert eine
- * superx-Session) danach wird die abstrakte Methode perform aufgerufen. Die
- * meisten Exceptions werden also Infomeldung an den User weitergegeben
- *
- * @param isAuthentificationCheckWanted
- * @throws IOException
- */
- public void run(boolean isAuthentificationCheckWanted) throws IOException,
- ServletException {
- try {
- if (isAuthentificationCheckWanted) {
- checkSessionType();
- }
-
- perform();
-
- } catch (Exception e) {
-
- }
- }
-
- abstract protected void perform() throws Exception;
-
- class DummyErrorListener implements ErrorListener {
-
- public void warning(TransformerException exception)
- throws TransformerException {
- }
-
- public void error(TransformerException exception)
- throws TransformerException {
- }
-
- public void fatalError(TransformerException exception)
- throws TransformerException {
- System.out.println(exception);
- }
-
- }
-}
-
-//Created on 30.09.2004 at 08:59:42
-
-//Created on 27.02.2006 at 18:50:31
-
-//refactored to servletBasis 10.8.2011
+package de.memtext.util;
+
+import java.io.IOException;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import javax.xml.transform.ErrorListener;
+import javax.xml.transform.TransformerException;
+
+import de.superx.servlet.ServletBasics;
+
+/**
+ * Abstrakte Basisklasse für ServletHelper-Objekt, nimmt request und response
+ * und den Logger auf. Objekte, die erweitern müssen (wie bei Thread) run()
+ * aufrufen. Dann wird die Authentifizierung geprüft und wenn die OK ist, wird
+ * die perform()-Methode des eigentlichen Objekts aufgerufen (die z.B. Maske
+ * aufbaut oder Tabelle holt). wenn eine Exception auftritt sorgt run() dafür,
+ * dass der Fehler gemeldet wird. Als Variablen enthält diese Klasse bereits den
+ * StringBuffer returnText mit dem geplanten Rückgabetext. Und userid mit der
+ * UserId (wenn Authentifizierung OK)
+ */
+public abstract class ServletHelper extends ServletBasics {
+
+
+ public ServletHelper(HttpServletRequest request, HttpServletResponse response, String sessiontype) throws IOException {
+ super(request, response, sessiontype);
+ }
+
+ /**
+ * Prüft falls gewünscht die Authentifizierung (existiert eine
+ * superx-Session) danach wird die abstrakte Methode perform aufgerufen. Die
+ * meisten Exceptions werden also Infomeldung an den User weitergegeben
+ *
+ * @param isAuthentificationCheckWanted
+ * @throws IOException
+ */
+ public void run(boolean isAuthentificationCheckWanted) throws IOException, ServletException {
+ try {
+ if (isAuthentificationCheckWanted) {
+ checkSessionType();
+ }
+
+ perform();
+
+ } catch (Exception e) {
+
+ }
+ }
+
+ abstract protected void perform() throws Exception;
+
+ class DummyErrorListener implements ErrorListener {
+
+ @Override
+ public void warning(TransformerException exception) throws TransformerException {
+ }
+
+ @Override
+ public void error(TransformerException exception) throws TransformerException {
+ }
+
+ @Override
+ public void fatalError(TransformerException exception) throws TransformerException {
+ System.out.println(exception);
+ }
+
+ }
+}
+
+//Created on 30.09.2004 at 08:59:42
+
+//Created on 27.02.2006 at 18:50:31
+
+//refactored to servletBasis 10.8.2011
diff --git a/src/de/superx/bianalysis/ColumnElement.java b/src/de/superx/bianalysis/ColumnElement.java
new file mode 100644
index 0000000..4010411
--- /dev/null
+++ b/src/de/superx/bianalysis/ColumnElement.java
@@ -0,0 +1,88 @@
+package de.superx.bianalysis;
+
+import de.superx.bianalysis.models.DimensionAttribute;
+import de.superx.bianalysis.models.Measure;
+
+public class ColumnElement {
+
+ public String caption;
+ public String header;
+ public String dimensionAttributeFilter;
+ public Measure measure;
+ public int columnNumber;
+
+ public ColumnElement(String caption, String dimensionAttributeFilter) {
+ this.caption = caption;
+ this.dimensionAttributeFilter = dimensionAttributeFilter;
+ }
+
+ public ColumnElement(String caption, String dimensionAttributeFilter, Measure measure, int col) {
+ this.caption = caption;
+ this.dimensionAttributeFilter = dimensionAttributeFilter;
+ this.measure = measure;
+ this.columnNumber = col;
+ }
+
+ public ColumnElement(Measure measure, int index) {
+ this.caption = "Kennzahl|" + measure.getId().composedId;
+ this.header = "Kennzahl|" + measure.getCaption();
+ this.measure = measure;
+ this.columnNumber = index;
+ }
+
+ public ColumnElement(ColumnElement currentColumnElement) {
+ this.caption = currentColumnElement.caption;
+ this.dimensionAttributeFilter = currentColumnElement.dimensionAttributeFilter;
+ this.measure = currentColumnElement.measure;
+ }
+
+ /**
+ * Builds the attribute part of a columns's 'field' member.
+ *
+ * The attribute part is a crucial component of the column's identifier
+ * and typically consists of IDs and associated values.
+ *
+ *
Example of an attribute part:
+ *
+ * "conf:123 : conf:124 |weiblich"
+ *
+ *
+ *
+ * In the context of a complete 'field' member, it might appear as:
+ *
+ * "conf:123: conf:124|weiblich || Kennzahl|res:123"
+ *
+ * where the part before "||" is the attribute part, and after is the measure.
+ *
+ * The 'field' member serves as a unique identifier for each column.
+ *
+ * @see ColumnElementBuilder For the complete column building process
+ */
+ public static String buildField(DimensionAttribute attr, String value) {
+ // The conformed id takes precedence, so that we can merge reports
+ String attrId = attr.getAttrConformedId();
+ if(attrId == null) {
+ attrId = attr.getStringId();
+ }
+
+ String dimId = attr.getDimConformedId();
+ if(dimId == null) {
+ dimId = attr.getDimId();
+ }
+
+ return dimId + ": " + attrId + "|" + value;
+ }
+
+ public static String buildHeader(DimensionAttribute attr, String value) {
+ return attr.getCaption() + ": " + attr.getCaption() + "|" + value;
+ }
+
+ public static String buildFilter(DimensionAttribute attr, String value) {
+ return attr.getDimensionTableAlias() + "." + attr.getColumnname() + " = '" + value + "'";
+ }
+
+ public void setHeader(String finalHeader) {
+ this.header = finalHeader;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/ColumnElementBuilder.java b/src/de/superx/bianalysis/ColumnElementBuilder.java
new file mode 100644
index 0000000..b3a5dc2
--- /dev/null
+++ b/src/de/superx/bianalysis/ColumnElementBuilder.java
@@ -0,0 +1,157 @@
+package de.superx.bianalysis;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+import org.apache.log4j.Logger;
+
+import de.superx.bianalysis.models.DimensionAttribute;
+import de.superx.bianalysis.models.Filter;
+import de.superx.bianalysis.models.Measure;
+import de.superx.common.NotYetImplementedException;
+
+public class ColumnElementBuilder {
+
+ private static Logger logger = Logger.getLogger(ColumnElementBuilder.class);
+
+ /**
+ * Lets assume we have the two Dimensions X and Y each with one Attribute. DA for X
+ * and DB for Y. Both Attributes have two possible values DA1, DA2 and DB1, DB2. There
+ * also exist two Measures M1, M2.
+ *
+ * If the users wants to see all attributes and measures the header of the cross table looks like this:
+ *
+ * +-----------------------+----------------------+
+ * | DA1 | DA2 |
+ * +-----------+-----------+-----------+----------+
+ * | DB1 | DB2 | DB1 | DB2 |
+ * +-----+-----+-----+-----+-----+-----+-----+----+
+ * | M1 | M2 | M1 | M2 | M1 | M2 | M1 | M2 |
+ * +=====+=====+=====+=====+=====+=====+=====+====+
+ * | | | | | | | | |
+ * +-----+-----+-----+-----+-----+-----+-----+----+
+ *
+ * This header would be defined like follows:
+ *
+ * "X: DA | DA1 || Y: DB | DB1 || Kennzahl| M1"
+ * "X: DA | DA1 || Y: DB | DB1 || Kennzahl| M2"
+ * "X: DA | DA1 || Y: DB | DB2 || Kennzahl| M1"
+ * "X: DA | DA1 || Y: DB | DB2 || Kennzahl| M2"
+ * "X: DA | DA2 || Y: DB | DB1 || Kennzahl| M1"
+ * "X: DA | DA2 || Y: DB | DB1 || Kennzahl| M2"
+ * "X: DA | DA2 || Y: DB | DB2 || Kennzahl| M1"
+ * "X: DA | DA2 || Y: DB | DB2 || Kennzahl| M2"
+ *
+ * Every single line is represented by one 'ColumnElement'.
+ * @throws NotYetImplementedException
+ */
+ public static List buildColumnElements(ReportMetadata metadata) {
+
+ List filters = metadata.filters;
+ List measures = metadata.measures;
+ List dimensionAttributes = metadata.topDimensionAttributes;
+
+ List columnElements = new ArrayList();
+ final String HEADER_DIVIDER = " || ";
+ final String KENNZAHL_IDENTIFIER = "Kennzahl|";
+
+ if(measures == null || measures.isEmpty()) {
+ // edge case 1: no measures were selected, simply return empty columnElements list
+ return columnElements;
+ }
+
+ // for every column there exists an offset of 'maxbridgelvl' if a hierarchy-attribute was selected
+ int colStartPoint = metadata.getHierarchyAttributes().size() * metadata.maxBridgeLvl;
+ if(dimensionAttributes == null || dimensionAttributes.isEmpty()) {
+ // edge case 2: no dimension attributes were selected, only display the measures
+ for (Measure measure : measures) {
+ columnElements.add(new ColumnElement(measure, colStartPoint + columnElements.size()));
+ }
+ return columnElements;
+ }
+
+ // for every single column combination (one list of combined attribute values) we build one 'ColumnElement' object
+ List> dimAttrCombinations = cartesianProductOfDimensionAttributeValues(dimensionAttributes, filters);
+ for (int i = 0; i < dimAttrCombinations.size(); i++) {
+ StringJoiner captionJoiner = new StringJoiner(HEADER_DIVIDER);
+ StringJoiner headerJoiner = new StringJoiner(HEADER_DIVIDER);
+ StringJoiner filterJoiner = new StringJoiner(" AND ");
+ List comb = dimAttrCombinations.get(i);
+ for(int j = 0; j < comb.size(); j++) {
+ DimensionAttribute attr = dimensionAttributes.get(j);
+ String value = comb.get(j);
+ captionJoiner.add(ColumnElement.buildField(attr, value));
+ headerJoiner.add(ColumnElement.buildHeader(attr, value));
+ filterJoiner.add(ColumnElement.buildFilter(attr, value));
+ }
+ String partialCaption = captionJoiner.toString();
+ String partialHeader = headerJoiner.toString();
+ String filter = filterJoiner.toString();
+ for (Measure measure : measures) {
+ String finalCaption = partialCaption + HEADER_DIVIDER + KENNZAHL_IDENTIFIER + measure.getId().composedId;
+ String finalHeader = partialHeader + HEADER_DIVIDER + KENNZAHL_IDENTIFIER + measure.getCaption();
+ ColumnElement colElement = new ColumnElement(finalCaption, filter, measure, colStartPoint + columnElements.size());
+ colElement.setHeader(finalHeader);
+ columnElements.add(colElement);
+ }
+ }
+
+ return columnElements;
+ }
+
+ /**
+ *
+ * Computes all possible combination of dimension attribute values.
+ * Each of the individual combinations is a specific column.
+ *
+ * Example
+ * Input: DimensionAttributes = {DA, DB}, each with two possible values DA1, DA2, DB1, DB2, Filters = { }
+ * Output: {{DA1, DB1}, {DA1, DB2}, {DA2, DB1}, {DA2, DB2}}
+ *
+ * If the user choose the following Filter = {DB2}
+ * Output: {{DA1, DB2}, {DA2, DB2}}
+ *
+ * @param dimensionAttributes The list of choosen dimension attributes.
+ * @param filters The list of choosen filters.
+ * @return A list containing the cartesian product of all the possible combination for a set of dimension attributes and filters.
+ */
+ private static List> cartesianProductOfDimensionAttributeValues(List dimensionAttributes, List filters){
+ List> allDimAttrVals = new ArrayList<>();
+ for (DimensionAttribute attr: dimensionAttributes) {
+ //if(attr.bridge != null) {
+ // continue;
+ //}
+ // did the user choose a filter for this attribute ?
+ Filter compoundFilter = Filter.findFilterById(filters, attr.getId());
+ if(compoundFilter != null) {
+ // if yes only use the filter values
+ allDimAttrVals.add(compoundFilter.filterValues);
+ } else {
+ // if no use all possible attribute values
+ allDimAttrVals.add(attr.getDimensionAttributeValues());
+ }
+ }
+ // compute and return all possible column combinations
+ return cartesian(allDimAttrVals);
+ }
+
+ private static List> cartesian(List> lists) {
+ List> result = new ArrayList<>();
+ if(lists.size() == 0) {
+ result.add(new ArrayList<>());
+ return result;
+ }
+ List curr = lists.get(0);
+ List> remainingLists = cartesian(lists.subList(1, lists.size()));
+ for (String val : curr) {
+ for (List list : remainingLists) {
+ List resultList = new ArrayList<>();
+ resultList.add(val);
+ resultList.addAll(list);
+ result.add(resultList);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/de/superx/bianalysis/ExcelSheetBuilder.java b/src/de/superx/bianalysis/ExcelSheetBuilder.java
new file mode 100644
index 0000000..9d216bf
--- /dev/null
+++ b/src/de/superx/bianalysis/ExcelSheetBuilder.java
@@ -0,0 +1,415 @@
+package de.superx.bianalysis;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.Footer;
+import org.apache.poi.ss.usermodel.Header;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+import de.superx.bianalysis.models.InfoItem;
+import de.superx.rest.model.Column;
+import de.superx.rest.model.ColumnType;
+import de.superx.rest.model.Result;
+import de.superx.rest.model.ResultType;
+
+public class ExcelSheetBuilder {
+
+ private Result result;
+ private XSSFWorkbook workbook;
+ private XSSFSheet sheet;
+ private String reportName;
+ private String reportDescription;
+ private String date;
+ private int leftDimensionAttributes;
+ private int topDimensionAttributes = 0;
+
+ private List visibleColumns;
+ private final boolean mergeCells = true;
+ private final int startingRow = 1;
+
+ private static HashMap defaultStyles = new HashMap<>();
+
+ public ExcelSheetBuilder(Result result) {
+ this.result = result;
+ this.visibleColumns = getVisibleColumns(result);
+ this.workbook = new XSSFWorkbook();
+ initializeDefaultStyles();
+
+ leftDimensionAttributes = this.result.info.leftDimensionAttributes.size();
+ if(this.result.info.topDimensionAttributes != null && this.result.info.topDimensionAttributes.size() > 0) {
+ topDimensionAttributes = this.result.info.topDimensionAttributes.size();
+ }
+ }
+
+ public XSSFWorkbook build() {
+ int rowNum = startingRow;
+ rowNum = createRowsFromGrid(createReportInfoGrid(), rowNum);
+ rowNum += 2; // rows between report info and header
+ int reportInfoEnd = rowNum;
+
+ String[][] grid = createHeaderGrid();
+
+ rowNum = createRowsFromGrid(grid, rowNum);
+ rowNum = createDataRows(rowNum);
+ rowNum = createTotalRow(rowNum);
+
+ if(mergeCells) {
+ mergeHeaderCells(grid, 0, reportInfoEnd);
+ }
+
+ styleHeaderCells(reportInfoEnd, grid);
+ styleDataCells(reportInfoEnd + grid.length);
+ styleTotalRowCells(rowNum);
+ styleReportInfoCells();
+
+ Footer footer = sheet.getFooter();
+ Header header = sheet.getHeader();
+ header.setLeft(reportName);
+ header.setRight(this.date);
+ footer.setRight("Seite &P von &N");
+
+ return workbook;
+ }
+
+ private void initializeDefaultStyles() {
+ //ReportInfoCells
+ XSSFCellStyle infoStyle = workbook.createCellStyle();
+ Font infoFont = workbook.createFont();
+ infoFont.setBold(true);
+ infoStyle.setFont(infoFont);
+
+ defaultStyles.put("info", Integer.valueOf(infoStyle.getIndex()));
+
+ //Header
+ XSSFCellStyle headerStyle = workbook.createCellStyle();
+ headerStyle.setBorderBottom(BorderStyle.THIN);
+ headerStyle.setBorderLeft(BorderStyle.THIN);
+ headerStyle.setBorderRight(BorderStyle.THIN);
+ headerStyle.setBorderTop(BorderStyle.THIN);
+ Font headerFont = workbook.createFont();
+ headerFont.setBold(true);
+ headerStyle.setFont(headerFont);
+
+ defaultStyles.put("header", Integer.valueOf(headerStyle.getIndex()));
+
+ //Data
+ XSSFCellStyle dataStyle = workbook.createCellStyle();
+ dataStyle.setBorderBottom(BorderStyle.HAIR);
+ dataStyle.setBorderLeft(BorderStyle.HAIR);
+ dataStyle.setBorderRight(BorderStyle.HAIR);
+ dataStyle.setBorderTop(BorderStyle.HAIR);
+
+ defaultStyles.put("data", Integer.valueOf(dataStyle.getIndex()));
+
+ //Total
+ XSSFCellStyle totalStyle = workbook.createCellStyle();
+ totalStyle.setBorderBottom(BorderStyle.THIN);
+ totalStyle.setBorderLeft(BorderStyle.THIN);
+ totalStyle.setBorderRight(BorderStyle.THIN);
+ totalStyle.setBorderTop(BorderStyle.DOUBLE);
+ Font totalFont = workbook.createFont();
+ totalFont.setBold(true);
+ totalStyle.setFont(totalFont);
+
+ defaultStyles.put("total", Integer.valueOf(totalStyle.getIndex()));
+
+ }
+
+
+ private void styleTotalRowCells(int rowNum) {
+ int current = rowNum;
+ Row row = sheet.getRow(--current);
+ for (int i = 0; i < visibleColumns.size(); i++) {
+ Cell cell = row.getCell(i);
+ cell.setCellStyle(getTotalCellStyle(workbook));
+ }
+ }
+
+ private int createTotalRow(int startFrom) {
+ de.superx.rest.model.Row totalRow = result.getTotalRow();
+ int rowNum = startFrom;
+ Row row = sheet.createRow(rowNum++);
+ Cell labelCell = row.createCell(0);
+ labelCell.setCellValue("Gesamt");
+ for (int i = 1; i < visibleColumns.size(); i++) {
+ Column col = visibleColumns.get(i);
+ Cell cell = row.createCell(i);
+ if(col.type.equals(ColumnType.StringColumn)) {
+ cell.setCellValue("");
+ } else {
+ Object obj = totalRow.cells.get(col.field);
+ if(obj == null) {
+ cell.setCellValue("");
+ continue;
+ }
+ Double value = Double.valueOf(String.valueOf(obj));
+ cell.setCellValue(value.doubleValue());
+ }
+ }
+ return rowNum;
+ }
+
+ private void styleReportInfoCells() {
+ Row row = sheet.getRow(startingRow);
+ Cell cell = row.getCell(0);
+ cell.setCellStyle(workbook.getCellStyleAt(defaultStyles.get("info").intValue()));
+ }
+
+ private String[][] createReportInfoGrid() {
+
+ List> gridList = new ArrayList<>();
+ gridList.add(List.of("Informationen zur BI-Analyse", ""));
+ gridList.add(List.of("Name:", this.reportName));
+ gridList.add(List.of("Beschreibung:", this.reportDescription));
+
+ String sachgebiet = this.result.info.sachgebiete.stream().collect(Collectors.joining(", "));
+ String theme = getInfoCaptions(this.result.info.facttables);
+ String measures = getInfoCaptions(this.result.info.measures);
+ String topAttributes = getInfoCaptions(this.result.info.topDimensionAttributes);
+ String leftAttributes = getInfoCaptions(this.result.info.leftDimensionAttributes);
+ String filter = this.result.info.filter.stream().collect(Collectors.joining(", "));
+ String lastUpdateBad = this.result.info.lastUpdateBiad;
+
+ if(sachgebiet != null) {
+ gridList.add(List.of("Sachgebiet:", sachgebiet));
+ }
+ if(theme != null) {
+ gridList.add(List.of("Thema:", theme));
+ }
+ if(measures != null) {
+ gridList.add(List.of("Kennzahlen:", measures));
+ }
+ if(leftAttributes != null) {
+ gridList.add(List.of("Zeilenattribute:", leftAttributes));
+ }
+ if(topAttributes != null) {
+ gridList.add(List.of("Spaltenattribute:", topAttributes));
+ }
+ if(filter != null) {
+ gridList.add(List.of("Filter:", filter));
+ }
+ if(lastUpdateBad != null) {
+ gridList.add(List.of("Letztes Update von BI-Analyse-Daten:", lastUpdateBad));
+ }
+ if(result.resultType.equals(ResultType.FlatTable)) {
+ gridList.add(List.of("Tabellentyp:", "Flache Tabelle"));
+ } else if(result.resultType.equals(ResultType.DrilldownTableGroupable)) {
+ gridList.add(List.of("Tabellentyp:", "Hierarchische Tabelle"));
+ }
+ return listToStringGrid(gridList);
+ }
+
+ private static String getInfoCaptions(List infoItems) {
+ if(infoItems != null && infoItems.size() > 0) {
+ return infoItems.stream().map(f->f.caption).collect(Collectors.joining(", "));
+ }
+ return "";
+ }
+
+ private static String[][] listToStringGrid(List> list) {
+ String[][] result = new String[list.size()][list.get(0).size()];
+ for (int i = 0; i < result.length; i++) {
+ for (int j = 0; j < result[i].length; j++) {
+ result[i][j] = list.get(i).get(j);
+ }
+ }
+ return result;
+ }
+
+ private static CellRangeAddress mergeCellByOffset(int firstRow, int lastRow, int firstCol, int lastCol, int xOffset, int yOffset) {
+ return new CellRangeAddress(firstRow + yOffset, lastRow + yOffset, firstCol + xOffset, lastCol + xOffset);
+ }
+
+ private void styleHeaderCells(int start, String[][] grid) {
+ for (int i = 0; i < grid.length; i++) {
+ Row row = this.sheet.getRow(i+start);
+ row.setHeightInPoints((short) 25);
+ for (int j = 0; j < grid[i].length; j++) {
+ Cell cell = row.getCell(j);
+ cell.setCellStyle(getHeaderStyle(workbook));
+ }
+ }
+ }
+
+ private static CellStyle getHeaderStyle(XSSFWorkbook workbook) {
+ return workbook.getCellStyleAt(defaultStyles.get("header").intValue());
+ }
+
+ private static CellStyle getDataCellStyle(XSSFWorkbook workbook) {
+ return workbook.getCellStyleAt(defaultStyles.get("data").intValue());
+ }
+
+ private static CellStyle getTotalCellStyle(XSSFWorkbook workbook) {
+ return workbook.getCellStyleAt(defaultStyles.get("total").intValue());
+ }
+
+ private void styleDataCells(int startDataCells) {
+ for (int i = startDataCells; i < startDataCells + this.result.rows.size(); i++) {
+ Row row = this.sheet.getRow(i);
+ for (int j = 0; j < this.visibleColumns.size(); j++) {
+ Cell cell = row.getCell(j);
+ if(this.visibleColumns.get(j).groupable) {
+ cell.setCellStyle(getHeaderStyle(workbook));
+ } else {
+ cell.setCellStyle(getDataCellStyle(workbook));
+ }
+ }
+ }
+ }
+
+ private void mergeHeaderCells(String grid[][], int xOffset, int yOffset) {
+ // merge header grid cells
+ if(topDimensionAttributes > 0) {
+ for(int i = 0; i < grid.length; i++) {
+ String lastCell = "";
+ int cellsToMerge = 0;
+ for (int j = 0; j < grid[i].length; j++) {
+ String currentCell = grid[i][j];
+ if(!currentCell.equals(lastCell) && cellsToMerge > 0) {
+ sheet.addMergedRegion(mergeCellByOffset(i, i, j - cellsToMerge - 1, j - 1, xOffset, yOffset));
+ }
+ if(currentCell.equals(lastCell)) {
+ cellsToMerge++;
+ } else {
+ cellsToMerge = 0;
+ }
+ lastCell = currentCell;
+ }
+ if(cellsToMerge > 0) {
+ int j = grid[i].length - 1;
+ sheet.addMergedRegion(mergeCellByOffset(i, i, j - cellsToMerge, j, xOffset, yOffset));
+ }
+ }
+ }
+
+ // merge left header cols
+ if(grid.length > 1) {
+ for (int i = 0; i < leftDimensionAttributes; i++) {
+ sheet.addMergedRegion(mergeCellByOffset(0, grid.length - 1, i, i, xOffset, yOffset));
+ }
+ }
+ }
+
+ private int createRowsFromGrid(String[][] grid, int startFrom) {
+ if(grid == null) {
+ return startFrom;
+ }
+ int rowNum = startFrom;
+ for (int i = 0; i < grid.length; i++) {
+ Row poiRow = sheet.createRow(rowNum++);
+ int colNum = 0;
+ for (int j = 0; j < grid[i].length; j++) {
+ Cell cell = poiRow.createCell(colNum++);
+ if(grid[i][j] != null && !grid[i][j].isBlank()) {
+ cell.setCellValue(grid[i][j]);
+ } else {
+ cell.setBlank();
+ }
+ }
+ }
+ return rowNum;
+ }
+
+ private int createDataRows(int startFrom) {
+ int rowNum = startFrom;
+ // build cells from row data without sumrow
+ List resultRows = result.rows.stream().filter(r -> r.aggregated != -1).collect(Collectors.toList());
+ Row[] rows = new Row[resultRows.size()];
+ for (int i = 0; i < resultRows.size(); i++) {
+ rows[i] = sheet.createRow(rowNum++);
+ rows[i].setHeightInPoints((short) 20);
+ }
+ for (int i = 0; i < visibleColumns.size(); i++) {
+ Column col = visibleColumns.get(i);
+ for(int j = 0; j < resultRows.size(); j++) {
+ Object obj = resultRows.get(j).cells.get(col.field);
+ String objVal = String.valueOf(obj);
+ Cell cell = rows[j].createCell(i);
+ if(obj == null) {
+ cell.setBlank();
+ continue;
+ }
+ if(col.type == ColumnType.IntegerColumn || col.type == ColumnType.DecimalColumn) {
+ Double value = Double.valueOf(objVal);
+ cell.setCellValue(value.doubleValue());
+ } else {
+ cell.setCellValue(obj.toString());
+ }
+ //if(col.groupable) {
+ // cell.setCellStyle(style);
+ //}
+ }
+ }
+ return rowNum;
+ }
+
+ private String[][] createHeaderGrid(){
+ int colSize = visibleColumns.size();
+ int rowSize = topDimensionAttributes + 1;
+ String[][] grid = new String[rowSize][colSize];
+
+ for(int i = 0; i < colSize; i++) {
+ Column column= this.visibleColumns.get(i);
+ String[] columnHeader = column.header.split("\\|\\|");
+ boolean isLeftDimensionAttributeColumn = (columnHeader.length == 1 && !columnHeader[0].contains("|"))? true : false;
+ for(int j = 0; j < rowSize; j++) {
+ if(isLeftDimensionAttributeColumn) {
+ grid[j][i] = columnHeader[0];
+ }else {
+ String header = columnHeader[j];
+ String[] headerValues = header.split("\\|");
+ grid[j][i] = headerValues[1];
+ }
+ }
+ }
+ return grid;
+ }
+
+ public ExcelSheetBuilder withFileName(String name) {
+ this.sheet = workbook.createSheet(name);
+ return this;
+ }
+
+ public ExcelSheetBuilder withReportName(String name) {
+ this.reportName = replaceEmptyString(name, "Nicht gespeicherte BI-Analyse");
+ return this;
+ }
+
+ public ExcelSheetBuilder withDescription(String description) {
+ this.reportDescription = replaceEmptyString(description, "-");
+ return this;
+ }
+
+ private static String replaceEmptyString(String value, String replacement) {
+ if(value == null || value.isBlank()) {
+ return replacement;
+ }
+ return value;
+ }
+
+ public ExcelSheetBuilder withDate(Date currentDate) {
+ this.date = new SimpleDateFormat("dd.MM.yyyy HH:mm").format(currentDate);
+ return this;
+ }
+
+ private static List getVisibleColumns(Result result) {
+ return result.columns
+ .stream()
+ .filter(col -> !col.hidden)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/src/de/superx/bianalysis/FaultyMetadataException.java b/src/de/superx/bianalysis/FaultyMetadataException.java
new file mode 100644
index 0000000..39dad05
--- /dev/null
+++ b/src/de/superx/bianalysis/FaultyMetadataException.java
@@ -0,0 +1,21 @@
+package de.superx.bianalysis;
+
+import de.superx.bianalysis.metadata.Identifier;
+
+public class FaultyMetadataException extends RuntimeException {
+
+ private static final long serialVersionUID = -5959640234409065198L;
+
+ public FaultyMetadataException(String message) {
+ super(message);
+ }
+
+ public FaultyMetadataException(Identifier id) {
+ super("Metadata Object with ID: '" + id.composedId + "' does not exist.");
+ }
+
+ public FaultyMetadataException(Identifier id, String metaType) {
+ super("Metadata " + metaType + " with ID: '" + id.composedId + "' does not exist.");
+ }
+
+}
diff --git a/src/de/superx/bianalysis/ReportDefinition.java b/src/de/superx/bianalysis/ReportDefinition.java
new file mode 100644
index 0000000..b5aa319
--- /dev/null
+++ b/src/de/superx/bianalysis/ReportDefinition.java
@@ -0,0 +1,57 @@
+package de.superx.bianalysis;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.models.Filter;
+import de.superx.bianalysis.service.DbMetaAdapter;
+
+public class ReportDefinition {
+
+ public List factTableIds;
+ public List leftDimensionAttributeIds;
+ public List topDimensionAttributeIds;
+ public List measureIds;
+ public List filters;
+ public boolean hideEmptyColumns;
+
+ public ReportDefinition() {
+ this.factTableIds = new ArrayList<>();
+ this.leftDimensionAttributeIds = new ArrayList<>();
+ this.topDimensionAttributeIds = new ArrayList<>();
+ this.measureIds = new ArrayList<>();
+ this.filters = new ArrayList<>();
+ this.hideEmptyColumns = false;
+ }
+
+ public ReportDefinition(ReportDefinition definition) {
+ super();
+ this.factTableIds = definition.factTableIds;
+ this.topDimensionAttributeIds = definition.topDimensionAttributeIds;
+ this.measureIds = definition.measureIds;
+ this.filters = definition.filters;
+ this.leftDimensionAttributeIds = new ArrayList<>();
+ this.hideEmptyColumns = definition.hideEmptyColumns;
+
+ }
+
+ public ReportMetadata getReportMetadata(DbMetaAdapter dbAdapter, Identifier factTableId) {
+ ReportMetadata reportMetadata = new ReportMetadata(this, factTableId, dbAdapter);
+ return reportMetadata;
+ }
+
+ public static List getAttributesForDefinitions(List definitions){
+ List ids = new ArrayList<>();
+ for (ReportDefinition def : definitions) {
+ for (Identifier id : def.topDimensionAttributeIds) {
+ ids.add(id);
+ }
+ for (Identifier id : def.leftDimensionAttributeIds) {
+ ids.add(id);
+ }
+ }
+ return ids;
+ }
+
+}
\ No newline at end of file
diff --git a/src/de/superx/bianalysis/ReportMetadata.java b/src/de/superx/bianalysis/ReportMetadata.java
new file mode 100644
index 0000000..4e2d405
--- /dev/null
+++ b/src/de/superx/bianalysis/ReportMetadata.java
@@ -0,0 +1,338 @@
+package de.superx.bianalysis;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.models.DimensionAttribute;
+import de.superx.bianalysis.models.FactTable;
+import de.superx.bianalysis.models.Filter;
+import de.superx.bianalysis.models.InfoItem;
+import de.superx.bianalysis.models.Measure;
+import de.superx.bianalysis.service.DbMetaAdapter;
+import de.superx.jdbc.entity.Sachgebiet;
+
+public class ReportMetadata {
+
+ public final FactTable factTable;
+ public final Sachgebiet sachgebiet;
+ public final List leftDimensionAttributes;
+ public final List topDimensionAttributes;
+ public final List measures;
+ public final List filters;
+
+ public String lastBiadUpdate;
+
+ // only used if hierarchy dimension is present in left dim attributes
+ public int maxBridgeLvl;
+ public int minBridgeLvl;
+
+ public DbMetaAdapter dbMetaAdapter;
+
+ public boolean hideEmptyColumns;
+
+ public ReportMetadata(ReportDefinition reportDefinition, Identifier factTableId, DbMetaAdapter dbAdapter) {
+ this.dbMetaAdapter = dbAdapter;
+ if(factTableId == null) { // merged Report
+ this.factTable = new FactTable();
+ this.sachgebiet = new Sachgebiet();
+ } else {
+ this.factTable = dbAdapter.getFactTable(factTableId);
+ this.sachgebiet = dbAdapter.getSachgebietById(this.factTable.getSachgebiettid());
+ }
+ List databaseOrderedLeftDimensionAttributes = dbAdapter.getDimensionAttributeMetadata(reportDefinition.leftDimensionAttributeIds, factTableId);
+ this.leftDimensionAttributes = reorderDimensionAttributesToReportOrder(databaseOrderedLeftDimensionAttributes, reportDefinition, false);
+ List databaseOrderedTopDimensionAttributes = dbAdapter.getDimensionAttributeMetadata(reportDefinition.topDimensionAttributeIds, factTableId);
+ this.topDimensionAttributes = reorderDimensionAttributesToReportOrder(databaseOrderedTopDimensionAttributes, reportDefinition, true);
+ List databaseOrderedMeasures = dbAdapter.getMeasureMetadata(reportDefinition.measureIds);
+ this.measures = reorderMeasuresToReportOrder(databaseOrderedMeasures, reportDefinition);
+ if (reportDefinition.filters != null) {
+ this.filters = dbAdapter.getFilterMetadata(reportDefinition.filters);
+ } else {
+ this.filters = new ArrayList();
+ }
+ this.setTopDimensionAttributeValues(dbAdapter);
+ if(factTableId != null) {
+ this.setMaxBridgeLvl();
+ } else {
+ // for merged report
+ this.setMaxBridgeLvlForConformed(reportDefinition.factTableIds);
+ }
+ this.lastBiadUpdate = dbAdapter.getLastUpdate(440);
+ this.hideEmptyColumns = reportDefinition.hideEmptyColumns;
+ }
+
+ public ReportMetadata(ReportMetadata metadata, List leftDimensionAttributes) {
+ this.dbMetaAdapter = metadata.dbMetaAdapter;
+ this.factTable = metadata.factTable;
+ this.sachgebiet = metadata.sachgebiet;
+ this.topDimensionAttributes = metadata.topDimensionAttributes;
+ this.measures = metadata.measures;
+ this.filters = metadata.filters;
+ this.leftDimensionAttributes = leftDimensionAttributes;
+ this.hideEmptyColumns = metadata.hideEmptyColumns;
+ }
+
+ public ReportMetadata() {
+ this.factTable = new FactTable();
+ this.sachgebiet = new Sachgebiet();
+ this.leftDimensionAttributes = new ArrayList<>();
+ this.topDimensionAttributes = new ArrayList<>();
+ this.measures = new ArrayList<>();
+ this.filters = new ArrayList<>();
+ }
+
+ public List getSortOrderLeftDimensionAttributes(){
+ return leftDimensionAttributes.stream().filter(d -> d.getSortOrderColumn() != null).collect(Collectors.toList());
+ }
+
+ private void setMaxBridgeLvl() {
+ List attrs = leftDimensionAttributes
+ .stream()
+ .filter(a -> a.isHierarchy() )
+ .collect(Collectors.toList());
+ if(attrs.size() > 1) {
+ throw new RuntimeException("NOT YET IMPLEMENTED: There can only be one hierarchy attribute.");
+ }
+ if(!attrs.isEmpty()) {
+ this.maxBridgeLvl = dbMetaAdapter.getBridgeMaxLevel(attrs.get(0), this);
+ this.minBridgeLvl = dbMetaAdapter.getBridgeMinLevel(getHierarchyFilter(), this.maxBridgeLvl, attrs.get(0).getTablename());
+ }
+ }
+
+ private void setMaxBridgeLvlForConformed(List factTableIds) {
+ List attrs = leftDimensionAttributes
+ .stream()
+ .filter(a -> a.isHierarchy())
+ .collect(Collectors.toList());
+ if(!attrs.isEmpty()) {
+ DimensionAttribute attr = attrs.get(0);
+ int lvl = 0;
+ for (Identifier fact : factTableIds) {
+ String name = dbMetaAdapter.getFactTableNameMaxBridgeLvl(fact, attr.getId());
+ if(name == null || name.isBlank()) {
+ continue;
+ }
+ int value = -1;
+ Identifier checkedAttr = dbMetaAdapter.checkIfFactTableHasDimensionAttribute(attr.getId(), fact);
+ if (checkedAttr != null && !checkedAttr.equals(attr.getId())) {
+ DimensionAttribute rolePlayingAttribute = dbMetaAdapter.getDimensionAttributeMetadataById(checkedAttr);
+ value = dbMetaAdapter.getBridgeMaxLevel(rolePlayingAttribute, this, name);
+ }
+ if (value > lvl) {
+ lvl = value;
+ }
+ }
+ this.maxBridgeLvl = lvl;
+ }
+ }
+
+ private void setTopDimensionAttributeValues(DbMetaAdapter dbAdapter) {
+ for(DimensionAttribute attr : this.topDimensionAttributes) {
+ Filter filter = getFilterForDimensionAttribute(attr.getId());
+ if(filter != null) {
+ attr.setDimensionAttributeValues(filter.filterValues);
+ } else {
+ attr.setDimensionAttributeValues(dbAdapter.getDimensionAttributeValues(attr, null, null));
+ }
+ }
+ }
+
+ private Filter getFilterForDimensionAttribute(Identifier id) {
+ return this.filters
+ .stream()
+ .filter(f -> f.dimensionAttributeId.equals(id))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static List reorderMeasuresToReportOrder(List measures, ReportDefinition reportDefinition) {
+ List orderedMeasures = new ArrayList();
+ reportDefinition.measureIds.forEach(measureId -> {
+ Measure nextMeasure = measures
+ .stream()
+ .filter( measure -> measure.getId().equals( measureId ) )
+ .findFirst()
+ .orElse(null);
+ orderedMeasures.add(nextMeasure);
+ });
+ return orderedMeasures;
+ }
+
+ public static List reorderDimensionAttributesToReportOrder(List dimensionAttributes, ReportDefinition reportDefinition, boolean isTopAttribute) {
+ List orderedDimensionAttributes = new ArrayList();
+ List attributeIds;
+ if (isTopAttribute) {
+ attributeIds = reportDefinition.topDimensionAttributeIds;
+ } else {
+ attributeIds = reportDefinition.leftDimensionAttributeIds;
+ }
+ attributeIds.forEach(attributeId -> {
+ DimensionAttribute nextAttribute = dimensionAttributes
+ .stream()
+ .filter( dimensionAttribute -> dimensionAttribute.getId().equals( attributeId ))
+ .findFirst()
+ .orElse(null);
+ orderedDimensionAttributes.add(nextAttribute);
+ });
+ return orderedDimensionAttributes;
+ }
+
+
+ public DimensionAttribute getDimById(Identifier id) {
+ DimensionAttribute attr = topDimensionAttributes
+ .stream()
+ .filter(a -> a.getDimensionId().equals(id))
+ .findFirst()
+ .orElse(null);
+
+ if(attr != null) {
+ return attr;
+ }
+
+ attr = leftDimensionAttributes
+ .stream()
+ .filter(a -> a.getDimensionId().equals(id))
+ .findFirst()
+ .orElse(null);
+
+ return attr;
+ }
+
+ public DimensionAttribute getDimAttrById(Identifier id) {
+ DimensionAttribute attr = topDimensionAttributes
+ .stream()
+ .filter(a -> a.getId().equals(id))
+ .findFirst()
+ .orElse(null);
+
+ if(attr != null) {
+ return attr;
+ }
+
+ attr = leftDimensionAttributes
+ .stream()
+ .filter(a -> a.getId().equals(id))
+ .findFirst()
+ .orElse(null);
+
+ return attr;
+ }
+
+ /**
+ * We want to join dimension tables only once. There we need a list of unique ids of
+ * the dimensions, otherwise we duplicate our joins.
+ */
+ public List getUniqueDimensionAttributes(){
+
+ Map joinTables = new HashMap();
+
+ for (DimensionAttribute attr : leftDimensionAttributes) {
+ if(!joinTables.containsKey(attr.getDimensionTableAlias())) {
+ joinTables.put(attr.getDimensionTableAlias(), attr);
+ }
+ }
+
+ for (DimensionAttribute attr : topDimensionAttributes) {
+ if(!joinTables.containsKey(attr.getDimensionTableAlias())) {
+ joinTables.put(attr.getDimensionTableAlias(), attr);
+ }
+ }
+
+ // join dimension if attribute occurs in filter
+ for (Filter filter : filters) {
+ Identifier attrId = filter.dimensionAttributeId;
+ DimensionAttribute attr = dbMetaAdapter.getDimensionAttributeMetadataById(attrId);
+ if(!joinTables.containsKey(attr.getDimensionTableAlias())) {
+ joinTables.put(attr.getDimensionTableAlias(), attr);
+ }
+ }
+
+ // join dimension if measure has a filter for an attribute
+ for (Measure measure : measures) {
+ if(measure.filterAttributeId != null) {
+ Identifier attrId = measure.filterAttributeId;
+ DimensionAttribute attr = dbMetaAdapter.getDimensionAttributeMetadataById(attrId);
+ if(!joinTables.containsKey(attr.getDimensionTableAlias())) {
+ joinTables.put(attr.getDimensionTableAlias(), attr);
+ }
+ }
+ }
+
+ return new ArrayList(joinTables.values());
+ }
+
+ public List getMeasureInfo() {
+ if(measures != null && !measures.isEmpty()) {
+ return measures.stream().map(m->
+ new InfoItem(m.getId().composedId, m.getCaption(), m.getDescription())).collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ public List getTopDimAttrAsInfo() {
+ if(topDimensionAttributes != null && !topDimensionAttributes.isEmpty()) {
+ return topDimensionAttributes.stream().map(m->
+ new InfoItem(m.getStringId(), m.getCaption(), m.getDescription())).collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ public List getFilterNoHierarchy() {
+ List filterNoBridge = new ArrayList<>();
+ for (Filter filter : this.filters) {
+ DimensionAttribute attr = dbMetaAdapter.getDimensionAttributeById(filter.dimensionAttributeId);
+ if(!attr.isHierarchy()) {
+ filterNoBridge.add(filter);
+ }
+ }
+ return filterNoBridge;
+ }
+
+ public List getLeftDimAttrAsInfo() {
+ if(leftDimensionAttributes != null && !leftDimensionAttributes.isEmpty()) {
+ return leftDimensionAttributes.stream().map(m->
+ new InfoItem(m.getStringId(), m.getCaption(), m.getDescription())).collect(Collectors.toList());
+ }
+ return null;
+ }
+
+ public List getFilterAsInfo(){
+ ArrayList filterList = new ArrayList();
+ DimensionAttribute dimAttr;
+ for (Filter filter: filters) {
+ dimAttr = filter.getDimAttribute(this);
+ if(dimAttr == null) {
+ dimAttr = dbMetaAdapter.getDimensionAttributeById(filter.dimensionAttributeId);
+ }
+ filterList.add(" (" + dimAttr.getCaption() + ") " + filter.getValuesAsString());
+ }
+ return filterList;
+ }
+
+ public List getHierarchyAttributes() {
+ return leftDimensionAttributes
+ .stream()
+ .filter(a -> a.isHierarchy())
+ .collect(Collectors.toList());
+ }
+
+ public List getHierarchyFilter(){
+ List hierarchyFilter = new ArrayList<>();
+ for (Filter filter : this.filters) {
+ if(isHierarchyFilter(filter)) {
+ hierarchyFilter.add(filter);
+ }
+ }
+ return hierarchyFilter;
+ }
+
+ public boolean isHierarchyFilter(Filter filter) {
+ return dbMetaAdapter.isAttributeHierarchyBridge(filter.dimensionAttributeId);
+ }
+
+}
\ No newline at end of file
diff --git a/src/de/superx/bianalysis/ResultBuilder.java b/src/de/superx/bianalysis/ResultBuilder.java
new file mode 100644
index 0000000..037ffcb
--- /dev/null
+++ b/src/de/superx/bianalysis/ResultBuilder.java
@@ -0,0 +1,536 @@
+package de.superx.bianalysis;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.sql.DataSource;
+
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+
+import de.superx.bianalysis.models.DimensionAttribute;
+import de.superx.bianalysis.models.InfoItem;
+import de.superx.bianalysis.models.Measure;
+import de.superx.rest.model.Column;
+import de.superx.rest.model.ColumnType;
+import de.superx.rest.model.Item;
+import de.superx.rest.model.Result;
+import de.superx.rest.model.ResultType;
+import de.superx.rest.model.Row;
+
+public class ResultBuilder {
+
+ private static final boolean IGNORE_SELF_LOOPS = true;
+
+ private DataSource dataSource;
+
+ private ReportMetadata reportMetadata;
+ private List columnElements;
+
+ Logger logger = Logger.getLogger(ResultBuilder.class);
+
+ public ResultBuilder() {}
+
+ // used for testing
+ public ResultBuilder(ReportMetadata metadata, List columns) {
+ this.reportMetadata = metadata;
+ this.columnElements = columns;
+ }
+
+ public ResultBuilder(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public void setReportMetadata(ReportMetadata reportMetadata) {
+ this.reportMetadata = reportMetadata;
+ }
+
+ public void setColumnElements(List columnElements) {
+ this.columnElements = columnElements;
+ }
+
+ private Row buildRowCells(ResultSet rs) {
+ Row row = new Row();
+ Map cells = new TreeMap();
+ if (reportMetadata.leftDimensionAttributes != null && !reportMetadata.leftDimensionAttributes.isEmpty()) {
+ int aggregationLvl = reportMetadata.leftDimensionAttributes.size() -1;
+ for (DimensionAttribute dimensionAttribute : reportMetadata.leftDimensionAttributes) {
+ if(dimensionAttribute.isHierarchy()) {
+ try {
+ String prevLbl = "";
+ int countLvl = 0;
+ aggregationLvl += reportMetadata.maxBridgeLvl - reportMetadata.minBridgeLvl - 1;
+ for (int i = reportMetadata.minBridgeLvl; i < reportMetadata.maxBridgeLvl; i++) {
+ Object cell = rs.getObject("col" + i);
+ String curLbl = (String) cell;
+
+ if(cell == null) {
+ // An empty cell means a lower aggregation level because
+ // of how the GROUP BY ROLLUP works.
+ aggregationLvl--;
+ }
+
+ if(IGNORE_SELF_LOOPS &&
+ curLbl != null &&
+ curLbl != "" &&
+ curLbl.equals(prevLbl)) {
+ // If the cell label is equal to the previous cell label
+ // then this row contains a self loop, meaning the node is
+ // both its own parent and child. This happens due to the
+ // GROUP BY ROLLUP part of the sql statement, which groups
+ // columns in which the same node can appear right next to
+ // each other in two columns.
+ continue;
+ }
+
+ String id = dimensionAttribute.getAttrConformedId();
+ if(id == null) {
+ id = dimensionAttribute.getStringId();
+ }
+
+ String cellKey = id + " (Ebene " + countLvl + ")";
+
+ if(countLvl == 0) {
+ cellKey = dimensionAttribute.getAttrConformedId();
+ }
+
+ if(cell != null && cellKey != null) {
+ cells.put(cellKey, cell);
+ row.rowKey += cellKey + cell;
+ countLvl++;
+ }
+
+ prevLbl = (String) cell;
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ } else {
+ try {
+ Object val = rs.getObject(dimensionAttribute.getDimensionColumnAlias());
+ if(val != null) {
+ String id = dimensionAttribute.getAttrConformedId();
+ if(id == null) {
+ id = dimensionAttribute.getStringId();
+ }
+ cells.put(id, val);
+ row.rowKey += id + val;
+ } else {
+ aggregationLvl--;
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ if(dimensionAttribute.getSortOrderColumn() != null) {
+ try {
+ String id = dimensionAttribute.getAttrConformedId();
+ if(id == null) {
+ id = dimensionAttribute.getStringId();
+ }
+ cells.put(id + "_sorting", rs.getObject(dimensionAttribute.getDimensionColumnAlias()+"_"+dimensionAttribute.getSortOrderColumn()));
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ try {
+ row.aggregated = aggregationLvl;
+ if(row.aggregated == -1) {
+ int colNum = this.reportMetadata.maxBridgeLvl + this.columnElements.size() - this.reportMetadata.measures.size();
+ for (Measure measure : reportMetadata.measures) {
+ cells.put(getTotalCellHeaderPrefix(reportMetadata) + measure.getId().composedId, rs.getObject("col" + (colNum++)));
+ }
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+
+ }
+ if (!columnElements.isEmpty()) {
+ for (ColumnElement columnElement : columnElements) {
+ try {
+ cells.put(columnElement.caption, rs.getObject("col" + columnElement.columnNumber));
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ row.cells = cells;
+ return row;
+ }
+
+
+ public static List buildColumns(ReportMetadata reportMetadata, List columnElements) {
+ List columns = new ArrayList();
+ if (reportMetadata.leftDimensionAttributes != null) {
+ reportMetadata.leftDimensionAttributes.forEach((dimensionAttribute) -> {
+ String id = dimensionAttribute.getAttrConformedId();
+ if(id == null) {
+ id = dimensionAttribute.getStringId();
+ }
+ if(dimensionAttribute.isHierarchy()) {
+ int cnt = 0;
+ for (int i = reportMetadata.minBridgeLvl; i < reportMetadata.maxBridgeLvl - 1; i++) {
+ if(cnt == 0) {
+ columns.add(new Column(id, dimensionAttribute.getCaption(), ColumnType.StringColumn, true));
+ } else {
+ String caption = id + " (Ebene " + cnt + ")";
+ String header = dimensionAttribute.getCaption() + " (Ebene " + (cnt) + ")";
+ columns.add(new Column(caption, header, ColumnType.HierarchyLevelColumn, true, id, false));
+ }
+ cnt += 1;
+ }
+ } else {
+ if(dimensionAttribute.getSortOrderColumn() != null) {
+ columns.add(new Column(id, dimensionAttribute.getCaption(), ColumnType.StringColumn, true, dimensionAttribute.getStringId() + "_sorting", false));
+ columns.add(new Column(id + "_sorting", dimensionAttribute.getCaption(), ColumnType.SortOrderColumn, false, dimensionAttribute.getStringId(), true));
+ }else {
+ columns.add(new Column(id, dimensionAttribute.getCaption(), ColumnType.StringColumn, true));
+ }
+ }
+ });
+ }
+ if (!columnElements.isEmpty()) {
+ columnElements.forEach((columnElement) -> {
+ Column col = new Column(columnElement.caption, columnElement.header, columnElement.measure.getMeasureType(), false);
+ col.aggregation = columnElement.measure.getAggregationType();
+ columns.add(col);
+ });
+ }
+ return columns;
+ }
+
+ public List getRowsForReport(String sqlStatement, Connection con) {
+
+ List rows = null;
+ try (Statement stmt = con.createStatement();
+ ResultSet rs = stmt.executeQuery(sqlStatement)) {
+ rows = buildRowList(rs);
+ } catch (SQLException e) {
+ System.out.println(sqlStatement);
+ throw new RuntimeException(e);
+ }
+
+ return rows;
+ }
+
+ public List buildRowList(ResultSet rs) throws SQLException {
+ List rows = new ArrayList<>();
+ while (rs.next()) {
+ Row row = buildRowCells(rs);
+ rows.add(row);
+ }
+ List result = new ArrayList<>();
+ if (reportMetadata.getHierarchyAttributes().size() > 0) {
+ HashMap keys = new HashMap<>();
+ for (Row row : rows) {
+ if (keys.containsKey(row.rowKey)) {
+ Row currentRow = keys.get(row.rowKey );
+ if (currentRow.aggregated > row.aggregated) {
+ keys.put(currentRow.rowKey , row);
+ }
+ // workaround: fix multiple aggregated rows, take only highest
+ if(currentRow.aggregated == -1 && row.aggregated == -1) {
+ for(String cell : currentRow.cells.keySet()) {
+ Number currentCellVal = (Number) currentRow.cells.get(cell);
+ Number candidateVal = (Number) row.cells.get(cell);
+ if(candidateVal.doubleValue() > currentCellVal.doubleValue()) {
+ keys.put(currentRow.rowKey, row);
+ }
+ }
+ }
+ } else {
+ keys.put(row.rowKey, row);
+ }
+ }
+ rows = new ArrayList(keys.values());
+ }
+
+ for (Row row : rows) {
+ if(row != null) {
+ result.add(row);
+ }
+ }
+ return rows;
+ }
+
+ public Result buildReport(List- sqlStatements, boolean isCreateRight) {
+ JdbcTemplate jt = new JdbcTemplate(dataSource);
+ Result report;
+ report = new Result();
+ if(isCreateRight) {
+ report.info.setSqlStatements(sqlStatements);
+ }
+
+ String sql = findByLabel(sqlStatements, "noAggregatesSQL").value;
+ String sqlColumnTotal = findByLabel(sqlStatements, "totalsColumnSQL").value;
+
+ List columns = buildColumns(reportMetadata, columnElements);
+ List
rows = null;
+ List totalColumns = null;
+
+ try (Connection con = jt.getDataSource().getConnection()){
+ rows = getRowsForReport(sql, con);
+ } catch (Exception e) {
+ logger.error(e);
+ e.printStackTrace();
+ report.info.setSegmentCaption(reportMetadata.factTable.getCaption());
+ report.info.setErrorMessage(e.getCause().getMessage());
+ return report;
+ }
+
+ // add one column for each measure to the report with the total sum
+ if(!reportMetadata.topDimensionAttributes.isEmpty()) {
+ try {
+ totalColumns = getTotalColumnResult(sqlColumnTotal, jt);
+ ResultBuilder.setTotalColumnToColumns(columns, reportMetadata);
+ ResultBuilder.setTotalColumnToRows(rows, totalColumns);
+ } catch (Exception e) {
+ e.printStackTrace();
+ report.info.setErrorMessage("Die Gesamtspalte konnte nicht ermittelt werden.");
+ }
+ }
+
+ setAttributesToReport(report, reportMetadata, rows, columns);
+
+ try {
+ if(reportMetadata.hideEmptyColumns) {
+ removeEmptyColumns(columns, rows);
+ }
+ } catch(Exception e ) {
+ logger.error(e);
+ }
+
+ return report;
+ }
+
+ public static void removeEmptyColumns(List columns, List rows) {
+ HashMap map = new HashMap<>();
+ for (Column col : columns) {
+ if(!map.containsKey(col.field)) {
+ map.put(col.field, Integer.valueOf(-1));
+ }
+ }
+
+ for (Row row : rows) {
+ for (String cellKey : row.cells.keySet()) {
+ Object value = row.cells.get(cellKey);
+ if(value instanceof Number) {
+ Number val = (Number) value;
+ if(val.intValue() != 0) {
+ if(map.containsKey(cellKey)) {
+ map.remove(cellKey);
+ }
+ }
+ } else {
+ if(value == null) {
+ continue;
+ }
+ if(map.containsKey(cellKey)) {
+ map.remove(cellKey);
+ }
+ }
+ }
+ }
+
+
+ for (Row row : rows) {
+ for(String key : map.keySet()) {
+ if(row.cells.containsKey(key)) {
+ row.cells.remove(key);
+ }
+ }
+ }
+
+ if(rows.size() > 1) {
+ for(Iterator iterator = columns.iterator(); iterator.hasNext(); ) {
+ if(map.containsKey(iterator.next().field))
+ iterator.remove();
+ }
+ }
+ }
+
+ public static void setAttributesToReport(Result report, ReportMetadata reportMetadata, List rows, List columns) {
+ report.setResultType(ResultType.DrilldownTableGroupable);
+ report.setRows(rows);
+ report.setColumns(columns);
+ if(reportMetadata.factTable.getCaption() != null) {
+ report.info.setSegmentCaption(reportMetadata.factTable.getCaption());
+ InfoItem facttableInfo = new InfoItem (reportMetadata.factTable.getId().composedId,
+ reportMetadata.factTable.getCaption(),
+ reportMetadata.factTable.getDescription());
+ report.info.addFacttable(facttableInfo);
+ report.info.addSachgebiet(reportMetadata.sachgebiet.name);
+ }
+ report.info.setMeasures(reportMetadata.getMeasureInfo());
+ report.info.setLeftDimensionAttributes(reportMetadata.getLeftDimAttrAsInfo());
+ report.info.setTopDimensionAttributes(reportMetadata.getTopDimAttrAsInfo());
+ report.info.setFilter(reportMetadata.getFilterAsInfo());
+ report.info.setLastUpdateBiad(reportMetadata.lastBiadUpdate);
+ report.info.hideEmptyColumns(reportMetadata.hideEmptyColumns);
+ }
+
+ private static String getTotalCellHeaderPrefix(ReportMetadata reportMetadata) {
+ String totalCellHeaderPrefix = "";
+ for (int i = 0; i < reportMetadata.topDimensionAttributes.size(); i++) {
+ DimensionAttribute attr = reportMetadata.topDimensionAttributes.get(i);
+ if(i == 0) {
+ totalCellHeaderPrefix += ColumnElement.buildField(attr, "Gesamt");
+ } else {
+ totalCellHeaderPrefix += ColumnElement.buildField(attr, " ");
+ }
+ }
+ totalCellHeaderPrefix += " || Kennzahl|";
+ return totalCellHeaderPrefix;
+ }
+
+ private static String getTotalCellHeaderPrefixHeader(ReportMetadata reportMetadata) {
+ String totalCellHeaderPrefix = "";
+ for (int i = 0; i < reportMetadata.topDimensionAttributes.size(); i++) {
+ DimensionAttribute attr = reportMetadata.topDimensionAttributes.get(i);
+ if(i == 0) {
+ totalCellHeaderPrefix += attr.getCaption() + ": " + attr.getCaption() + "| Gesamt ";
+ } else {
+ totalCellHeaderPrefix += " || " + attr.getCaption() + ": " + attr.getCaption() + "| ";
+ }
+ }
+ totalCellHeaderPrefix += " || Kennzahl|";
+ return totalCellHeaderPrefix;
+ }
+
+ public List getTotalColumnResult(String sqlStatement, JdbcTemplate jt) {
+ if(sqlStatement.isEmpty()) {
+ return null;
+ }
+ List rows = null;
+ rows = jt.query(sqlStatement, new Object[0], new RowMapper() {
+ @Override
+ public Row mapRow(ResultSet rs, int rowNum) {
+ Row row = new Row();
+ Map cells = new TreeMap();
+ int numCols = reportMetadata.maxBridgeLvl;
+ try {
+ for (DimensionAttribute attr : reportMetadata.leftDimensionAttributes) {
+ if(attr.isHierarchy()) {
+ String prevCell = "";
+ for (int i = reportMetadata.minBridgeLvl; i < reportMetadata.maxBridgeLvl; i++) {
+ Object cell = rs.getObject("col" + i);
+ if(cell == null || cell.equals(prevCell)) {
+ continue;
+ }
+ String cellKey = attr.getStringId() + " (Ebene " + i + ")";
+ if(i == 0) {
+ cellKey = attr.getStringId();
+ }
+ row.rowKey += cellKey + cell;
+ prevCell = (String) cell;
+ }
+ } else {
+ Object val = rs.getObject(attr.getDimensionColumnAlias());
+ if(val != null) {
+ String id = attr.getAttrConformedId();
+ if(id == null) {
+ id = attr.getStringId();
+ }
+ cells.put(id, val);
+ row.rowKey += id + val;
+ }
+ }
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ for (Measure measure : reportMetadata.measures) {
+ try {
+ String key = getTotalCellHeaderPrefix(reportMetadata) + measure.getId().composedId;
+ Object val = rs.getObject("col" + numCols++);
+ cells.put(key, val);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+ row.cells = cells;
+ return row;
+ }
+ });
+
+ if(reportMetadata.getHierarchyAttributes().size() > 0) {
+ HashMap rowKeyValue = new HashMap<>();
+ for (Row row : rows) {
+ boolean replace = false;
+ if(rowKeyValue.containsKey(row.rowKey)) {
+ Row alreadyThere = rowKeyValue.get(row.rowKey);
+ for(String key : alreadyThere.cells.keySet()) {
+ Number candidateVal = (Number) row.cells.get(key);
+ if(candidateVal == null) {
+ continue;
+ }
+ Number alreadyVal = (Number) alreadyThere.cells.get(key);
+ if(alreadyVal == null) {
+ continue;
+ }
+ if(candidateVal.doubleValue() > alreadyVal.doubleValue()) {
+ replace = true;
+ }
+ }
+ if(replace) {
+ rowKeyValue.put(row.rowKey, row);
+ replace = false;
+ }
+ } else {
+ rowKeyValue.put(row.rowKey, row);
+ }
+ }
+ return new ArrayList<>(rowKeyValue.values());
+ }
+
+ return rows;
+ }
+
+ public static void setTotalColumnToRows(List rows, List result) {
+ for (Row row : rows) {
+ for (Row r : result) {
+ if(r.rowKey.equals(row.rowKey)) {
+ row.cells.putAll(r.cells);
+ }
+ }
+ }
+ //for (int i = 0; i < rows.size(); i++) {
+ // Row row = rows.get(i);
+ // if(row.aggregated == -1) {
+ // continue;
+ // }
+ // row.cells.putAll(result.get(i).cells);
+ //}
+ }
+
+ public static void setTotalColumnToColumns(List columns, ReportMetadata reportMetadata) {
+ for (Measure measure : reportMetadata.measures) {
+ String field = getTotalCellHeaderPrefix(reportMetadata) + measure.getId().composedId;
+ String header = getTotalCellHeaderPrefixHeader(reportMetadata) + measure.getCaption();
+ Column col = new Column(field, header, measure.getMeasureType(), false);
+ col.setHidden(true);
+ col.setTotalColumn(true);
+ columns.add(col);
+ }
+
+ }
+
+ private static Item findByLabel(List- items, String label) {
+ return items.stream()
+ .filter(s -> s.label.equals(label))
+ .findAny()
+ .get();
+ }
+
+}
diff --git a/src/de/superx/bianalysis/ResultMerger.java b/src/de/superx/bianalysis/ResultMerger.java
new file mode 100644
index 0000000..45a3990
--- /dev/null
+++ b/src/de/superx/bianalysis/ResultMerger.java
@@ -0,0 +1,158 @@
+package de.superx.bianalysis;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.models.DimensionAttribute;
+import de.superx.bianalysis.models.FactTable;
+import de.superx.bianalysis.models.Filter;
+import de.superx.bianalysis.models.InfoItem;
+import de.superx.bianalysis.service.DbMetaAdapter;
+import de.superx.jdbc.entity.Sachgebiet;
+import de.superx.rest.model.Column;
+import de.superx.rest.model.Result;
+import de.superx.rest.model.Row;
+
+public class ResultMerger {
+
+ private DbMetaAdapter dbAdapter;
+
+ public ResultMerger(DbMetaAdapter dbAdapter) {
+ this.dbAdapter = dbAdapter;
+ }
+
+ public Result buildMergedReport(ReportDefinition definition, List reportResults) {
+
+ Result result = new Result();
+ ReportMetadata metadata = new ReportMetadata(definition, null, dbAdapter);
+
+ List columnElements = ColumnElementBuilder.buildColumnElements(metadata);
+ List columns = ResultBuilder.buildColumns(metadata, columnElements);
+ if(!metadata.topDimensionAttributes.isEmpty()) {
+ ResultBuilder.setTotalColumnToColumns(columns, metadata);
+ }
+
+ // create list of merged rows
+ List
> allRows = Result.getRowsFromReports(reportResults);
+ List rows = mergeRows(allRows);
+
+ if(metadata.hideEmptyColumns) {
+ ResultBuilder.removeEmptyColumns(columns, rows);
+ }
+
+ ResultBuilder.setAttributesToReport(result, metadata, rows, columns);
+
+ // override merge report specific attributes
+ result.setSubResults(reportResults);
+ List factTablesInfo = getFactTablesAsInfo(dbAdapter, definition.factTableIds);
+ result.info.setSegmentCaption(factTablesInfo.stream().map(f -> f.caption).collect(Collectors.joining(", ")));
+ result.info.setSachgebiete(getSachgebieteAsInfo(dbAdapter, definition.factTableIds));
+ result.info.setFacttables(factTablesInfo);
+
+ for (Result r : reportResults) {
+ if(r.info.error != null && !r.info.error.isBlank()) {
+ result.info.setErrorMessage(r.info.error);
+ break;
+ }
+ }
+ return result;
+ }
+
+ private static List getSachgebieteAsInfo(DbMetaAdapter dbAdapter, List factTableIds) {
+ List sachgebiete = new ArrayList();
+ List tids = new ArrayList<>();
+ for (Identifier id : factTableIds) {
+ FactTable factTable = dbAdapter.getFactTable(id);
+ Sachgebiet sachgebiet = dbAdapter.getSachgebietById(factTable.getSachgebiettid());
+ Integer tid = sachgebiet.tid;
+ if(!tids.contains(tid)) {
+ tids.add(tid);
+ sachgebiete.add(sachgebiet.name.trim());
+ }
+ }
+ return sachgebiete;
+ }
+
+ private static List getFactTablesAsInfo(DbMetaAdapter dbAdapter, List factTableIds) {
+ List facttables = new ArrayList();
+ for (Identifier id : factTableIds) {
+ FactTable factTable = dbAdapter.getFactTable(id);
+ facttables.add(new InfoItem(factTable.getId().composedId, factTable.getCaption(), factTable.getDescription()));
+ }
+ return facttables;
+ }
+
+ public ReportDefinition createFactTableSpecificReportDefinition(ReportDefinition reportDefinition,
+ Identifier factTableId) {
+ ReportDefinition definition = new ReportDefinition();
+ definition.hideEmptyColumns = reportDefinition.hideEmptyColumns;
+ definition.factTableIds.add(factTableId);
+ for (int i = 0; i < reportDefinition.leftDimensionAttributeIds.size(); i++) {
+ Identifier attr = reportDefinition.leftDimensionAttributeIds.get(i);
+ Identifier checkedAttr = dbAdapter.checkIfFactTableHasDimensionAttribute(attr, factTableId);
+ if(checkedAttr != null) {
+ definition.leftDimensionAttributeIds.add(checkedAttr);
+ }
+ }
+ for (int i = 0; i < reportDefinition.topDimensionAttributeIds.size(); i++) {
+ Identifier attr = reportDefinition.topDimensionAttributeIds.get(i);
+ Identifier checkedAttr = dbAdapter.checkIfFactTableHasDimensionAttribute(attr, factTableId);
+ if(checkedAttr != null) {
+ definition.topDimensionAttributeIds.add(checkedAttr);
+ }
+ }
+ for (int i = 0; i < reportDefinition.measureIds.size(); i++) {
+ Identifier measure = reportDefinition.measureIds.get(i);
+ if(dbAdapter.checkIfFactTableHasMeasure(measure, factTableId)) {
+ definition.measureIds.add(measure);
+ }
+ }
+ for (int i = 0; i < reportDefinition.filters.size(); i++) {
+ Filter filter = reportDefinition.filters.get(i);
+ Identifier checkedAttr = dbAdapter.checkIfFactTableHasDimensionAttribute(filter.dimensionAttributeId, factTableId);
+ if(checkedAttr != null) {
+ Filter roleFilter = new Filter(filter);
+ roleFilter.dimensionAttributeId = checkedAttr;
+ definition.filters.add(roleFilter);
+ }
+ }
+ return definition;
+ }
+
+ public static List mergeRows(List> rows) {
+ List result = new ArrayList<>();
+
+ for (List inputRows : rows) {
+ for (Row row : inputRows) {
+
+ Row rowRepl = new Row(row.aggregated);
+ rowRepl.rowKey = row.rowKey;
+
+ for(String key : row.cells.keySet()){
+ String newKey = key;
+ rowRepl.cells.put(newKey, row.cells.get(key));
+ }
+
+ if(!result.contains(rowRepl)) {
+ result.add(rowRepl);
+ } else {
+ // row with the same rowkey exists -> add only cells
+ Row found = result.get(result.indexOf(rowRepl));
+ for(String key : rowRepl.cells.keySet()){
+ if(!found.cells.containsKey(key)) {
+ found.cells.put(key, rowRepl.cells.get(key));
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/StoredReport.java b/src/de/superx/bianalysis/StoredReport.java
new file mode 100644
index 0000000..c04614f
--- /dev/null
+++ b/src/de/superx/bianalysis/StoredReport.java
@@ -0,0 +1,82 @@
+package de.superx.bianalysis;
+
+import java.util.ArrayList;
+
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+
+import de.superx.rest.model.Result;
+import de.superx.rest.model.TreeNode;
+
+@Table(value ="metadata\".\"rw_report_definitions")
+public class StoredReport {
+
+ @Id
+ public int id;
+
+ public String name;
+
+ public String description;
+
+ public String definition;
+
+ @Column(value = "show_total_column")
+ @JsonProperty("show_total_column")
+ public int showTotalColumn;
+
+ @Transient
+ public Boolean isReadOnly = Boolean.FALSE;
+
+ @Transient
+ public ReportDefinition reportDefinition;
+
+ @Transient
+ public Result exportedResult;
+
+ @Transient
+ public ArrayList hierarchy;
+
+ public StoredReport(String name, ReportDefinition reportDefinition, Result exportedResult) {
+ super();
+ this.name = name;
+ this.reportDefinition = reportDefinition;
+ this.exportedResult = exportedResult;
+ }
+
+ public StoredReport() {
+ super();
+ }
+
+ public static void setReportDefinitionJson(StoredReport report) {
+ ObjectWriter ow = new ObjectMapper().writer();
+ String reportDefinitionJson = null;
+ try {
+ reportDefinitionJson = ow.writeValueAsString(report.reportDefinition);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ report.definition = reportDefinitionJson;
+ }
+
+ public static void setReportDefinitionFromJson(StoredReport report) {
+ ObjectMapper mapper = JsonMapper.builder().findAndAddModules().build();
+ ReportDefinition reportDefinition = null;
+ try {
+ reportDefinition = mapper.readValue(report.definition, ReportDefinition.class);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ report.reportDefinition = reportDefinition;
+ report.definition = "";
+ }
+
+}
diff --git a/src/de/superx/bianalysis/bin/BiAnalysisCLI.java b/src/de/superx/bianalysis/bin/BiAnalysisCLI.java
new file mode 100644
index 0000000..68ccd17
--- /dev/null
+++ b/src/de/superx/bianalysis/bin/BiAnalysisCLI.java
@@ -0,0 +1,302 @@
+package de.superx.bianalysis.bin;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.core.util.DefaultIndenter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.metadata.MetaImport;
+import de.superx.bianalysis.metadata.MetaJson;
+import de.superx.bianalysis.metadata.MetadataImporter;
+import de.superx.bianalysis.metadata.models.json.MetaDimension;
+import de.superx.bianalysis.metadata.models.json.MetaDimensionAttribute;
+import de.superx.bianalysis.metadata.models.json.MetaFact;
+import de.superx.bianalysis.metadata.models.json.MetaMeasure;
+import de.superx.bianalysis.metadata.models.json.MetaObject;
+import de.superx.bianalysis.metadata.models.yml.MetaYml;
+import de.superx.servlet.SuperXManager;
+import de.superx.util.PathAndFileUtils;
+
+public class BiAnalysisCLI {
+
+ private static final String DEFAULT_RELEASE_BRANCH = "2025_12";
+
+ public static void main(String[] args) throws IOException {
+ Options options = createOptions();
+ CommandLine parsedArgs = readArgs(args, options);
+ if(parsedArgs.hasOption("-i")) {
+ addMissingIdsInMetadataDir(parsedArgs);
+ } else if(parsedArgs.hasOption("-m")) {
+ convertJsonFilesToSql();
+ } else if(parsedArgs.hasOption("-y")) {
+ generateYmlForJsonFile(parsedArgs);
+ } else if(parsedArgs.hasOption("-d")) {
+ generateWikiMarkdown(parsedArgs);
+ } else {
+ printHelp(options);
+ }
+ }
+
+ private static void generateWikiMarkdown(CommandLine parsedArgs) throws IOException {
+ SuperXManager.setWEB_INFPfad(PathAndFileUtils.getWebinfPath());
+ String facttable = parsedArgs.getOptionValue("d");
+ String filePath = PathAndFileUtils.getReportGeneratorDir("hisinone");
+ String ymlPath = PathAndFileUtils.getDbtTransformDirectory("hisinone") + File.separator + "docs_and_tests";
+ MetadataImporter importer = new MetadataImporter(ymlPath);
+ Logger.getLogger(MetadataImporter.class).setLevel(Level.ERROR);
+ importer.deserializeMetadataFromJsonFiles(filePath);
+
+ String docDirectory = PathAndFileUtils.getModulePath("biad");
+ docDirectory = String.join(File.separator, docDirectory, "conf", "his1", "edustore_doc");
+
+ for(MetaFact fact : importer.getAllFactTables()) {
+ if("all".equals(facttable) || fact.getFacttable().equals(facttable)) {
+ PrintWriter writer = new PrintWriter(docDirectory + File.separator + fact.getFacttable() + "_mediawiki.txt", "UTF-8");
+ writer.println("===Kennzahlen ===");
+ writer.println(";Kennzahlen " + fact.getCaption());
+ writer.println();
+ writer.println("{| class=\"wikitable\"\n ! Kennzahl !! Beschreibung");
+ writer.println("|-");
+ for(int i = 0; i < fact.getMeasures().size(); i++) {
+ MetaMeasure m = fact.getMeasures().get(i);
+ writer.println("| "+m.getCaption());
+ writer.println("| "+m.getDescription());
+ if(i != fact.getMeasures().size() - 1) {
+ writer.println("|-");
+
+ }
+ }
+ writer.println("|-\n|}");
+ writer.println();
+ writer.println();
+
+ writer.println("===Dimension ===");
+ writer.println(";"+fact.getCaption());
+ writer.println(":"+fact.getDescription());
+ writer.println();
+ writer.println("'''Dimension und Dimensionsattribut'''");
+ writer.println();
+ for(MetaDimension d : fact.getDimensions()) {
+
+ String dimCaption = d.getCaption();
+ if((dimCaption == null || dimCaption.isBlank()) && d.getConformedDimension() != null) {
+ dimCaption = d.getConformedDimension().getCaption();
+ }
+
+ String dimDescription = d.getDescription();
+ if((dimDescription == null || dimDescription.isBlank()) && d.getConformedDimension() != null) {
+ dimDescription = d.getConformedDimension().getDescription();
+ }
+
+ writer.println(";"+dimCaption);
+ writer.println(":"+dimDescription);
+
+ List attributes = d.getAttributes();
+ if(attributes == null || attributes.size() == 0) {
+ attributes = d.getConformedDimension().getAttributes();
+ }
+
+ for(MetaDimensionAttribute a : attributes) {
+ String caption = a.getCaption() == null ? a.getConfDimAttrRef().getCaption() : a.getCaption();
+ writer.println("*"+caption);
+ try {
+ String desc = a.getDescription() == null ? a.getConfDimAttrRef().getDescription() : a.getDescription();
+ if(!desc.equals("null")) {
+ writer.println("*:"+desc);
+ }
+ } catch (Exception e) {
+ // TODO: handle exception
+ }
+ }
+ writer.println();
+ }
+ writer.close();
+ }
+ }
+
+ }
+
+ private static void generateYmlForJsonFile(CommandLine parsedArgs) {
+ String file = parsedArgs.getOptionValue("y");
+ if(file == null || !new File(file).exists()) {
+ throw new RuntimeException("File " + file +" is not valid.");
+ }
+ MetadataImporter importer = new MetadataImporter();
+ Logger.getLogger(MetadataImporter.class).setLevel(Level.ERROR);
+ importer.setShouldReadYMLDoc(false);
+ importer.deserializeMetadataFromJsonFiles(file);
+ MetaImport metaImport = importer.getMetaImports().get(0);
+ MetaYml yml = importer.createYMLFileForMetaJson(metaImport);
+ System.out.println(MetadataImporter.writeYmlToString(yml));
+ }
+
+ private static void addMissingIdsInMetadataDir(CommandLine parsedArgs) {
+ String[] files = parsedArgs.getOptionValues("i");
+ BasicConfigurator.configure(); // initializes console logging to stdout
+ try {
+ MetadataImporter metaImporter = new MetadataImporter();
+ metaImporter.setShouldReadYMLDoc(false);
+ metaImporter.deserializeMetadataFromJsonFiles(files);
+ for (MetaJson meta : metaImporter.getMetaJsons()) {
+ List allIds = meta.getIds();
+ boolean isFileUpdateNecessary = false;
+ for (MetaObject obj : meta.getMetaObjects()) {
+ if(obj.getId() == null) {
+ Identifier id = Identifier.getNewIdentifierValue(allIds, obj.getNamespace());
+ obj.setId(id);
+ allIds.add(id);
+ isFileUpdateNecessary = true;
+ }
+ if(obj.getDefaultRelease() == null) {
+ obj.setDefaultRelease(DEFAULT_RELEASE_BRANCH);
+ isFileUpdateNecessary = true;
+ }
+ }
+ if(isFileUpdateNecessary) {
+ writeMetaImportToFile(meta);
+ Logger.getRootLogger().info("Updated file " + meta.getFile().getPath());
+ }
+ }
+ if (!metaImporter.errorMessages.isEmpty()) {
+ System.out.println(metaImporter.getPrintableErrorMessages());
+ System.exit(1);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void convertJsonFilesToSql() {
+ SuperXManager.setWEB_INFPfad(PathAndFileUtils.getWebinfPath());
+ String filePath = PathAndFileUtils.getReportGeneratorDir("hisinone");
+ String ymlPath = PathAndFileUtils.getDbtTransformDirectory("hisinone");
+
+ String out =
+ "DROP TABLE IF EXISTS metadata.facttable; " +
+ "DROP TABLE IF EXISTS metadata.measure; " +
+ "DROP TABLE IF EXISTS metadata.measure_filter; " +
+ "DROP TABLE IF EXISTS metadata.dimension; " +
+ "DROP TABLE IF EXISTS metadata.dimension_attribute; ";
+
+ Path schemaSqlDir = Path.of("superx", "WEB-INF", "conf", "edustore", "db", "install", "schluesseltabellen");
+
+ out += readLinesWithNewline(new File(schemaSqlDir.toString() + File.separator + "biad_create_meta_tables.sql"));
+ out += readLinesWithNewline(new File(schemaSqlDir.toString() + File.separator + "biad_alter_meta_tables.sql"));
+ out += readLinesWithNewline(new File(schemaSqlDir.toString() + File.separator + "biad_metadaten_fuellen.sql"));
+
+ Logger.getLogger(MetadataImporter.class).setLevel(Level.ERROR);
+ MetadataImporter importer = new MetadataImporter(ymlPath);
+ importer.deserializeMetadataFromJsonFiles(filePath);
+ out += String.join("\n", importer.getAllUpsertStrings(false));
+
+ if(!importer.errorMessages.isEmpty()) {
+ System.out.println(importer.getPrintableErrorMessages());
+ System.exit(1);
+ } else {
+ System.out.println(out);
+
+ }
+
+ }
+
+ private static CommandLine readArgs(String[] args, Options options) {
+ CommandLineParser parser = new GnuParser();
+ try {
+ return parser.parse(options, args, false);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ return null;
+ }
+
+ private static void printHelp(Options options) {
+ HelpFormatter help = new HelpFormatter();
+ help.setOptionComparator(null);
+ help.setWidth(200);
+ help.printHelp("This tool streamlines common tasks during development for the BIAnalysis.", options);
+ }
+
+ private static Options createOptions() {
+ Options options = new Options();
+
+ OptionBuilder.withDescription("convert metadata directory to sql");
+ OptionBuilder.withLongOpt("convert-metadata");
+ Option outMeta = OptionBuilder.create("m");
+
+ OptionBuilder.withDescription("generate yml documentation for json file");
+ OptionBuilder.withLongOpt("generate-yml");
+ OptionBuilder.withArgName("json-file");
+ OptionBuilder.hasArg(true);
+ Option generateYml = OptionBuilder.create("y");
+
+
+ OptionBuilder.withDescription("generate wiki documentation for measures and dimensions");
+ OptionBuilder.withLongOpt("generate-doc");
+ OptionBuilder.withArgName("facttable");
+ OptionBuilder.hasArg(true);
+ Option generateDoc = OptionBuilder.create("d");
+
+ OptionBuilder.withLongOpt("add-ids");
+ OptionBuilder.withDescription("add missing ids to json files");
+ OptionBuilder.withArgName("directories");
+ OptionBuilder.hasArgs();
+ Option updateIds = OptionBuilder.create("i");
+
+ options.addOption(updateIds);
+ options.addOption(generateYml);
+ options.addOption(generateDoc);
+ options.addOption(outMeta);
+ options.addOption(new Option("h", "help", false, "get help"));
+
+ return options;
+ }
+
+ public static void writeMetaImportToFile(MetaJson meta) {
+ ObjectMapper mapper = new ObjectMapper();
+ DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(" ", DefaultIndenter.SYS_LF);
+ DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
+ printer.indentObjectsWith(indenter);
+ printer.indentArraysWith(indenter);
+ try {
+ mapper.writer(printer).writeValue(meta.getFile(), meta);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String readLinesWithNewline(File file) {
+ String result = "";
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ result += line;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return result+"\n";
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/Identifier.java b/src/de/superx/bianalysis/metadata/Identifier.java
new file mode 100644
index 0000000..aff28eb
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/Identifier.java
@@ -0,0 +1,74 @@
+package de.superx.bianalysis.metadata;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@JsonSerialize(using = IdentifierSerializer.class)
+public class Identifier {
+
+ private static final String ID_SEPARATOR = ":";
+
+ @JsonIgnore
+ public Integer value;
+ @JsonIgnore
+ public String namespace;
+
+ public String composedId;
+
+ public Identifier(String composedId) {
+ this.composedId = composedId;
+ String[] result = composedId.split(ID_SEPARATOR);
+ this.namespace = result[0];
+ this.value = Integer.valueOf(result[1]);
+ }
+
+ public Identifier(Identifier id) {
+ this.value = id.value;
+ this.namespace = id.namespace;
+ this.composedId = id.composedId;
+ }
+
+ @JsonIgnore
+ public static Identifier getNewIdentifierValue(List list, String namespace) {
+ List values = list
+ .stream()
+ .filter(i->i.value!=null)
+ .map(i->i.value)
+ .collect(Collectors.toList());
+ Integer value;
+ if(values.isEmpty()) {
+ value = Integer.valueOf(1);
+ } else {
+ value = Integer.valueOf(Collections.max(values).intValue() + 1);
+ }
+ return new Identifier(namespace + ID_SEPARATOR + value);
+ }
+
+ @Override
+ @JsonIgnore
+ public boolean equals(Object obj) {
+ if(obj == null || !(obj instanceof Identifier)) {
+ return false;
+ }
+ Identifier id = (Identifier) obj;
+ return id.composedId.equals(this.composedId);
+ }
+
+ @Override
+ @JsonIgnore
+ public int hashCode() {
+ return this.value.hashCode() + this.namespace.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return composedId;
+ }
+
+
+
+}
diff --git a/src/de/superx/bianalysis/metadata/IdentifierSerializer.java b/src/de/superx/bianalysis/metadata/IdentifierSerializer.java
new file mode 100644
index 0000000..39b4891
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/IdentifierSerializer.java
@@ -0,0 +1,27 @@
+package de.superx.bianalysis.metadata;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class IdentifierSerializer extends StdSerializer{
+
+ public IdentifierSerializer() {
+ this(null);
+ }
+
+ public IdentifierSerializer(Class t) {
+ super(t);
+ }
+
+ @Override
+ public void serialize(
+ Identifier id, JsonGenerator jgen, SerializerProvider provider)
+ throws IOException, JsonProcessingException {
+ jgen.writeRawValue('"'+id.composedId+'"');
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/MetaImport.java b/src/de/superx/bianalysis/metadata/MetaImport.java
new file mode 100644
index 0000000..6e74a50
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/MetaImport.java
@@ -0,0 +1,148 @@
+package de.superx.bianalysis.metadata;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import de.superx.bianalysis.FaultyMetadataException;
+import de.superx.bianalysis.metadata.models.json.MetaDimension;
+import de.superx.bianalysis.metadata.models.json.MetaDimensionAttribute;
+import de.superx.bianalysis.metadata.models.json.MetaFact;
+import de.superx.bianalysis.metadata.models.json.MetaMeasure;
+import de.superx.bianalysis.metadata.models.json.MetaMeasureFilter;
+import de.superx.bianalysis.metadata.models.json.MetaObject;
+
+public class MetaImport extends MetaJson {
+
+ public List facts;
+
+ private static Logger log = Logger.getLogger(MetaImport.class);
+
+ @JsonIgnore
+ private Map keysForMeasureFilter = new HashMap<>();
+
+ @JsonIgnore
+ public List conformedDimensions;
+
+ @JsonIgnore
+ public void setConformedDimensions(List conformedDimensions){
+ this.conformedDimensions = conformedDimensions;
+ if(this.conformedDimensions != null) {
+ for (MetaDimension dim : this.conformedDimensions) {
+ for (MetaDimensionAttribute attr : dim.getAttributes()) {
+ attr.setDimension(dim);
+ keysForMeasureFilter.put(dim.getDimension()+"."+attr.getDimColumn(), attr);
+ }
+ }
+ }
+ }
+
+ @JsonIgnore
+ @Override
+ public void init() {
+ this.allMetaObj = new ArrayList();
+ for (MetaFact fact : this.facts) {
+ allMetaObj.add(fact);
+ for (MetaDimension dim : fact.getDimensions()) {
+ allMetaObj.add(dim);
+ if(dim.getRefTo() != null && !dim.getRefTo().isEmpty()) {
+ MetaDimension conformedDim = findByRef(dim);
+ dim.setConformedDimension(conformedDim);
+ }
+ for (MetaDimensionAttribute attr : dim.getAttributes()) {
+ allMetaObj.add(attr);
+ if(attr.getRefTo() != null && !attr.getRefTo().isEmpty()) {
+ attr.setConformedDimensionAttribute(findByRefAttr(dim.getConformedDimension().getDimension(), attr));
+ }
+ keysForMeasureFilter.put(dim.getDimension()+"."+attr.getDimColumn(), attr);
+ }
+ }
+ if(fact.getMeasures() != null) {
+ for (MetaMeasure measure : fact.getMeasures()) {
+ allMetaObj.add(measure);
+ MetaMeasureFilter filter = measure.getFilter();
+ if(filter != null) {
+ if(filter.getDimensionRef() != null && !filter.getDimensionRef().isBlank()) {
+ MetaDimensionAttribute attr = keysForMeasureFilter.get(filter.getDimensionRef());
+ if(attr == null) {
+ throw new FaultyMetadataException("Could not resolve dimensionRef '" + filter.getDimensionRef() +
+ "' (" + file.getName() + " -> " + fact.getFacttable() + ")");
+ }
+ filter.setAttribute(attr);
+ allMetaObj.add(filter);
+ } else if(filter.getFactColumnRef() != null && !filter.getFactColumnRef().isBlank()) {
+ allMetaObj.add(filter);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private MetaDimensionAttribute findByRefAttr(String dimensionTable, MetaDimensionAttribute attribute) {
+ String attributeColumn = attribute.getRefTo();
+ MetaDimensionAttribute confAttr = null;
+ for (MetaDimension confDim : this.conformedDimensions) {
+ if(!confDim.getDimension().equals(dimensionTable)) {
+ continue;
+ }
+ for (MetaDimensionAttribute attr : confDim.getAttributes()) {
+ if(attr.getDimColumn().equals(attributeColumn)) {
+ confAttr = attr;
+ break;
+ }
+ }
+ }
+ if(confAttr == null) {
+ throw new FaultyMetadataException(
+ "Could not resolve attribute reference '" + attributeColumn + "' ("
+ + file.getName() + " -> "
+ + attribute.getDimension().getFact().getFacttable() + " -> "
+ + attribute.getDimension().getRefTo() + ")"
+ );
+ }
+ return confAttr;
+ }
+
+
+ @JsonIgnore
+ private MetaDimension findByRef(MetaDimension dim) {
+ String refTo = dim.getRefTo();
+ MetaDimension resolvedRefTo = null;
+ for (MetaDimension dimConf : this.conformedDimensions) {
+ if (dimConf.getDimension() == null) {
+ log.error("Missing dimension attribute for " + dimConf.getCaption());
+ continue;
+ }
+ if (dimConf.getDimension().equals(refTo)) {
+ resolvedRefTo = dimConf;
+ break;
+ }
+ }
+ if (resolvedRefTo == null) {
+ throw new FaultyMetadataException("Could not resolve dimension reference '" + refTo + "' (" + file.getName() + " -> " + dim.getFact().getFacttable() + ")");
+ }
+ return resolvedRefTo;
+ }
+
+ @JsonIgnore
+ public List getDimensionsWithoutRefTo() {
+ List dims = new ArrayList<>();
+ for (MetaFact fact : facts) {
+ for (MetaDimension dim : fact.getDimensions()) {
+ if(dim.getRefTo() == null) {
+ dims.add(dim);
+ }
+ }
+ }
+ return dims;
+ }
+
+
+}
diff --git a/src/de/superx/bianalysis/metadata/MetaImportConformedDimensions.java b/src/de/superx/bianalysis/metadata/MetaImportConformedDimensions.java
new file mode 100644
index 0000000..f84652e
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/MetaImportConformedDimensions.java
@@ -0,0 +1,38 @@
+package de.superx.bianalysis.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import de.superx.bianalysis.metadata.models.json.MetaDimension;
+import de.superx.bianalysis.metadata.models.json.MetaDimensionAttribute;
+import de.superx.bianalysis.metadata.models.json.MetaMeasure;
+import de.superx.bianalysis.metadata.models.json.MetaObject;
+
+public class MetaImportConformedDimensions extends MetaJson {
+
+ @JsonProperty("conformed_dimensions")
+ public List conformedDimensions;
+
+ @Override
+ public void init() {
+ this.allMetaObj = new ArrayList();
+ for (MetaDimension metaDimension : this.conformedDimensions) {
+ metaDimension.setConformed(true);
+ this.allMetaObj.add(metaDimension);
+ for (MetaDimensionAttribute attr : metaDimension.getAttributes()) {
+ attr.setDimension(metaDimension);
+ this.allMetaObj.add(attr);
+ }
+ }
+
+ for (MetaObject metaObject : allMetaObj) {
+ metaObject.setNamespace(this.namespace);
+ if(metaObject.getId() != null) {
+ metaObject.getId().namespace = this.namespace;
+ }
+ }
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/MetaJson.java b/src/de/superx/bianalysis/metadata/MetaJson.java
new file mode 100644
index 0000000..4d06aa4
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/MetaJson.java
@@ -0,0 +1,58 @@
+package de.superx.bianalysis.metadata;
+
+import java.io.File;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import de.superx.bianalysis.metadata.models.json.MetaObject;
+
+public abstract class MetaJson {
+
+ public String namespace;
+
+ @JsonIgnore
+ protected File file;
+
+ @JsonIgnore
+ protected List allMetaObj;
+
+ @JsonIgnore
+ public abstract void init();
+
+ @JsonIgnore
+ public File getFile() {
+ return this.file;
+ }
+
+ @JsonIgnore
+ public List getMetaObjects() {
+ return this.allMetaObj;
+ }
+
+ @JsonIgnore
+ public void setFile(File file) {
+ this.file = file;
+ }
+
+ @JsonIgnore
+ public List getIds() {
+ return this.getMetaObjects()
+ .stream()
+ .map(o -> o.getId())
+ .filter(i -> i != null && i.composedId != null)
+ .collect(Collectors.toList());
+ }
+
+ @JsonIgnore
+ public void setNamespaceToMetaObjects() {
+ for (MetaObject metaObject : allMetaObj) {
+ metaObject.setNamespace(this.namespace);
+ if(metaObject.getId() != null) {
+ metaObject.getId().namespace = this.namespace;
+ }
+ }
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/MetadataImporter.java b/src/de/superx/bianalysis/metadata/MetadataImporter.java
new file mode 100644
index 0000000..177c0c6
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/MetadataImporter.java
@@ -0,0 +1,593 @@
+package de.superx.bianalysis.metadata;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FilenameFilter;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.log4j.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import com.fasterxml.jackson.core.util.DefaultIndenter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+
+import de.superx.bianalysis.StoredReport;
+import de.superx.bianalysis.metadata.models.json.MetaDimension;
+import de.superx.bianalysis.metadata.models.json.MetaDimensionAttribute;
+import de.superx.bianalysis.metadata.models.json.MetaFact;
+import de.superx.bianalysis.metadata.models.json.MetaObject;
+import de.superx.bianalysis.metadata.models.yml.MetaYml;
+import de.superx.bianalysis.metadata.models.yml.MetaYmlModel;
+import de.superx.bianalysis.metadata.models.yml.MetaYmlModelColumns;
+import de.superx.util.PathAndFileUtils;
+
+/**
+ * Provides functionality for updating the tables in the metadata schema.
+ * The tables are updated by reading the metadata information from various
+ * metaimport.json files and transforming that information into executable sql.
+ *
+ * The BIAnalysis Tool uses the tables to read information about the different
+ * meta objects and more importantly to figure out their relationships, e.g. what
+ * dimension is part of which facttable or which attribute belongs to which
+ * dimension.
+ *
+ * To learn more about the metadata concept for the BIAnalysis Tool see:
+ * doc\bi_analysis\report_wizard\metadaten.adoc
+ *
+ */
+public final class MetadataImporter {
+
+ /**
+ * Each file containing metadata information must have the following file suffix.
+ */
+ private static final String METAIMPORT_FILE_SUFFIX = "_metaimport.json";
+
+ protected static final String CONFORMED_DIMENSIONS_FILE_SUFFIX = "conformed_dimensions" + METAIMPORT_FILE_SUFFIX;
+
+ /**
+ * Holds all Metaimport objects with which this instance was initalized.
+ * (One MetaImport object corresponds to exactly one deserialized json file)
+ */
+ private List metaImports = new ArrayList<>();
+
+ public List errorMessages = new ArrayList<>();
+
+ /**
+ * SQL String for deleting from all metadata tables except 'custom' releases.
+ */
+ public static final String TRUNCATE_METADATA_SQL =
+ "DELETE FROM metadata.facttable WHERE default_release != 'custom' or default_release is null; " +
+ "DELETE FROM metadata.measure WHERE default_release != 'custom' or default_release is null; " +
+ "DELETE FROM metadata.measure_filter WHERE default_release != 'custom' or default_release is null; " +
+ "DELETE FROM metadata.dimension WHERE default_release != 'custom' or default_release is null; " +
+ "DELETE FROM metadata.dimension_attribute WHERE default_release != 'custom' or default_release is null; ";
+
+ private static Logger log = Logger.getLogger(MetadataImporter.class);
+
+ private boolean shouldReadYMLDoc = true;
+
+ private String ymlDir = "";
+
+ public MetadataImporter() {}
+
+ public MetadataImporter(String ymlDir) {this.ymlDir = ymlDir;}
+
+ /**
+ * Calling this method initalizes the MetadataImporter by deserializing all unique meta objects
+ * from the provided json files. Faulty json files are ignored.
+ *
+ * @param paths Path(s) to the metadata file(s). A path can point to a directory or a file.
+ * Multiple paths and/or directories can be provided.
+ */
+ public void deserializeMetadataFromJsonFiles(String... paths) {
+
+ ObjectMapper mapper = JsonMapper.builder().findAndAddModules().build();
+ List conformedDimension = new ArrayList<>();
+ List conformedDims = new ArrayList<>();
+
+ for (String path : paths) {
+ List metaFiles = readMetaImportFiles(path);
+ for (File file : metaFiles) {
+ MetaJson meta = null;
+ try{
+ if(file.getName().endsWith(CONFORMED_DIMENSIONS_FILE_SUFFIX)) {
+ meta = mapper.readValue(file, MetaImportConformedDimensions.class);
+ conformedDimension.add((MetaImportConformedDimensions) meta);
+ } else {
+ meta = mapper.readValue(file, MetaImport.class);
+ conformedDims.addAll(((MetaImport) meta).getDimensionsWithoutRefTo());
+ }
+
+ } catch(JsonMappingException e) {
+ String message = "Could not deserialize metadata from file: " + file.getName() + "\n";
+ message += e.getMessage();
+ errorMessages.add(message);
+ } catch(Exception e) {
+ errorMessages.add(ExceptionUtils.getFullStackTrace(e));
+ }
+ if(meta != null) {
+ log.info("Read metadata from file: " + file.getName());
+ meta.setFile(file);
+ metaImports.add(meta);
+ }
+ }
+ }
+
+ // gather all conformed dimensions
+ List confDims = new ArrayList<>();
+ confDims.addAll(conformedDims);
+ for (MetaImportConformedDimensions conf : conformedDimension) {
+ confDims.addAll(conf.conformedDimensions);
+ }
+
+ // resolve conformed references ('ref_to' attributes)
+ for (MetaJson metaJson : metaImports) {
+ if (conformedDimension.size() > 0 && metaJson instanceof MetaImport) {
+ ((MetaImport) metaJson).setConformedDimensions(confDims);
+ }
+ try {
+ metaJson.init();
+ metaJson.setNamespaceToMetaObjects();
+ } catch (Exception e) {
+ errorMessages.add(ExceptionUtils.getFullStackTrace(e));
+ }
+ }
+
+ if(shouldReadYMLDoc) {
+ addDescriptionsFromYMLFiles();
+ }
+
+ }
+
+ public List readStoredReports() {
+ List result = new ArrayList<>();
+ try {
+ String dir = PathAndFileUtils.getStoredReportDir("hisinone");
+ File[] files = new File(dir).listFiles();
+ if(files == null) {
+ return result;
+ }
+ for (File file : files) {
+ try {
+ ObjectMapper mapper = JsonMapper.builder().findAndAddModules().build();
+ StoredReport report = mapper.readValue(file, StoredReport.class);
+ UpsertStringBuilder builder = new UpsertStringBuilder()
+ .forTable("metadata", "rw_report_definitions")
+ .withIntCol("id", Integer.valueOf(report.id))
+ .withStringCol("name", report.name)
+ .withStringCol("definition", report.definition)
+ .withIntCol("show_total_column", Integer.valueOf(report.showTotalColumn));
+ result.add(builder.build(true));
+ } catch (JsonMappingException e) {
+ String message = "Could not deserialize stored report from file: " + file.getName() + "\n";
+ message += e.getMessage();
+ errorMessages.add(message);
+ }
+ }
+ // After inserting the stored reports with a fixed id we need to re-sync the
+ // id column of the rw_report_definitions table
+ if(result.size() != 0) {
+ result.add("SELECT setval(pg_get_serial_sequence('metadata.rw_report_definitions', 'id'),"
+ + "(SELECT max(id) FROM metadata.rw_report_definitions ));");
+ }
+ } catch(Exception e) {
+ errorMessages.add("Unable to read stored report:\n");
+ errorMessages.add(ExceptionUtils.getFullStackTrace(e));
+ }
+ return result;
+ }
+
+ public void addDescriptionsFromYMLFiles() {
+ String dir = ymlDir;
+ if(ymlDir == null || ymlDir.isBlank()) {
+ dir = PathAndFileUtils.getDbtTransformDirectory("hisinone");
+ }
+ HashMap map = getMarkdownDefinitions(dir);
+ addYMLDescriptionsToMetaObjects(dir, map);
+ }
+
+ public void addYMLDescriptionsToMetaObjects(String ymlDir, HashMap mdDefs){
+ log.info("Adding descriptions from yml files");
+ HashMap descriptions = createDescriptions(new File(ymlDir), mdDefs);
+ List objs = getAllMetaObjectsWithConformed();
+ for (MetaObject metaObj : objs ) {
+ String docIdentifier = metaObj.getDocIdentifier();
+ if(docIdentifier == null || docIdentifier.isBlank()) {
+ continue;
+ }
+ // only use yml doc if json description does not exist
+ if(metaObj.getDescription() == null || metaObj.getDescription().isBlank()) {
+ String desc = descriptions.get(docIdentifier);
+ if(desc == null) {
+ log.warn("Missing yml description for: " + docIdentifier);
+ } else {
+ metaObj.setDescription(desc);
+ if(desc.isBlank()) {
+ log.warn("Empty yml description for MetaObject: " + docIdentifier);
+ }
+ }
+ }
+ }
+ }
+
+ public MetaYml createYMLFileForMetaJson(MetaJson metaJson) {
+
+ MetaYml newYml = new MetaYml();
+ List newYmlModels = new ArrayList<>();
+ newYml.setVersion(1);
+ newYml.setModels(newYmlModels);
+
+ if(metaJson instanceof MetaImport) {
+ MetaImport metaimport = (MetaImport) metaJson;
+ for (MetaFact fact : metaimport.facts) {
+ MetaYmlModel factModel = new MetaYmlModel(fact.getFacttable(), " ");
+ newYmlModels.add(factModel);
+ List factCols = new ArrayList<>();
+ for(MetaDimension dim : fact.getDimensions()) {
+ if(dim.getRefTo() == null) {
+ factCols.add(new MetaYmlModelColumns(dim.getFactColumn(), " ", "not_null"));
+ MetaYmlModel dimModel = new MetaYmlModel(dim.getDimension(), " ");
+ newYmlModels.add(dimModel);
+ List dimCols = new ArrayList<>();
+ for(MetaDimensionAttribute attr : dim.getAttributes()) {
+ dimCols.add(new MetaYmlModelColumns(attr.getDimColumn(), " ", "not_null"));
+ }
+ dimModel.setColumns(dimCols);
+ }
+ }
+ factModel.setColumns(factCols);
+ }
+ } else {
+ MetaImportConformedDimensions metaimport = (MetaImportConformedDimensions) metaJson;
+ for(MetaDimension dim : metaimport.conformedDimensions) {
+ MetaYmlModel dimModel = new MetaYmlModel(dim.getDimension(), " ");
+ newYmlModels.add(dimModel);
+ List dimCols = new ArrayList<>();
+ for(MetaDimensionAttribute attr : dim.getAttributes()) {
+ dimCols.add(new MetaYmlModelColumns(attr.getDimColumn(), " ", "not_null"));
+ }
+ dimModel.setColumns(dimCols);
+ }
+ }
+ return newYml;
+ }
+
+ private HashMap createDescriptions(File startDir, HashMap mdDefs){
+ HashMap result = new HashMap<>();
+ for (MetaYml yml : getDescriptionYMLs(startDir)) {
+ for (MetaYmlModel model : yml.getModels()) {
+ String modelName = model.getName();
+ String modelDesc = model.getDescription();
+ result.put(modelName, getDescription(modelDesc, mdDefs));
+ for (MetaYmlModelColumns column : model.getColumns()) {
+ String colName = column.getName();
+ String colDesc = column.getDescription();
+ result.put(modelName + "." + colName, getDescription(colDesc, mdDefs));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static String getDescription(String desc, HashMap mdDefs) {
+ if(desc == null) {
+ return "";
+ }
+ if(desc.startsWith("{{")) {
+ String[] parts = desc.split("\"");
+ String docRef = parts[1];
+ return mdDefs.get(docRef);
+ }
+ return desc;
+ }
+
+ private List getDescriptionYMLs(File startDir){
+ List ymls = new ArrayList<>();
+ List files = getFiles(startDir, "", ".yml");
+ ObjectMapper mapperYml = new ObjectMapper(new YAMLFactory());
+ for (String f : files) {
+ File file = new File(startDir + File.separator + f);
+ MetaYml doc = null;
+ try {
+ doc = mapperYml.readValue(file, MetaYml.class);
+ } catch (Exception e) {
+ String message = "Could not read documentation from file: " + file.getName() + "\n";
+ errorMessages.add(message);
+ errorMessages.add(ExceptionUtils.getFullStackTrace(e));
+ }
+ if(doc != null) {
+ log.info("Read documentation from file: " + file.getName());
+ ymls.add(doc);
+ }
+ }
+ return ymls;
+ }
+
+
+ /**
+ * Gathers all metadata json files.
+ *
+ * @param path A path to a metadata json file or a directory containing metadata json files.
+ * @return A list of files matching the metadata json suffix.
+ */
+ private static List readMetaImportFiles(String path) {
+ File metaimportPath = new File(PathAndFileUtils.getDbtJsonPath(path));
+ List metaimportFiles = new ArrayList<>();
+ if (metaimportPath.isDirectory()) {
+ metaimportPath.list(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ if (name.endsWith(METAIMPORT_FILE_SUFFIX)) {
+ File file = new File(dir.getAbsolutePath() + File.separator + name);
+ metaimportFiles.add(file);
+ return true;
+ }
+ return false;
+ }
+ });
+ } else {
+ metaimportFiles.add(metaimportPath);
+ }
+ return metaimportFiles;
+ }
+
+ private static List getFiles(File startDir, String subDir, String extension) {
+ List filtered = new ArrayList();
+ for (File file : startDir.listFiles()) {
+ String name = file.getName();
+ if(file.isDirectory()) {
+ filtered.addAll(getFiles(file, subDir + File.separator + name, extension));
+ }
+ String filename = name.strip().toLowerCase();
+ if(filename.endsWith(extension)) {
+ filtered.add(subDir + File.separator + name);
+ }
+ }
+ return filtered;
+ }
+
+ /*
+
+ /**
+ * Generates the sql upsert strings for all unique, deserialized MetaObjects.
+ *
+ * @param hasOnConflictConstruct If set to true generates upsert strings with the postgres-specific "ON CONFLICT" clause.
+ * @return All the generated upserts from the metadata files.
+ */
+ public List getAllUpsertStrings(boolean hasOnConflictConstruct) {
+
+ List upsertStmts = new ArrayList<>();
+ List ids = new ArrayList<>();
+
+ for (MetaJson meta : metaImports) {
+ for(MetaObject obj: meta.getMetaObjects()) {
+ Identifier id = obj.getId();
+ if(id == null) {
+ String message = String.format("Missing ID for Element '%s' in file: %s.", obj.getCaption(), meta.getFile().getAbsolutePath());
+ errorMessages.add(message);
+ continue;
+ }
+ if(ids.contains(id)) {
+ String message = String.format("Duplicate ID '%s'. Ignoring Element '%s'.", obj.getCaption(), obj.getId().composedId);
+ errorMessages.add(message);
+ continue;
+ }
+ ids.add(obj.getId());
+ String stmt = obj.getUpsertBuilder().build(hasOnConflictConstruct);
+ upsertStmts.add(stmt);
+ }
+ }
+ return upsertStmts;
+ }
+
+ public void updateMetadataForH2Database(DataSource dataSource) throws Exception {
+ String metaFilesDir = String.join(File.separator, new String[] {"test", "resources", "db", "fixtures", "reportwizard", "metadata"});
+ deserializeMetadataFromJsonFiles(metaFilesDir);
+ JdbcTemplate jt = new JdbcTemplate(dataSource);
+ String upserts = String.join("\n", getAllUpsertStrings(false).toString());
+ jt.execute(upserts);
+ }
+
+ /**
+ * Updates tables in the metadata schema.
+ *
+ * @param metaPath Location of the metadata file or directory.
+ * @param dataSource The datasource on which the sql is executed.
+ * @throws Exception
+ */
+ public void updateMetadataSchema(String project, DataSource dataSource) throws Exception {
+ String metaFilesDir = PathAndFileUtils.getReportGeneratorDir(project);
+ deserializeMetadataFromJsonFiles(metaFilesDir);
+
+ try (Connection con = dataSource.getConnection()) {
+ try (Statement st = con.createStatement()) {
+ log.info("Update Metadata for BIAnalysis.");
+ st.execute(TRUNCATE_METADATA_SQL);
+ List upserts = getAllUpsertStrings(true);
+ upserts.addAll(readStoredReports());
+ for (String sql : upserts) {
+ log.info(sql);
+ try (Statement stUpsert = con.createStatement()) {
+ stUpsert.execute(sql);
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+ }
+
+
+ // execute sql in "attributes_sql" in metadata files to build the attributes
+ // dynamically
+ for (MetaJson i : this.metaImports) {
+ for (MetaObject obj : i.getMetaObjects()) {
+ if (!(obj instanceof MetaDimension)) continue;
+ MetaDimension dim = (MetaDimension) obj;
+ if (dim.getAttributesSql() == null) continue;
+
+ String sqlDone = "";
+ String sql = "select param_val from unload_params where param_id = '" + dim.getAttributesSql() + "';";
+ try (Statement stAttr = con.createStatement(); ResultSet rs = stAttr.executeQuery(sql)) {
+ if(rs.next()) {
+ sqlDone = rs.getString("param_val");
+ }
+ }
+
+ try (Statement stAttr = con.createStatement(); ResultSet rs = stAttr.executeQuery(sqlDone)) {
+ int numAttributes = 0;
+ while (rs.next()) {
+
+ MetaDimensionAttribute attribute = new MetaDimensionAttribute();
+ attribute.setDimension(dim);
+ attribute.setCaption(rs.getString("caption"));
+ attribute.setDimColumn(rs.getString("dim_column"));
+
+ // create a new 'on the fly' identifier for the new metadata
+ // attribute
+ Identifier id = Identifier.getNewIdentifierValue(i.getIds(), dim.getNamespace());
+ Integer val = Integer.valueOf(id.value.intValue() + numAttributes);
+ attribute.setId(new Identifier(dim.getNamespace() + ":" +val));
+ numAttributes++;
+
+ String stmt = attribute.getUpsertBuilder().build(true);
+ try (Statement stUpsert = con.createStatement()) {
+ stUpsert.execute(stmt);
+ }
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ public static String writeYmlToString(MetaYml yml) {
+ YAMLFactory yf = new YAMLFactory()
+ .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
+ .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
+ ObjectMapper mapper = new ObjectMapper(yf);
+ DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(" ", DefaultIndenter.SYS_LF);
+ DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
+ printer.indentObjectsWith(indenter);
+ printer.indentArraysWith(indenter);
+ try {
+ //mapper.writer(printer).writeValue(file, yml);
+ return mapper.writer(printer).writeValueAsString(yml);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getPrintableErrorMessages() {
+ String output = "";
+ if(!errorMessages.isEmpty()) {
+ output += "The following errors occured:\n";
+ for (String message : errorMessages) {
+ output += message + "\n";
+ }
+ }
+ return output;
+ }
+
+ public HashMap getMarkdownDefinitions(String ymlDir) {
+ List files = getFiles(new File(ymlDir), "", ".md");
+ HashMap map = new HashMap<>();
+ for (String file : files) {
+ try {
+ try (BufferedReader br = new BufferedReader(new FileReader(ymlDir + File.separator + file))) {
+ String line;
+ String key = null;
+ boolean readHeading = false;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("{% docs ")) {
+ key = line.split(" ")[2];
+ map.put(key, "");
+ } else if(key != null && line.startsWith("# ")) {
+ readHeading = true;
+ } else {
+ if(readHeading && !line.isBlank()) {
+ map.put(key, line);
+ key = null;
+ readHeading = false;
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ String message = "ERROR getting markdown definitions from file: " + file + "\n";
+ errorMessages.add(message);
+ errorMessages.add(ExceptionUtils.getFullStackTrace(e));
+ }
+ }
+ return map;
+ }
+
+ public Optional getMetaImport(String fileName) {
+ return metaImports.stream()
+ .filter(json -> (json instanceof MetaImport) && json.file.getName().equals(fileName))
+ .map(json -> (MetaImport) json)
+ .findFirst();
+ }
+
+ public Optional getMetaJson(String fileName) {
+ return metaImports.stream()
+ .filter(json -> json.file.getName().equals(fileName))
+ .findFirst();
+ }
+
+ public List getMetaImports() {
+ return metaImports.stream()
+ .filter(json -> (json instanceof MetaImport))
+ .map(json -> (MetaImport) json)
+ .collect(Collectors.toList());
+ }
+
+ public List getMetaJsons() {
+ return metaImports.stream().collect(Collectors.toList());
+ }
+
+ public List getAllMetaObjects(){
+ return metaImports.stream()
+ .filter(json -> (json instanceof MetaImport))
+ .map(meta -> ((MetaImport) meta).getMetaObjects())
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+ }
+
+ public List getAllMetaObjectsWithConformed(){
+ return metaImports.stream()
+ .map(meta -> meta.getMetaObjects())
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+ }
+
+ public List getAllFactTables(){
+ return metaImports.stream()
+ .filter(json -> (json instanceof MetaImport))
+ .map(meta -> ((MetaImport) meta).facts)
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+ }
+
+ public void setShouldReadYMLDoc(boolean shouldReadYMLDoc) {
+ this.shouldReadYMLDoc = shouldReadYMLDoc;
+ }
+
+
+}
diff --git a/src/de/superx/bianalysis/metadata/UpsertStringBuilder.java b/src/de/superx/bianalysis/metadata/UpsertStringBuilder.java
new file mode 100644
index 0000000..3ba8bfc
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/UpsertStringBuilder.java
@@ -0,0 +1,99 @@
+package de.superx.bianalysis.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+public class UpsertStringBuilder {
+
+ private StringJoiner values;
+ private StringJoiner columns;
+ private StringJoiner onConflict;
+ private String schema;
+ private String tablename;
+
+ private List builders = new ArrayList<>();
+
+ public UpsertStringBuilder() {
+ values = new StringJoiner(", ");
+ columns = new StringJoiner(", ");
+ onConflict = new StringJoiner(", ");
+ }
+
+ public void addUpsertStringBuilder(UpsertStringBuilder builder) {
+ this.builders.add(builder);
+ }
+
+ public UpsertStringBuilder forTable(String schema, String tablename) {
+ this.tablename = tablename;
+ this.schema = schema;
+ return this;
+ }
+
+ public UpsertStringBuilder withStringCol(String colName, String value) {
+ appendToSelection(colName);
+ if(value == null) {
+ values.add("null");
+ } else {
+ values.add("'"+value+"'");
+ }
+ return this;
+ }
+
+ public UpsertStringBuilder withStringCol(String colName, Object value) {
+ if(value != null) {
+ return this.withStringCol(colName, value.toString());
+ }
+ return this.withStringCol(colName, "unknown");
+ }
+
+ public UpsertStringBuilder withStringCol(String colName, String value, String defaultVal) {
+ if(value != null) {
+ return this.withStringCol(colName, value);
+ }
+ return this.withStringCol(colName, defaultVal);
+ }
+
+ public UpsertStringBuilder withStringCol(String colName, Object value, Object defaultVal) {
+ if(value != null) {
+ return this.withStringCol(colName, value);
+ }
+ return this.withStringCol(colName, defaultVal);
+ }
+
+ public UpsertStringBuilder withIntCol(String colName, Integer value) {
+ appendToSelection(colName);
+ values.add(String.valueOf(value));
+ return this;
+ }
+
+ public UpsertStringBuilder withIdCol(String colName, Identifier id) {
+ if(id != null) {
+ return this.withStringCol(colName, id.composedId);
+ }
+ return this.withStringCol(colName, null);
+ }
+
+ private void appendToSelection(String colName) {
+ onConflict.add(String.format("%s = EXCLUDED.%s", colName, colName));
+ columns.add(colName);
+ }
+
+ public String build(boolean hasOnConflictConstruct) {
+ String result = "INSERT INTO %s.%s(%s) VALUES(%s)";
+ result = String.format(result, this.schema, this.tablename, this.columns, this.values);
+ if(hasOnConflictConstruct) {
+ // TODO: log message for on id conflict
+ //result += " ON CONFLICT(id) DO UPDATE SET " + this.onConflict;
+ result += " ON CONFLICT(id) DO NOTHING";
+ }
+ result += ";\n";
+ if(this.builders.size() > 0) {
+ for (UpsertStringBuilder upsertStringBuilder : builders) {
+ result += upsertStringBuilder.build(hasOnConflictConstruct);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaDimension.java b/src/de/superx/bianalysis/metadata/models/json/MetaDimension.java
new file mode 100644
index 0000000..e4d0bc1
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaDimension.java
@@ -0,0 +1,269 @@
+package de.superx.bianalysis.metadata.models.json;
+
+import java.util.ArrayList;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonInclude(Include.NON_DEFAULT)
+@JsonPropertyOrder({ "id", "default_release", "ref_to", "caption", "dimension", "fact_column", "alias", "bridge", "attributes"})
+public class MetaDimension extends MetaObject {
+
+ @JsonProperty("ref_to")
+ private String refTo;
+ private String dimension;
+ @JsonProperty("fact_column")
+ private String factColumn;
+ private String alias;
+ private String view;
+ @JsonProperty("id_column")
+ private String idColumn;
+ private List attributes;
+
+ @JsonProperty("is_hierarchy")
+ private boolean isHierarchy;
+ @JsonProperty("is_historical")
+ private boolean isHistorical;
+
+ @JsonProperty("attributes_sql")
+ private String attributesSql;
+
+ @JsonIgnore
+ private MetaFact fact;
+
+ // true if dimension is from the conformed_dimensions_metaimport.json
+ @JsonIgnore
+ private boolean isConformed = false;
+
+ // isConformed must be false
+ // the correpsonding dimension from the conformed_dimensions_metaimport.json
+ // referenced by 'ref_to'
+ @JsonIgnore
+ private MetaDimension conformedDimension;
+
+ public MetaDimension() {
+ super("dimension");
+ }
+
+ public void setConformedDimension(MetaDimension dimension) {
+ this.conformedDimension = dimension;
+ }
+
+ @Override
+ public UpsertStringBuilder getUpsertBuilder() {
+ Identifier factId = (isConformed) ? null : this.fact.id;
+ UpsertStringBuilder builder = new UpsertStringBuilder();
+ if(conformedDimension == null) {
+ builder = super.getUpsert()
+ .withIdCol("facttable_id", factId)
+ .withStringCol("joincolumn", this.factColumn)
+ .withStringCol("alias", this.alias)
+ .withStringCol("is_hierarchy", String.valueOf(this.isHierarchy))
+ .withStringCol("is_historical", String.valueOf(this.isHistorical))
+ //.withStringCol("attributes_sql", this.attributesSql)
+ .withStringCol("tablename", this.dimension)
+ .withStringCol("id_column", this.idColumn);
+ } else {
+ builder = new UpsertStringBuilder()
+ .forTable("metadata", this.sourceTable)
+ .withStringCol("namespace", this.namespace)
+ .withIdCol("id", this.id)
+ .withIntCol("default_release", Integer.valueOf(1));
+ builder = builder.withIdCol("facttable_id", factId);
+
+ if(this.idColumn != null && !this.idColumn.isBlank()) {
+ builder = builder.withStringCol("id_column", idColumn);
+ } else {
+ builder = builder.withStringCol("id_column", this.conformedDimension.getIdColumn());
+ }
+
+ if(this.caption != null && !this.caption.isBlank()) {
+ builder = builder.withStringCol("caption", caption);
+ } else {
+ builder = builder.withStringCol("caption", this.conformedDimension.getCaption());
+ }
+
+ if(this.factColumn != null && !this.factColumn.isBlank()) {
+ builder = builder.withStringCol("joincolumn", this.factColumn);
+ } else {
+ builder = builder.withStringCol("joincolumn", this.conformedDimension.getFactColumn());
+ }
+
+ if(this.alias != null && !this.factColumn.isBlank()) {
+ builder = builder.withStringCol("alias", this.alias);
+ } else {
+ builder = builder.withStringCol("alias", this.conformedDimension.getAlias());
+ }
+
+ if(this.isHierarchy) {
+ builder = builder.withStringCol("is_hierarchy", String.valueOf(isHierarchy));
+ } else {
+ builder = builder.withStringCol("is_hierarchy", String.valueOf(conformedDimension.isHierarchy));
+ }
+
+ if(this.isHistorical) {
+ builder = builder.withStringCol("is_historical", String.valueOf(isHistorical));
+ } else {
+ builder = builder.withStringCol("is_historical", String.valueOf(conformedDimension.isHistorical));
+ }
+
+ if(this.conformedDimension.getDimension() != null && !this.conformedDimension.getDimension().isBlank()) {
+
+ if(view != null && !view.isBlank()) {
+ builder = builder.withStringCol("tablename", this.view);
+ } else {
+ builder = builder.withStringCol("tablename", this.conformedDimension.getDimension());
+ }
+
+ } else {
+ builder = builder.withStringCol("tablename", this.dimension);
+ }
+ builder = builder.withIdCol("conformed", this.conformedDimension.id);
+ }
+
+ if(this.description != null && !this.description.isBlank()) {
+ builder = builder.withStringCol("description", this.description);
+ } else {
+ if(conformedDimension != null) {
+ builder = builder.withStringCol("description", conformedDimension.getDescription());
+ } else {
+ builder = builder.withStringCol("description", "");
+ }
+ }
+ return builder;
+ }
+
+ public String getRefTo() {
+ return refTo;
+ }
+
+ public void setRefTo(String refTo) {
+ this.refTo = refTo;
+ }
+
+ public String getDimension() {
+ return dimension;
+ }
+
+ public void setDimension(String dimension) {
+ this.dimension = dimension;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public List getAttributes() {
+ if(this.attributes == null) {
+ return new ArrayList<>();
+ }
+ return attributes;
+ }
+
+ public void setAttributes(List attributes) {
+ for (MetaDimensionAttribute metaDimensionAttribute : attributes) {
+ metaDimensionAttribute.setDimension(this);
+ }
+ this.attributes = attributes;
+ }
+
+ public MetaFact getFact() {
+ return fact;
+ }
+
+ public void setFact(MetaFact fact) {
+ this.fact = fact;
+ }
+
+ @JsonIgnore
+ public boolean isConformed() {
+ return isConformed;
+ }
+
+ @JsonIgnore
+ public void setConformed(boolean isConformed) {
+ this.isConformed = isConformed;
+ }
+
+ public MetaDimension getConformedDimension() {
+ return conformedDimension;
+ }
+
+ public String getFactColumn() {
+ return factColumn;
+ }
+
+ public void setFactColumn(String factColumn) {
+ this.factColumn = factColumn;
+ }
+
+ public void addAttribute(MetaDimensionAttribute metaDimensionAttribute) {
+ if(this.attributes == null) {
+ this.attributes = new ArrayList<>();
+ }
+ this.attributes.add(metaDimensionAttribute);
+ }
+
+ @Override
+ @JsonIgnore
+ public String getDocIdentifier() {
+ if(this.conformedDimension != null) {
+ return conformedDimension.getDocIdentifier();
+ }
+ return this.dimension;
+ }
+
+ public boolean isHierarchy() {
+ return isHierarchy;
+ }
+
+ public void setHierarchy(boolean isHierarchy) {
+ this.isHierarchy = isHierarchy;
+ }
+
+ public boolean isHistorical() {
+ return isHistorical;
+ }
+
+ public void setHistorical(boolean isHistorical) {
+ this.isHistorical = isHistorical;
+ }
+
+ public String getView() {
+ return view;
+ }
+
+ public void setView(String view) {
+ this.view = view;
+ }
+
+ public String getIdColumn() {
+ return idColumn;
+ }
+
+ public void setIdColumn(String idColumn) {
+ this.idColumn = idColumn;
+ }
+
+ public String getAttributesSql() {
+ return attributesSql;
+ }
+
+ public void setAttributesSql(String attributesSql) {
+ this.attributesSql = attributesSql;
+ }
+
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaDimensionAttribute.java b/src/de/superx/bianalysis/metadata/models/json/MetaDimensionAttribute.java
new file mode 100644
index 0000000..7f70479
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaDimensionAttribute.java
@@ -0,0 +1,179 @@
+package de.superx.bianalysis.metadata.models.json;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+
+@JsonInclude(Include.NON_DEFAULT)
+@JsonPropertyOrder({ "id", "default_release", "ref", "caption", "dim_column"})
+public class MetaDimensionAttribute extends MetaObject {
+
+ @JsonProperty("dim_column")
+ private String dimColumn;
+
+ @JsonProperty("sort_order_column")
+ private String sortOrderColumn;
+
+ @JsonProperty("hierarchical_filter")
+ private boolean hierarchicalFilter;
+
+ @JsonIgnore
+ private MetaDimension dimension;
+
+ @JsonIgnore
+ private MetaDimensionAttribute confDimAttrRef;
+
+ @JsonProperty("ref_to")
+ private String refTo;
+
+ @JsonProperty("filter_selection")
+ private String filterSelection;
+
+ public MetaDimensionAttribute() {
+ super("dimension_attribute");
+ }
+
+ public MetaDimensionAttribute(String attrColumn) {
+ super("dimension_attribute");
+ this.refTo = attrColumn;
+ //this.refTo = dimensionTable + "." + attrColumn;
+ }
+
+ public void setConformedDimensionAttribute(MetaDimensionAttribute attribute) {
+ this.confDimAttrRef = attribute;
+ }
+
+ @Override
+ public UpsertStringBuilder getUpsertBuilder() {
+ UpsertStringBuilder builder = new UpsertStringBuilder();
+ if(confDimAttrRef == null) {
+ builder = super.getUpsert()
+ .withIdCol("dimension_id", this.dimension.id)
+ .withStringCol("columnname", this.dimColumn)
+ .withStringCol("sort_order_column", this.sortOrderColumn)
+ .withStringCol("filter_selection", this.filterSelection);
+ } else {
+ builder = new UpsertStringBuilder()
+ .forTable("metadata", this.sourceTable)
+ .withStringCol("namespace", this.namespace)
+ .withIdCol("id", this.id)
+ .withIntCol("default_release", Integer.valueOf(1));
+
+ if(getCaption() != null && !getCaption().isBlank()) {
+ builder = builder.withStringCol("caption", caption);
+ } else {
+ builder = builder.withStringCol("caption", confDimAttrRef.getCaption());
+ }
+
+ if(getDimColumn() != null && !getDimColumn().isBlank()) {
+ builder = builder.withStringCol("columnname", this.dimColumn);
+ } else {
+ builder = builder.withStringCol("columnname", confDimAttrRef.getDimColumn());
+ }
+
+ if(getFilterSelection() != null && !getFilterSelection().isBlank()) {
+ builder = builder.withStringCol("filter_selection", this.filterSelection);
+ } else {
+ builder = builder.withStringCol("filter_selection", confDimAttrRef.getFilterSelection());
+ }
+
+ if(getSortOrderColumn() != null && !getSortOrderColumn().isBlank()) {
+ builder = builder.withStringCol("sort_order_column", this.sortOrderColumn);
+ } else {
+ builder = builder.withStringCol("sort_order_column", confDimAttrRef.getSortOrderColumn());
+ }
+
+ builder = builder.withIdCol("dimension_id", this.dimension.id);
+ builder = builder.withIdCol("conformed", this.confDimAttrRef.id);
+ }
+
+ if(confDimAttrRef != null && confDimAttrRef.hierarchicalFilter) {
+ builder = builder.withStringCol("hierarchical_filter", String.valueOf(confDimAttrRef.hierarchicalFilter));
+ } else {
+ builder = builder.withStringCol("hierarchical_filter", String.valueOf(hierarchicalFilter));
+ }
+
+ if(this.description != null && !this.description.isBlank()) {
+ builder = builder.withStringCol("description", this.description);
+ } else {
+ if(confDimAttrRef != null) {
+ builder = builder.withStringCol("description", confDimAttrRef.getDescription());
+ } else {
+ builder = builder.withStringCol("description", "");
+ }
+ }
+
+ return builder;
+ }
+
+ public String getDimColumn() {
+ return dimColumn;
+ }
+
+ public void setDimColumn(String dimColumn) {
+ this.dimColumn = dimColumn;
+ }
+
+ public String getSortOrderColumn() {
+ return sortOrderColumn;
+ }
+
+ public void setSortOrderColumn(String sortOrderColumn) {
+ this.sortOrderColumn = sortOrderColumn;
+ }
+
+ public String getFilterSelection() {
+ return filterSelection;
+ }
+
+ public void setFilterSelection(String filterSelection) {
+ this.filterSelection = filterSelection;
+ }
+
+ public MetaDimension getDimension() {
+ return dimension;
+ }
+
+ public void setDimension(MetaDimension dimension) {
+ this.dimension = dimension;
+ }
+
+ public MetaDimensionAttribute getConfDimAttrRef() {
+ return confDimAttrRef;
+ }
+
+ public void setConfDimAttrRef(MetaDimensionAttribute confDimAttrRef) {
+ this.confDimAttrRef = confDimAttrRef;
+ }
+
+ public String getRefTo() {
+ return refTo;
+ }
+
+ public void setRefTo(String refTo) {
+ this.refTo = refTo;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getDocIdentifier() {
+ if(refTo != null) {
+ return this.confDimAttrRef.getDocIdentifier();
+ }
+ return this.dimension.getDocIdentifier()+"."+this.dimColumn;
+ }
+
+ public boolean isHierarchicalFilter() {
+ return hierarchicalFilter;
+ }
+
+ public void setHierarchicalFilter(boolean hierarchicalFilter) {
+ this.hierarchicalFilter = hierarchicalFilter;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaFact.java b/src/de/superx/bianalysis/metadata/models/json/MetaFact.java
new file mode 100644
index 0000000..bb74767
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaFact.java
@@ -0,0 +1,77 @@
+package de.superx.bianalysis.metadata.models.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+
+@JsonPropertyOrder({ "id", "default_release", "caption", "sachgebiettid", "facttable", "conformed_dimensions", "dimensions", "measures" })
+public class MetaFact extends MetaObject {
+
+ private Integer sachgebiettid;
+ private String facttable;
+ private List dimensions;
+ private List measures;
+
+ public MetaFact() {
+ super("facttable");
+ }
+
+ @Override
+ public UpsertStringBuilder getUpsertBuilder() {
+ UpsertStringBuilder builder = super.getUpsert()
+ .withIntCol("sachgebiettid", this.sachgebiettid)
+ .withStringCol("tablename", this.facttable)
+ .withStringCol("description", super.getDescription());
+
+ return builder;
+ }
+
+ public Integer getSachgebiettid() {
+ return sachgebiettid;
+ }
+
+ public void setSachgebiettid(Integer sachgebiettid) {
+ this.sachgebiettid = sachgebiettid;
+ }
+
+ public String getFacttable() {
+ return facttable;
+ }
+
+ public void setFacttable(String facttable) {
+ this.facttable = facttable;
+ }
+
+ public List getDimensions() {
+ return dimensions;
+ }
+
+ public void setDimensions(List dimensions) {
+ for (MetaDimension metaDimension : dimensions) {
+ metaDimension.setFact(this);
+ }
+ this.dimensions = dimensions;
+ }
+
+ public List getMeasures() {
+ return measures;
+ }
+
+ public void setMeasures(List measures) {
+ for (MetaMeasure metaMeasure : measures) {
+ metaMeasure.setFact(this);
+ }
+ this.measures = measures;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getDocIdentifier() {
+ return this.facttable;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaMeasure.java b/src/de/superx/bianalysis/metadata/models/json/MetaMeasure.java
new file mode 100644
index 0000000..41d51e0
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaMeasure.java
@@ -0,0 +1,82 @@
+package de.superx.bianalysis.metadata.models.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+import de.superx.rest.model.ColumnType;
+
+@JsonPropertyOrder({ "id", "default_release"} )
+public class MetaMeasure extends MetaObject {
+
+ private String factcolumn;
+ private String aggregation;
+ private ColumnType type;
+ private MetaMeasureFilter filter;
+
+ @JsonIgnore
+ private MetaFact fact;
+
+ public MetaMeasure() {
+ super("measure");
+ }
+
+ @Override
+ public UpsertStringBuilder getUpsertBuilder() {
+ return super.getUpsert()
+ .withIdCol("facttable_id", (this.fact != null) ? this.fact.id : null)
+ .withIdCol("measure_filter_id", (this.filter != null) ? this.filter.id : null)
+ .withStringCol("columnname", this.factcolumn)
+ .withStringCol("aggregation_type", this.aggregation)
+ .withStringCol("description", this.description)
+ .withStringCol("measure_type", this.type, ColumnType.IntegerColumn);
+ }
+
+ public String getFactcolumn() {
+ return factcolumn;
+ }
+
+ public void setFactcolumn(String factcolumn) {
+ this.factcolumn = factcolumn;
+ }
+
+ public String getAggregation() {
+ return aggregation;
+ }
+
+ public void setAggregation(String aggregation) {
+ this.aggregation = aggregation;
+ }
+
+ public ColumnType getType() {
+ return type;
+ }
+
+ public void setType(ColumnType type) {
+ this.type = type;
+ }
+
+ public MetaMeasureFilter getFilter() {
+ return filter;
+ }
+
+ public void setFilter(MetaMeasureFilter filter) {
+ this.filter = filter;
+ }
+
+ public MetaFact getFact() {
+ return fact;
+ }
+
+ public void setFact(MetaFact fact) {
+ this.fact = fact;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getDocIdentifier() {
+ return this.factcolumn;
+ }
+
+}
+
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaMeasureFilter.java b/src/de/superx/bianalysis/metadata/models/json/MetaMeasureFilter.java
new file mode 100644
index 0000000..6bb03ca
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaMeasureFilter.java
@@ -0,0 +1,106 @@
+package de.superx.bianalysis.metadata.models.json;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+
+@JsonPropertyOrder({ "id", "default_release"} )
+public class MetaMeasureFilter extends MetaObject {
+
+ private String dimensionRef;
+
+ private String factColumnRef;
+
+ @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
+ private List included;
+
+ @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
+ private List excluded;
+
+ @JsonIgnore
+ private MetaDimensionAttribute attribute;
+
+ public MetaMeasureFilter() {
+ super("measure_filter");
+ }
+
+ @Override
+ public UpsertStringBuilder getUpsertBuilder() {
+
+ UpsertStringBuilder builder = super.getUpsert();
+ builder.withStringCol("included_values", concatValues(included));
+ builder.withStringCol("excluded_values", concatValues(excluded));
+
+ if(this.dimensionRef != null) {
+ builder.withIdCol("dimension_attribute_id", this.attribute.id);
+ } else if(this.factColumnRef != null){
+ builder.withStringCol("fact_column_filter", this.factColumnRef);
+ }
+
+ return builder;
+ }
+
+ private static String concatValues(List values) {
+ if (values == null || values.isEmpty()) {
+ return null;
+ }
+ String result = "";
+ int size = values.size();
+ for (int i = 0; i < size - 1; i++) {
+ result += "''" + values.get(i) + "'', ";
+ }
+ result += "''" + values.get(size - 1) + "''";
+ return result;
+ }
+
+ public String getDimensionRef() {
+ return dimensionRef;
+ }
+
+ public void setDimensionRef(String dimensionRef) {
+ this.dimensionRef = dimensionRef;
+ }
+
+ public List getIncluded() {
+ return included;
+ }
+
+ public void setIncluded(List included) {
+ this.included = included;
+ }
+
+ public List getExcluded() {
+ return excluded;
+ }
+
+ public void setExcluded(List excluded) {
+ this.excluded = excluded;
+ }
+
+ public MetaDimensionAttribute getAttribute() {
+ return attribute;
+ }
+
+ public void setAttribute(MetaDimensionAttribute attribute) {
+ this.attribute = attribute;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getDocIdentifier() {
+ return "";
+ }
+
+ public String getFactColumnRef() {
+ return factColumnRef;
+ }
+
+ public void setFactColumnRef(String factColumnRef) {
+ this.factColumnRef = factColumnRef;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/json/MetaObject.java b/src/de/superx/bianalysis/metadata/models/json/MetaObject.java
new file mode 100644
index 0000000..c11fe54
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/json/MetaObject.java
@@ -0,0 +1,112 @@
+package de.superx.bianalysis.metadata.models.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import de.superx.bianalysis.metadata.Identifier;
+import de.superx.bianalysis.metadata.UpsertStringBuilder;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(Include.NON_NULL)
+public abstract class MetaObject {
+
+ protected Identifier id;
+ protected String caption;
+ protected String description;
+
+ @JsonProperty("default_release")
+ protected String defaultRelease;
+
+ @JsonIgnore
+ protected String sourceTable;
+
+ @JsonIgnore
+ protected String namespace;
+
+ protected MetaObject(String sourceTable) {
+ this.sourceTable = sourceTable;
+ }
+
+ /**
+ * Returns the documentation identifier for a specific meta object.
+ * The Identifier is used in a *.md file and is referenced in the yml
+ * file like the following: '{{ doc("") }}'.
+ */
+ @JsonIgnore
+ public abstract String getDocIdentifier();
+
+ @JsonIgnore
+ public abstract UpsertStringBuilder getUpsertBuilder();
+
+ @JsonIgnore
+ protected UpsertStringBuilder getUpsert() {
+ return new UpsertStringBuilder()
+ .forTable("metadata", this.sourceTable)
+ .withStringCol("namespace", this.namespace)
+ .withIdCol("id", this.id)
+ .withStringCol("default_release", this.defaultRelease)
+ .withStringCol("caption", this.caption);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(!(obj instanceof MetaObject)) {
+ return false;
+ } else if(((MetaObject)obj).id == null) {
+ return false;
+ }
+ return this.id.equals(((MetaObject)obj).id);
+ }
+
+ public Identifier getId() {
+ return id;
+ }
+
+ public void setId(Identifier id) {
+ this.id = id;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ public String getSourceTable() {
+ return sourceTable;
+ }
+
+ public void setSourceTable(String sourceTable) {
+ this.sourceTable = sourceTable;
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getDefaultRelease() {
+ return defaultRelease;
+ }
+
+ public void setDefaultRelease(String defaultRelease) {
+ this.defaultRelease = defaultRelease;
+ }
+
+}
diff --git a/src/de/superx/bianalysis/metadata/models/yml/MetaYml.java b/src/de/superx/bianalysis/metadata/models/yml/MetaYml.java
new file mode 100644
index 0000000..802adb6
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/yml/MetaYml.java
@@ -0,0 +1,32 @@
+package de.superx.bianalysis.metadata.models.yml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class MetaYml {
+
+ private int version;
+ private List models;
+
+ public int getVersion() {
+ return version;
+ }
+
+ public void setVersion(int version) {
+ this.version = version;
+ }
+
+ public List getModels() {
+ if(this.models == null) {
+ return new ArrayList();
+ }
+ return models;
+ }
+
+ public void setModels(List models) {
+ this.models = models;
+ }
+}
diff --git a/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModel.java b/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModel.java
new file mode 100644
index 0000000..9cdccf7
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModel.java
@@ -0,0 +1,49 @@
+package de.superx.bianalysis.metadata.models.yml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class MetaYmlModel {
+
+ private String name;
+ private String description;
+ private List columns;
+
+ public MetaYmlModel() { }
+
+ public MetaYmlModel(String name, String description) {
+ super();
+ this.name = name;
+ this.description = description;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public List getColumns() {
+ if(this.columns == null) {
+ return new ArrayList();
+ }
+ return columns;
+ }
+
+ public void setColumns(List columns) {
+ this.columns = columns;
+ }
+}
diff --git a/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModelColumns.java b/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModelColumns.java
new file mode 100644
index 0000000..7a12c53
--- /dev/null
+++ b/src/de/superx/bianalysis/metadata/models/yml/MetaYmlModelColumns.java
@@ -0,0 +1,52 @@
+package de.superx.bianalysis.metadata.models.yml;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class MetaYmlModelColumns {
+
+ private String name;
+ private String description;
+ private List