diff --git a/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java b/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java index 7d10f8d..b01bd24 100644 --- a/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java +++ b/src/main/java/tech/ailef/dbadmin/external/controller/DataExportController.java @@ -2,9 +2,9 @@ package tech.ailef.dbadmin.external.controller; import java.io.ByteArrayOutputStream; import java.io.IOException; - import java.io.StringWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -12,10 +12,14 @@ import java.util.stream.Collectors; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; 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.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -41,7 +45,8 @@ import tech.ailef.dbadmin.external.misc.Utils; @Controller @RequestMapping(value = { "/${dbadmin.baseUrl}/export", "/${dbadmin.baseUrl}/export/" }) public class DataExportController { - + private static final Logger logger = LoggerFactory.getLogger(DataExportFormat.class); + @Autowired private DbAdmin dbAdmin; @@ -53,8 +58,11 @@ public class DataExportController { public ResponseEntity export(@PathVariable String className, @RequestParam(required = false) String query, @RequestParam String format, @RequestParam(required=false) Boolean raw, @RequestParam MultiValueMap otherParams) { - if (format == null) - format = "CSV"; + if (raw == null) raw = false; + + DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); + List fieldsToInclude = otherParams.getOrDefault("fields[]", new ArrayList<>()); + DataExportFormat exportFormat = null; try { exportFormat = DataExportFormat.valueOf(format.toUpperCase()); @@ -62,124 +70,148 @@ public class DataExportController { throw new DbAdminException("Unsupported export format: " + format); } - List fieldToInclude = otherParams.getOrDefault("fields[]", new ArrayList<>()); - - DbObjectSchema schema = dbAdmin.findSchemaByClassName(className); - - List fields = schema.getSortedFields().stream().filter(f -> fieldToInclude.contains(f.getName())) - .collect(Collectors.toList()); - Set queryFilters = Utils.computeFilters(schema, otherParams); - List results = repository.search(schema, query, queryFilters); - if (raw == null) raw = false; - switch (exportFormat) { case CSV: return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"export_" + schema.getJavaClass().getSimpleName() + ".csv\"") - .body(toCsv(results, fields, raw).getBytes()); + .body(toCsv(results, fieldsToInclude, raw).getBytes()); case XLSX: String sheetName = schema.getJavaClass().getSimpleName(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"export_" + schema.getJavaClass().getSimpleName() + ".xlsx\"") - .body(toXlsx(sheetName, results, fields, raw)); + .body(toXlsx(sheetName, results, fieldsToInclude, raw)); default: - throw new DbAdminException("Unable to detect export format"); + throw new DbAdminException("Invalid DataExportFormat"); } } - private byte[] toXlsx(String sheetName, List items, List fields, boolean raw) { + private byte[] toXlsx(String sheetName, List items, List fields, boolean raw) { Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(sheetName); + CellStyle headerStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerStyle.setFont(headerFont); + int rowIndex = 0; + Row headerRow = sheet.createRow(rowIndex++); + for (int i = 0; i < fields.size(); i++) { + Cell headerCell = headerRow.createCell(i); + headerCell.setCellValue(fields.get(i)); + headerCell.setCellStyle(headerStyle); + } + for (DbObject item : items) { Row row = sheet.createRow(rowIndex++); int cellIndex = 0; - for (DbField field : fields) { + + List record = getRecord(item, fields, raw); + + for (String value : record) { Cell cell = row.createCell(cellIndex++); - - if (raw) { - if (field.isForeignKey()) { - String cellValue = ""; - DbObject traverse = item.traverse(field); - if (traverse != null) cellValue = traverse.getPrimaryKeyValue().toString(); - cell.setCellValue(cellValue); - } else { - String cellValue = ""; - DbFieldValue fieldValue = item.get(field); - if (fieldValue.getValue() != null) cellValue = fieldValue.getValue().toString(); - cell.setCellValue(cellValue); - } - } else { - if (field.isForeignKey()) { - DbObject linkedItem = item.traverse(field); - cell.setCellValue(linkedItem.getPrimaryKeyValue() + " (" + linkedItem.getDisplayName() + ")"); - } else { - cell.setCellValue(item.get(field).getFormattedValue()); - } - } + cell.setCellValue(value); } } - // lets write the excel data to file now + ByteArrayOutputStream fos = new ByteArrayOutputStream(); try { workbook.write(fos); fos.close(); workbook.close(); } catch (IOException e) { - throw new DbAdminException("Error writing XLSX file"); + throw new DbAdminException("Error during serialization for XLSX workbook", e); } return fos.toByteArray(); } - private String toCsv(List items, List fields, boolean raw) { + private String toCsv(List items, List fields, boolean raw) { if (items.isEmpty()) return ""; StringWriter sw = new StringWriter(); - String[] header = fields.stream().map(f -> f.getName()).toArray(String[]::new); - - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setHeader(header).build(); + CSVFormat csvFormat = + CSVFormat.DEFAULT.builder() + .setHeader(fields.toArray(String[]::new)) + .build(); try (final CSVPrinter printer = new CSVPrinter(sw, csvFormat)) { for (DbObject item : items) { - printer.printRecord(fields.stream().map(f -> { - if (raw) { - if (f.isForeignKey()) { - DbObject traverse = item.traverse(f); - if (traverse == null) return ""; - else return traverse.getPrimaryKeyValue().toString(); - } else { - DbFieldValue fieldValue = item.get(f); - if (fieldValue.getValue() == null) return ""; - else return fieldValue.getValue().toString(); - } - - } else { - if (f.isForeignKey()) { - DbObject linkedItem = item.traverse(f); - return linkedItem.getPrimaryKeyValue() + " (" + linkedItem.getDisplayName() + ")"; - } else { - return item.get(f).getFormattedValue(); - } - } - })); + printer.printRecord(getRecord(item, fields, raw)); } return sw.toString(); } catch (IOException e) { - throw new DbAdminException(e); + throw new DbAdminException("Error during creation of CSV file", e); } } + + /** + * Builds and returns a record (i.e a row) for a spreadsheet file (CSV or XLSX) as a list of Strings. + * Each column contains the value of a database column, potentially with some processing applied if + * the {@code raw} parameter is true. + * + * @param item the object to create the record for + * @param fields the fields to include (this might contain {@code ComputedColumn} fields) + * @param raw whether to export raw values or performing standard processing (foreign key resolution, formatting) + * @return a record for a spreadsheet file as a list of Strings + */ + private List getRecord(DbObject item, List fields, boolean raw) { + List record = new ArrayList<>(); + + Set dbFields = item.getSchema().getSortedFields().stream().map(f -> f.getName()) + .collect(Collectors.toSet()); + Set computedFields = new HashSet<>(item.getSchema().getComputedColumnNames()); + + for (String field : fields) { + // Physical field + if (dbFields.contains(field)) { + DbField dbField = item.getSchema().getFieldByName(field); + if (dbField.isForeignKey()) { + DbObject linkedItem = item.traverse(dbField); + + if (linkedItem == null) record.add(""); + else { + if (raw) { + record.add(linkedItem.getPrimaryKeyValue().toString()); + } else { + record.add(linkedItem.getPrimaryKeyValue() + " (" + linkedItem.getDisplayName() + ")"); + } + } + } else { + if (raw) { + DbFieldValue fieldValue = item.get(dbField); + if (fieldValue.getValue() == null) record.add(""); + else record.add(fieldValue.getValue().toString()); + } else { + + record.add(item.get(dbField).getFormattedValue()); + } + + } + + } + // Computed column field + else if (computedFields.contains(field)) { + Object computedValue = item.compute(field); + record.add(computedValue.toString()); + } + else { + logger.info("Missing field `" + field + "` requested for export"); + } + } + + return record; + } } diff --git a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java index 9ada87d..baf5bcb 100644 --- a/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java +++ b/src/main/java/tech/ailef/dbadmin/external/dbmapping/DbObject.java @@ -168,6 +168,10 @@ public class DbObject { return schema.getComputedColumnNames(); } + public DbObjectSchema getSchema() { + return schema; + } + public Object compute(String column) { Method method = schema.getComputedColumn(column); diff --git a/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java b/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java index 7a3365d..f414f01 100644 --- a/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java +++ b/src/main/java/tech/ailef/dbadmin/external/exceptions/DbAdminException.java @@ -29,6 +29,10 @@ public class DbAdminException extends RuntimeException { public DbAdminException() { } + public DbAdminException(String msg, Throwable e) { + super(msg, e); + } + public DbAdminException(Throwable e) { super(e); } diff --git a/src/main/java/tech/ailef/dbadmin/internal/InternalDbAdminConfiguration.java b/src/main/java/tech/ailef/dbadmin/internal/InternalDbAdminConfiguration.java index cbf0c2f..82df0fa 100644 --- a/src/main/java/tech/ailef/dbadmin/internal/InternalDbAdminConfiguration.java +++ b/src/main/java/tech/ailef/dbadmin/internal/InternalDbAdminConfiguration.java @@ -24,8 +24,8 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** - * Configuration class for the "internal" data source. Place in the root "internal" - * package so as to allow component scanning and detection of models and repositories. + * Configuration class for the "internal" data source. This is place in the root "internal" + * package, so as to allow component scanning and detection of models and repositories. */ @ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true) @ComponentScan diff --git a/src/main/resources/templates/model/list.html b/src/main/resources/templates/model/list.html index b41b49b..fdc8e4b 100644 --- a/src/main/resources/templates/model/list.html +++ b/src/main/resources/templates/model/list.html @@ -31,6 +31,17 @@ +
+
+ + +
+
Active filters

Remove them from the right sidebar.

@@ -140,8 +151,8 @@ [[ ${schema.getTableName()} ]] -