0

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.

Schmebi
  • 355
  • 2
  • 16

2 Answers2

0

Leave it to MailMerger to clone the contents. Try something like:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.xml.bind.JAXBException;

import org.docx4j.XmlUtils;
import org.docx4j.dml.CTNonVisualDrawingProps;
import org.docx4j.jaxb.XPathBinderAssociationIsPartialException;
import org.docx4j.model.fields.merge.DataFieldName;
import org.docx4j.model.fields.merge.MailMerger.OutputField;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;

public class FieldsMailMerge {

    public static void main(String[] args) throws Exception {
        
        
        WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(
                new java.io.File(
                        System.getProperty("user.dir") + "/_tmp_textbox.docx"));
        
        List<Map<DataFieldName, String>> data = prepareForMailMerge();
            
//          System.out.println(XmlUtils.marshaltoString(wordMLPackage.getMainDocumentPart().getJaxbElement(), true, true));
            
            WordprocessingMLPackage output = org.docx4j.model.fields.merge.MailMerger.getConsolidatedResultCrude(wordMLPackage, data, true);
            
//          System.out.println(XmlUtils.marshaltoString(output.getMainDocumentPart().getJaxbElement(), true, true));
            
            output.save(new java.io.File(
                    System.getProperty("user.dir") + "/OUT_FieldsMailMerge.docx") );
            
        
    }

    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 1");
        map.put(new DataFieldName("Field2"), "Plutext 1");
        data.add(map);

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

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

      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 long setRandomValue()
          {
            final Random random = new Random();
            final int min = 1000;
            final int max = 9999;
            return random.nextInt((max - min) + 1) + min;
          }
}
JasonPlutext
  • 15,352
  • 4
  • 44
  • 84
  • Remember to also call setRandomIdsForDocPr – JasonPlutext May 13 '21 at 10:23
  • Sadly this one doesn't work for me, but thank you for the example code. The only thing that's really different is that you not calling generatePagesFromTemplate and use getConsolidatedResultCrude() instead, right? The document doesn't get merged, it doesn't matter if the field is in or outside a textbox. – Schmebi May 17 '21 at 11:25
  • I tried to run your FieldMailMerge example, it also doesn't works for me with the same file (MERGEFIELD.docx). – Schmebi May 17 '21 at 12:42
  • It worked with a docx wrapped around the document.xml you had posted on GitHub. – JasonPlutext May 17 '21 at 21:59
  • That's strange, but thank your for you help :) – Schmebi May 18 '21 at 06:18
  • This is working for me, although for each map in the list => it creates a new page which is probably intended. – Onkar Singh May 25 '23 at 19:05
0

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.

Schmebi
  • 355
  • 2
  • 16
  • This is fine, but just to be clear, it is not a "mail merge" in the Microsoft Word sense of using MERGEFIELD. – JasonPlutext May 17 '21 at 22:01
  • Yeah I know that this is not the classic mail merge, but it's enoth for my case. I don't know why your version doesn't work for me. Thank you so much for your help, it really helped me a lot, it wasn't clear for me at the beginning how I should work with this libary. This project is now finished for me, a other person will continue working on it. :) – Schmebi May 18 '21 at 06:18