POI Apache: ${my_placeholder} рассматривается как три разных запуска

У меня есть шаблон .docx с заполнителями, такими как ${programming_language}, ${education} и т. д.

Ключевые слова-заполнители должны легко отличаться от других простых слов, поэтому они заключены в ${ }.

for (XWPFTable table : doc.getTables()) {
  for (XWPFTableRow row : table.getRows()) {
    for (XWPFTableCell cell : row.getTableCells()) {
      for (XWPFParagraph paragraph : cell.getParagraphs()) {
        for (XWPFRun run : paragraph.getRuns()) {
          System.out.println("run text: " + run.text());
          /** replace text here, etc. */
        }
      }
    }
  }
}

Я хочу извлечь заполнители вместе с окружающими символами ${ }. Проблема в том, что кажется, что окружающие символы рассматриваются как разные прогоны...

run text: ${
run text: programming_language
run text: }
run text: Some plain text here 
run text: ${
run text: education
run text: }

Вместо этого я хотел бы добиться следующего эффекта:

run text: ${programming_language}
run text: Some plain text here
run text: ${education}

Я пытался использовать другие окружающие символы, такие как: { }, < >, # # и т. д.

Я не хочу делать какие-то странные конкатенации runs и т. д. Я хочу, чтобы это было в одном XWPFRun.

Если я не найду подходящего решения, то сделаю так: VAR_PROGRAMMING_LANGUGE, VAR_EDUCATION, кажется.


person weno    schedule 13.12.2020    source источник
comment
Учитывая, что вы не можете контролировать, когда Word решит разделить данные на разные прогоны, даже если они имеют одинаковое форматирование, почему бы не обновить свою логику, чтобы справиться с прогонами, охватывающими текст?   -  person Gagravarr    schedule 13.12.2020
comment
Что вы подразумеваете под текстовым охватом?   -  person weno    schedule 13.12.2020
comment
Текст, который вы хотите, будет в 1+ прогонах, возможно, не единственный в любом из этих прогонов. Word несколько случайно (и несколько на основе истории) решит разрезать текст на столько прогонов, сколько ему захочется, и вы не можете это контролировать. Вы просто должны справиться с этим!   -  person Gagravarr    schedule 14.12.2020


Ответы (1)


Текущий apache poi 4.1.2 предоставляет TextSegment для обработки с этими Word проблемами с запуском текста. XWPFParagraph.searchText ищет строку в абзаце и возвращает TextSegment. Это обеспечивает доступ к началу и концу этого текста в этом абзаце (BeginRun и EndRun). Он также обеспечивает доступ к положению начального символа в начале цикла и позиции конечного символа в конце цикла (BeginChar и EndChar). Кроме того, он предоставляет доступ к индексу текстового элемента в текстовом прогоне (BeginText и EndText). Это всегда должно быть 0, потому что текстовые прогоны по умолчанию имеют только один текстовый элемент.

Имея это, мы можем сделать следующее:

Замените найденную частичную строку в начале выполнения заменой. Для этого возьмите текстовую часть, которая была перед искомой строкой, и соедините с ней замену. После этого стартовый тираж полностью содержит замену.

Удалите все текстовые прогоны между начальным прогоном и конечным прогоном, так как они содержат части искомой строки, которые больше не нужны.

Пусть останется только текстовая часть после искомой строки в конце прогона.

Таким образом, мы можем заменить текст, который находится в нескольких текстовых прогонах.

Следующий пример показывает это.

import java.io.*;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

public class WordReplaceTextSegment {

 static public void replaceTextSegment(XWPFParagraph paragraph, String textToFind, String replacement) {
  TextSegment foundTextSegment = null;
  PositionInParagraph startPos = new PositionInParagraph(0, 0, 0);
  while((foundTextSegment = paragraph.searchText(textToFind, startPos)) != null) { // search all text segments having text to find

System.out.println(foundTextSegment.getBeginRun()+":"+foundTextSegment.getBeginText()+":"+foundTextSegment.getBeginChar());
System.out.println(foundTextSegment.getEndRun()+":"+foundTextSegment.getEndText()+":"+foundTextSegment.getEndChar());

   // 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
   }

  }
 }

 public static void main(String[] args) throws Exception {

  XWPFDocument doc = new XWPFDocument(new FileInputStream("source.docx"));

  String textToFind = "${This is the text to find}"; // might be in different runs
  String replacement = "Replacement text";

  for (XWPFParagraph paragraph : doc.getParagraphs()) { //go through all paragraphs
   if (paragraph.getText().contains(textToFind)) { // paragraph contains text to find
    replaceTextSegment(paragraph, textToFind, replacement);
   }
  }

  FileOutputStream out = new FileOutputStream("result.docx");
  doc.write(out);
  out.close();
  doc.close();

 }
}

Приведенный выше код работает не во всех случаях, потому что XWPFParagraph.searchText содержит ошибки. Поэтому я предоставлю лучший метод searchText:

/**
 * 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.
 *
 * @param searched
 * @param startPos
 */
static TextSegment searchText(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 < rArray.length; runPos++) {
    for (int runPos = startRun; runPos < runs.size(); runPos++) {
        //int beginTextPos = 0, beginCharPos = 0, textPos = 0, charPos; //int beginTextPos = 0, beginCharPos = 0 must be outside the for loop
        int textPos = 0, charPos;
        //CTR ctRun = rArray[runPos];
        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.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;
}

Это будет называться так:

...
while((foundTextSegment = searchText(paragraph, textToFind, startPos)) != null) {
...
person Axel Richter    schedule 14.12.2020