Замена текста в Apache POI XWPF
Я только что нашел библиотеку Apache POI очень полезную для редактирования файлов Word с помощью Java. В частности, я хочу редактировать файл DOCX, используя классы Apache POI XWPF. Я не нашел подходящего метода / документации, после чего я мог бы сделать это. Может кто-нибудь, пожалуйста, объясните пошагово, как заменить текст в файле DOCX.
** Текст может быть в строке / абзаце или в строке / столбце таблицы
Заранее спасибо:)
11 ответов
Вам нужен метод 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"));
Вот что мы сделали для замены текста с помощью 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;
// }
Моя задача состояла в том, чтобы заменить тексты в формате ${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;
}
Здесь 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("{"))) {
while (!runsText.contains("}")) {
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 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);
}
}
}
}
Первый кусок кода дает мне исключение NullPointerException, кто-нибудь знает, что не так?
run.getText (int position) - из документации: Возвращает: текст этого текста run или ноль, если не задано
Просто проверьте, не является ли оно нулевым, прежде чем вызывать на нем
И, кстати, если вы хотите заменить текст, вам нужно установить его в положение, из которого вы его получите, в данном случае r.setText(text, 0);. В противном случае текст будет добавлен без замены
На момент написания ни один из ответов не заменил должным образом.
Гаграварский ответ не включает случаи, когда заменяемые слова разбиваются на серии; Решение 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-библиотеку для замены, проверьте это: https://github.com/deividasstr/docx-word-replacer
Ответ, принятый здесь, нуждается в еще одном обновлении наряду с обновлением Джастина Скилса. r.setText(текст, 0); Причина: если не обновить setText с помощью переменной pos, вывод будет комбинацией старой строки и строки замены.
Основываясь на ответе Дмитрия Столбова здесь, а также на проблемах и ограничениях, с которыми он столкнулся, а также на остальных ответах, я пришел с классом ниже, который реализует метод 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://stackru.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;
}
}
Я предлагаю свое решение для замены текста между #, например: Эта # закладка # должна быть заменена. Это заменить в:
- абзацы;
- столы;
- колонтитулы.
Кроме того, он учитывает ситуации, когда символ # и закладка находятся в отдельных циклах (замените переменную между различными циклами).
Здесь ссылка на код: https://gist.github.com/aerobium/bf02e443c079c5caec7568e167849dda
Основываясь на ответе Дмитрия, который мне очень помог, я нашел сценарий, в котором первый запуск находит только$
символ, и это оставляет текст без изменений.
Я немного изменил код, чтобы обрабатывать и эти сенари.
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);
}