Замена текста в Apache POI XWPF

Я только что нашел библиотеку Apache POI очень полезной для редактирования файлов Word с помощью Java. В частности, я хочу отредактировать файл DOCX, используя классы XWPF Apache POI. Я не нашел подходящего метода / документации, с помощью которого я мог бы это сделать. Может кто-нибудь объяснить по шагам, как заменить текст в файле DOCX.

** Текст может быть в строке / абзаце или в строке / столбце таблицы.

Заранее спасибо :)


person Gagan93    schedule 08.03.2014    source источник
comment
У меня проблема с заменой (stackoverflow.com/q/49678959/8030651), возможно, у вас есть ответ ...   -  person kozmo    schedule 06.04.2018


Ответы (10)


Вам нужен метод XWPFRun.setText (String). Просто прокрутите файл до тех пор, пока не найдете интересующий XWPFRun, определите, каким должен быть новый текст, и замените его. (Выполнение - это последовательность текста с одинаковым форматированием)

Вы должны уметь делать что-то вроде:

XWPFDocument doc = new XWPFDocument(OPCPackage.open("input.docx"));
for (XWPFParagraph p : doc.getParagraphs()) {
    List<XWPFRun> runs = p.getRuns();
    if (runs != null) {
        for (XWPFRun r : runs) {
            String text = r.getText(0);
            if (text != null && text.contains("needle")) {
                text = text.replace("needle", "haystack");
                r.setText(text, 0);
            }
        }
    }
}
for (XWPFTable tbl : doc.getTables()) {
   for (XWPFTableRow row : tbl.getRows()) {
      for (XWPFTableCell cell : row.getTableCells()) {
         for (XWPFParagraph p : cell.getParagraphs()) {
            for (XWPFRun r : p.getRuns()) {
              String text = r.getText(0);
              if (text != null && text.contains("needle")) {
                text = text.replace("needle", "haystack");
                r.setText(text,0);
              }
            }
         }
      }
   }
}
doc.write(new FileOutputStream("output.docx"));
person Gagravarr    schedule 08.03.2014
comment
спасибо за это, но это не чтение данных таблицы. Есть ли другой класс, который мне нужно использовать для этого? - person Gagan93; 08.03.2014
comment
и вы определили RUN как текст с аналогичным форматированием, верно? Разбивает текст с похожим форматированием на части. Как это исправить? - person Gagan93; 08.03.2014
comment
Apache POI просто дает вам текст в файле, он не контролирует, как Word решает структурировать его с точки зрения прогонов ... А Word, как известно, делает странные вещи! При необходимости проверьте соседние участки на предмет наличия части текста. - person Gagravarr; 08.03.2014
comment
Я использую Apache PIO 3.10, и похоже, что getCells() на XWPFTableRow теперь getTableCells(). - person Justin Skiles; 24.04.2014
comment
Первый кусок кода дает мне исключение NullPointerException, кто-нибудь знает, что не так? - person Cole; 06.06.2014
comment
@Cole Лучше всего задать новый вопрос, включить код, который вы используете, полученную трассировку стека и уточнить, где именно он дает NPE - person Gagravarr; 06.06.2014
comment
Пожалуйста, обновите свой ответ. tbl.getRow() должно быть tbl.getRows() и row.getCells() должно быть row.getTableCells(). - person Justin Skiles; 26.09.2014
comment
@Cole (null! = Текст) && text.contains ($ {упражнение}) - person Ced; 02.01.2017
comment
Ненавижу говорить вам об этом, но такой подход не работает. В некоторых случаях это МОЖЕТ работать, но каждый раз, когда я пытаюсь это сделать, текст произвольно разбивается на несколько прогонов. Это не зависит от форматирования или пунктуации ... пробежки могут прерываться в любом месте текста. Так что поиск и замена в рамках отдельных прогонов обречены на провал. Единственный выбор - заменить на уровне абзаца (вероятно, неприемлемо из-за потери форматирования) или найти текст на уровне абзаца, затем сопоставить прогоны со смещениями найденного текста и соответствующим образом манипулировать всеми перекрывающимися прогонами. - person Gullbyrd; 04.02.2017
comment
Это не всегда работает, потому что word иногда решает разбить одно слово в верблюжьем регистре на несколько прогонов. - person Monir; 16.05.2017
comment
Я хочу заменить {10} на соответствующий текст в таблице, но слова не в том же XWPFRun. - person martian; 09.01.2018
comment
Если вы хотите заменить определенные значения в шаблоне документа (в моем случае), мой пост в теме: stackoverflow.com/questions/19137818/ может вам помочь. Это немного наоборот, но в моем случае решены проблемы с разделением прогонов и получение определенного слова в отдельном прогоне. - person EnGoPy; 04.02.2020

Вот что мы сделали для замены текста с помощью Apache POI. Мы обнаружили, что не стоило хлопот и проще заменить текст всего XWPFParagraph вместо выполнения. Цикл можно произвольно разделить посередине слова, поскольку Microsoft Word отвечает за то, где создаются циклы в абзаце документа. Следовательно, текст, который вы, возможно, ищете, может быть наполовину за один прогон и наполовину за другой. Использование полного текста абзаца, удаление его существующих прогонов и добавление новой серии с измененным текстом, кажется, решает проблему замены текста.

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

Также следует отметить, что приведенное ниже работает, если текст, который вы вставляете, содержит \ n возвращаемых символов. Мы не смогли найти способ вставить возврат, не создав прогон для каждого раздела перед возвратом и пометив его как addCarriageReturn (). Ваше здоровье

    package com.healthpartners.hcss.client.external.word.replacement;

import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

public class TextReplacer {
    private String searchValue;
    private String replacement;

    public TextReplacer(String searchValue, String replacement) {
        this.searchValue = searchValue;
        this.replacement = replacement;
    }

    public void replace(XWPFDocument document) {
        List<XWPFParagraph> paragraphs = document.getParagraphs();

    for (XWPFParagraph xwpfParagraph : paragraphs) {
        replace(xwpfParagraph);
    }
}

private void replace(XWPFParagraph paragraph) {
    if (hasReplaceableItem(paragraph.getText())) {
        String replacedText = StringUtils.replace(paragraph.getText(), searchValue, replacement);

        removeAllRuns(paragraph);

        insertReplacementRuns(paragraph, replacedText);
    }
}

private void insertReplacementRuns(XWPFParagraph paragraph, String replacedText) {
    String[] replacementTextSplitOnCarriageReturn = StringUtils.split(replacedText, "\n");

    for (int j = 0; j < replacementTextSplitOnCarriageReturn.length; j++) {
        String part = replacementTextSplitOnCarriageReturn[j];

        XWPFRun newRun = paragraph.insertNewRun(j);
        newRun.setText(part);

        if (j+1 < replacementTextSplitOnCarriageReturn.length) {
            newRun.addCarriageReturn();
        }
    }       
}

private void removeAllRuns(XWPFParagraph paragraph) {
    int size = paragraph.getRuns().size();
    for (int i = 0; i < size; i++) {
        paragraph.removeRun(0);
    }
}

private boolean hasReplaceableItem(String runText) {
    return StringUtils.contains(runText, searchValue);
}

//REVISIT The below can be removed if Michele tests and approved the above less versatile replacement version

//  private void replace(XWPFParagraph paragraph) {
//      for (int i = 0; i < paragraph.getRuns().size()  ; i++) {
//          i = replace(paragraph, i);
//      }
//  }

//  private int replace(XWPFParagraph paragraph, int i) {
//      XWPFRun run = paragraph.getRuns().get(i);
//      
//      String runText = run.getText(0);
//      
//      if (hasReplaceableItem(runText)) {
//          return replace(paragraph, i, run);
//      }
//      
//      return i;
//  }

//  private int replace(XWPFParagraph paragraph, int i, XWPFRun run) {
//      String runText = run.getCTR().getTArray(0).getStringValue();
//      
//      String beforeSuperLong = StringUtils.substring(runText, 0, runText.indexOf(searchValue));
//      
//      String[] replacementTextSplitOnCarriageReturn = StringUtils.split(replacement, "\n");
//      
//      String afterSuperLong = StringUtils.substring(runText, runText.indexOf(searchValue) + searchValue.length());
//      
//      Counter counter = new Counter(i);
//      
//      insertNewRun(paragraph, run, counter, beforeSuperLong);
//      
//      for (int j = 0; j < replacementTextSplitOnCarriageReturn.length; j++) {
//          String part = replacementTextSplitOnCarriageReturn[j];
//
//          XWPFRun newRun = insertNewRun(paragraph, run, counter, part);
//          
//          if (j+1 < replacementTextSplitOnCarriageReturn.length) {
//              newRun.addCarriageReturn();
//          }
//      }
//      
//      insertNewRun(paragraph, run, counter, afterSuperLong);
//      
//      paragraph.removeRun(counter.getCount());
//      
//      return counter.getCount();
//  }

//  private class Counter {
//      private int i;
//      
//      public Counter(int i) {
//          this.i = i;
//      }
//      
//      public void increment() {
//          i++;
//      }
//      
//      public int getCount() {
//          return i;
//      }
//  }

//  private XWPFRun insertNewRun(XWPFParagraph xwpfParagraph, XWPFRun run, Counter counter, String newText) {
//      XWPFRun newRun = xwpfParagraph.insertNewRun(counter.i);
//      newRun.getCTR().set(run.getCTR());
//      newRun.getCTR().getTArray(0).setStringValue(newText);
//      
//      counter.increment();
//      
//      return newRun;
//  }
person Kyle Willkomm    schedule 07.08.2014

моей задачей было заменить тексты формата $ {key} значениями карты в документе word docx. Вышеупомянутые решения были хорошей отправной точкой, но не учитывали все случаи: $ {key} можно распределить не только по нескольким запускам, но и по нескольким текстам в рамках одного запуска. Поэтому я получил следующий код:

    private void replace(String inFile, Map<String, String> data, OutputStream out) throws Exception, IOException {
    XWPFDocument doc = new XWPFDocument(OPCPackage.open(inFile));
    for (XWPFParagraph p : doc.getParagraphs()) {
        replace2(p, data);
    }
    for (XWPFTable tbl : doc.getTables()) {
        for (XWPFTableRow row : tbl.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                for (XWPFParagraph p : cell.getParagraphs()) {
                    replace2(p, data);
                }
            }
        }
    }
    doc.write(out);
}

private void replace2(XWPFParagraph p, Map<String, String> data) {
    String pText = p.getText(); // complete paragraph as string
    if (pText.contains("${")) { // if paragraph does not include our pattern, ignore
        TreeMap<Integer, XWPFRun> posRuns = getPosToRuns(p);
        Pattern pat = Pattern.compile("\\$\\{(.+?)\\}");
        Matcher m = pat.matcher(pText);
        while (m.find()) { // for all patterns in the paragraph
            String g = m.group(1);  // extract key start and end pos
            int s = m.start(1);
            int e = m.end(1);
            String key = g;
            String x = data.get(key);
            if (x == null)
                x = "";
            SortedMap<Integer, XWPFRun> range = posRuns.subMap(s - 2, true, e + 1, true); // get runs which contain the pattern
            boolean found1 = false; // found $
            boolean found2 = false; // found {
            boolean found3 = false; // found }
            XWPFRun prevRun = null; // previous run handled in the loop
            XWPFRun found2Run = null; // run in which { was found
            int found2Pos = -1; // pos of { within above run
            for (XWPFRun r : range.values())
            {
                if (r == prevRun)
                    continue; // this run has already been handled
                if (found3)
                    break; // done working on current key pattern
                prevRun = r;
                for (int k = 0;; k++) { // iterate over texts of run r
                    if (found3)
                        break;
                    String txt = null;
                    try {
                        txt = r.getText(k); // note: should return null, but throws exception if the text does not exist
                    } catch (Exception ex) {

                    }
                    if (txt == null)
                        break; // no more texts in the run, exit loop
                    if (txt.contains("$") && !found1) {  // found $, replace it with value from data map
                        txt = txt.replaceFirst("\\$", x);
                        found1 = true;
                    }
                    if (txt.contains("{") && !found2 && found1) {
                        found2Run = r; // found { replace it with empty string and remember location
                        found2Pos = txt.indexOf('{');
                        txt = txt.replaceFirst("\\{", "");
                        found2 = true;
                    }
                    if (found1 && found2 && !found3) { // find } and set all chars between { and } to blank
                        if (txt.contains("}"))
                        {
                            if (r == found2Run)
                            { // complete pattern was within a single run
                                txt = txt.substring(0, found2Pos)+txt.substring(txt.indexOf('}'));
                            }
                            else // pattern spread across multiple runs
                                txt = txt.substring(txt.indexOf('}'));
                        }
                        else if (r == found2Run) // same run as { but no }, remove all text starting at {
                            txt = txt.substring(0,  found2Pos);
                        else
                            txt = ""; // run between { and }, set text to blank
                    }
                    if (txt.contains("}") && !found3) {
                        txt = txt.replaceFirst("\\}", "");
                        found3 = true;
                    }
                    r.setText(txt, k);
                }
            }
        }
        System.out.println(p.getText());

    }

}

private TreeMap<Integer, XWPFRun> getPosToRuns(XWPFParagraph paragraph) {
    int pos = 0;
    TreeMap<Integer, XWPFRun> map = new TreeMap<Integer, XWPFRun>();
    for (XWPFRun run : paragraph.getRuns()) {
        String runText = run.text();
        if (runText != null && runText.length() > 0) {
            for (int i = 0; i < runText.length(); i++) {
                map.put(pos + i, run);
            }
            pos += runText.length();
        }

    }
    return map;
}
person ron    schedule 31.12.2016
comment
Хорошо ли работает? Сохраняется ли после этого файловая структура и т. Д.? Я пробовал ваш код, но мне не удалось заставить его работать. Вы можете добавить несколько комментариев - person Ced; 02.01.2017
comment
привет, да, у меня он работает хорошо, и структура цела. Какие проблемы у тебя ? Я добавлю несколько комментариев и обновлю код. - person ron; 04.01.2017
comment
Работает как шарм. Большое тебе спасибо! - person Alexander Pavlov; 15.03.2019
comment
это не сработало, если бы у меня было два токена $ {} в одном прогоне. Я думаю, это регулярное выражение - person spy; 06.05.2021

Если кому-то нужно также сохранить форматирование текста, этот код работает лучше.

private static Map<Integer, XWPFRun> getPosToRuns(XWPFParagraph paragraph) {
    int pos = 0;
    Map<Integer, XWPFRun> map = new HashMap<Integer, XWPFRun>(10);
    for (XWPFRun run : paragraph.getRuns()) {
        String runText = run.text();
        if (runText != null) {
            for (int i = 0; i < runText.length(); i++) {
                map.put(pos + i, run);
            }
            pos += runText.length();
        }
    }
    return (map);
}

public static <V> void replace(XWPFDocument document, Map<String, V> map) {
    List<XWPFParagraph> paragraphs = document.getParagraphs();
    for (XWPFParagraph paragraph : paragraphs) {
        replace(paragraph, map);
    }
}

public static <V> void replace(XWPFDocument document, String searchText, V replacement) {
    List<XWPFParagraph> paragraphs = document.getParagraphs();
    for (XWPFParagraph paragraph : paragraphs) {
        replace(paragraph, searchText, replacement);
    }
}

private static <V> void replace(XWPFParagraph paragraph, Map<String, V> map) {
    for (Map.Entry<String, V> entry : map.entrySet()) {
        replace(paragraph, entry.getKey(), entry.getValue());
    }
}

public static <V> void replace(XWPFParagraph paragraph, String searchText, V replacement) {
    boolean found = true;
    while (found) {
        found = false;
        int pos = paragraph.getText().indexOf(searchText);
        if (pos >= 0) {
            found = true;
            Map<Integer, XWPFRun> posToRuns = getPosToRuns(paragraph);
            XWPFRun run = posToRuns.get(pos);
            XWPFRun lastRun = posToRuns.get(pos + searchText.length() - 1);
            int runNum = paragraph.getRuns().indexOf(run);
            int lastRunNum = paragraph.getRuns().indexOf(lastRun);
            String texts[] = replacement.toString().split("\n");
            run.setText(texts[0], 0);
            XWPFRun newRun = run;
            for (int i = 1; i < texts.length; i++) {
                newRun.addCarriageReturn();
                newRun = paragraph.insertNewRun(runNum + i);
                /*
                    We should copy all style attributes
                    to the newRun from run
                    also from background color, ...
                    Here we duplicate only the simple attributes...
                 */
                newRun.setText(texts[i]);
                newRun.setBold(run.isBold());
                newRun.setCapitalized(run.isCapitalized());
                // newRun.setCharacterSpacing(run.getCharacterSpacing());
                newRun.setColor(run.getColor());
                newRun.setDoubleStrikethrough(run.isDoubleStrikeThrough());
                newRun.setEmbossed(run.isEmbossed());
                newRun.setFontFamily(run.getFontFamily());
                newRun.setFontSize(run.getFontSize());
                newRun.setImprinted(run.isImprinted());
                newRun.setItalic(run.isItalic());
                newRun.setKerning(run.getKerning());
                newRun.setShadow(run.isShadowed());
                newRun.setSmallCaps(run.isSmallCaps());
                newRun.setStrikeThrough(run.isStrikeThrough());
                newRun.setSubscript(run.getSubscript());
                newRun.setUnderline(run.getUnderline());
            }
            for (int i = lastRunNum + texts.length - 1; i > runNum + texts.length - 1; i--) {
                paragraph.removeRun(i);
            }
        }
    }
}
person Thierry Bodhuin    schedule 24.10.2016

Существует реализация replaceParagraph, которая заменяет ${key} на value (параметр fieldsForReport) и сохраняет формат путем объединения runs содержимого ${key}.

private void replaceParagraph(XWPFParagraph paragraph, Map<String, String> fieldsForReport) throws POIXMLException {
    String find, text, runsText;
    List<XWPFRun> runs;
    XWPFRun run, nextRun;
    for (String key : fieldsForReport.keySet()) {
        text = paragraph.getText();
        if (!text.contains("${"))
            return;
        find = "${" + key + "}";
        if (!text.contains(find))
            continue;
        runs = paragraph.getRuns();
        for (int i = 0; i < runs.size(); i++) {
            run = runs.get(i);
            runsText = run.getText(0);
            if (runsText.contains("${") || (runsText.contains("$") && runs.get(i + 1).getText(0).substring(0, 1).equals("{"))) {
                //As the next run may has a closed tag and an open tag at 
                //the same time, we have to be sure that our building string 
                //has a fully completed tags 
                while (!openTagCountIsEqualCloseTagCount(runsText))) {
                    nextRun = runs.get(i + 1);
                    runsText = runsText + nextRun.getText(0);
                    paragraph.removeRun(i + 1);
                }
                run.setText(runsText.contains(find) ?
                        runsText.replace(find, fieldsForReport.get(key)) :
                        runsText, 0);
            }
        }
    }
}

private boolean openTagCountIsEqualCloseTagCount(String runText) {
    int openTagCount = runText.split("\\$\\{", -1).length - 1;
    int closeTagCount = runText.split("}", -1).length - 1;
    return openTagCount == closeTagCount;
}

Реализация replaceParagraph

Модульный тест

person Dmitry Stolbov    schedule 11.04.2018
comment
Быстрый вопрос к тебе, Дима: Что такое переменная testString в #openTagCountIsEqualCloseTagCount? Вы когда-нибудь хотели заменить это на runText, но забыли? - person ivan_drago; 08.04.2020
comment
@ivan_drago, ты прав. Это была ошибка. Произведен рефакторинг openTagCountIsEqualCloseTagCount. Спасибо! - person Dmitry Stolbov; 09.04.2020
comment
Это самое точное решение, которое я нашел в Интернете. Он знает, что состав выполнения очень непредсказуем, и вам нужно найти тег, который вы хотите заменить. - person Diego Juliao; 16.06.2021

Первый кусок кода дает мне исключение NullPointerException, кто-нибудь знает, что не так?

run.getText (int position) - из документации: Возвращает: текст этого текста запускается или null, если не установлен

Просто проверьте, не является ли он нулевым, прежде чем вызывать для него contains ()

И, кстати, если вы хотите заменить текст, вам нужно установить его в положение, из которого вы его получили, в данном случае r.setText (text, 0) ;. В противном случае текст будет добавлен, а не заменен

person birya    schedule 18.07.2014

Для принятого здесь ответа требуется еще одно обновление вместе с обновлением Джастина Скилза. r.setText (текст, 0); Причина: если не обновить setText с помощью переменной pos, на выходе будет комбинация старой строки и строки замены.

person Sherin    schedule 01.04.2015
comment
Подтверждаю обнаружение странности. На самом деле run.setText( new_text, 0 ) у меня работает нормально, но run.setText( new_text ) действительно добавляет new_text к существующему тексту XWPFRun. Неужели ошибка? PS с использованием Jython. - person mike rodent; 14.02.2016

На момент написания ни один из ответов не заменил должным образом.

В ответе Гаграварса не учитываются случаи, когда заменяемые слова разбиваются на части; Решение Thierry Boduins иногда оставляло слова для замены пустыми, когда они были после других слов для замены, также оно не проверяет таблицы.

Используя ответ Gagtavars в качестве основы, я также проверил запуск перед текущим запуском, если текст обоих прогонов содержит слово для замены, добавив блок else. Мое дополнение в котлине:

if (text != null) {
        if (text.contains(findText)) {
            text = text.replace(findText, replaceText)
            r.setText(text, 0)
        } else if (i > 0 && p.runs[i - 1].getText(0).plus(text).contains(findText)) {
            val pos = p.runs[i - 1].getText(0).indexOf('$')
            text = textOfNotFullSecondRun(text, findText)
            r.setText(text, 0)
            val findTextLengthInFirstRun = findTextPartInFirstRun(p.runs[i - 1].getText(0), findText)
            val prevRunText = p.runs[i - 1].getText(0).replaceRange(pos, findTextLengthInFirstRun, replaceText)
            p.runs[i - 1].setText(prevRunText, 0)
        }
    }

private fun textOfNotFullSecondRun(text: String, findText: String): String {
    return if (!text.contains(findText)) {
        textOfNotFullSecondRun(text, findText.drop(1))
    } else {
        text.replace(findText, "")
    }
}

private fun findTextPartInFirstRun(text: String, findText: String): Int {
    return if (text.contains(findText)) {
        findText.length
    } else {
        findTextPartInFirstRun(text, findText.dropLast(1))
    }
}

это список прогонов в абзаце. То же самое с блоком поиска в таблице. С этим решением у меня еще не было проблем. Все форматирование не повреждено.

Изменить: я сделал java lib для замены, проверьте это: https://github.com/deividasstr/docx-word-replacer

person Deividas Strioga    schedule 08.02.2018
comment
Мы использовали эту библиотеку для замены текста, и в нашей функции она работает как шарм! Спасибо! - person Antimo; 25.05.2018
comment
Я рад это слышать! Спасибо! Если вам нужны дополнительные функции или исправления, просто дайте мне знать! - person Deividas Strioga; 25.05.2018

Я предлагаю свое решение для замены текста между #, например: Эту закладку # следует заменить. Это заменить в:

  • абзацы;
  • столы;
  • нижние колонтитулы.

Также учитываются ситуации, когда символ № и закладка находятся в отдельных прогонах (заменить переменную между разными прогонами).

Ссылка на код: https://gist.github.com/aerobium/bf02e443c079c1678caec75

person Optio    schedule 03.10.2017

Основываясь на ответе Дмитрия Столбова здесь, а также на проблемах и ограничениях, с которыми он столкнулся, а также на остальных ответах, я пришел с классом ниже, который реализует метод generateDocument, который выполняет поиск в абзацах и таблицах.

Здесь я решил несколько проблем, обнаруженных в ответах, например:

  • .setText (x, 0) заменять, а не добавлять
  • проблемы с абзацами, содержащими \ t. Когда мы выполняем run.getText (int position) при запуске с этим char, мы получаем null, поэтому мы не можем использовать .contains () поверх него.
  • объединение выполняется вместе, когда keyTag для замены разделяется на несколько запусков

Это нормально работает, но мне нужно немного понять, как решить возникшую у меня проблему. Иногда значение, которое нужно заменить в файле, превышает размер заменяемого тега, и это приводит к нарушению выравнивания. Например:

шаблон:  введите описание изображения здесь

выходной файл:  введите описание изображения здесь

Произошло то, что {# branch #} и {# insurCompanyCorporateName #} были заменены более крупными строками, после тега {# branch #} есть несколько элементов \ t, в сочетании с тем фактом, что значение {# insurCompanyCorporateName #} также больше, чем тег, подтолкнул содержимое вперед, разделив его на следующую строку.

Мне было интересно, есть ли у кого-нибудь представление о том, как я мог бы понять во время выполнения, если значения, которые я заменяю, приводят к разделению строк документа или нарушают положение других элементов на странице. В этом случае я хотел бы, чтобы моя программа понимала, что он должен, например, удалить некоторые \ t после ветки. Или, может быть, разделите {# insurCompanyCorporateName #} на новую строку, но сделав новую строку, начинающуюся ниже исходного тега, или что-то в этом роде.

Что?

Класс:

package com.idoine.struts2.action.shared;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.*;
import org.json.JSONObject;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

/**
 * Created by migue on 11/11/2020.
 */
public class DocumentGeneratorAction {

    public static ByteArrayInputStream generateDocument(String templatePath, JSONObject fields){
        /** used as reference: https://stackoverflow.com/a/49765239/5936443   [at 11/11/2020]
         This method is responsible for generating a document as a ByteArrayInputStream, using an exisiting word template at templatePath
         It replaces any keyTags in the document by the corresponding value in the JSONObject fields
         it assumes the keyTags come preceeded by the separator "{#" and proceeded by "#}", in the following form: {#keyTag#}
         */
        try {
            XWPFDocument doc = new XWPFDocument(OPCPackage.open(templatePath));

            // search in paragraphs
            for(XWPFParagraph p : doc.getParagraphs()){
                replaceFieldsParagraph(p, fields);
            }

            // search in tables
            for(XWPFTable t : doc.getTables()){
                replaceFieldsTable(t, fields);
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            doc.write(out);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(out.toByteArray());
            return inputStream;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidFormatException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void replaceFieldsParagraph(XWPFParagraph paragraph, JSONObject fields){
        /** this method is responsible for replacing any ocurrences in the paragraph of any of the keyTags
         * present in the JSONObject fields by the corresponding value */
        String text = paragraph.getText(); //all the text from each run concatenated
        String findStr;
        if( !text.contains("{#")) //paragraph doesn't have keys to replace
            return;

        // for each field to replace, search it in the curr paragraph
        for( String key : fields.keySet()){
            findStr = "{#" + key + "#}";
            // if paragraph doesn't have current key, we skip to next key
            if( text.contains(findStr)) {
                mergeRunsWithSplittedKeyTags(paragraph);
                for (XWPFRun run : paragraph.getRuns()) {
                    // check if current run has current key
                    checkAndReplaceFieldRun(run, findStr, String.valueOf(fields.get(key)));
                }
            }
        }
    }

    public static void replaceFieldsTable(XWPFTable table, JSONObject fields){
        /** this method is responsible for replacing any ocurrences in the table of any of the keyTags
         * present in the JSONObject fields by the corresponding value */

        if( table.getNumberOfRows() > 0){
            for(XWPFTableRow row : table.getRows()){                                        // iterate over rows
                for( XWPFTableCell cell : row.getTableCells()){                             // iterate over columns
                    if( cell.getParagraphs() != null && cell.getParagraphs().size()>0){
                        for(XWPFParagraph paragraph : cell.getParagraphs()){                // get cell paragraphs
                            replaceFieldsParagraph(paragraph, fields);                      // replacing existing keyTags in paragraph
                        }
                    }
                }
            }
        }
    }

    public static void checkAndReplaceFieldRun(XWPFRun run, String findStr, String value){
        String runText = run.getText(0);
        if( runText!= null && runText.contains(findStr)){
            runText = runText.replace(findStr, value);
            run.setText(runText, 0);
        }
    }

    public static void mergeRunsWithSplittedKeyTags(XWPFParagraph paragraph){
        /**
         A run is a part of the paragraph that has the same formatting.
         Word separates the text in paragraphs by different runs in a almost 'random' way,
         sometimes the tag we are looking for is splitted across multiple runs.
         This method merges the runs that have a keyTag or part of one,
         so that the keyTag starting with "{#" and ending with "#}" is in the same run
        */
        String runText;
        XWPFRun run, nextRun;

        List<XWPFRun> runs = paragraph.getRuns();

        for( int i=0 ; i<runs.size(); i++){
            run = runs.get(i);
            runText = run.getText(0);
            if( runText != null &&
                    (runText.contains("{#") ||  // current run has the complete separator "{#"
                        (runText.contains("{") && (runs.get(i + 1).getText(0)!=null && runs.get(i + 1).getText(0).substring(0, 1).equals("#"))))){   //current run has the first char, next run has the second char

                while( !openTagMatchesCloseTag(runText) ){
                    nextRun = runs.get(i + 1);
                    runText = runText + nextRun.getText(0);
                    paragraph.removeRun(i + 1);
                }
                run.setText(runText, 0); // if we don't set with arg pos=0 it doesn't replace the contents, it adds to them and repeats chars
            }
        }
    }

    public static boolean openTagMatchesCloseTag(String runText){
        /** This method validates if we have a complete run.
         * Either by having no keyTags present, or by having a complete keyTag.
         * If we have parts of a keyTag, but not the complete one, returns false.*/
        int incompleteOpenTagCount = runText.split("\\{", -1).length - 1;   // "{"
        int completeOpenTagCount = runText.split("\\{#", -1).length - 1;    // "{#"
        int completeCloseTagCount = runText.split("#}", -1).length - 1;     // "#}"

        if(completeOpenTagCount>0){  // we already have open and close tags, compare the counts
            return completeOpenTagCount == completeCloseTagCount;
        } else {
            if( incompleteOpenTagCount>0 ){   // we only have a "{" not the whole "{#"
                return false;
            }
        }

        //doesn't have neither "{" nor "{#", so there's no need to close tags
        return true;
    }

}
person Miguel Pinto    schedule 12.11.2020