How to get the outer if field

Hi Support,

I got a question about getting the outer If Field with Aspose words with Java. Look at the following If field,
{IF {MERGEFIELD Account.Currency} <> USD 
       {IF {MERGEFIELD Account.Currency} = CAD 100 200}
       {IF {MERGEFIELD Account.Currency} = RMB 1000 2000}
  }

If I already get the FieldIf instance "fcad" of "{IF {MERGEFIELD Account.Currency} = CAD 100 200}",  how can I get the outer IF field by "fcad".

Thanks
Tony Shen

@stlcn,

One way to meet this requirement is as follows:

Document doc = new Document("D:\\temp\\t167923.docx");

FieldIf if2 = null;
for(Field field : doc.getRange().getFields()) {
    if (field.getType() == FieldType.FIELD_IF) {
        if (((FieldIf) field).getRightExpression().equals("CAD")) {
            if2 = (FieldIf) field;
        }
    }
}
        // Find the IF field containing the if2
FieldIf outerIf = null;
if (if2!= null){
    for(Field field : doc.getRange().getFields()) {
        if (field.getType() == FieldType.FIELD_IF) {
            if (((FieldIf) field).getFieldCode().contains(if2.getFieldCode())) {
                outerIf = (FieldIf) field;
                break;
            }
        }
    }
}

Sample input document: t167923.zip (9.0 KB)

@awais.hafeez

Thanks for the information.

I got anther question. For a IF field, I found its getTrueText() and get FalseText() returning the evaluated text. As the above example, it returned 200 and 2000. Do we have any way to get the raw text? I mean I would like the getTrueText() to return “IF {MERGEFIELD Account.Currency} = CAD 100 200”, and getFalseText() to return “IF {MERGEFIELD Account.Currency} = RMB 1000 2000”.

Thanks
Tony Shen

@stlcn,

We have logged this requirement in our issue tracking system. The ID of this issue is WORDSNET-16235. Our product team will further look into the details of this problem and we will keep you updated on the status of this issue. We apologize for any inconvenience.

@awais.hafeez
Thanks for the provide the feedback to the engineering team. Actually we have the same requirement to the Left and Right expressions of the If field. So could you please comment on the issue and address these requirements?

Thanks
Tony Shen

@stlcn,

We have passed this requirement as well to our product team. We will keep you posted.

@stlcn,

Regarding WORDSNET-16235, it is to update you that we have planned to include the fix of this issue in next release of Aspose.Words i.e. 18.7. We will inform you via this thread as soon as the next release containing the fix of this issue will be available during the start of next month.

@awais.hafeez Thanks for taking care of our tickets. Once you work on the fix, please go through the entire thread of this ticket and meet all the requirements. The following is a summary,

  1. An efficient way to get the outer If expression instance;
  2. For the truePart and falsePart we can get the raw text instead of evaluate result;
  3. For the condition part, we can get the raw text for left expression and right expression;

Any questions please let me know.

Thanks
Tony Shen

@stlcn,

We are checking these scenarios and will update you soon.

@stlcn,

Regarding WORDSNET-16235, I am afraid, we will not be able to include the fix in the 18.7 release as promised. The reason is that after further investigation we are not sure that we should add so many new properties or methods to the API just to obtain the unevaluated arguments. We are still discussing the best way of doing so.

In the meantime we provide you following workaround code allowing to obtain unevaluated IF field arguments. As we understood from your description, the problem is that the IF field contains nested fields but you want to retrieve nested fields’ code rather than fields’ result comprising the IF arguments (which is what the existing properties like TrueText, FalseText return).

Document doc = new Document("D:\\temp\\t167923.docx");

FieldIfWrapper wrapper = new FieldIfWrapper((FieldIf) doc.getRange().getFields().get(0));
wrapper.Parse();

if ("{ MERGEFIELD  Account.Currency }".equals(wrapper.LeftExpression()))
    System.out.println("true");
else
    System.out.println("false");

if ("USD".equals(wrapper.RightExpression()))
    System.out.println("true");
else
    System.out.println("false");

if ("{ IF { MERGEFIELD  Account.Currency } = CAD 100 200 }".equals(wrapper.TrueText()))
    System.out.println("true");
else
    System.out.println("false");

if ("{ IF { MERGEFIELD  Account.Currency } = RMB 1000 2000 }".equals(wrapper.FalseText()))
    System.out.println("true");
else
    System.out.println("false");

wrapper = new FieldIfWrapper((FieldIf) doc.getRange().getFields().get(2));
FieldIf outerIf = wrapper.GetOuterFieldIf();

if (doc.getRange().getFields().get(0).getStart() == outerIf.getStart())
    System.out.println("true");
else
    System.out.println("false");

doc.save("D:\\Temp\\awjava-18.6.docx");

/// <summary>
/// Parses FieldIf arguments to unevaluated strings. Skips nested fields result.
/// </summary>
public static class FieldIfWrapper {
    public FieldIfWrapper(FieldIf field) {
        mField = field;
    }

    /// <summary>
    /// Call this before obtaining arguments.
    /// </summary>
    public void Parse() {
        Node currentNode = mField.getStart();

        while (true) {
            currentNode = FindNextInlineNode(currentNode);
            if (currentNode == mField.getSeparator())
                break;

            ProcessNode(currentNode);
        }

        FlushArgument();
    }

    /// <summary>
    /// Finds and returns a parent IF field or null if not found.
    /// </summary>
    /// <returns></returns>
    public FieldIf GetOuterFieldIf() {
        Node node = FindPreviousInlineNode(mField.getStart());
        while (node != null && (node.getNodeType() != NodeType.FIELD_START || ((FieldStart) node).getFieldType() != FieldType.FIELD_IF))
            node = FindPreviousInlineNode(node);

        if (node == null)
            return null;

        return (FieldIf) ((FieldStart) node).getField();
    }

    private Node FindPreviousInlineNode(Node node) {
        if (node.getPreviousSibling() != null)
            return node.getPreviousSibling();

        CompositeNode parentParagraph = (CompositeNode) node.getParentNode().getPreviousSibling();

        while (parentParagraph.getCount() == 0)
            parentParagraph = (CompositeNode) parentParagraph.getPreviousSibling();

        return parentParagraph.getLastChild();
    }

    private Node FindNextInlineNode(Node node) {
        if (node.getNextSibling() != null)
            return node.getNextSibling();

        CompositeNode parentParagraph = (CompositeNode) node.getParentNode().getNextSibling();

        while (parentParagraph.getCount() == 0)
            parentParagraph = (CompositeNode) parentParagraph.getNextSibling();

        return parentParagraph.getFirstChild();
    }

    private void ProcessNode(Node node) {
        switch (node.getNodeType()) {
            case NodeType.RUN:
                ProcessRun((Run) node);
                break;
            case NodeType.FIELD_START:
                if (!IsInNestedField())
                    StartArgument();

                mFieldNestingLevel++;

                mArgumentBuilder.append("{");

                break;
            case NodeType.FIELD_SEPARATOR:
                mFieldResultNestingLevel++;
                break;
            case NodeType.FIELD_END:
                mFieldResultNestingLevel--;
                mArgumentBuilder.append("}");

                mFieldNestingLevel--;

                if (!IsInNestedField())
                    FlushArgument();

                break;
        }
    }

    private void ProcessRun(Run run) {
        String text = run.getText();
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);

            if (Character.isWhitespace(c)) {
                if (mIsInArgument && !IsInNestedField())
                    FlushArgument();

                if (!IsOutputLocked() && IsInNestedField())
                    mArgumentBuilder.append(c);
            } else {
                if (!mIsInArgument && !IsInNestedField())
                    StartArgument();

                if (!IsOutputLocked())
                    mArgumentBuilder.append(c);
            }
        }
    }

    private void StartArgument() {
        FlushArgument();
        mIsInArgument = true;
    }

    private void FlushArgument() {
        if (mArgumentBuilder.length() > 0) {
            mArguments.add(mArgumentBuilder.toString());
            mArgumentBuilder.setLength(0);

        }

        mIsInArgument = false;
    }

    private boolean IsInNestedField() {
        return (mFieldNestingLevel > 0) ? true : false;
    }

    private boolean IsOutputLocked() {
        return (mFieldResultNestingLevel > 0) ? true : false;
    }

    public String LeftExpression() {
        return mArguments.get(1);
    }

    public String RightExpression() {
        return mArguments.get(3);
    }

    public String TrueText() {
        return mArguments.get(4);
    }

    public String FalseText() {
        return mArguments.get(5);
    }

    private FieldIf mField;
    private boolean mIsInArgument;
    private int mFieldNestingLevel;
    private int mFieldResultNestingLevel;
    private StringBuilder mArgumentBuilder = new StringBuilder();
    private ArrayList<String> mArguments = new ArrayList<String>();
}

Please note this is not an ultimate solution and it may fail against complex documents, for example, containing section breaks somewhere within the IF field. But it covers most of cases including the test document sent by you. If you find that the code fails against some of your documents, please provide us those failing test documents and we will try to update the workaround.

Please check if above workaround satisfies your requirements, if yes then we will probably close the issue. Otherwise we will still have to provide a more robust solution in future releases.

We apologize for your inconvenience.

@awais.hafeez Thanks for your feedback and the workaround.

After we evaluated the workaround, we have to say no to this workaround.
This ticket as well as ticket How to make all the merge fields merged I submitted before are for the same purpose, to workaround your library behavior change with IF expression. With the lib 15.2 we are using, all the merge fields in an IF expression will be merged, but they will not with Aspose.words 17.4 and later.

We have logic to check if a template is fully merged after call document.getMailMerge().execute() like below,
document.getMailMerge().getFieldNames();
If there are unmerged fields return, we assume something wrong with the final document. But with current Aspose.words, there are some unmerged fields in IF expression is reasonable, which always break our logic.

To make your work around works, we also need to write a lot of code and test with our thousands of templates in production to avoid any regression issue to our customers. It doesn’t make sense to us to spend so much effort on the workaround which we already know having limitations.

I have an alternative request, if you can provide a fix like that, it will be perfect.

As I said in above, we call document.getMailMerge().getFieldNames() to get all the unmerged fields, if you can build a similar method for example document.getMailMerge().getUnmergedFieldNames(), with this method only the merge fields need to be merged but not are returned, namely the merge fields in IF expression which don’t conform to IF condition are not returned. In this way, our issue can be resolved perfectly. Can you investigate this way?

Any more questions please let me know.

Thanks
Tony Shen

@stlcn,

Thanks for the additional information. We are investigating your feedback and possible solutions. We will keep you posted on further updates.

@stlcn,

After further investigation, I am afraid, we are unable to fully understand your requirements and why you did not accept the workaround.

From the following paragraph:

We have concluded so far that the getUnmergedFieldNames() method you want to obtain returns only the merge fields that will not be merged in advance (“unmerged” in the method’s name is quite confusing). If we are correct, then this seems possible (by composing another workaround or introducing a method in the Aspose.Words API) with some limitations, and can be achieved in the following way:

  1. Collect all merge fields in the document
  2. Create a clone of the document
  3. Execute a mail merge over the clone using the same data source as that will be used for a “real” merge
  4. Collect merge fields again then return the names

As you can see, the main limitation is that you have to provide the same data source for this method as you will use for the subsequent mail merge. The reason behind is clear: we cannot know in advance which fields will be merged without having a data source. For example, an IF condition might contain merge fields and which fields will be merged in the true/false arguments will depend on the values provided. And even this does not guarantee the correct result: imagine some IF condition depends on the current timestamp - it can be true at the moment of running getUnmergedFieldNames() but false at the moment of merge and vice versa.

So, if this implementation suits you, then we are ready to provide a workaround or consider including it into the API, yet the workaround would be more preferable. Please let us if this suits your requirement?

@stlcn,

Regarding WORDSNET-16235, we have completed the work on your issue and come to a conclusion that we would not be able to implement the fix to this issue in Aspose.Words API. Your issue (WORDSNET-16235) has now been closed with ‘Won’t Fix’ resolution/status. Here are a few reasons:

This is a rare requirement and no other customer has asked about it.
We also did not clearly understand every aspect of your requirement
We are not sure if you are satisfied with the workaround suggested earlier
We would like to avoid adding rare/uncommon functionality in API that would not be used by others.

@awais.hafeez
Thanks for your reply and sorry for the late response.

The workaround doesn’t work for us. For example. the following IF expression is in a template,
IF (MergeFieldC MergeFieldA MergeFieldB)
Our procedure is like below,

  1. Execute a mail merge over the template;
  2. Call document.getMailMerge().getFieldNames() to collect unmerged fields;

In the above example, if the “MergeFieldC” evaluate to true, then document.getMailMerge().getFieldNames() returns MergeFieldB; if the “MergeFieldC” evaluate to false, then document.getMailMerge().getFieldNames() returns MergeFieldA. In our application, if there are any unmerged fields kept in the document, we look that as an error and block generating a PDF file for our customers.

With my definition of getUnmergedFieldNames(), we expect it won’t return any merge fields if the IF expression is merged well. Let’s take the above example again. If the “MergeFieldC” evaluate to true and MergeFieldA is merged to expected value, we expect “getUnmergedFieldNames()” return nothing, because MergeFieldB needn’t to be merged; If MergeFieldA is not merged we expect “getUnmergedFieldNames()” return MergeFieldA only, so we can know there is something wrong with our merging datasource.

If “MergeFieldC” is not merged, we also expect MergeFieldC is returned by “getUnmergedFieldNames”.

In one word, we need a way to know if a template is fully merged, which means the merge fields that should be merged are merged. We need to avoid sending our customer a PDF with unmerged fields.

Is this clear to you?

Thanks
Tony Shen

@stlcn,

Thanks for the additional information. We have logged another issue in our issue tracking system to provide solution for this requirement. The ID of this new issue is WORDSNET-17341 . We will further look into the details of this requirement and we will keep you updated on the status of this issue.

@awais.hafeez
Any progress on the issue WORDSNET-17341?

Thanks
Tony Shen

@stlcn,

Unfortunately, this issue is not resolved yet. We are currently doing analysis of this issue. We will keep you posted on further updates and let you know when this issue is resolved. We apologize for your inconvenience.

@stlcn,

Regarding WORDSNET-17341, after further discussion we have come up with a simple solution that might satisfy you. All you need is to wrap your data source into IMailMergeDataSource and use another IMailMergeDataSource to intercept fields that are not in the wrapped one, i.e. will leave a merge field unmerged. (actually, you can use your data source directly, but since we do not know what you use, we have used a wrapped IMailMergeDataSource in the code sample).

public class SafeDataSource : IMailMergeDataSource
{
    public SafeDataSource(IMailMergeDataSource realDataSource)
    {
        mRealDataSource = realDataSource;
    }

    public bool MoveNext()
    {
        return mRealDataSource.MoveNext();
    }

    public IMailMergeDataSource GetChildDataSource(string tableName)
    {
        return mRealDataSource.GetChildDataSource(tableName);
    }

    public bool GetValue(string fieldName, out object fieldValue)
    {
        if (!mRealDataSource.GetValue(fieldName, out fieldValue))
        {
            // This field won't be merged.
            IsMergeFailed = true;

            return false;
        }
        else
        {
            return true;
        }
    }

    private readonly IMailMergeDataSource mRealDataSource;

    public string TableName => mRealDataSource.TableName;
    public bool IsMergeFailed { get; private set; }
}

To recap, please just check for the presence of the field in the GetValue method - it can be done in different ways, for example, by working with a DataTable directly etc. Note that Aspose.Words will only call this method only for the “correct” IF part (either true or false, depending on the condition) so a missing field means a failure as you requested.

Hopefully, this will help you this time, but otherwise please let us know.