Improved CSV export code

This commit is contained in:
Francesco 2023-10-15 11:48:44 +02:00
parent e06422c105
commit 4e4a078952
5 changed files with 123 additions and 72 deletions

View File

@ -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,6 +45,7 @@ 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<byte[]> export(@PathVariable String className, @RequestParam(required = false) String query,
@RequestParam String format, @RequestParam(required=false) Boolean raw,
@RequestParam MultiValueMap<String, String> otherParams) {
if (format == null)
format = "CSV";
if (raw == null) raw = false;
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
List<String> 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<String> fieldToInclude = otherParams.getOrDefault("fields[]", new ArrayList<>());
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
List<DbField> fields = schema.getSortedFields().stream().filter(f -> fieldToInclude.contains(f.getName()))
.collect(Collectors.toList());
Set<QueryFilter> queryFilters = Utils.computeFilters(schema, otherParams);
List<DbObject> 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<DbObject> items, List<DbField> fields, boolean raw) {
private byte[] toXlsx(String sheetName, List<DbObject> items, List<String> 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) {
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());
List<String> record = getRecord(item, fields, raw);
for (String value : record) {
Cell cell = row.createCell(cellIndex++);
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<DbObject> items, List<DbField> fields, boolean raw) {
private String toCsv(List<DbObject> items, List<String> 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<String> getRecord(DbObject item, List<String> fields, boolean raw) {
List<String> record = new ArrayList<>();
Set<String> dbFields = item.getSchema().getSortedFields().stream().map(f -> f.getName())
.collect(Collectors.toSet());
Set<String> 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;
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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

View File

@ -31,6 +31,17 @@
</label>
</div>
</div>
<div th:each="field : ${schema.getComputedColumnNames()}">
<div class="form-check">
<input class="form-check-input" type="checkbox"
th:value="${field}" th:id="|__check_${field}|"
th:name="fields[]"
checked>
<label class="form-check-label" th:for="|__check_${field}|">
[[ ${field} ]]
</label>
</div>
</div>
<div th:if="${!activeFilters.isEmpty()}">
<h5 class="fw-bold mt-3 mb-0">Active filters</h5>
<p class="text-muted">Remove them from the right sidebar.</p>
@ -140,8 +151,8 @@
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
[[ ${schema.getTableName()} ]]
</span>
<button type="button" class="btn p-0 m-0 ms-3" data-bs-toggle="modal" data-bs-target="#csvExportModal">
<i class="bi bi-file-earmark-spreadsheet" style="font-size: 1.5rem;"></i>
<button title="Open export data window" type="button" class="btn" data-bs-toggle="modal" data-bs-target="#csvExportModal">
<i class="bi bi-file-earmark-spreadsheet export-icon" style="font-size: 1.5rem;"></i>
</button>
</h3>