-
-
Notifications
You must be signed in to change notification settings - Fork 23
/
MailMerge.java
152 lines (124 loc) · 5.95 KB
/
MailMerge.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package org.dstadler.poi.mailmerge;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.logging.Logger;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.dstadler.commons.logging.jdk.LoggerFactory;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1;
/**
* Simple application which performs a "mail-merge" of a Microsoft Word template
* document which contains replacement templates in the form of ${name}, ${first-name}, ...
* and an Microsoft Excel spreadsheet which contains a list of entries that are merged in.
*
* Call this application with parameters <word-template> <excel/csv-template> <output-file>
*
* The resulting document has all resulting documents concatenated.
*/
public class MailMerge {
private static final Logger log = LoggerFactory.make();
public static void main(String[] args) throws Exception {
LoggerFactory.initLogging();
if(args.length != 3) {
throw new IllegalArgumentException("Usage: MailMerge <word-template> <excel/csv-template> <output-file>");
}
File wordTemplate = new File(args[0]);
File excelFile = new File(args[1]);
String outputFile = args[2];
if(!wordTemplate.exists() || !wordTemplate.isFile()) {
throw new IllegalArgumentException("Could not read Microsoft Word template " + wordTemplate);
}
if(!excelFile.exists() || !excelFile.isFile()) {
throw new IllegalArgumentException("Could not read data file " + excelFile);
}
new MailMerge().merge(wordTemplate, excelFile, outputFile);
}
private void merge(File wordTemplate, File dataFile, String outputFile) throws Exception {
log.info("Merging data from " + wordTemplate + " and " + dataFile + " into " + outputFile);
// read the data-rows from the CSV or XLS(X) file
Data data = new Data();
data.read(dataFile);
// now open the word file and apply the changes
try (InputStream is = new FileInputStream(wordTemplate)) {
try (XWPFDocument doc = new XWPFDocument(is)) {
// apply the lines and concatenate the results into the document
applyLines(data, doc);
log.info("Writing overall result to " + outputFile);
try (OutputStream out = new FileOutputStream(outputFile)) {
doc.write(out);
}
}
}
}
private void applyLines(Data dataIn, XWPFDocument doc) throws XmlException {
// small hack to not having to rework the commandline parsing just now
String includeIndicator = System.getProperty("org.dstadler.poi.mailmerge.includeindicator");
CTBody body = doc.getDocument().getBody();
// read the current full Body text
String srcString = body.xmlText();
// apply the replacements line-by-line
boolean first = true;
List<String> headers = dataIn.getHeaders();
for(List<String> data : dataIn.getData()) {
log.info("Applying to template: " + data);
// if the option is set ignore lines which do not have the indicator set
if(includeIndicator != null) {
int indicatorPos = headers.indexOf(includeIndicator);
Preconditions.checkState(indicatorPos >= 0,
"An include-indicator is set via system properties as %s, but there is no such column, had: %s",
includeIndicator, headers);
if(!StringUtils.equalsAnyIgnoreCase(data.get(indicatorPos), "1", "true")) {
log.info("Skipping line " + data + " because include-indicator was not set");
continue;
}
}
String replaced = srcString;
for(int fieldNr = 0;fieldNr < headers.size();fieldNr++) {
String header = headers.get(fieldNr);
String value = data.get(fieldNr);
// ignore columns without headers as we cannot match them
if(header == null) {
continue;
}
// use empty string for data-cells that have no value
if(value == null) {
value = "";
}
replaced = replaced.replace("${" + header + "}", value);
}
// check for missed replacements or formatting which interferes
if(replaced.contains("${")) {
log.warning("Still found template-marker after doing replacement: " +
StringUtils.abbreviate(StringUtils.substring(replaced, replaced.indexOf("${")), 200));
}
appendBody(body, replaced, first);
first = false;
}
}
private static void appendBody(CTBody src, String append, boolean first) throws XmlException {
XmlOptions optionsOuter = new XmlOptions();
optionsOuter.setSaveOuter();
String srcString = src.xmlText();
String prefix = srcString.substring(0,srcString.indexOf(">")+1);
final String mainPart;
// exclude template itself in first appending
if(first) {
mainPart = "";
} else {
mainPart = srcString.substring(srcString.indexOf(">")+1,srcString.lastIndexOf("<"));
}
String suffix = srcString.substring( srcString.lastIndexOf("<") );
String addPart = append.substring(append.indexOf(">") + 1, append.lastIndexOf("<"));
XmlObject makeBody = CTDocument1.Factory.parse(prefix+mainPart+addPart+suffix);
src.set(makeBody);
}
}