Постановка задачи
Задача: у нас есть документ формата *.docx содержащий шаблон некоторого пользовательского отчёта. Соответственно необходимо наполнить его данными, но при этом обязательно сохранить пользовательское форматирование.
Например, если в документе встречается шаблон вида "{шаблон}" и он имеет некоторое форматирование (цвет, шрифт, размер, заголовок и пр), то после замены это форматирование должно быть сохранено.
Больше того, если мы встречаем многострочный шаблон вида{начало-шаблона-для-каждого-сотрудника
<вывести-фио-сотрудника> - <вывести-возраст-сотрудника>
конец-шаблона-для-каждого-сотрудника}
То мы должны скопировать содержимое многострочного шаблона столько раз, сколько у нас имеется сотрудников с сохранением всего того, что находится в шаблоне (таблицы, параграфы, заголовки. Любой степени вложенности друг в друга).
До кучи добавим что многострочные шаблоны могут быть вложены друг в друга, но это уже немного за пределами данной публикации.
Зачем и для кого написана данная статья
К сожалению, в сети почти нет примером решения подобной задачи относительно формата .docx на java.
Чтобы восполнить этот недостаток и написана данная статья.Я пишу её в том виде, в каком хотел бы найти в сети когда начинал работать над данной задачей.
Отказ от ответственности и предупреждение о грязном коде
Ниже будет много примеров относительно грязного кода. Я отдельно акцентирую этот момент, что код грязный, но рабочий в том смысле что он работает у меня, в теле документа. Весьма вероятно что сей код упускает пограничные случаи вроде колонтитулов, футеров и прочих "особенностей".
Возможно вам потребуется доработать предложенные примеры кода самостоятельно.
В любом случае вы используете его на свой страх и риск.
Почему XWPFDocument из API Apache POI это "ужас ужасный", а его разработчиков надо сослать в арктику считать снежинки на аналоговых калькуляторах
Просто два факта
Документ XWPFDocument состоит из элементов типа IBodyElement. Собственно IBodyElement это общий интерфейс за которым может скрываться параграф - XWPFParagraph, таблица - XWPFTable или неведомая хрень под названием CONTENTCONTROL.
Каждый IBodyElement имеет метод "получить родителя"(getBody), то есть тот компонент на котором он сам располагается.
Параграф может располагаться в ячейке таблицы. Таблица внутри параграфа и так далее.
Метод getBody() возвращает интерфейс IBody у которого можно запросить список всех элементов которые на нём располагаются getElementType().
Логично?
Логично.
Но это только пока...
Допустим мы стоим на каком-то параграфе (в котором мы нашли ��нтересующий нас текст). Как узнать контейнер для этого параграфа, то есть тот элемент на котором он лежит?
Достаточно просто, применяем getBody() от параграфа (или от IBodyElement-а в общем случае) и получаем элемент типа IBody.
Чувствуете уже этот лёгкий элемент безумия?
Элемент имеет тип IBodyElement, а его родитель - IBody, хотя это, явно, тот же самый элемент.
Ладно, допустим в этом есть какой-то тайный смысл. Но как нам узнать родителя элемента который является родителем для нашего параграфа? Другими словами как узнать внутри какого элемента лежит наш IBody?
А никак! То есть вообще никак. По крайней мере мне этот способ неизвестен и только очень отдалённый намёк на него даёт метод getPart() возвращающий POIXMLDocumentPart что является более низким уровнем управления пакетом OOXML.Просто попробуйте решить самые простые задачи вроде: заменить все вхождения одного текста на другой. Попробуйте вставить новый параграф после заданного IBodyElement-а или любую другую задачу для решения которых пришлось писать довольно нетривиальный код в примерах ниже.
Переходим к примерам кода:
Общий пример обработки .docx файла в контроллере спринга
@PostMapping(value = "/docx", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public ResponseEntity<Resource> generateReportForTemplate(@RequestPart("file") MultipartFile file, @RequestParam Map<String, String> allParams) { MediaType contentType = MediaType.valueOf(file.getContentType()); if (contentType.equals(MediaType.valueOf("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))) { XWPFDocument doc = new XWPFDocument(OPCPackage.open(file.getInputStream())); ByteArrayOutputStream os = new ByteArrayOutputStream(); //обработка шаблонов в документе doc doc.write(os); doc.close(); Resource fileResource = new InputStreamResource(new ByteArrayInputStream(os.toByteArray())); os.close(); return ResponseEntity.ok() .contentType(contentType) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getOriginalFilename() + "\"") .body(fileResource); } else throw new RuntimeException("Неподдерживаемый формат файла. Только docx-файлы!"); }
Рекурсивная обработка всех элементов IBodyElement которые только содержит документ
Интерфейс для рекурсивной обработки.
/** * Интерфейс - обработчика */ public static interface ProcessInterface { /** * Вызывается при рекурсивной обработки каждого элемента IBodyElement. Если вернёт истину, значит цели достигнуты и дальнейшая рекурсия прекращается */ public boolean process(IBodyElement iBodyElement); }
И сами методы для рекурсивной обработки всех IBodyElement которые находятся внутри некоторого кастомного IBodyElement-а или же вообще всех внутри документа
/** * Бежим по корневому элементу iBodyElement и для каждого входящего в него элемента рекурсивно вызываем метод processInterface.process * Вызвать как * for (IBodyElement iBodyElement : doc.getBodyElements()) * if (processDoc(iBodyElement, (ib) -> {...})) break; * * @param iBodyElement - элемент по потомкам которого рекурсивно бежим * @param processInterface - интерфейс для обратного вызова метода * @return - истина, если дальше бежать уже не надо (переданная функция сделала своё дело) и ложь, если не сделала */ public static boolean process(IBodyElement iBodyElement, ProcessInterface processInterface) { if (BodyElementType.TABLE.equals(iBodyElement.getElementType())) { for (XWPFTableRow row : ((XWPFTable) iBodyElement).getRows()) { for (XWPFTableCell cell : row.getTableCells()) { for (IBodyElement ibe : cell.getBodyElements()) { if (processInterface.process(ibe)) return true; else if (process(ibe, processInterface)) return true; } } } return false; } else if (BodyElementType.PARAGRAPH.equals(iBodyElement.getElementType())) { return processInterface.process(iBodyElement); } return false; }
/** * Бежим по всему документу и для каждого входящего в него элемента IBodyElement рекурсивно вызываем метод processInterface.process * При этом вложенные IBodyElement-ы не обрабатываются! * @param doc - документ * @param processInterface - интерфейс для обратного вызова метода * @return - истина, если дальше бежать уже не надо (переданная функция сделала своё дело) и ложь, если не сделала */ public static boolean process(XWPFDocument doc, ProcessInterface processInterface) { for (IBodyElement iBodyElement : doc.getBodyElements()) if (processInterface.process(iBodyElement)) return true; return false; }
Примеры использования указанных методов ниже
Пример замены одного текста на другой
В примере использован доработанный код взятый из
https://stackoverflow.com/questions/71347456/update-content-of-references-to-text-mark-in-docx
Также можете посмотреть в сторону https://www.baeldung.com/java-replace-pattern-word-document-doc-docx
/** * По всему документу заменить старый текст на новый * * @param doc - документ * @param oldText - старый текст * @param newText - новый текст */ public static void replaceText(XWPFDocument doc, String oldText, String newText) { process(doc, (iBodyElement) -> { return process(iBodyElement, (ib) -> { if (ib.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph p = (XWPFParagraph) ib; replaceTextSegment(p, oldText, newText); } return false; }); }); } /** * По всему вложенному содержимому iBodyElement заменить старый текст на новый * * @param iBodyElement - элемент внутри которого заменяем старый текст на новый * @param oldText - старый текст * @param newText - новый текст */ public static void replaceText(IBodyElement iBodyElement, String oldText, String newText) { process(iBodyElement, (ib) -> { if (ib.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph p = (XWPFParagraph) ib; replaceTextSegment(p, oldText, newText); } return false; }); } /** * Замена текста в пределах параграфа. Использовать как * if (paragraph.getText().contains(textToFind)) { // paragraph contains text to find * replaceTextSegment(paragraph, textToFind, replacement); * } */ static public void replaceTextSegment(XWPFParagraph paragraph, String textToFind, String replacement) { TextSegment foundTextSegment = null; PositionInParagraph startPos = new PositionInParagraph(0, 0, 0); while ((foundTextSegment = searchTextExt(paragraph, textToFind, startPos)) != null) { // search all text segments having text to find // maybe there is text before textToFind in begin run XWPFRun beginRun = paragraph.getRuns().get(foundTextSegment.getBeginRun()); String textInBeginRun = beginRun.getText(foundTextSegment.getBeginText()); String textBefore = textInBeginRun.substring(0, foundTextSegment.getBeginChar()); // we only need the text before // maybe there is text after textToFind in end run XWPFRun endRun = paragraph.getRuns().get(foundTextSegment.getEndRun()); String textInEndRun = endRun.getText(foundTextSegment.getEndText()); String textAfter = textInEndRun.substring(foundTextSegment.getEndChar() + 1); // we only need the text after if (foundTextSegment.getEndRun() == foundTextSegment.getBeginRun()) { textInBeginRun = textBefore + replacement + textAfter; // if we have only one run, we need the text before, then the replacement, then the text after in that run } else { textInBeginRun = textBefore + replacement; // else we need the text before followed by the replacement in begin run endRun.setText(textAfter, foundTextSegment.getEndText()); // and the text after in end run } beginRun.setText(textInBeginRun, foundTextSegment.getBeginText()); // runs between begin run and end run needs to be removed for (int runBetween = foundTextSegment.getEndRun() - 1; runBetween > foundTextSegment.getBeginRun(); runBetween--) { paragraph.removeRun(runBetween); // remove not needed runs } } } /** * this methods parse the paragraph and search for the string searched. * If it finds the string, it will return true and the position of the String * will be saved in the parameter startPos. * <p> * while((foundTextSegment = searchText(paragraph, textToFind, startPos)) != null) * * @param searched * @param startPos */ public static TextSegment searchTextExt(XWPFParagraph paragraph, String searched, PositionInParagraph startPos) { int startRun = startPos.getRun(), startText = startPos.getText(), startChar = startPos.getChar(); int beginRunPos = 0, candCharPos = 0; boolean newList = false; //CTR[] rArray = paragraph.getRArray(); //This does not contain all runs. It lacks hyperlink runs for ex. java.util.List<XWPFRun> runs = paragraph.getRuns(); int beginTextPos = 0, beginCharPos = 0; //must be outside the for loop for (int runPos = startRun; runPos < runs.size(); runPos++) { int textPos = 0, charPos; CTR ctRun = runs.get(runPos).getCTR(); XmlCursor c = ctRun.newCursor(); c.selectPath("./*"); try { while (c.toNextSelection()) { XmlObject o = c.getObject(); if (o instanceof CTText) { if (textPos >= startText) { String candidate = ((CTText) o).getStringValue(); if (runPos == startRun) { charPos = startChar; } else { charPos = 0; } for (; charPos < (candidate==null ? 0 : candidate.length()); charPos++) { if ((candidate.charAt(charPos) == searched.charAt(0)) && (candCharPos == 0)) { beginTextPos = textPos; beginCharPos = charPos; beginRunPos = runPos; newList = true; } if (candidate.charAt(charPos) == searched.charAt(candCharPos)) { if (candCharPos + 1 < searched.length()) { candCharPos++; } else if (newList) { TextSegment segment = new TextSegment(); segment.setBeginRun(beginRunPos); segment.setBeginText(beginTextPos); segment.setBeginChar(beginCharPos); segment.setEndRun(runPos); segment.setEndText(textPos); segment.setEndChar(charPos); return segment; } } else { candCharPos = 0; } } } textPos++; } else if (o instanceof CTProofErr) { c.removeXml(); } else if (o instanceof CTRPr) { //do nothing } else { candCharPos = 0; } } } finally { c.dispose(); } } return null; }
Получить текст из элементов или всего документа
/** * Получить весь текст документа */ public static String getText(XWPFDocument doc) throws IOException { XWPFWordExtractor ex = new XWPFWordExtractor(doc); return ex.getText();//xdoc.getDocument().toString(); } /** * Получить весь текст из набора элементов (включая вложенные) */ public static String getText(List<IBodyElement> iBodyElements){ StringBuilder sb = new StringBuilder(); iBodyElements.forEach(iBodyElement -> process(iBodyElement, ib -> { if (ib.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph p = (XWPFParagraph) ib; sb.append(p.getText()); } return false; })); return sb.toString(); } /** * Получить из текста список патернов обрамлённых открывающимся и закрывающимся тэгами. Считается что вложенности нет * * @param text - текст в котором ищем вхождение патернов * @param begin - отркывающий тэг * @param end - закрывающий тэг * @param withTags - если истина, то вернёт патерны всместе с тэгами * @return - список найденных патернов */ public static List<String> getAllTextsBetweenTags(String text, String begin, String end, Boolean withTags) { List<String> list = new ArrayList<>(); int ibegin = 0; int iend = 0; while (true) { ibegin = text.indexOf(begin, iend); iend = text.indexOf(end, ibegin + 1); if (ibegin == -1 || iend == -1) break; list.add((withTags ? begin : "") + text.substring(ibegin + begin.length(), iend) + (withTags ? end : "")); } return list; }
Клонирование элементов
/** * Копировать один iBodyElement и вставить его в body на позицию курсора (важно, курсор должен указывать на body, иначе получим искл) * Если курсор нулл или боду нулл, то новый элемент будет вставлен сразу после текущего на его body * * @param body - место куда вставляем новый элемент (контейнер под него) * @param cursor - курсор для вставки * @param iBodyElement - клонируемый элементы * @return - копию склонированного элемента */ public static IBodyElement cloneIbodyElement(IBody body, IBodyElement iBodyElement, XmlCursor cursor) { IBodyElement iBodyElementNew = null; if (iBodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) { if (cursor == null || body == null) { if (body == null) body = iBodyElement.getBody(); if (cursor == null) { cursor = ((XWPFParagraph) iBodyElement).getCTP().newCursor(); cursor.toNextSibling(); } } iBodyElementNew = body.insertNewParagraph(cursor); UtilXWPFDocument.cloneParagraph((XWPFParagraph) iBodyElementNew, (XWPFParagraph) iBodyElement); } if (iBodyElement.getElementType().equals(BodyElementType.TABLE)) { if (cursor == null || body == null) { body = iBodyElement.getBody(); cursor = ((XWPFTable) iBodyElement).getCTTbl().newCursor(); } iBodyElementNew = body.insertNewTbl(cursor); ((XWPFTable) iBodyElementNew).getCTTbl().set(((XWPFTable) iBodyElement).getCTTbl()); UtilXWPFDocument.copyTable((XWPFTable) iBodyElement, (XWPFTable) iBodyElementNew); } cursor.toNextToken(); return iBodyElementNew; } /** * Скопировать набор IBodyElement в позицию определяемую cursor или, при его отсутвии, последним из элементов в списке копирования * * @param cursor - курсор для вставки копируемых элементов. Если пуст, то вставка производится после последнего элемента в списке * @param copis - список копируемых элементов * @return - список скопированных элементов */ public static List<IBodyElement> cloneIbodyElements(IBody body, XmlCursor cursor, List<IBodyElement> copis) { if (cursor == null || body == null) { IBodyElement lastElement = copis.get(copis.size() - 1); if (body == null) body = lastElement.getBody(); if (cursor == null) { cursor = lastElement.getElementType().equals(BodyElementType.PARAGRAPH) ? ((XWPFParagraph) lastElement).getCTP().newCursor() : ((XWPFTable) lastElement).getCTTbl().newCursor(); cursor.toNextSibling(); } } List<IBodyElement> resp = new ArrayList<>(); for (IBodyElement ib : copis) { //создадим новый элемент и скопируем туда текст из сохранённого IBodyElement new_ib = UtilXWPFDocument.cloneIbodyElement(body, ib, cursor); resp.add(new_ib); } return resp; } /** * Клонирует параграф в новый, пустой, существующий параграф. * Новый параграф должен быть уже создан * } */ public static void cloneParagraph(XWPFParagraph clone, XWPFParagraph source) {//https://stackoverflow.com/questions/23112924/make-an-exact-copy-of-a-paragraph-including-all-contents-and-properties CTPPr pPr = clone.getCTP().isSetPPr() ? clone.getCTP().getPPr() : clone.getCTP().addNewPPr(); pPr.set(source.getCTP().getPPr()); for (XWPFRun r : source.getRuns()) { XWPFRun nr = clone.createRun(); cloneRun(nr, r); } } public static void cloneRun(XWPFRun clone, XWPFRun source) { CTRPr rPr = clone.getCTR().isSetRPr() ? clone.getCTR().getRPr() : clone.getCTR().addNewRPr(); rPr.set(source.getCTR().getRPr()); clone.setText(source.getText(0)); } /** Клонирует таблицу в новую, пустую, существующую таблицу * https://stackoverflow.com/questions/48322534/apache-poi-how-to-copy-tables-from-one-docx-to-another-docx * * XWPFTable newTbl = output_doc.insertNewTbl(cursor); * copyTable(table, newTbl); */ public static void copyTable(XWPFTable source, XWPFTable target) { target.getCTTbl().setTblPr(source.getCTTbl().getTblPr()); target.getCTTbl().setTblGrid(source.getCTTbl().getTblGrid()); //newly created table has one row by default. we need to remove the default row. target.removeRow(0); for (int r = 0; r < source.getRows().size(); r++) { XWPFTableRow targetRow = target.createRow(); XWPFTableRow row = source.getRows().get(r); targetRow.getCtRow().setTrPr(row.getCtRow().getTrPr()); for (int c = 0; c < row.getTableCells().size(); c++) { //newly created row has 1 cell XWPFTableCell targetCell = targetRow.createCell(); XWPFTableCell cell = row.getTableCells().get(c); targetCell.getCTTc().setTcPr(cell.getCTTc().getTcPr()); XmlCursor cursor = targetCell.getParagraphArray(0).getCTP().newCursor(); for (int p = 0; p < cell.getBodyElements().size(); p++) { IBodyElement elem = cell.getBodyElements().get(p); if (elem instanceof XWPFParagraph) { XWPFParagraph targetPar = targetCell.insertNewParagraph(cursor); cursor.toNextToken(); XWPFParagraph par = (XWPFParagraph) elem; //copyParagraph(par, targetPar); cloneParagraph(targetPar, par); } else if (elem instanceof XWPFTable) { XWPFTable targetTable = targetCell.insertNewTbl(cursor); XWPFTable table = (XWPFTable) elem; copyTable(table, targetTable); cursor.toNextToken(); } } //newly created cell has one default paragraph we need to remove targetCell.removeParagraph(targetCell.getParagraphs().size() - 1); } } }
Удаление элементов
/** * Очистить параграф не удаляя его */ public static void clearParagraph(final XWPFParagraph p) { //p.getCTP().getRList().clear(); for (XWPFRun r : p.getRuns()) r.setText("", 0); } /** * Очистить таблицу не удаляя его. То есть очистить все парагрфы внутри данной таблицы с любым уровнем вложенности */ public static void clearTable(final XWPFTable table) { process(table,iBodyElement->{ if (iBodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph p = (XWPFParagraph) iBodyElement; clearParagraph(p); } return false; }); } /** * Очистить боди-элемент от текста */ public static void clearElement(final IBodyElement iBodyElement) { if (iBodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) clearParagraph((XWPFParagraph) iBodyElement); if (iBodyElement.getElementType().equals(BodyElementType.TABLE)) clearTable((XWPFTable) iBodyElement); } /** * Удаление заданного IBodyElement (таблицы или параграфа) */ public static void removeBodyElement(IBodyElement iBodyElement) { IBody body = iBodyElement.getBody(); if (body instanceof XWPFDocument) { final XWPFDocument doreplacedent = (XWPFDocument) body; final int index = doreplacedent.getBodyElements().indexOf(iBodyElement); if (index != -1) { doreplacedent.removeBodyElement(index); } } else if (body instanceof XWPFHeaderFooter) { final XWPFHeaderFooter headerFooter = (XWPFHeaderFooter) body; if (iBodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) headerFooter.removeParagraph((XWPFParagraph) iBodyElement); if (iBodyElement.getElementType().equals(BodyElementType.TABLE)) headerFooter.removeTable((XWPFTable) iBodyElement); } else if (body instanceof XWPFTableCell) { final XWPFTableCell cell = (XWPFTableCell) body; if (iBodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) { final int index = cell.getParagraphs().indexOf(iBodyElement); if (index != -1) cell.removeParagraph(index); } if (iBodyElement.getElementType().equals(BodyElementType.TABLE)) { final int index = cell.getTables().indexOf(iBodyElement); if (index != -1) cell.removeTable(index); } } else { throw new IllegalStateException("can't delete"); } } /** * Удалить с body принадлежащие ему bodyElements начиная с ibegin и заканчивая iend */ public static void deleteBodyElementsFromBody(IBody body, int ibegin, int iend) { List<IBodyElement> dels = Lists.newArrayList(body.getBodyElements().subList(ibegin, iend + 1).iterator()); //если мы удаляем всё, что есть в теле, то это особый случай, иначе документ будет повреждён (видимо потому, что мы взяли курсор с последнего параграфа) if (body.getBodyElements().equals(dels)) { UtilXWPFDocument.clearParagraph((XWPFParagraph) dels.get(dels.size() - 1));//очищаем текст параграфа без его удаления dels = Lists.newArrayList(dels.subList(0, dels.size() - 1).iterator()); //и не удаляем этот параграф } for (IBodyElement ib : dels) UtilXWPFDocument.removeBodyElement(ib); }
Обработка текста для всего документа и пример её использования для удаления со всего документа всего текста начиная с некого открывающего тэга и заканчивая закрывающим
/** * Удалить из документа весь текст начиная с открывающего тэга и кончая закрывающим тэгом включитлеьно. Отк и закр тэги должны располагаться на одном и том же уровне вложенности * * @param doc - документ * @param sbegin - открывающий тэг * @param send - закрыывающий тэг */ public static void deleteText(XWPFDocument doc, String sbegin, String send) { class TemplateWork extends TemplateWorkAbstract { public TemplateWork(String sbegin, String send) { super(sbegin, send); } @Override public void processTemplate() { deleteBodyElementsFromBody(body, ibegin, iend); } } TemplateWorkAbstract templateWork = new TemplateWork(sbegin, send); findAndProcessTemplate(doc, templateWork); } /** * Класс для обработки шаблонов в методе findAndProcessTemplate */ @FieldDefaults(level = AccessLevel.PUBLIC) public static abstract class TemplateWorkAbstract { IBody body; String sbegin;//отркывающий тэг для шаблона String send;//закрывающий тэг для шаблона int ibegin = -1;//позиция параграфа где был найден открывающий тэг шаблона внутри body.getBodyElements.get(?) int iend = -1;//поцизийия параграфа где был найден закрывающий тэг шаблона public TemplateWorkAbstract(String sbegin, String send) { this.sbegin = sbegin; this.send = send; } public void reset() {//сбросить body = null; ibegin = -1; iend = -1; } public void findBegin(XWPFParagraph p, int i) {//событие нахождения начала шаблона, где p - параграф в котором был найден открывающий тэг ibegin = i; } public void findEnd(XWPFParagraph p, int i) {//событие нахождения конца шаблона, где p - параграф в котором был найден закрывающий тэг iend = i; } public abstract void processTemplate();//обработка шаблона } /** * Найти в документе все шаблоны начинающийся на тэг templateWork.sbegin и заканчивающийся на тэг templateWork.send и обработать их, последовательно для каждого вызывая templateWork.processTemplate() * * @param doc - документ * @param templateWork - объект класса наследуемого от TemplateWorkAbstract который содержит инф для обработки событий нахождения начала шаблона, нахождения конца шаблона и обработки самого шаблона */ public static void findAndProcessTemplate(XWPFDocument doc, TemplateWorkAbstract templateWork) { templateWork.reset(); while (process(doc, (iBodyElement) -> { return process(iBodyElement, (ib) -> { return templateFind(templateWork, ib); }); })) { templateProcess(templateWork); } } /** * Выполнить обработку шаблона находящегося в templateWork. Сначала требуется выполнить метод {@see templateFind} */ private static void templateProcess(TemplateWorkAbstract templateWork) { templateWork.processTemplate(); templateWork.reset(); } /** * Найти вхождение шаблона описываемого templateWork внутри заданного IBodyElement (вернёт данные внутри templateWork) */ private static boolean templateFind(TemplateWorkAbstract templateWork, IBodyElement ib) { if (ib.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph p = (XWPFParagraph) ib; if (p.getText().contains(templateWork.sbegin)) { templateWork.body = p.getBody(); int i = 0; for (IBodyElement ww : p.getBody().getBodyElements()) { if (ww.getElementType().equals(BodyElementType.PARAGRAPH)) { XWPFParagraph ee = (XWPFParagraph) ww; if (ee.getText().contains(templateWork.sbegin)) templateWork.findBegin(ee, i); else if (ee.getText().contains(templateWork.send)) { templateWork.findEnd(ee, i); break; } } i++; } if (templateWork.iend == -1) throw new RuntimeException("Не найдено окончание шаблона для: " + templateWork.sbegin); else { return true; } } } return false; }
Практические примеры использования приведённых выше функций
//По всему документу заменить вхождение одного текста на другой UtilXWPFDocument.replaceText(doc, "старый текст", "новый текст"); //Выбрать все шаблоны вида <dataset>некий многострочный шаблон</dataset> из документа List<String> stringDatasets = UtilXWPFDocument.getAllTextsBetweenTags(UtilXWPFDocument.getText(doc), "<dataset>", "</dataset>", true); //И затем удалить эти шаблоны из документа UtilXWPFDocument.deleteText(doc, "<dataset>", "</dataset>"); //Пример рекурсивной обработки списка iBodyElement-ов (и всех вложенных в них элементов) полученных, например как doc.getBodyElements() iBodyElements.forEach(iBodyElement -> { UtilXWPFDocument.process(iBodyElement, (ib) -> { //некий код возвращающий обработки каждого ib-элемента return false; }); }); //Пример рекурсивной обработки каждого ib-элемента в документе. //Обработка прекращается когда функция обработки вернёт истину boolean isProcessing=process(doc, (iBodyElement) -> { return process(iBodyElement, (ib) -> { return некоторая-функция-обработки(ib); }); });
