Skip to content

Why I can’t do a multipage mail merge?

What I am currently trying is doing a multi page mail merge, the problem is that currently i just overwrite the content with my second map.

This are the important code snippets:

public static void main(final String[] args) throws Docx4JException, JAXBException
  {
    final WordprocessingMLPackage word = Docx4J.load(new File(filePath));
    final MainDocumentPart document = word.getMainDocumentPart();

    generatePagesFromTemplate(document);

    final List<Map<DataFieldName, String>> data = prepareForMailMerge();
    for (int i = 0; i < data.size(); i++)
    {
      MailMerger.performMerge(word, data.get(i), false);
    }
    // This is a workaround, otherwise the document would have an error
    setRandomIdsForDocPr(document);

    word.save(new File(outputPath));
    createOutputXml(document); // just writes document.getXML() in a file
    openFile(outputPath);
  }

  private static void generatePagesFromTemplate(final MainDocumentPart document, final int nrOfSheets)
  {
    final List<Object> pageContent = document.getContent();

    // This is needed if you don't want a endless loop
    final int nrOfElements = pageContent.size();

    // Make a copy of the first sheet, to the nr of pages that exist
    for (int sheetNr = 1; sheetNr < nrOfSheets; sheetNr++)
    {
      addPageBreak(document);

      for (int i = 0; i < nrOfElements; i++)
      {
        final Object tmp = pageContent.get(i);

        document.addObject(tmp);
        System.out.println("Added object: " + tmp.toString());
      }
    }
  }

  private static void setRandomIdsForDocPr(final MainDocumentPart document)
      throws JAXBException, XPathBinderAssociationIsPartialException
  {
    final String xpath = "//wp:docPr";
    final List<Object> docPr = document.getJAXBNodesViaXPath(xpath, false);

    for (int i = 0; i < docPr.size(); i++)
    {
      final CTNonVisualDrawingProps props = (CTNonVisualDrawingProps) docPr.get(i);
      props.setId(setRandomValue());
    }
  }

private static List<Map<DataFieldName, String>> prepareForMailMerge()
  {
    final List<Map<DataFieldName, String>> data = new ArrayList<Map<DataFieldName, String>>();

    // Instance 1
    Map<DataFieldName, String> map = new HashMap<DataFieldName, String>();
    map.put(new DataFieldName("Field1"), "Daffy duck");
    map.put(new DataFieldName("Field2"), "Plutext");
    data.add(map);

    // Instance 2
    map = new HashMap<DataFieldName, String>();
    map.put(new DataFieldName("Field1"), "duck Daffy");
    map.put(new DataFieldName("Field2"), "ThisPlutext");
    data.add(map);

    // Choose how to treat the MERGEFIELD in the output
    MailMerger.setMERGEFIELDInOutput(OutputField.KEEP_MERGEFIELD);
    return data;
  }


Here you can find my document as XML (docPr has different ids in reality, just used this file for a other question)

So I think there are 2 ways to approach this:

  1. Change the names of the merge fields that every merge field is unique
  2. Only merge on one page so I don’t have to rename every field

I think there must be a way to do handle this for every page or am I wrong?

I also tried out Variable Replace but this doesn’t work for textfields, I now try to get into content controls, maybe this will solve my problem.

Answer

Okay I just implemented my own simple Mail Merge for text objects. I hope it’s maybe usefull for someone in the future 🙂

  1. Simple Version:
 private static void textboxMailMerge(final MainDocumentPart document)
      throws XPathBinderAssociationIsPartialException, JAXBException
  {
    final String xpath = "//w:t";
    final List<Object> text = document.getJAXBNodesViaXPath(xpath, false);

    for (int i = 0; i < text.size(); i++)
    {
      @SuppressWarnings("unchecked")
      final JAXBElement<Text> tmp = (JAXBElement<Text>) text.get(i);
      final Text txt = new Text();
      txt.setValue("Test" + i);
      tmp.setValue(txt);
    }
  }
  1. More complex version, I think it would be possible to simplify it more, but for me it works 🙂
private static void textboxMailMerge(final MainDocumentPart document, final int nrOfSheets)
  {
    final String xpath = "//wps:txbx/w:txbxContent/w:p/w:r/w:t";

    try
    {
      final List<Object> textList = document.getJAXBNodesViaXPath(xpath, false);

      final int dataSize = data.size();
      final int listSize = textList.size();

      // check if the size is valid
      if (dataSize == listSize / nrOfSheets)
      {
        // iterate over every text element
        for (int i = 0; i < listSize; i++)
        {
          // iterate over every map
          for (final Map<DataFieldName, String> map : data)
          {
            // iterate over every value from the map
            for (final String value : map.values())
            {
              @SuppressWarnings("unchecked")
              final JAXBElement<Text> element = (JAXBElement<Text>) textList.get(i);
              final Text txt = new Text();

              txt.setValue(value);
              element.setValue(txt);
            }
          }
        }
      }
      else
      {
        System.err.println("The size of the text elements isn't equal with the size of the data map!");
      }
    }
    catch (XPathBinderAssociationIsPartialException | JAXBException e)
    {
      e.printStackTrace();
    }
  }

This is what the data variable looks like: List<Map<DataFieldName, String>> data So the complex version is just iterating over all data and replacing the text object of the mergefields with the given text, the name of the merge field will stay the same.

I will test the complex version tomorrow, but i think it should work 🙂


The only thing that annoys me a bit is that unchecked cast warning, maybe someone knows how to fix this. I looked at some questions, espacilly this one is quite popular but I don’t unterstand what I’m doing wrong here.