Replacing a text in Apache POI XWPF
Asked Answered
W

11

58

I just found Apache POI library very useful for editing Word files using Java. Specifically, I want to edit a DOCX file using Apache POI's XWPF classes. I found no proper method / documentation following which I could do this. Can somebody please explain in steps, how to replace some text in a DOCX file.

** The text may be in a line / paragraph or in a table row/column

Wily answered 8/3, 2014 at 11:46 Comment(0)
J
84

The method you need is XWPFRun.setText(String). Simply work your way through the file until you find the XWPFRun of interest, work out what you want the new text to be, and replace it. (A run is a sequence of text with the same formatting)

You should be able to do something like:

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"));
Jerricajerrie answered 8/3, 2014 at 12:0 Comment(14)
thanks for this, but this is not reading the table data. Is there some other class which I need to use for that ??Wily
and you defined RUN as text with similar formatting, Right ? It is breaking the text with similar formatting into parts. How to correct that ?Wily
Apache POI just gives you the text in the file, it has no control over how Word chooses to structure it in terms of Runs... And Word is known to do strange things! If need be, check nearby runs for part of the text.Jerricajerrie
I'm using Apache PIO 3.10 and it appears that getCells() on XWPFTableRow is now getTableCells().Candlelight
The first chunk of code is giing me a NullPointerException, anyone know what is wrong?Ariminum
@Ariminum Your best bet would be to ask a new question, include the code you're using, the stacktrace you get, and clarify where exactly it's giving the NPEJerricajerrie
Please update your answer. tbl.getRow() should be tbl.getRows() and row.getCells() should be row.getTableCells().Candlelight
@Ariminum (null != text) && text.contains("${exercice}")Noli
Hate to tell you this, but this approach does not work. It MIGHT work in some cases, but every time I try to do it, the text is arbitrarily broken up into multiple runs. It doesn't depend on formatting or punctuation... the runs can be broken anywhere in the text. So searching and replacing within individual runs is doomed to failure. The only choices are to replace at the paragraph level (probably not acceptable because of loss of formatting) or to find the text at the paragraph level, then map the runs to the offsets of the found text, and manipulate all overlapping runs accordingly.Fascist
This doesn't always work because word sometimes decides to split single camel case word into multiple runs.Parsaye
I want to replace {10} to the corresponding text in a table, but the words not in the same XWPFRun.Emaciated
If you want to replace specific values in a template document (in my case) my post in topic : #19138318 might help you. It's little bit way around but in my case solved problems with divided runs and get specific word in separated run.Manifesto
I had the same probleme that @Fascist and I write the solution here [https://mcmap.net/q/859924/-seperated-text-line-in-apache-poi-xwpfrun-object]Calendar
for String text = r.getText(0); what is the parameter 0 means? I check api but have not explain this parameter means...Bimanous
H
21

Here is what we did for text replacement using Apache POI. We found that it was not worth the hassle and simpler to replace the text of an entire XWPFParagraph instead of a run. A run can be randomly split in the middle of a word as Microsoft Word is in charge of where runs are created within the paragraph of a document. Therefore the text you might be searching for could be half in one run and half in another. Using the full text of a paragraph, removing its existing runs, and adding a new run with the adjusted text seems to solve the problem of text replacement.

However there is a cost of doing the replacement at the paragraph level; you lose the formatting of the runs in that paragraph. For example if in the middle of your paragraph you had bolded the word "bits", and then when parsing the file you replaced the word "bits" with "bytes", the word "bytes" would no longer be bolded. Because the bolding was stored with a run that was removed when the paragraph's entire body of text was replaced. The attached code has a commented out section that was working for replacement of text at the run level if you need it.

It should also be noted that the below works if the text you are inserting contains \n return characters. We could not find a way to insert returns without creating a run for each section prior to the return and marking the run addCarriageReturn(). Cheers

    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;
//  }
Hog answered 7/8, 2014 at 15:40 Comment(0)
P
17

my task was to replace texts of the format ${key} with values of a map within a word docx document. The above solutions were a good starting point but did not take into account all cases: ${key} can be spread not only across multiple runs but also across multiple texts within a run. I therefore ended up with the following code:

    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;
}
Peper answered 31/12, 2016 at 9:46 Comment(6)
Is it working well ? Is the file structure etc intact after ? I tried your code, but I didn't manage to make it work. You might wanna add some commentsNoli
hello, yes it is working well for me and the structure is intact. what problems do you have ? i will add some comments and update the code.Peper
this didn't work if i had two ${} tokens in the same run. It's the regex i thinkHeathenism
Suggestion for improval: paragraph.getRuns() does not return runs that e.g. contain fields. Using paragraph.getIRuns() (that returns IRunElements) gives you more runs.Docent
best answer ! thanks) Perfectly works with placeholders taking two runs.Publisher
One small code cleanup would be nice: found3 inside for is always false. Because u jump out from for at the start if it is true. So I would remove all && !found3 occurrences inside IFs inside most nested for. So two occurrences.Sawhorse
B
16

There is the replaceParagraph implementation that replaces ${key} with value (the fieldsForReport parameter) and saves format by merging runs contents ${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;
}

Implementation replaceParagraph

Unit test

Behave answered 11/4, 2018 at 1:48 Comment(2)
This is the most accurate solution that I've found across the web. It's aware the run composition is very unpredictable and you need to find the tag that you want to replace.Surprising
It doesn't work, becuase runsText equal to $ at line: run.setText(runsText.contains(find) ? runsText.replace(find, fieldsForReport.get(key)) : runsText, 0);Publisher
A
13

If somebody needs also to keep the formatting of the text, this code works better.

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);
            }
        }
    }
}
Amplexicaul answered 24/10, 2016 at 19:48 Comment(0)
C
5

As of the date of writing, none of the answers replace properly.

Gagravars answer does not include cases where words to replace are split in runs; Thierry Boduins solution sometimes left words to replace blank when they were after other words to replace, also it does not check tables.

Using Gagtavars answer as base I have also checked the run before current run if the text of both runs contain the word to replace, adding else block. My addition in kotlin:

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

it is the list of runs in a paragraph. Same with the search block in the table. With this solution I did not have any issues yet. All formatting is intact.

Edit: I made a java lib for replacing, check it out: https://github.com/deividasstr/docx-word-replacer

Condense answered 8/2, 2018 at 7:33 Comment(2)
We have been using this library to text replacement and in our feature, it works like a charm! Thank you!Ashly
Your java lib is really nice ! But you use "contains()" so to can't make difference between $rabbit and $rabbitYellow during replacement. @see #42904861 So you can update source with : Pattern.compile("(?<!\\S)" + Pattern.quote(bookmark) + "(?!\\S)");Herbie
R
4

The first chunk of code is giing me a NullPointerException, anyone know what is wrong?

run.getText(int position) - from documentation: Returns: the text of this text run or null if not set

Just check if it is not null before calling contains() on it

And btw if you want to replace the text you need to set it in position from which you get it, in this case r.setText(text, 0);. Otherwise text will be added not replaced

Ripen answered 18/7, 2014 at 9:41 Comment(0)
B
4

The answer accepted here needs one more update along with Justin Skiles update. r.setText(text, 0); Reason: If not updating setText with pos variable, the output will be the combination of old string and replace string.

Bouncer answered 1/4, 2015 at 9:48 Comment(1)
I confirm finding an oddity. run.setText( new_text, 0 ) works OK for me, in fact, but run.setText( new_text ) does indeed append new_text to the existing text of the XWPFRun. A bug, surely? PS using Jython.Bootblack
I
2

I suggest my solution for replacing text between #, for example: This #bookmark# should be replaced. It is replace in:

  • paragraphs;
  • tables;
  • footers.

Also, it takes into account situations, when symbol # and bookmark are in the separated runs (replace variable between different runs).

Here link to the code: https://gist.github.com/aerobium/bf02e443c079c5caec7568e167849dda

Intramolecular answered 3/10, 2017 at 11:15 Comment(0)
A
2

Based on Dmitry Stolbov answer here and the problems and limitations encountered by it and the rest of the responses I came with the below class, that implements the method generateDocument that searches in paragraphs and tables.

Here I solved several problems found in the responses like:

  • the .setText(x, 0) to replace and not add
  • problems with paragraphs containing "\t". When we do run.getText(int position) on a run with this char we get null so we can't use the .contains() over it.
  • merging runs together when the keyTag to replace is splitted across multiple runs

This works fine but I need some insights on how to solve a problem I' having. Sometimes the value to replace in the file is larger than the tag to replace, and that ends up screwing up the alignments. For example:

the template: enter image description here

the output file: enter image description here

What happened is that the {#branch#} and {#insurCompanyCorporateName#} were replaced by larger strings, after the {#branch#} tag there are several "\t" elements and that, combined to the fact that {#insurCompanyCorporateName#} value is also larger that the tag, pushed the contents forward making it split to the next line.

I was wondering if anyone has some insights on how I could maybe understand at runtime if the values I'm replacing make the document split lines, or mess up the position of further elements in the page. In this case I would like my program to understand that he should remove some "\t" after the branch for example. Or maybe split the {#insurCompanyCorporateName#} to a new line, but making the new line starting bellow the original tag or something.

Thoghts?

The class:

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://mcmap.net/q/1631959/-replacing-a-text-in-apache-poi-xwpf   [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;
    }

}
Attribution answered 12/11, 2020 at 12:12 Comment(0)
L
1

Based on Dmitry's answer which helped me a lot, I found a senario where first run finds only the $ character and this leaves the text unchanged.

I changed the code a little bit in order to handle these senarios too.

if (runsText.contains("${") || (runsText.contains("$") && runs.get(i + 1).getText(0).substring(0, 1).equals("{"))) {
  String tempRunsText = runsText;
  if (runsText.contains("$") && runs.get(i + 1).getText(0).charAt(0) == '{') {
    nextRun = runs.get(i + 1);
    tempRunsText = runsText + nextRun.getText(0);
  }
  //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(tempRunsText)) {
    nextRun = runs.get(i + 1);

    if (!nextRun.getText(0).equals("{")  || (nextRun.getText(0).equals("{") && !tempRunsText.contains("{"))) {
      tempRunsText = tempRunsText + nextRun.getText(0);
    }

    paragraph.removeRun(i + 1);
  }

  runsText = tempRunsText;

  run.setText(runsText.contains(find) ?
    runsText.replace(find, fieldsForReport.get(key)) :
    runsText, 0);
}
Ligetti answered 25/8, 2022 at 8:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.