我创建了一个库来发布我的解决方案,因为它有很多代码:https ://github.com/phip1611/docx4j-search-and-replace-util
工作流程如下:
第一步:
// (this method was part of your question)
List<Text> texts = getAllElementFromObject(docxDocument.getMainDocumentPart(), Text.class);
这样,我们以正确的顺序获得所有实际的文本内容,但中间没有样式标记。我们可以编辑文本对象(通过 setValue)并保留样式。
导致的问题:搜索文本/占位符可以拆分为多个文本实例(因为在原始文档之间可能存在不可见的样式标记),例如${FOOBAR}
、${
+ FOOBAR}
或$
+ {FOOB
+AR}
第二步:
将所有文本对象连接到一个完整的字符串/“完整字符串”
Optional<String> completeStringOpt = texts.stream().map(Text::getValue).reduce(String::concat);
第三步:
创建一个类TextMetaItem
。每个 TextMetaItem 都知道它的文本对象,它的内容在哪里开始和结束在完整的字符串中。例如,如果“foo”和“bar”的文本对象产生完整的字符串“foobar”,则索引0-2
属于"foo"-Text-object
和。建个3-5
"bar"-Text-object
List<TextMetaItem>
static List<TextMetaItem> buildMetaItemList(List<Text> texts) {
final int[] index = {0};
final int[] iteration = {0};
List<TextMetaItem> list = new ArrayList<>();
texts.forEach(text -> {
int length = text.getValue().length();
list.add(new TextMetaItem(index[0], index[0] + length - 1, text, iteration[0]));
index[0] += length;
iteration[0]++;
});
return list;
}
第四步:
构建一个Map<Integer, TextMetaItem>
键是完整字符串中的索引/字符的位置。这意味着地图的长度等于completeString.length()
static Map<Integer, TextMetaItem> buildStringIndicesToTextMetaItemMap(List<Text> texts) {
List<TextMetaItem> metaItemList = buildMetaItemList(texts);
Map<Integer, TextMetaItem> map = new TreeMap<>();
int currentStringIndicesToTextIndex = 0;
// + 1 important here!
int max = metaItemList.get(metaItemList.size() - 1).getEnd() + 1;
for (int i = 0; i < max; i++) {
TextMetaItem currentTextMetaItem = metaItemList.get(currentStringIndicesToTextIndex);
map.put(i, currentTextMetaItem);
if (i >= currentTextMetaItem.getEnd()) {
currentStringIndicesToTextIndex++;
}
}
return map;
}
中期结果:
现在您有足够的元数据可以将您想要对完整字符串执行的每个操作委托给相应的 Text 对象!(要更改文本对象的内容,您只需调用 (#setValue())这就是 Docx4J 编辑文本所需的全部内容。所有样式信息等都将被保留!
最后一步:搜索和替换
构建一个方法来查找所有可能出现的占位符。您应该创建一个类FoundResult(int start, int end)
,以便在完整字符串中存储找到的值(占位符)的开始和结束索引
public static List<FoundResult> findAllOccurrencesInString(String data, String search) {
List<FoundResult> list = new ArrayList<>();
String remaining = data;
int totalIndex = 0;
while (true) {
int index = remaining.indexOf(search);
if (index == -1) {
break;
}
int throwAwayCharCount = index + search.length();
remaining = remaining.substring(throwAwayCharCount);
list.add(new FoundResult(totalIndex + index, search));
totalIndex += throwAwayCharCount;
}
return list;
}
使用它,我建立了一个新的ReplaceCommand
s 列表。AReplaceCommand
是一个简单的类并存储 aFoundResult
和新值。
接下来,您必须从最后一项到第一项排序此列表(按完整字符串中的位置排序)
现在您可以编写一个全部替换算法,因为您知道需要对哪个文本对象执行什么操作。我们做了 (2) 以便替换操作不会使其他FoundResult
s 的索引无效。
3.1.) 找到需要更改的文本对象 3.2.) 在它们上调用 getValue() 3.3.) 将字符串编辑为新值 3.4.) 在文本对象上调用 setValue()
这是执行所有魔术的代码。它执行单个 ReplaceCommand。
/**
* @param texts All Text-objects
* @param replaceCommand Command
* @param map Lookup-Map from index in complete string to TextMetaItem
*/
public static void executeReplaceCommand(List<Text> texts, ReplaceCommand replaceCommand, Map<Integer, TextMetaItem> map) {
TextMetaItem tmi1 = map.get(replaceCommand.getFoundResult().getStart());
TextMetaItem tmi2 = map.get(replaceCommand.getFoundResult().getEnd());
if (tmi2.getPosition() - tmi1.getPosition() > 0) {
// it can happen that text objects are in-between
// we can remove them (set to null)
int upperBorder = tmi2.getPosition();
int lowerBorder = tmi1.getPosition() + 1;
for (int i = lowerBorder; i < upperBorder; i++) {
texts.get(i).setValue(null);
}
}
if (tmi1.getPosition() == tmi2.getPosition()) {
// do replacement inside a single Text-object
String t1 = tmi1.getText().getValue();
int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());
String keepBefore = t1.substring(0, beginIndex);
String keepAfter = t1.substring(endIndex + 1);
tmi1.getText().setValue(keepBefore + replaceCommand.getNewValue() + keepAfter);
} else {
// do replacement across two Text-objects
// check where to start and replace
// the Text-objects value inside both Text-objects
String t1 = tmi1.getText().getValue();
String t2 = tmi2.getText().getValue();
int beginIndex = tmi1.getPositionInsideTextObject(replaceCommand.getFoundResult().getStart());
int endIndex = tmi2.getPositionInsideTextObject(replaceCommand.getFoundResult().getEnd());
t1 = t1.substring(0, beginIndex);
t1 = t1.concat(replaceCommand.getNewValue());
t2 = t2.substring(endIndex + 1);
tmi1.getText().setValue(t1);
tmi2.getText().setValue(t2);
}
}