Постановка задачи
Задача: у нас есть документ формата *.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);
});
});