All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dstadler.poi.mailmerge.MailMerge Maven / Gradle / Ivy

Go to download

A small application which allows to repeatedly replace markers in a Microsoft Word document with items taken from a CSV/Microsoft Excel file.

The newest version!
package org.dstadler.poi.mailmerge;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
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 a 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 pages concatenated.
 */
public class MailMerge {
    private static final Logger log = LoggerFactory.make();

    /**
     * Main method to run Mail-Merge as application
     *
     * @param args Expects three arguments: template-file, excel/csv-file and output-word-file
     * @throws IOException If processing fails
     */
    public static void main(String[] args) throws IOException {
        LoggerFactory.initLogging();

        if(args.length != 3) {
            throw new IllegalArgumentException("Usage: MailMerge   ");
        }

        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, new File(outputFile));
    }

    /**
     * Invoke mail-merge with the given input and output files.
     *
     * @param wordTemplate The word-template to use
     * @param dataFile The Excel/CSV file which contains one row for each resulting page
     * @param outputFile The output word-document
     * @throws IOException If processing fails
     */
    public void merge(File wordTemplate, File dataFile, File outputFile) throws IOException {
        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 document template 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
                try {
                    applyLines(data, doc);
                } catch (XmlException e) {
                    throw new IOException("Merging failed for template " + doc + " and data-file " + data, e);
                }

                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
        List headers = dataIn.getHeaders();
        List replacedDocs = new ArrayList<>();
        for(List 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 = replaceDataItems(data, srcString, headers);
            replacedDocs.add(replaced);
        }

        appendBody(body, replacedDocs);
    }

    private static String replaceDataItems(List data, String srcString, List headers) {
        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 + "}", StringEscapeUtils.escapeXml11(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));
        }
        return replaced;
    }

    private static void appendBody(CTBody src, List appendDocs) throws XmlException {
        XmlOptions optionsOuter = new XmlOptions();
        optionsOuter.setSaveOuter();
        String srcString = src.xmlText();

        // exclude template itself in first appending
        String prefix = srcString.substring(0,srcString.indexOf(">")+1);
        String suffix = srcString.substring( srcString.lastIndexOf("<") );

        // rebuild the XML by adding prefix, new main part and suffix together
        StringBuilder document = new StringBuilder(prefix);
        Iterator it = appendDocs.iterator();
        while (it.hasNext()) {
            String append = it.next();

            document.append(append, append.indexOf(">") + 1, append.lastIndexOf("<"));

            // remove original item from the list to free memory early
            it.remove();
        }
        document.append(suffix);

        // produce resulting XML-structure
        XmlObject makeBody = CTDocument1.Factory.parse(document.toString());
        src.set(makeBody);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy