argos/dmp-backend/web/src/main/java/eu/eudat/logic/utilities/documents/word/WordBuilder.java

544 lines
28 KiB
Java

package eu.eudat.logic.utilities.documents.word;
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.models.data.components.commons.datafield.CheckBoxData;
import eu.eudat.models.data.components.commons.datafield.ComboBoxData;
import eu.eudat.models.data.components.commons.datafield.UploadData;
import eu.eudat.models.data.components.commons.datafield.WordListData;
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.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
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<String, Integer> 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 Map<ParagraphStyle, ApplierWithValue<XWPFDocument, Object, XWPFParagraph>> options = new HashMap<>();
private CTAbstractNum cTAbstractNum;
private BigInteger numId;
private Integer indent;
private static final ObjectMapper mapper = new ObjectMapper();
public WordBuilder(Environment environment) {
this.cTAbstractNum = CTAbstractNum.Factory.newInstance();
this.cTAbstractNum.setAbstractNumId(BigInteger.valueOf(1));
this.indent = 0;
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", "<br>"));
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<String, String>)item).get("id");
String fileName = ((Map<String, String>)item).get("name");
String fileType = ((Map<String, String>)item).get("type");
int format;
format = IMAGE_TYPE_MAP.getOrDefault(fileType, 0);
try {
FileInputStream image = new FileInputStream(environment.getProperty("file.storage") + imageId);
ImageInputStream iis = ImageIO.createImageInputStream(new File(environment.getProperty("file.storage") + imageId));
Iterator<ImageReader> 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 = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getLeft().intValue();
int marginRightInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getRight().intValue();
int pageWidthInDXA = 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 = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getTop().intValue();
int marginBottomInDXA = mainDocumentPart.getDocument().getBody().getSectPr().getPgMar().getBottom().intValue();
int pageHeightInDXA = 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);
}
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<DatasetProfilePage> 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<Section> 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<FieldSet> 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<FieldSet> 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<Field> fields, XWPFDocument mainDocumentPart, Integer indent, Boolean createListing, VisibilityRuleService visibilityRuleService) {
if (createListing) this.addListing(mainDocumentPart, indent, false, false);
boolean hasValue = false;
List<Field> tempFields = fields.stream().sorted(Comparator.comparingInt(Field::getOrdinal)).collect(Collectors.toList());
List<Field> 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(fileFormat.equals(MediaType.IMAGE_JPEG_VALUE) || fileFormat.equals(MediaType.IMAGE_PNG_VALUE) || fileFormat.equals(MediaType.IMAGE_GIF_VALUE)){
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(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;
}
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<Map<String, Object>> 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 <String, Object> map = new HashMap<>();
map.put("label", field.getValue().toString());
mapList.add(map);
}
}
StringBuilder sb = new StringBuilder();
int index = 0;
for (Map<String, Object> map: mapList) {
for (Map.Entry<String, Object> 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<String, String> 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<String, String> customParse(String value) {
Map<String, String> 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;
}
}