diff --git a/src/main/java/no/nibio/vips/logic/entity/Observation.java b/src/main/java/no/nibio/vips/logic/entity/Observation.java index 1e8027b0595316fa42e4f7a312f514aeb8dd0c64..fae7f4f5a202eacd37b7d08afd12994a43679530 100755 --- a/src/main/java/no/nibio/vips/logic/entity/Observation.java +++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java @@ -703,6 +703,8 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse // Specific geoInfo trumps location. This is to be interpreted // as that the observation has been geographically masked by // choice of the observer + this.locationPointOfInterestId, + this.location != null ? this.location.getName() : null, this.location != null && this.geoinfo == null ? this.location.getGeoJSON() : this.getGeoinfo(), this.getObservationHeading(), this.getObservationText(), diff --git a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java index 7ba4386cc08c8a77c18dc94621cce7539cd049c3..5082c7089796662fdd85982ab4ff6529e0e41b90 100644 --- a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java +++ b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java @@ -33,6 +33,8 @@ public class ObservationListItem implements Comparable{ private Date timeOfObservation; private String organismName, cropOrganismName; private String observationTimeSeriesLabel; + private Integer locationPointOfInterestId; + private String locationPointOfInterestName; private String geoInfo; private String observationHeading; private String observationText; @@ -53,6 +55,8 @@ public class ObservationListItem implements Comparable{ Integer cropOrganismId, String cropOrganismName, String observationTimeSeriesLabel, + Integer poiId, + String poiName, String geoinfo, String observationHeading, String observationText, @@ -60,8 +64,7 @@ public class ObservationListItem implements Comparable{ Boolean locationIsPrivate, Boolean isPositive, String observationData, - ObservationDataSchema observationDataSchema - ){ + ObservationDataSchema observationDataSchema){ this.observationId = observationId; this.observerId = observerId; this.observerName = observerName; @@ -72,6 +75,8 @@ public class ObservationListItem implements Comparable{ this.cropOrganismId = cropOrganismId; this.cropOrganismName = cropOrganismName; this.observationTimeSeriesLabel = observationTimeSeriesLabel; + this.locationPointOfInterestId = poiId; + this.locationPointOfInterestName = poiName; this.geoInfo = geoinfo; this.observationHeading = observationHeading; this.observationText = observationText; @@ -199,6 +204,22 @@ public class ObservationListItem implements Comparable{ this.observationTimeSeriesLabel = observationTimeSeriesLabel; } + public Integer getLocationPointOfInterestId() { + return locationPointOfInterestId; + } + + public void setLocationPointOfInterestId(Integer locationPointOfInterestId) { + this.locationPointOfInterestId = locationPointOfInterestId; + } + + public String getLocationPointOfInterestName() { + return locationPointOfInterestName; + } + + public void setLocationPointOfInterestName(String locationPointOfInterestName) { + this.locationPointOfInterestName = locationPointOfInterestName; + } + /** * @return the geoInfo */ 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 df362f8538e454defc4e1bef1f2b3597e5e959e8..046ff1ac5327332b60f0357bc13d1b7f15537fd3 100755 --- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java +++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java @@ -192,7 +192,7 @@ public class ObservationService { 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"); + LOGGER.info("Generate xlsx file for observations for user {} from {} to {}", user != null ? user.getUserId() : "unregistered", fromStr, toStr); LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); 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 82d1d38f74aed7aaa6424b146533891933f2f907..918b3a1485456a8c7b83a8d189df906f98f62d50 100644 --- a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java +++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java @@ -10,6 +10,8 @@ import no.nibio.vips.observationdata.ObservationDataSchema; import org.apache.poi.common.usermodel.HyperlinkType; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.ss.usermodel.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -20,38 +22,74 @@ import java.util.*; public final class ExcelFileGenerator { + private static Logger LOGGER = LoggerFactory.getLogger(ExcelFileGenerator.class); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final int COL_INDEX_ID = 0; - private static final int COL_INDEX_DATE = 1; - private static final int COL_INDEX_LOCATION = 2; - private static final int COL_INDEX_OBSERVER_ID = 3; - private static final int COL_INDEX_OBSERVER_NAME = 4; - private static final int COL_INDEX_OBSERVATION_TIME_SERIES_ID = 5; - private static final int COL_INDEX_OBSERVATION_TIME_SERIES_LABEL = 6; - private static final int COL_INDEX_ORGANISM = 7; - private static final int COL_INDEX_CROP_ORGANISM = 8; - private static final int COL_INDEX_HEADING = 9; - private static final int COL_INDEX_DESCRIPTION = 10; - private static final int COL_START_INDEX_DATA = 11; + private enum ColumnIndex { + ID(false, 0, 0, "observationId"), + DATE(false, 1, 1, "timeOfObservation"), + POI_NAME(false, 2, 2, "location"), + OBSERVER_ID(true, null, 3, "observerId"), + OBSERVER_NAME(true, null, 4, "observer"), + OBSERVATION_TIME_SERIES_ID(false, 3, 5, "observationTimeSeriesId"), + OBSERVATION_TIME_SERIES_LABEL(false, 4, 6, "observationTimeSeriesLabel"), + ORGANISM(false, 5, 7, "organism"), + CROP_ORGANISM(false, 6, 8, "cropOrganismId"), + HEADING(false, 7, 9, "observationHeading"), + DESCRIPTION(false, 8, 10, "observationText"), + BROADCAST(false, 9, 11, "isBroadcast"), + POSITIVE(false, 10, 12, "isPositiveRegistration"), + INDEX_DATA(false, 11, 13, null); + + private final boolean isSensitive; + private final Integer openIndex; + private final Integer adminIndex; + private final String rbKey; + + ColumnIndex(boolean isSensitive, Integer openIndex, Integer adminIndex, String rbKey) { + this.isSensitive = isSensitive; + this.openIndex = openIndex; + this.adminIndex = adminIndex; + this.rbKey = rbKey; + } + + public static List<ColumnIndex> forUser(boolean isAdmin) { + if (!isAdmin) { + return Arrays.stream(ColumnIndex.values()).filter(columnIndex -> !columnIndex.isSensitive).toList(); + } + return Arrays.stream(ColumnIndex.values()).toList(); + } + + public String getColumnHeading(ResourceBundle rb) { + return rbKey != null && !rbKey.isBlank() ? rb.getString(rbKey) : ""; + } + + public Integer getIndex(boolean admin) { + if (admin) { + return adminIndex; + } + return openIndex; + } + } public static byte[] generateExcel(VipsLogicUser user, ULocale locale, List<ObservationListItem> observations) throws IOException { ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale()); - + boolean isAdmin = user != null && (user.isSuperUser() || user.isOrganizationAdmin()); + LOGGER.info("Create Excel file containing {} observations for {} user", observations.size(), isAdmin ? "admin": "regular"); try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { // Create main mainSheet for all observations, with header row Sheet mainSheet = workbook.createSheet(rb.getString("allObservations")); - createHeaderRow(mainSheet, rb); + createHeaderRow(isAdmin, mainSheet, rb); int mainSheetRowIndex = 1; // Add one row for each observation in list of all observations for (ObservationListItem item : observations) { - createItemRow(workbook, mainSheet, mainSheetRowIndex++, item); + createItemRow(isAdmin, workbook, mainSheet, mainSheetRowIndex++, item, rb); } - autoSizeColumns(mainSheet, 0, COL_INDEX_DESCRIPTION); + autoSizeColumns(mainSheet, 0, ColumnIndex.INDEX_DATA.getIndex(isAdmin) - 1); // Prepare list of observations for each type of pest Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations); @@ -61,24 +99,24 @@ public final class ExcelFileGenerator { 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); + Sheet pestSheet = workbook.createSheet(sanitizeSheetName(pestId, pestName)); + Row headerRow = createHeaderRow(isAdmin, pestSheet, rb); // Add column titles for observation data Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema()); - int pestSheetColIndex = COL_START_INDEX_DATA; + int pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin); for (String key : dataColumnTitles.keySet()) { headerRow.createCell(pestSheetColIndex++).setCellValue(dataColumnTitles.get(key)); } int pestSheetRowIndex = 1; for (ObservationListItem item : observationsForPest) { - Row row = createItemRow(workbook, pestSheet, pestSheetRowIndex++, item); + Row row = createItemRow(isAdmin, workbook, pestSheet, pestSheetRowIndex++, item, rb); if (item.getObservationData() != null) { Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class); if (observationDataMap != null) { - pestSheetColIndex = COL_START_INDEX_DATA; + pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin); for (String key : dataColumnTitles.keySet()) { Object value = observationDataMap.get(key); if (value instanceof Number) { @@ -90,7 +128,7 @@ public final class ExcelFileGenerator { } } } - autoSizeColumns(pestSheet, COL_INDEX_DATE, COL_START_INDEX_DATA + dataColumnTitles.size()); + autoSizeColumns(pestSheet, ColumnIndex.ID.getIndex(isAdmin), ColumnIndex.INDEX_DATA.getIndex(isAdmin) + dataColumnTitles.size()); } workbook.write(out); @@ -98,6 +136,21 @@ public final class ExcelFileGenerator { } } + /** + * Create sheet name without invalid characters and within size limits + * + * @param pestId The id of the pest + * @param pestName The name of the pest + * @return a sanitized string to be used as sheet name + */ + private static String sanitizeSheetName(Integer pestId, String pestName) { + if (pestName == null || pestName.isBlank()) { + return "Id=" + pestId; + } + return pestName.replaceAll("[\\[\\]\\*/:?\\\\]", "_").substring(0, Math.min(pestName.length(), 31)); + } + + /** * Auto-size columns of given sheet, from startIndex to endIndex, to ensure that content fits * @@ -131,23 +184,6 @@ public final class ExcelFileGenerator { 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 * @@ -170,67 +206,74 @@ public final class ExcelFileGenerator { } /** - * Create first row of given sheet, with standard set of column titles + * Create first row of given sheet, with column titles dependent on user privileges * - * @param sheet The sheet to which a row will be added - * @param rb A resource bundle enabling localized messages + * @param isAdmin Whether the user is a logged in admin + * @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) { + public static Row createHeaderRow(boolean isAdmin, Sheet sheet, ResourceBundle rb) { Row headerRow = sheet.createRow(0); - headerRow.createCell(COL_INDEX_ID).setCellValue(rb.getString("observationId")); - headerRow.createCell(COL_INDEX_DATE).setCellValue(rb.getString("timeOfObservation")); - headerRow.createCell(COL_INDEX_LOCATION).setCellValue(rb.getString("location")); - headerRow.createCell(COL_INDEX_OBSERVER_ID).setCellValue(rb.getString("observerId")); - headerRow.createCell(COL_INDEX_OBSERVER_NAME).setCellValue(rb.getString("observer")); - headerRow.createCell(COL_INDEX_OBSERVATION_TIME_SERIES_ID).setCellValue(rb.getString("observationTimeSeriesId")); - headerRow.createCell(COL_INDEX_OBSERVATION_TIME_SERIES_LABEL).setCellValue(rb.getString("observationTimeSeriesLabel")); - 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")); - headerRow.createCell(COL_INDEX_DESCRIPTION).setCellValue(rb.getString("observationText")); + for (ColumnIndex columnIndex : ColumnIndex.forUser(isAdmin)) { + headerRow.createCell(columnIndex.getIndex(isAdmin)).setCellValue(columnIndex.getColumnHeading(rb)); + } return headerRow; } /** * Create row with given index, for given observation list item * + * @param isAdmin Whether the user is a logged in admin * @param workbook The current workbook * @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(XSSFWorkbook workbook, Sheet sheet, int rowIndex, ObservationListItem item) throws JsonProcessingException { + private static Row createItemRow(boolean isAdmin, XSSFWorkbook workbook, Sheet sheet, int rowIndex, ObservationListItem item, ResourceBundle rb) throws JsonProcessingException { LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); - String pointOfInterestName = getPointOfInterestName(item.getGeoInfo()); - Row row = sheet.createRow(rowIndex); - addObservationLinkToFirstCol(workbook, row, item.getObservationId()); - row.createCell(COL_INDEX_DATE).setCellValue(localDateOfObservation.format(DATE_FORMATTER)); - row.createCell(COL_INDEX_LOCATION).setCellValue(pointOfInterestName); - row.createCell(COL_INDEX_OBSERVER_ID).setCellValue(item.getObserverId()); - row.createCell(COL_INDEX_OBSERVER_NAME).setCellValue(item.getObserverName()); + addObservationLinkToFirstCol(isAdmin, workbook, row, item.getObservationId()); + row.createCell(ColumnIndex.DATE.getIndex(isAdmin)).setCellValue(localDateOfObservation.format(DATE_FORMATTER)); + row.createCell(ColumnIndex.POI_NAME.getIndex(isAdmin)).setCellValue(item.getLocationPointOfInterestName()); + if (isAdmin) { + row.createCell(ColumnIndex.OBSERVER_ID.getIndex(isAdmin)).setCellValue(item.getObserverId()); + row.createCell(ColumnIndex.OBSERVER_NAME.getIndex(isAdmin)).setCellValue(item.getObserverName()); + } if(item.getObservationTimeSeriesId() != null) { - row.createCell(COL_INDEX_OBSERVATION_TIME_SERIES_ID).setCellValue(item.getObservationTimeSeriesId()); - row.createCell(COL_INDEX_OBSERVATION_TIME_SERIES_LABEL).setCellValue(item.getObservationTimeSeriesLabel()); + row.createCell(ColumnIndex.OBSERVATION_TIME_SERIES_ID.getIndex(isAdmin)).setCellValue(item.getObservationTimeSeriesId()); + row.createCell(ColumnIndex.OBSERVATION_TIME_SERIES_LABEL.getIndex(isAdmin)).setCellValue(item.getObservationTimeSeriesLabel()); } + row.createCell(ColumnIndex.ORGANISM.getIndex(isAdmin)).setCellValue(item.getOrganismName()); + row.createCell(ColumnIndex.CROP_ORGANISM.getIndex(isAdmin)).setCellValue(item.getCropOrganismName()); + row.createCell(ColumnIndex.HEADING.getIndex(isAdmin)).setCellValue(item.getObservationHeading()); + row.createCell(ColumnIndex.DESCRIPTION.getIndex(isAdmin)).setCellValue(item.getObservationText()); - 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()); - row.createCell(COL_INDEX_DESCRIPTION).setCellValue(item.getObservationText()); + row.createCell(ColumnIndex.BROADCAST.getIndex(isAdmin)).setCellValue(getBooleanStringValue(rb, item.getBroadcastMessage())); + row.createCell(ColumnIndex.POSITIVE.getIndex(isAdmin)).setCellValue(getBooleanStringValue(rb, item.getIsPositive())); return row; } - private static void addObservationLinkToFirstCol(Workbook workbook, Row row, Integer observationId) { - Cell cell = row.createCell(COL_INDEX_ID); + private static String getBooleanStringValue(ResourceBundle rb, Boolean value) { + if (value == null) { + return null; + } else if (value) { + return rb.getString("yes"); + } + return rb.getString("no"); + } + + private static void addObservationLinkToFirstCol(boolean isAdmin, Workbook workbook, Row row, Integer observationId) { + Cell cell = row.createCell(ColumnIndex.ID.getIndex(isAdmin)); cell.setCellValue(observationId); CreationHelper creationHelper = workbook.getCreationHelper(); Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); - hyperlink.setAddress("https://www.vips-landbruk.no/observations/" + observationId); + //hyperlink.setAddress("https://www.vips-landbruk.no/observations/" + observationId); + // TODO Dette må fikses før deploy til prod + hyperlink.setAddress("https://testvips.nibio.no/observations/" + observationId); cell.setHyperlink(hyperlink); CellStyle hlinkStyle = workbook.createCellStyle(); 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 75d9c6bccf0247ce999554491d6e301611260011..3eea5dd6740a54ae9b8a7bcd02ac18b5408b5930 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties @@ -1059,3 +1059,6 @@ thresholdDSVTempMin=Minimum temperature for DSV calculation observationTimeSeriesId=Timeseries observationTimeSeriesLabel=Timeseries label observationId=Observation +isBroadcast=Is broadcast +yes=Yes +no=No 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 c00c7ea409d43e16f65b11ff990a7f1a6fa5d241..5a4ae4498c686c218a11c4bd282fdcfa432062dd 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 @@ -1058,3 +1058,6 @@ thresholdDSVTempMin=Minimumstemperatur for beregning av DSV observationTimeSeriesId=Tidsserie observationTimeSeriesLabel=Tidsseriemerkelapp observationId=Observasjon +isBroadcast=Er kringkastet +yes=Ja +no=Nei diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties index 0384f07d5ec425c9d03406f2c84337117731f304..e118325c6d1829c0f06ba616f7f1a19c540cce12 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties @@ -1040,3 +1040,6 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +isBroadcast=Is broadcast +yes=Yes +no=No