Problem with IFieldMergingCallback- Mustache tags- and inserting RTF snippet

I am trying to insert an RTF document in place of a mustache tag using the IFieldMergingCallback.

No matter what I do, I cannot get it to replace the {{x}} tag in the document. The result is always is placed after the paragraph.

I have a RTF document which looks like this (omitting the full RTF):

{\rtf1\ansi … {{#foreach row}}The house is {{x}}.{{/foreach row} }

I am inserting another RTF string (which i convert to a document) like this:

{\rtf1\ansi … ‘RED’ … }

Using the callback (either text or RTF format) I get:

The house is .
RED <— inserted document on next line

Without the callback (using Text format i get the correct value / Rtf Format puts the raw RTF string in place) I get:

The house is RED.

I am using almost exactly the example(s) from this thread Insert RTF text into a table using ExecuteWithRegions - including the insertDocument and MergeFieldCallbacks code.

Here is the test case I am running against (both snippets are converted to documents):

@Test
// @Ignore
public void should_merge_rtf_fields_into_rtf() throws Exception
{
    final String source = ""
            + "{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf200"
            + "{\fonttbl\f0\fswiss\fcharset0 Helvetica;}"
            + "{\colortbl;\red255\green255\blue255;\red0\green31\blue103;\red102\green0\blue141;}"
            + "\margl1440\margr1440\vieww10800\viewh8400\viewkind0"
            + "\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural"
            + "\f0\fs24 \cf0 \{\{#foreach row\}\}The "
            + "\i\fs28 \cf2 \ul \ulc2 house"
            + "\i0\fs24 \cf0 \ulnone is "
            + "\b \cf3 \{\{\ul x\ulnone \}\}"
            + "\b0 \cf0 .\{\{/foreach row\}\}}";
    final String snippet = ""
            + "{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf200"
            + "{\fonttbl\f0\fswiss\fcharset0 Helvetica;}"
            + "{\colortbl;\red255\green255\blue255;\red110\green5\blue0;}"
            + "\margl1440\margr1440\vieww10800\viewh8400\viewkind0"
            + "\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural"
            + "\f0\b\fs24 \cf0 Big and \cf2 \ul \ulc2 red}";
    // Merge field (RTF snippet)
    final Map<String, Object> customData = Maps.newHashMap();
    customData.put("x", snippet);
    final Dataset data = new Dataset(customData);

    // Perform merge
    final String actual = this.processor.merge(source, data, TextFormat.TXT);
    assertThat(actual, equalTo(""
            + "The house is Big and Red."));
}

Here is my insertDocument() method:

static void insertDocument(Node target, final Document source) throws Exception
{
    // Make sure the target is either a paragraph or table.
    final int nodeType = target.getNodeType();
    if (nodeType != NodeType.PARAGRAPH & nodeType != NodeType.TABLE)
    {
        throw new IllegalArgumentException("The destination should be a paragraph or table.");
    }
    // We will be inserting into the parent of the destination paragraph.
    final CompositeNode<?> parent = target.getParentNode();
    // This object will be translating styles and lists during the import.
    final NodeImporter importer = new NodeImporter(
            source,
            target.getDocument(),
            ImportFormatMode.USE_DESTINATION_STYLES);

    for (final Section section : source.getSections())
    {
        // Loop through all block level nodes (paragraphs and tables) in the
        // body of the section.
        for (final Node sourceNode : (Iterable)section.getBody())
        {
            // Skip the node if is last empty paragraph in a section.
            if (sourceNode.getNodeType() == NodeType.PARAGRAPH)
            {
                final Paragraph para = (Paragraph)sourceNode;
                if (para.isEndOfSection() && !para.hasChildNodes())
                {
                    continue;
                }
            }

            // Clone of the node, suitable for insertion into
            // the destination document.
            final Node newNode = importer.importNode(sourceNode, true);
            // Insert the new node after the current target
            parent.insertAfter(newNode, target);
            target = newNode;
        }
    }
}

Here is my callback:

private final class MergedFieldCallback implements IFieldMergingCallback
{
    @Override
    public void fieldMerging(final FieldMergingArgs args) throws Exception
    {
        final String fieldName = args.getDocumentFieldName();
        final Object fieldValue = args.getFieldValue();
        final Document document = args.getDocument();
        final DocumentBuilder builder = new DocumentBuilder(document);
        // Move to merge field and insert into paragraph
        if (builder.moveToMergeField(fieldName))
        {
            final Document snippet = createDocument(fieldValue.toString());
            final Paragraph paragraph = builder.getCurrentParagraph();
            insertDocument(paragraph, snippet);

            // The paragraph that contained the field might be empty
            // now and you probably want to delete it.
            if (!paragraph.hasChildNodes())
            {
                paragraph.remove();
            }

            // Set text of mergefield to nothing.
            args.setText(null);
        }
    }

    @Override
    public void imageFieldMerging(final ImageFieldMergingArgs args) throws Exception
    {
        throw new UnsupportedOperationException();
    }
}

Hi there,

Thanks for your inquiry. The insertDocument method inserts the document after current Paragraph (in your case, after the paragraph which contain the mail merge field).

In this case, I suggest you please join the paragraphs - mail merge paragraph and first paragraph of inserted document. Please check the highlighted code snippet below. Hope this helps you. Please let us know if you have any more queries.

Document doc = new Document(MyDir + "in.docx");
Document rtfDoc = new Document(MyDir + "in.rtf");
// insert document after first paragraph node
Paragraph para = (Paragraph)doc.getChild(NodeType.PARAGRAPH, 0, true);
insertDocument(para, rtfDoc);
if (para.getNextSibling() != null)
{
    Paragraph nextPara = (Paragraph)para.getNextSibling();
    // Move all content from the nextPara paragraph into the first.
    while (nextPara.hasChildNodes())
        para.appendChild(nextPara.getFirstChild());
    nextPara.remove();
}
doc.save(MyDir + "Out.docx");

That gets me closer - at least they are on the same line but it still seems to be after the paragraph.

Now, I get “The house is. Red” vs. what I need “The house is Red.”

I took the sample you provided and tied that into my callback.

Hi there,

Thanks for your inquiry. It would be great if you please share following detail for investigation purposes.

  • Please attach your input Word document.
  • Please attach the output Word file that shows the undesired behavior.
  • Please attach your target Word document showing the desired behavior. You can use Microsoft Word to create your target Word document. I will investigate as to how you are expecting your final document be generated like.

As soon as you get these pieces of information to us we’ll start our investigation into your issue.

I believe this is what we want. The goal is to put Input.doc into Original.doc in place of the {{x}}.

Everything we are doing is literally exactly as originally stated above.

The target document is an RTF, initially based off a String:

Template to merge/insert the snippet into:

final String source = ""
    + "{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf200"
    + "{\fonttbl\f0\fswiss\fcharset0 Helvetica;}"
    + "{\colortbl;\red255\green255\blue255;\red0\green31\blue103;\red102\green0\blue141;}"
    + "\margl1440\margr1440\vieww10800\viewh8400\viewkind0"
    + "\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural"
    + "\f0\fs24 \cf0 \{\{#foreach row\}\}The "
    + "\i\fs28 \cf2 \ul \ulc2 house"
    + "\i0\fs24 \cf0 \ulnone is "
    + "\b \cf3 {\{\ul x\ulnone \}\}"
    + "\b0 \cf0 .\{\{/foreach row\}\}}";

RTF snippet to replace the merge field ({{x}}:

final String snippet = ""
    +"{\rtf1\ansi\ansicpg1252\cocoartf1265\cocoasubrtf200"
    +"{\fonttbl\f0\fswiss\fcharset0 Helvetica;}"
    +"{\colortbl;\red255\green255\blue255;\red110\green5\blue0;}"
    +"\margl1440\margr1440\vieww10800\viewh8400\viewkind0"
    +"\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural"
    +"\f0\b\fs24 \cf0 Big and \cf2 \ul \ulc2 red}";

Set the “x” merge field (mustache tag):

// Merge field (RTF snippet)
final Map<String, Object> customData = Maps.newHashMap();
customData.put("{{x}}", snippet);
final Dataset data = new Dataset(customData);

We convert both strings into Documents and then use the IMergeFieldCallback from the various examples also outlined above.

The inputs are (in RTF but excluding the tags for brevity):

Source (not the tag is before the period): {{#foreach row}}The house is {{x}}.{{/foreach row}}
Snippet: RED

The output should be (in RTF):

The house is RED. (replace the {{x}} tag).

NOT:

The house is . RED (outside of the {{x}} tag and after the paragraph)

We have a solution that works now, as long as you don’t repeat the same mustache tag in a document. Here is the new callback code:

@Override
public void fieldMerging(final FieldMergingArgs args) throws Exception
{
    final String fieldName = args.getDocumentFieldName();
    final Object fieldValue = args.getFieldValue();
    final Document document = args.getDocument();
    final DocumentBuilder builder = new DocumentBuilder(document);

    if (fieldValue != null)
    {
        // Handle RTF callbacks
        if (fieldValue.toString().contains(MAGIC_NUMBER))
        {
            // Move to merge field and insert into paragraph
            // (but don’t delete the field yet)
            if (builder.moveToMergeField(fieldName, true, false))
            {
                final Node currentNode = builder.getCurrentNode();
                final Paragraph paragraph = builder.getCurrentParagraph();
                final Document snippet = createDocument(fieldValue.toString());

                LOG.debug("Merge callback for {}:\n\t"
                                + "Para : ‘{}’\n\t"
                                + "Node : ‘{}’\n\t"
                                + "Value: ‘{}’",
                        new Object[] {
                                fieldName,
                                paragraph.getText(),
                                currentNode.getText(),
                                snippet.getText() });

                insertDocument(paragraph, snippet);
                if (paragraph.getNextSibling() != null)
                {
                    Paragraph nextPara = (Paragraph)paragraph.getNextSibling();
                    // move back to the merge field (and remove the placeholder this time)
                    builder.moveToDocumentStart(); //see edit below
                    if(builder.moveToMergeField(fieldName))
                    {
                        // Move all content from the nextPara paragraph into the first.
                        while (nextPara.hasChildNodes()){
                            paragraph.insertBefore(nextPara.getFirstChild(),builder.getCurrentNode());
                        }
                        nextPara.remove();
                    }
                }

                // The paragraph that contained the field might be empty
                // now and you probably want to delete it.
                if (!paragraph.hasChildNodes())
                {
                    paragraph.remove();
                }

                // Set text of mergefield to nothing.
                args.setText(null);
            }
        }
    }
}

However, when we try putting the same tag into a document more than once, we get a TextProcessingException: “The reference node is not a child of this node.” For example,

The house is {{x}}.
The door is {{y}}.

works, but

The house is {{x}}.
The door is {{x}}.

throws an exception.

EDIT:** I found out that moveToMergeField() moves the cursor based on its current position, so it was skipping over tags that should be hit first. To remedy this, I move the cursor back to the beginning of the document for each field, so it always hits the first tag matching the field. (See my edit above.)

Hi there,

Thanks for your inquiry. In case you are using an older version of Aspose.Words, I would suggest you please upgrade to the latest version (v14.5.0) from here.

Please use the following code example to achieve your requirement. Following code example does the followings:

  1. Move the cursor to the mail merge field and insert bookmark BM0
  2. Insert paragraph break and bookmark BM1
  3. Move the cursor to the BM0 (at the position of mail merge field) and insert document
  4. Join the paragraph (in which mail merge filed exists) and the first paragraph of inserted document
  5. After insert the document, join the last paragraph of inserted document and paragraph (see point 2). In your case dot (.) will be in next paragraph.

I have attached the input and output documents with this post for your kind reference.

Hope this helps you. Please let us know if you have any more queries.

Document doc = new Document(MyDir + "Original.doc");
String[] input_names = new String[]{"x"};
Object[] input_data = new Object[]{"x"};
doc.getMailMerge().setUseNonMergeFields(true);
doc.getMailMerge().setFieldMergingCallback(new MailMerge_InsertDocument());
doc.getMailMerge().execute(input_names, input_data);
doc.save(MyDir + "Out.docx");
public class MailMerge_InsertDocument implements IFieldMergingCallback {
    private DocumentBuilder mBuilder;
    public void fieldMerging(FieldMergingArgs e) throws Exception {
        if (mBuilder == null) {
            mBuilder = new DocumentBuilder(e.getDocument());
        }
        System.out.println(e.getFieldName());
        if (e.getFieldName().equals("x")) {
            mBuilder.moveToMergeField(e.getFieldName(), false, false);
            mBuilder.startBookmark("BM0");
            mBuilder.endBookmark("BM0");
            mBuilder.insertBreak(BreakType.PARAGRAPH_BREAK);
            mBuilder.startBookmark("BM1");
            mBuilder.endBookmark("BM1");
            mBuilder.moveToBookmark("BM0");
            Paragraph para = mBuilder.getCurrentParagraph();
            Document rtfDoc = new Document(MyDir + "Input.doc");
            insertDocument(para, rtfDoc);
            // Join first paragraph of document and mail merge paragraph
            if (para.getNextSibling() != null)
            {
                Paragraph nextPara = (Paragraph)para.getNextSibling();
                // Move all content from the nextPara paragraph into the first.
                while (nextPara.hasChildNodes())
                    para.appendChild(nextPara.getFirstChild());
                nextPara.remove();
            }
            // Join last paragraph of inserted document and split paragraph
            mBuilder.moveToBookmark("BM1");
            para = (Paragraph) mBuilder.getCurrentParagraph().getPreviousSibling();
            if (para.getNextSibling() != null)
            {
                Paragraph nextPara = (Paragraph)para.getNextSibling();
                // Move all content from the nextPara paragraph into the first.
                while (nextPara.hasChildNodes())
                    para.appendChild(nextPara.getFirstChild());
                nextPara.remove();
            }
            e.getField().remove();
            mBuilder.getDocument().getRange().getBookmarks().get("BM0").remove();
            mBuilder.getDocument().getRange().getBookmarks().get("BM1").remove();
        }
    }
    public void imageFieldMerging(ImageFieldMergingArgs args) throws Exception {
    }
}