Кошмар работы с *.docx файлами на Java посредством класса XWPFDocument из API Apache POI

от автора

Постановка задачи

Задача: у нас есть документ формата *.docx содержащий шаблон некоторого пользовательского отчёта. Соответственно необходимо наполнить его данными, но при этом обязательно сохранить пользовательское форматирование.
Например, если в документе встречается шаблон вида «{шаблон}» и он имеет некоторое форматирование (цвет, шрифт, размер, заголовок и пр), то после замены это форматирование должно быть сохранено.

Больше того, если мы встречаем многострочный шаблон вида
{начало-шаблона-для-каждого-сотрудника
<вывести-фио-сотрудника> - <вывести-возраст-сотрудника>
конец-шаблона-для-каждого-сотрудника}

То мы должны скопировать содержимое многострочного шаблона столько раз, сколько у нас имеется сотрудников с сохранением всего того, что находится в шаблоне (таблицы, параграфы, заголовки. Любой степени вложенности друг в друга).
До кучи добавим что многострочные шаблоны могут быть вложены друг в друга, но это уже немного за пределами данной публикации.

Зачем и для кого написана данная статья

К сожалению, в сети почти нет примером решения подобной задачи относительно формата .docx на java.
Чтобы восполнить этот недостаток и написана данная статья.Я пишу её в том виде, в каком хотел бы найти в сети когда начинал работать над данной задачей.

Отказ от ответственности и предупреждение о грязном коде

Ниже будет много примеров относительно грязного кода. Я отдельно акцентирую этот момент, что код грязный, но рабочий в том смысле что он работает у меня, в теле документа. Весьма вероятно что сей код упускает пограничные случаи вроде колонтитулов, футеров и прочих «особенностей».
Возможно вам потребуется доработать предложенные примеры кода самостоятельно.
В любом случае вы используете его на свой страх и риск.

Почему XWPFDocument из API Apache POI это «ужас ужасный», а его разработчиков надо сослать в арктику считать снежинки на аналоговых калькуляторах

Просто два факта

  1. Документ XWPFDocument состоит из элементов типа IBodyElement. Собственно IBodyElement это общий интерфейс за которым может скрываться параграф — XWPFParagraph, таблица — XWPFTable или неведомая хрень под названием CONTENTCONTROL.
    Каждый IBodyElement имеет метод «получить родителя»(getBody), то есть тот компонент на котором он сам располагается.
    Параграф может располагаться в ячейке таблицы. Таблица внутри параграфа и так далее.
    Метод getBody() возвращает интерфейс IBody у которого можно запросить список всех элементов которые на нём располагаются getElementType().
    Логично?
    Логично.
    Но это только пока…
    Допустим мы стоим на каком-то параграфе (в котором мы нашли интересующий нас текст). Как узнать контейнер для этого параграфа, то есть тот элемент на котором он лежит?
    Достаточно просто, применяем getBody() от параграфа (или от IBodyElement-а в общем случае) и получаем элемент типа IBody.
    Чувствуете уже этот лёгкий элемент безумия?
    Элемент имеет тип IBodyElement, а его родитель — IBody, хотя это, явно, тот же самый элемент.
    Ладно, допустим в этом есть какой-то тайный смысл. Но как нам узнать родителя элемента который является родителем для нашего параграфа? Другими словами как узнать внутри какого элемента лежит наш IBody?
    А никак! То есть вообще никак. По крайней мере мне этот способ неизвестен и только очень отдалённый намёк на него даёт метод getPart() возвращающий POIXMLDocumentPart что является более низким уровнем управления пакетом OOXML.

  2. Просто попробуйте решить самые простые задачи вроде: заменить все вхождения одного текста на другой. Попробуйте вставить новый параграф после заданного 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);             });         });


ссылка на оригинал статьи https://habr.com/ru/articles/870544/