package eu.eudat.logic.utilities.documents.word; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import eu.eudat.logic.services.forms.VisibilityRuleService; import eu.eudat.logic.utilities.documents.types.ParagraphStyle; import eu.eudat.logic.utilities.interfaces.ApplierWithValue; import eu.eudat.logic.utilities.json.JavaToJson; import eu.eudat.models.data.components.commons.datafield.*; import eu.eudat.models.data.user.components.datasetprofile.Field; import eu.eudat.models.data.user.components.datasetprofile.FieldSet; import eu.eudat.models.data.user.components.datasetprofile.Section; import eu.eudat.models.data.user.composite.DatasetProfilePage; import eu.eudat.models.data.user.composite.PagedDatasetProfile; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.*; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTLvl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STNumberFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.math.BigInteger; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.apache.poi.xwpf.usermodel.Document.*; public class WordBuilder { private static final Logger logger = LoggerFactory.getLogger(WordBuilder.class); private static final Map IMAGE_TYPE_MAP = Stream.of(new Object[][]{ {"image/jpeg", PICTURE_TYPE_JPEG}, {"image/png", PICTURE_TYPE_PNG}, {"image/gif", PICTURE_TYPE_GIF}, {"image/tiff", PICTURE_TYPE_TIFF}, {"image/bmp", PICTURE_TYPE_BMP}, {"image/wmf", PICTURE_TYPE_WMF} } ).collect(Collectors.toMap(objects -> (String) objects[0], o -> (Integer) o[1])); private final Map> options = new HashMap<>(); private final CTAbstractNum cTAbstractNum; private BigInteger numId; private Integer indent; private final ObjectMapper mapper; public WordBuilder(Environment environment) { this.cTAbstractNum = CTAbstractNum.Factory.newInstance(); this.cTAbstractNum.setAbstractNumId(BigInteger.valueOf(1)); this.indent = 0; this.mapper = new ObjectMapper(); this.buildOptions(environment); } private void buildOptions(Environment environment) { this.options.put(ParagraphStyle.TEXT, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); XWPFRun run = paragraph.createRun(); if (item != null) run.setText(" " + item); run.setFontSize(11); return paragraph; }); this.options.put(ParagraphStyle.HTML, (mainDocumentPart, item) -> { Document htmlDoc = Jsoup.parse(((String)item).replaceAll("\n", "
")); HtmlToWorldBuilder htmlToWorldBuilder = HtmlToWorldBuilder.convert(mainDocumentPart, htmlDoc, indent > 0 ? (indent/2.0F) * 0.8F : 0.8F); return htmlToWorldBuilder.getParagraph(); }); this.options.put(ParagraphStyle.TITLE, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Title"); paragraph.setAlignment(ParagraphAlignment.CENTER); XWPFRun run = paragraph.createRun(); run.setText((String)item); run.setBold(true); run.setFontSize(14); return paragraph; }); this.options.put(ParagraphStyle.HEADER1, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading1"); XWPFRun run = paragraph.createRun(); run.setText((String)item); // run.setBold(true); // run.setFontSize(12); // run.setStyle("0"); return paragraph; }); this.options.put(ParagraphStyle.HEADER2, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading2"); XWPFRun run = paragraph.createRun(); run.setText(" " + item); // run.setBold(true); // run.setFontSize(12); return paragraph; }); this.options.put(ParagraphStyle.HEADER3, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading3"); XWPFRun run = paragraph.createRun(); run.setText(" " + item); // run.setBold(true); // run.setFontSize(11); return paragraph; }); this.options.put(ParagraphStyle.HEADER4, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading4"); XWPFRun run = paragraph.createRun(); run.setText((String)item); return paragraph; }); this.options.put(ParagraphStyle.HEADER5, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading5"); XWPFRun run = paragraph.createRun(); run.setText(" " + item); return paragraph; }); this.options.put(ParagraphStyle.HEADER6, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setStyle("Heading6"); XWPFRun run = paragraph.createRun(); run.setText(" " + item); return paragraph; }); this.options.put(ParagraphStyle.FOOTER, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); XWPFRun run = paragraph.createRun(); run.setText((String)item); return paragraph; }); this.options.put(ParagraphStyle.COMMENT, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); XWPFRun run = paragraph.createRun(); run.setText(" " + item); run.setItalic(true); return paragraph; }); this.options.put(ParagraphStyle.IMAGE, (mainDocumentPart, item) -> { XWPFParagraph paragraph = mainDocumentPart.createParagraph(); paragraph.setPageBreak(true); XWPFRun run = paragraph.createRun(); String imageId = ((Map)item).get("id"); String fileName = ((Map)item).get("name"); String fileType = ((Map)item).get("type"); int format; format = IMAGE_TYPE_MAP.getOrDefault(fileType, 0); try { ImageInputStream iis = ImageIO.createImageInputStream(new File(environment.getProperty("file.storage") + imageId)); Iterator readers = ImageIO.getImageReaders(iis); if (readers.hasNext()) { ImageReader reader = readers.next(); reader.setInput(iis); int initialImageWidth = reader.getWidth(0); int initialImageHeight = reader.getHeight(0); float ratio = initialImageHeight / (float) initialImageWidth; int marginLeftInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getLeft()).intValue(); int marginRightInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getRight()).intValue(); int pageWidthInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgSz().getW()).intValue(); int pageWidth = Math.round((pageWidthInDXA - marginLeftInDXA - marginRightInDXA) / (float) 20); // /20 converts dxa to points int imageWidth = Math.round(initialImageWidth * (float) 0.75); // *0.75 converts pixels to points int width = Math.min(imageWidth, pageWidth); int marginTopInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getTop()).intValue(); int marginBottomInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getBottom()).intValue(); int pageHeightInDXA = ((BigInteger) mainDocumentPart.getDocument().getBody().getSectPr().getPgSz().getH()).intValue(); int pageHeight = Math.round((pageHeightInDXA - marginTopInDXA - marginBottomInDXA) / (float) 20); // /20 converts dxa to points int imageHeight = Math.round(initialImageHeight * ((float) 0.75)); // *0.75 converts pixels to points int height = Math.round(width * ratio); if (height > pageHeight) { // height calculated with ratio is too large. Image may have Portrait (vertical) orientation. Recalculate image dimensions. height = Math.min(imageHeight, pageHeight); width = Math.round(height / ratio); } FileInputStream image = new FileInputStream(environment.getProperty("file.storage") + imageId); run.addPicture(image, format, fileName, Units.toEMU(width), Units.toEMU(height)); paragraph.setPageBreak(false); } } catch (IOException | InvalidFormatException e){ logger.error(e.getMessage(), e); } return paragraph; }); } public XWPFDocument build(XWPFDocument document, PagedDatasetProfile pagedDatasetProfile, VisibilityRuleService visibilityRuleService) throws IOException { createPages(pagedDatasetProfile.getPages(), document, true, visibilityRuleService); XWPFNumbering numbering = document.createNumbering(); BigInteger tempNumId = BigInteger.ONE; boolean found = false; while (!found) { Object o = numbering.getAbstractNum(tempNumId); found = (o == null); if (!found) tempNumId = tempNumId.add(BigInteger.ONE); } cTAbstractNum.setAbstractNumId(tempNumId); XWPFAbstractNum abstractNum = new XWPFAbstractNum(cTAbstractNum); BigInteger abstractNumID = numbering.addAbstractNum(abstractNum); this.numId = numbering.addNum(abstractNumID); createPages(pagedDatasetProfile.getPages(), document, false, visibilityRuleService); return document; } private void createPages(List datasetProfilePages, XWPFDocument mainDocumentPart, Boolean createListing, VisibilityRuleService visibilityRuleService) { datasetProfilePages.forEach(item -> { try { createSections(item.getSections(), mainDocumentPart, ParagraphStyle.HEADER4, 0, createListing, visibilityRuleService, item.getOrdinal() + 1, null); } catch (Exception e) { logger.error(e.getMessage(), e); } }); } private void createSections(List
sections, XWPFDocument mainDocumentPart, ParagraphStyle style, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService, Integer page, String sectionString) { if (createListing) this.addListing(mainDocumentPart, indent, false, true); boolean hasValue = false; for (Section section: sections) { int paragraphPos = -1; String tempSectionString = sectionString != null ? sectionString + "." + (section.getOrdinal() + 1) : "" + (section.getOrdinal() + 1); if (visibilityRuleService.isElementVisible(section.getId())) { if (!createListing) { XWPFParagraph paragraph = addParagraphContent(page + "." + tempSectionString + " " + section.getTitle(), mainDocumentPart, style, numId); CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl(); number.setVal(BigInteger.valueOf(indent)); paragraphPos = mainDocumentPart.getPosOfParagraph(paragraph); } createSections(section.getSections(), mainDocumentPart, ParagraphStyle.HEADER4, 1, createListing, visibilityRuleService, page, tempSectionString); hasValue = createCompositeFields(section.getCompositeFields(), mainDocumentPart, 2, createListing, visibilityRuleService, page, tempSectionString); if (!hasValue && paragraphPos > -1) { mainDocumentPart.removeBodyElement(paragraphPos); } } } } private Boolean createCompositeFields(List
compositeFields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService, Integer page, String section) { if (createListing) this.addListing(mainDocumentPart, indent, true, true); boolean hasValue = false; boolean returnedValue = false; for (FieldSet compositeField: compositeFields) { if (visibilityRuleService.isElementVisible(compositeField.getId()) && hasVisibleFields(compositeField, visibilityRuleService)) { char c = 'a'; int paragraphPos = -1; if (compositeField.getTitle() != null && !compositeField.getTitle().isEmpty() && !createListing) { XWPFParagraph paragraph = addParagraphContent(page + "." + section + "." + (compositeField.getOrdinal() +1) + " " + compositeField.getTitle(), mainDocumentPart, ParagraphStyle.HEADER6, numId); CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl(); number.setVal(BigInteger.valueOf(indent)); paragraphPos = mainDocumentPart.getPosOfParagraph(paragraph); if(compositeField.getMultiplicityItems() != null && !compositeField.getMultiplicityItems().isEmpty()){ addParagraphContent(c + ".\n", mainDocumentPart, ParagraphStyle.HEADER6, numId); } } hasValue = createFields(compositeField.getFields(), mainDocumentPart, 3, createListing, visibilityRuleService); if(hasValue){ returnedValue = true; } if (compositeField.getMultiplicityItems() != null && !compositeField.getMultiplicityItems().isEmpty()) { List
list = compositeField.getMultiplicityItems().stream().sorted(Comparator.comparingInt(FieldSet::getOrdinal)).collect(Collectors.toList()); for (FieldSet multiplicityFieldset : list) { if(!createListing){ c++; addParagraphContent(c + ".\n", mainDocumentPart, ParagraphStyle.HEADER6, numId); } hasValue = createFields(multiplicityFieldset.getFields(), mainDocumentPart, 3, createListing, visibilityRuleService); if(hasValue){ returnedValue = true; } } } if (hasValue && compositeField.getHasCommentField() && compositeField.getCommentFieldValue() != null && !compositeField.getCommentFieldValue().isEmpty() && !createListing) { XWPFParagraph paragraph = addParagraphContent("Comment: " + compositeField.getCommentFieldValue(), mainDocumentPart, ParagraphStyle.COMMENT, numId); CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl(); number.setVal(BigInteger.valueOf(indent)); } if (!hasValue && paragraphPos > -1) { mainDocumentPart.removeBodyElement(paragraphPos); } } } return returnedValue; } private Boolean createFields(List fields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService) { if (createListing) this.addListing(mainDocumentPart, indent, false, false); boolean hasValue = false; List tempFields = fields.stream().sorted(Comparator.comparingInt(Field::getOrdinal)).collect(Collectors.toList()); List formats = tempFields.stream().filter(f -> { try { String fTemp = this.formatter(f); return fTemp != null && !fTemp.isEmpty(); } catch (IOException e) { logger.error(e.getMessage(), e); } return false; }).collect(Collectors.toList()); for (Field field: tempFields) { if (visibilityRuleService.isElementVisible(field.getId())) { if (!createListing) { try { if(field.getViewStyle().getRenderStyle().equals("upload")){ boolean isImage = false; for(UploadData.Option type: ((UploadData)field.getData()).getTypes()){ String fileFormat = type.getValue(); if(IMAGE_TYPE_MAP.containsKey(fileFormat)){ isImage = true; break; } } if(isImage){ if (!field.getValue().toString().isEmpty()) { XWPFParagraph paragraph = addParagraphContent(mapper.convertValue(field.getValue(), Map.class), mainDocumentPart, ParagraphStyle.IMAGE, numId); if (paragraph != null) { CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl(); number.setVal(BigInteger.valueOf(indent)); hasValue = true; } } } } else if (field.getValue() != null && !field.getValue().toString().isEmpty()) { this.indent = indent; String format = this.formatter(field); if (field.getViewStyle().getRenderStyle().equals("tags")) { format = getCommaSeparatedFormatsFromJson(format, "name"); } else if (field.getViewStyle().getRenderStyle().equals("combobox") && field.getData() instanceof AutoCompleteData) { format = getCommaSeparatedFormatsFromJson(format, "label"); } if(format != null && !format.isEmpty()){ if(format.charAt(0) == '['){ format = format.substring(1, format.length() - 1).replaceAll(",", ", "); } if(formats.size() > 1){ format = "\t• " + format; } } XWPFParagraph paragraph = addParagraphContent(format, mainDocumentPart, field.getViewStyle().getRenderStyle().equals("richTextarea") ? ParagraphStyle.HTML : ParagraphStyle.TEXT, numId); if (paragraph != null) { CTDecimalNumber number = paragraph.getCTP().getPPr().getNumPr().addNewIlvl(); number.setVal(BigInteger.valueOf(indent)); hasValue = true; } } } catch (IOException e) { logger.error(e.getMessage(), e); } } } } return hasValue; } private String getCommaSeparatedFormatsFromJson(String format, String attribute){ if((format == null || format.isEmpty()) || (attribute == null || attribute.isEmpty())){ return null; } try { List> array = this.mapper.readValue(JavaToJson.objectStringToJson(format), ArrayList.class); StringBuilder multipleFormats = new StringBuilder(); for (Map node : array) { multipleFormats.append(node.get(attribute)).append(", "); } if (multipleFormats.length() > 0) { multipleFormats.setLength(multipleFormats.length() - 2); } return multipleFormats.toString(); } catch (JsonProcessingException e) { return format; } } public XWPFParagraph addParagraphContent(Object content, XWPFDocument mainDocumentPart, ParagraphStyle style, BigInteger numId) { if (content != null) { if (content instanceof String && ((String)content).isEmpty()) { return null; } XWPFParagraph paragraph = this.options.get(style).apply(mainDocumentPart, content); if (paragraph != null) { if (numId != null) { paragraph.setNumID(numId); } return paragraph; } } return null; } private void addListing(XWPFDocument document, int indent, Boolean question, Boolean hasIndication) { CTLvl cTLvl = this.cTAbstractNum.addNewLvl(); String textLevel = ""; for (int i = 0; i <= indent; i++) { textLevel += "%" + (i + 1) + "."; } if (question) { cTLvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL); cTLvl.addNewLvlText().setVal(""); cTLvl.setIlvl(BigInteger.valueOf(indent)); } else if (!question && hasIndication) { cTLvl.addNewNumFmt().setVal(STNumberFormat.DECIMAL); cTLvl.addNewLvlText().setVal(""); cTLvl.setIlvl(BigInteger.valueOf(indent)); } if (!question && !hasIndication) { cTLvl.addNewNumFmt().setVal(STNumberFormat.NONE); cTLvl.addNewLvlText().setVal(""); cTLvl.setIlvl(BigInteger.valueOf(indent)); } } private String formatter(Field field) throws IOException { String comboboxType = null; if (field.getValue() == null) { return null; } switch (field.getViewStyle().getRenderStyle()) { case "researchers": case "projects": case "organizations": case "externalDatasets": case "dataRepositories": case "pubRepositories": case "journalRepositories": case "taxonomies": case "licenses": case "publications": case "registries": case "services": case "tags": case "currency": comboboxType = "autocomplete"; case "combobox": { if (comboboxType == null) { comboboxType = ((ComboBoxData) field.getData()).getType(); } if (comboboxType.equals("autocomplete")) { mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); if (field.getValue() == null) return null; List> mapList = new ArrayList<>(); if (!field.getValue().equals("") && field.getValue().toString() != null) { try { mapList = Arrays.asList(mapper.readValue(field.getValue().toString(), HashMap[].class)); }catch (Exception e) { // logger.warn(e.getMessage(), e); // logger.info("Moving to fallback parsing"); Map map = new HashMap<>(); map.put("label", field.getValue().toString()); mapList.add(map); } } StringBuilder sb = new StringBuilder(); int index = 0; for (Map map: mapList) { for (Map.Entry entry : map.entrySet()) { if (entry.getValue() != null && (entry.getKey().equals("label") || entry.getKey().equals("description") || entry.getKey().equals("name"))) { sb.append(entry.getValue()); break; } } if (index != mapList.size() - 1) sb.append(", "); index++; } return sb.toString(); } else if (comboboxType.equals("wordlist")) { WordListData wordListData = (WordListData) field.getData(); if (field.getValue() != null){ ComboBoxData.Option selectedOption = null; if (!wordListData.getOptions().isEmpty()) { for (ComboBoxData.Option option : wordListData.getOptions()) { if (option.getValue().equals(field.getValue())) { selectedOption = option; } } } return selectedOption != null ? selectedOption.getLabel() : field.getValue().toString(); } return ""; } } case "booleanDecision": if (field.getValue() != null && field.getValue().equals("true")) return "Yes"; if (field.getValue() != null && field.getValue().equals("false")) return "No"; return null; case "radiobox": return field.getValue() != null ? field.getValue().toString() : null; case "checkBox": CheckBoxData data = (CheckBoxData) field.getData(); if (field.getValue() == null || field.getValue().equals("false")) return null; return data.getLabel(); case "datepicker": case "datePicker":{ Instant instant; if (!((String)field.getValue()).isEmpty()) { try { instant = Instant.parse((String) field.getValue()); } catch (DateTimeParseException ex) { instant = LocalDate.parse((String) field.getValue()).atStartOfDay().toInstant(ZoneOffset.UTC); } return field.getValue() != null ? DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault()).format(instant) : ""; } return (String) field.getValue(); } case "freetext": case "textarea": case "richTextarea": return field.getValue() != null ? field.getValue().toString(): ""; case "datasetIdentifier": case "validation": if (field.getValue() != null && !field.getValue().toString().isEmpty()) { Map identifierData; try { identifierData = mapper.readValue(field.getValue().toString(), HashMap.class); } catch (Exception ex) { // logger.warn(ex.getLocalizedMessage(), ex); // logger.info("Reverting to custom parsing"); identifierData = customParse(field.getValue().toString()); } return "id: " + identifierData.get("identifier") + ", Validation Type: " + identifierData.get("type"); } return ""; } return null; } private boolean hasVisibleFields(FieldSet compositeFields, VisibilityRuleService visibilityRuleService) { return compositeFields.getFields().stream().anyMatch(field -> visibilityRuleService.isElementVisible(field.getId())); } private Map customParse(String value) { Map result = new LinkedHashMap<>(); String parsedValue = value.replaceAll("[^a-zA-Z0-9\\s:=,]", ""); StringTokenizer commaTokens = new StringTokenizer(parsedValue, ", "); String delimeter = parsedValue.contains("=") ? "=" : ":"; while (commaTokens.hasMoreTokens()) { String token = commaTokens.nextToken(); StringTokenizer delimiterTokens = new StringTokenizer(token, delimeter); result.put(delimiterTokens.nextToken(), delimiterTokens.nextToken()); } return result; } }