diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java index 20012d16f..ac1a078ea 100644 --- a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java @@ -1,15 +1,16 @@ package ru.spcex.clearing.reports.services; import org.apache.poi.xwpf.usermodel.*; +import org.slf4j.Logger; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; +import java.io.*; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Сервис для работы с DOCX файлами @@ -19,18 +20,61 @@ public class DOCXService { /** * При инициализации сохраняет все имеющиеся ключи (ищет по маске keyPattern), найденные в текстовых элементах документа */ - private final Map bodyElementForKey = new HashMap<>(); - private final Pattern KEY_PATTERN = Pattern.compile("\\$\\{.*?}"); - private final int MAX_POSITION = 128; + private List keyPositions = new LinkedList<>(); /** - * Инициализация шаблона - * - * @param templateFile файл шаблона + * Паттерн поиска ключей в файле */ - public void init(File templateFile) throws IOException { + private Pattern KEY_PATTERN = Pattern.compile("\\$\\{.*?}"); + + /** + * (ре)Инициализация сервиса + */ + public void init(String keyPattern) { + KEY_PATTERN = Pattern.compile(keyPattern); + keyPositions = new LinkedList<>(); + } + + /** + * Вставка значений в шаблон. Шаблон может содержать любое количество ключей, при этом эти ключи должны содержаться в + * valuesMap. + * + * @param templateFile Файл шаблона + * @param outputFile Выходной файл. Должен быть либо файлом (будет перезаписан), либо отсутствовать + * @param valuesMap Мапа значений. В файле шаблона будут искаться ключи этой мапы и заменяться на соответствующие значения. + * @param log Если будет передан null, то метод будет выбрасывать исключения. В противном случае будет записан лог и возвращено false + * @param checkKeys True - строгий режим, где будет проверено наличие всех ключей из файла в valuesMap. + * False - мягкий режим, отсутствующие ключи будут пропущены + * + * @return true - если вставка прошла успешно + */ + public boolean insertValues(File templateFile, + File outputFile, + Map valuesMap, + Logger log, + boolean checkKeys) throws Exception { assert templateFile != null && templateFile.exists() && templateFile.isFile(); - try (InputStream inputStream = new FileInputStream(templateFile)) { + + if (outputFile == null || outputFile.isDirectory()) { + String logMsg = "Can't open output file. Output file: %s".formatted(outputFile == null ? "null" : outputFile.getAbsolutePath()); + if (log == null) throw new Exception(logMsg); + log.error(logMsg); + return false; + } + + if (outputFile.exists()) { + if (log != null) log.warn("Output file {} exist and will be overwritten", outputFile.getAbsolutePath()); + boolean deleteOk = outputFile.delete(); + if (!deleteOk) { + String logMsg = "Can't delete file %s".formatted(outputFile.getAbsolutePath()); + if (log == null) throw new Exception(logMsg); + log.error(logMsg); + return false; + } + } + + try (InputStream inputStream = new FileInputStream(templateFile); + OutputStream outputStream = new FileOutputStream(outputFile)) { XWPFDocument doc = new XWPFDocument(inputStream); for (IBodyElement element : doc.getBodyElements()) { @@ -41,10 +85,70 @@ public class DOCXService { } } - System.out.println("hello"); + if (checkKeys) { + List notPresentInMap = new LinkedList<>(); + boolean checkOk = checkIntersection(valuesMap, notPresentInMap); + if (!checkOk) { + String logMsg = "Can't insert values in file %s. Keys not in map: %s." + .formatted(templateFile.getAbsolutePath(), + String.join(", ", notPresentInMap)); + if (log == null) throw new Exception(logMsg); + log.error(logMsg); + return false; + } + } + + insertValues(valuesMap); + + doc.write(outputStream); + } + + return true; + } + + /** + * Проверка прочтённых ключей (keyPositions) и переданных (valuesMap.keySet()) на соответствие. + * Для успешной вставки мапа должна содержать все ключи, имеющиеся в файле. + * Метод вызывается для проверки только в строгом режиме + */ + private boolean checkIntersection(Map valuesMap, List notPresentInMap) { + Set keysFromMap = valuesMap.keySet(); + Set keysFromFile = keyPositions.stream().map(keyPosition -> keyPosition.key).collect(Collectors.toSet()); + List intersection = keysFromMap.stream() + .filter(keysFromFile::contains) + .collect(Collectors.toList()); + if (intersection.size() == keysFromFile.size()) return true; + + List notPresent = new LinkedList<>(keysFromFile); + notPresent.removeAll(intersection); + if (notPresentInMap != null) notPresentInMap.addAll(notPresent); + + return false; + } + + /** + * Вставка значений в элементы XWPFRun + */ + private void insertValues(Map valuesMap) { + for (KeyPositionInDoc keyPosition : keyPositions) { + String key = keyPosition.key; + + String valueFromMap = valuesMap.get(key); + if (valueFromMap == null) continue; + + XWPFRun xwpfRun = keyPosition.run; + int pos = keyPosition.pos; + do { + String srcText = xwpfRun.getText(pos); + xwpfRun.setText(srcText.replace(key, valueFromMap), pos); + } while (xwpfRun.getText(pos).contains(key)); } } + /** + * Сбор ключей из таблицы. + * Ячейка таблицы может являться как параграфом, так и таблицей, поэтому метод работает рекурсивно + */ private void collectKeysFromTable(XWPFTable srcTable) { for (XWPFTableRow row : srcTable.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { @@ -59,17 +163,20 @@ public class DOCXService { } } + /** + * Сбор ключей из параграфа + */ private void collectKeysFromParagraph(XWPFParagraph paragraph) { - for (XWPFRun run : paragraph.getRuns()) { try { + int MAX_POSITION = 128; for (int currPos = 0; currPos < MAX_POSITION; currPos++) { String currText = run.getText(currPos); if (currText == null) break; Matcher matcher = KEY_PATTERN.matcher(currText); while (matcher.find()) { String key = matcher.group(); - bodyElementForKey.put(key, new TextPositionOnRun(run, currPos)); + keyPositions.add(new KeyPositionInDoc(key, run, currPos)); } } } catch (IndexOutOfBoundsException ignored) { @@ -78,13 +185,16 @@ public class DOCXService { } } - static class TextPositionOnRun { + static class KeyPositionInDoc { + String key; XWPFRun run; int pos; - public TextPositionOnRun(XWPFRun run, int pos) { + public KeyPositionInDoc(String key, XWPFRun run, int pos) { + this.key = key; this.run = run; this.pos = pos; } } + } diff --git a/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java index 6aad9d549..528d64be2 100644 --- a/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java +++ b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java @@ -1,31 +1,33 @@ package ru.spcex.clearing.reports.services; -import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.junit.jupiter.api.Test; -import java.io.*; +import java.io.File; +import java.util.HashMap; +import java.util.Map; class DOCXServiceTest { - @Test - public void testREC_ACTV() { - String inputFilename = "REC.ACTV_TEST.docx"; - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(inputFilename); - InputStream testStream = getClass().getClassLoader().getResourceAsStream("1.docx"); - OutputStream outputStream = new FileOutputStream("test.docx")) { - XWPFDocument templateDoc = new XWPFDocument(inputStream); - XWPFDocument testDoc = new XWPFDocument(testStream); - testDoc.getParagraphs().get(6).getRuns().get(1).setText("========АОВЛДЫАОЫВЛДОАЛЫВДАОЫВЛДАОЫВ========"); - testDoc.write(outputStream); - System.out.println("h"); - } catch (IOException e) { - e.printStackTrace(); - } - } @Test - public void test() throws IOException { + public void test() throws Exception { + Map valuesMap = new HashMap<>(); + valuesMap.put("${company.fullName}", "======== COMPANY FULL NAME ========="); + valuesMap.put("${count}", "======== COUNT ========="); + valuesMap.put("${profileDocument.issueDate}", "======== ISSUE DATE ========="); + valuesMap.put("${test1}", "======== TEST 1 ========="); + valuesMap.put("${test2}", "======== TEST 2 ========="); + valuesMap.put("${test3}", "======== TEST 3 ========="); + DOCXService service = new DOCXService(); - service.init(new File(getClass().getClassLoader().getResource("REC.ACTV_TEST.docx").getPath())); - System.out.println("p"); + service.insertValues(new File(getClass().getClassLoader().getResource("REC.ACTV_TEST.docx").getPath()), + new File("output.docx"), + valuesMap, + null, + false); + + File file = new File("output.docx"); + System.out.println("test"); + file.delete(); } + } \ No newline at end of file diff --git a/clearing-parent/reports-service/src/test/resources/REC.ACTV_TEST.docx b/clearing-parent/reports-service/src/test/resources/REC.ACTV_TEST.docx index aca71b0e5..66865e6cc 100644 Binary files a/clearing-parent/reports-service/src/test/resources/REC.ACTV_TEST.docx and b/clearing-parent/reports-service/src/test/resources/REC.ACTV_TEST.docx differ