diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java index c23dd322845d1f9a574eed3192676edd64ea4a7d..128a5bf9c241cb2103a9bc04d34486a1d313def7 100755 --- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java +++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java @@ -188,25 +188,26 @@ public class ObservationService { @QueryParam("locale") String localeStr, @QueryParam("isPositive") Boolean isPositive ) { - LOGGER.info("Generate xlsx file for observations"); VipsLogicUser user = getVipsLogicUser(userUUID); ULocale locale = new ULocale(localeStr != null ? localeStr : user != null ? user.getOrganizationId().getDefaultLocale() : userBean.getOrganization(organizationId).getDefaultLocale()); - + LOGGER.info("Generate xlsx file for observations for user {}", user != null ? user.getUserId() : "unregistered"); LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); String filenameTimestamp = now.format(formatter); try { - byte[] excelFile = ExcelFileGenerator.generateExcel(getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive), locale); + List<ObservationListItem> observations = getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive); + byte[] excelFile = ExcelFileGenerator.generateExcel(observations, locale); return Response .ok(excelFile) - .header("Content-Disposition", "attachment; filename=\"" +filenameTimestamp+ "-observations.xlsx\"") + .header("Content-Disposition", "attachment; filename=\"" + filenameTimestamp + "-observations.xlsx\"") .build(); } catch (IOException e) { + LOGGER.error(e.getMessage()); return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build(); } } diff --git a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java index 4380680bd9a820e78107e340796ee2a4256e8206..93918b0187535e3322a9515a25eb3fc4acdafea6 100644 --- a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java +++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java @@ -1,7 +1,11 @@ package no.nibio.vips.logic.util; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.util.ULocale; import no.nibio.vips.logic.entity.rest.ObservationListItem; +import no.nibio.vips.observationdata.ObservationDataSchema; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.ss.usermodel.*; @@ -10,45 +14,186 @@ import java.io.IOException; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.ResourceBundle; +import java.util.*; + +public final class ExcelFileGenerator { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final int COL_INDEX_DATE = 0; + private static final int COL_INDEX_LOCATION = 1; + private static final int COL_INDEX_ORGANISM = 2; + private static final int COL_INDEX_CROP_ORGANISM = 3; + private static final int COL_INDEX_HEADING = 4; + private static final int COL_START_INDEX_DATA = 5; + + public static byte[] generateExcel(List<ObservationListItem> observations, ULocale locale) throws IOException { + ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale()); -public class ExcelFileGenerator { - public static byte[] generateExcel(List<ObservationListItem> filteredObservationListItems, ULocale locale) throws IOException { try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { - ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale()); - // Prøvde å bruke i18n, fikk Invalid char (/) found at index (13) in sheet name 'Observasjoner/f?rstefunn' - Sheet sheet = workbook.createSheet("Observations"); + // Create main mainSheet for all observations, with header row + Sheet mainSheet = workbook.createSheet(rb.getString("allObservations")); + createHeaderRow(mainSheet, rb); - Row headerRow = sheet.createRow(0); - headerRow.createCell(0).setCellValue(rb.getString("timeOfObservation")); - headerRow.createCell(1).setCellValue(rb.getString("organism")); - headerRow.createCell(2).setCellValue(rb.getString("cropOrganismId")); - headerRow.createCell(3).setCellValue(rb.getString("observationHeading")); + int mainSheetRowIndex = 1; + // Add one row for each observation in list of all observations + for (ObservationListItem item : observations) { + createItemRow(mainSheet, mainSheetRowIndex++, item); + } + autoSizeColumns(mainSheet, 0, COL_INDEX_HEADING); - int rowNum = 1; - for (ObservationListItem item : filteredObservationListItems) { - // TODO Better way of getting current timezone? - LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + // Prepare list of observations for each type of pest + Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations); - Row row = sheet.createRow(rowNum++); - row.createCell(0).setCellValue(localDateOfObservation.format(formatter)); - row.createCell(1).setCellValue(item.getOrganismName()); - row.createCell(2).setCellValue(item.getCropOrganismName()); - row.createCell(3).setCellValue(item.getObservationHeading()); + // Create sheets for each individual pest type + for (Integer pestId : pestObservations.keySet()) { + List<ObservationListItem> observationsForPest = pestObservations.get(pestId); + ObservationListItem firstObservationForPest = observationsForPest.get(0); + String pestName = firstObservationForPest.getOrganismName(); + Sheet pestSheet = workbook.createSheet(pestName); + Row headerRow = createHeaderRow(pestSheet, rb); - } + // Add column titles for observation data + Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema()); + int pestSheetColIndex = COL_START_INDEX_DATA; + for (String key : dataColumnTitles.keySet()) { + headerRow.createCell(pestSheetColIndex++).setCellValue(dataColumnTitles.get(key)); + } + + int pestSheetRowIndex = 1; + for (ObservationListItem item : observationsForPest) { + Row row = createItemRow(pestSheet, pestSheetRowIndex++, item); - // Auto-size columns to fit content - for (int i = 0; i <= 3; i++) { - sheet.autoSizeColumn(i); + if (item.getObservationData() != null) { + Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class); + pestSheetColIndex = COL_START_INDEX_DATA; + for (String key : dataColumnTitles.keySet()) { + Object value = observationDataMap.get(key); + if (value instanceof Number) { + row.createCell(pestSheetColIndex++).setCellValue(((Number) value).intValue()); + } else { + row.createCell(pestSheetColIndex++).setCellValue(value != null ? value.toString() : ""); + } + } + } + } + autoSizeColumns(pestSheet, COL_INDEX_DATE, COL_START_INDEX_DATA + dataColumnTitles.size()); } workbook.write(out); return out.toByteArray(); } } + + /** + * Auto-size columns of given sheet, from startIndex to endIndex, to ensure that content fits + * + * @param sheet The sheet of which to auto-size columns + * @param startIndex The index of the first column to auto-size + * @param endIndex The index of the last column to auto-size + */ + private static void autoSizeColumns(Sheet sheet, int startIndex, int endIndex) { + for (int i = startIndex; i <= endIndex; i++) { + sheet.autoSizeColumn(i); + } + } + + /** + * Create map with data property name as key, and corresponding localized name as value + * + * @param observationDataSchema The observation data schema which contains localized names and other info + * @return result map + */ + private static Map<String, String> getObservationDataColumnTitles(ObservationDataSchema observationDataSchema) throws JsonProcessingException { + JsonNode rootNode = objectMapper.readTree(observationDataSchema.getDataSchema()); + JsonNode propertiesNode = rootNode.path("properties"); + Map<String, String> resultMap = new HashMap<>(); + Iterator<Map.Entry<String, JsonNode>> fields = propertiesNode.fields(); + while (fields.hasNext()) { + Map.Entry<String, JsonNode> field = fields.next(); + String key = field.getKey(); + String value = field.getValue().path("title").asText(); + resultMap.put(key, value); + } + return resultMap; + } + + /** + * Find the name of the point of interest in given geoInfo string + * + * @param geoInfo The geoInfo which might contain the name of the point of interest + * @return the point of interest name, or empty string + */ + private static String getPointOfInterestName(String geoInfo) throws JsonProcessingException { + JsonNode rootNode = objectMapper.readTree(geoInfo); + JsonNode featuresNode = rootNode.path("features"); + if (featuresNode.isArray() && !featuresNode.isEmpty()) { + JsonNode firstFeature = featuresNode.get(0); + JsonNode propertiesNode = firstFeature.path("properties"); + return propertiesNode.path("pointOfInterestName").asText(); + } + return ""; + } + + /** + * Create map with pestId as key, and list of corresponding observations as value + * + * @param observations The original list of observations + * @return result map + */ + private static Map<Integer, List<ObservationListItem>> getObservationsForEachPest(List<ObservationListItem> observations) { + Map<Integer, List<ObservationListItem>> pestObservations = new HashMap<>(); + for (ObservationListItem observation : observations) { + Integer pestId = observation.getOrganismId(); + if (!pestObservations.containsKey(pestId)) { + List<ObservationListItem> observationList = new ArrayList<>(); + observationList.add(observation); + pestObservations.put(pestId, observationList); + } else { + pestObservations.get(pestId).add(observation); + } + } + return pestObservations; + } + + /** + * Create row with given index, for given observation list item + * + * @param sheet The sheet to which a row will be added + * @param rowIndex The index of the row + * @param item The item of which to add data + * @return the newly created row + */ + private static Row createItemRow(Sheet sheet, int rowIndex, ObservationListItem item) throws JsonProcessingException { + LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + String pointOfInterestName = getPointOfInterestName(item.getGeoInfo()); + + Row row = sheet.createRow(rowIndex); + row.createCell(COL_INDEX_DATE).setCellValue(localDateOfObservation.format(DATE_FORMATTER)); + row.createCell(COL_INDEX_LOCATION).setCellValue(pointOfInterestName); + row.createCell(COL_INDEX_ORGANISM).setCellValue(item.getOrganismName()); + row.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(item.getCropOrganismName()); + row.createCell(COL_INDEX_HEADING).setCellValue(item.getObservationHeading()); + return row; + } + + /** + * Create first row of given sheet, with standard set of column titles + * + * @param sheet The sheet to which a row will be added + * @param rb A resource bundle enabling localized messages + * @return the newly created header row + */ + public static Row createHeaderRow(Sheet sheet, ResourceBundle rb) { + Row headerRow = sheet.createRow(0); + headerRow.createCell(COL_INDEX_DATE).setCellValue(rb.getString("timeOfObservation")); + headerRow.createCell(COL_INDEX_LOCATION).setCellValue(rb.getString("location")); + headerRow.createCell(COL_INDEX_ORGANISM).setCellValue(rb.getString("organism")); + headerRow.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(rb.getString("cropOrganismId")); + headerRow.createCell(COL_INDEX_HEADING).setCellValue(rb.getString("observationHeading")); + return headerRow; + } } diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties index 80a357d25a61df2d96e5e3f7fbbfb66433f71d15..ac9819608aa1a1c0d740535f4016846db737bb89 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties @@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel observationDataField_unit = Measuring unit +allObservations = All observations + observationDeleted = Observation was deleted observationHeading = Observation heading diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties index db00fc266365213c9c50b7885c415baa578a339a..ccc99bf289cefcae94c4472346ae762d02977cb3 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties @@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f observationDataField_unit = M\u00e5leenhet +allObservations = Alle observasjoner + observationDeleted = Observasjonen ble slettet observationHeading = Observasjonstittel