java.util.EmptyStackException when calling range.replace()

After upgrading to Aspose.Words 22.4 (before I was using 20.7) I get the following exception reported from some users of my program:

java.util.EmptyStackException
at java.util.Stack.peek(Stack.java:102)
at com.aspose.words.zzYnM.zzY2G(Unknown Source)
at com.aspose.words.zzWDc.zzXYe(Unknown Source)
at com.aspose.words.zzWDc.zzXYe(Unknown Source)
at com.aspose.words.zzWDc.zzXYe(Unknown Source)
at com.aspose.words.zzWDc.zzXYe(Unknown Source)
at com.aspose.words.zzWDc.update(Unknown Source)
at com.aspose.words.zzYnM.(Unknown Source)
at com.aspose.words.zzZE7.zz38(Unknown Source)
at com.aspose.words.zzZE7.zzWjK(Unknown Source)
at com.aspose.words.Range.zzXWJ(Unknown Source)
at com.aspose.words.Range.replace(Unknown Source)
at de.isd_service.koko.tools.office.word.KokoWordDocAsposeImpl.replaceAnyFieldsInRange(KokoWordDocAsposeImpl.java:439)

The code in question is this:

FindReplaceOptions options = new FindReplaceOptions();
		options.setReplacingCallback(replacingCallback);

		Pattern p = Pattern.compile("\\{([\\w$]+)\\}");
		try {
			if (range.replace(p, "", options) > 0) {
				foundAField = true;
			}
		} catch (Exception e) {
			ComTools.logComExceptionInfo(KokoLevel.WARNING, "Fehler bei replaceAnyFieldsInBody():", e);
		}

Any ideas what to do about it?

Thank you!
Dirk

@DirkSteinkamp Could you please attach the input document that will allow us to reproduce the problem on our side? Also, please provide the implementation of IReplacingCallback you use in your code. We will check the issue and provide you more information.

Please find an example of one of the documents that cause trouble attached and the requested class. I don’t know if it will work out of context, but otherwise you’d need the full program which I can’t post here due to proprietary reasons.

I hope you’ll be able to dig into it and find something useful :-).

TNSUB.docx (20.6 KB)

class ValueSourceReplaceEvaluator implements IReplacingCallback {

	private ValueSource2 valueSource;

	private boolean deleteRequested;

	private List<Node> deleteColNodes = new ArrayList<>();
	private List<Node> deleteRowNodes = new ArrayList<>();
	private List<Node> deleteTableNodes = new ArrayList<>();
	private List<Paragraph> deleteParaNodes = new ArrayList<>();

	/**
	 * Erzeugt ein neues ValueSourceReplaceEvaluator-Objekt.
	 *
	 * @param valueSource
	 */
	public ValueSourceReplaceEvaluator(ValueSource2 valueSource) {
		this.valueSource = valueSource;
	}

	@Override
	public int replacing(ReplacingArgs e) throws Exception {
		String fieldName = e.getMatch().group(1).trim();

		Object valueAsObj = valueSource.getValue4Field(fieldName);

		String value;
		if (valueAsObj == ValueSource.UNKNOWN) {
			value = fieldName;
		} else {
			value = Persister.object2DisplayString(valueAsObj);
		}
		if (value == null) {
			value = "";
		}

		if (StringTools.containsIgnoreCase(value, "<delete>")) {
			deleteRequested = true;
		}

		if ("<deleteRow>".equalsIgnoreCase(value)) {
			deleteRowNodes.add(e.getMatchNode().getParentNode());
		} else if ("<deleteCol>".equalsIgnoreCase(value)) {
			deleteColNodes.add(e.getMatchNode().getParentNode());
		} else if ("<deletePara>".equalsIgnoreCase(value)) {
			Paragraph para = (Paragraph) e.getMatchNode().getParentNode();
			deleteParaNodes.add(para);
		} else if ("<deleteTable>".equalsIgnoreCase(value)) {
			deleteTableNodes.add(e.getMatchNode().getParentNode());
		} else {
			e.setReplacement(value);
		}
		return ReplaceAction.REPLACE;
	}
}

@DirkSteinkamp Thank you for additional information. Unfortunately, I did nit manage to reproduce the problem on my side. Could you please also provide the replacement values for the placeholders in the document? Maybe using the real values will allow to reproduce the problem.

I understand. Don’t know if I can easily provide a minimal example to demonstrate the issue, though – it would probably take me some hours to pin it down exactly into a reproducible minimal example.

Anyhow: I followed the stack trace, and jad shows the following code in zzYnM (whatever class your obfuscator created this from :wink: …):

		case 24 : // '\030'
			if (((Node) zzXKG.peek()).getNodeType() != 22)
				((Node) zzXKG.peek()).getNodeType();
			Node node1;
			if ((node1 = (Node) zzXKG.pop()).getNodeType() == 23)
				zzXKG.pop();
			return 1;

I’d say this line looks suspicious:

			if ((node1 = (Node) zzXKG.pop()).getNodeType() == 23)

and might better read:

			if ((node1 = (Node) zzXKG.peek()).getNodeType() == 23)

Might you consider having this checked, before I put in some hours of work?

Thank you!

regarding replacement values: hard to do – they are evaluated “on demand” if some field in {curlyBraces} is found. Some come from a database, others are generated by simple JavaScript-expressions by the callback … so unfortunately I can’t easily provide replacement values.
But one thing I can tell: the log file I have about the issue doesn’t mention any evaluated replacement values at this point (they should be in the log), so the issue seems to occur before the IReplacingCallback is even called.
In the example document there are some word fields that are replaced beforehand, which seem to work fine. The only thing left to replace is the “{Fix}” value – and that seems to result in trouble while processing.

plus one more thing: the “word fields” are replaced with values that contain newlines – maybe that’s related.

I managed to narrow it down somewhat: this is the file after replacing the word-fields, but before processing the “{curlyField}” – this should be the state right before the replace()-call.

Tätigkeitsnachweis.docx (89.1 KB)

@DirkSteinkamp It looks like I managed to reproduce the problem. Here is simple code that produces the exception:

Document doc = new Document("C:\\Temp\\in.docx");

// This code causes the problem.
// It improperly removes the field leaving field end not removed.
Iterable<FieldStart> starts = doc.getChildNodes(NodeType.FIELD_START, true);
for (FieldStart s : starts)
{
    Node current = s;
    while (current.getNodeType() != NodeType.FIELD_END)
    {
        Node next = current.getNextSibling();
        current.remove();
        current = next;
    }
}

Pattern p = Pattern.compile("\\{([\\w$]+)\\}");
try
{
    if (doc.getRange().replace(p, "test") > 0)
    {
        System.out.println("replaced");
    }
}
catch (Exception e)
{
    e.printStackTrace();
}

doc.save("C:\\Temp\\out.docx");

As you can see the code that causes the problem improperly removes the fields from the document. Could you please share your code used for replacing fields with values? Looks like it causes the problem.

Wow, amazing find! Thank you.

Looks like I used some example code, which is to be found e.g. here:
https://www.javatips.net/api/Aspose_Words_Java-master/Aspose.Words-for-Java-master/Examples/src/main/java/com/aspose/words/examples/programming_documents/fields/ConvertFieldsInParagraph.java

My code actually calls convertFieldsToStaticText() – see below:

private static class FieldsHelper extends DocumentVisitor {

    private int mFieldDepth = 0;
    private ArrayList<Node> mNodesToSkip = new ArrayList<>();
    private int mTargetFieldType;

    /**
     * Converts any fields of the specified type found in the descendants of the node into static text.
     *
     * @param compositeNode
     *            The node in which all descendants of the specified FieldType will be converted to static text.
     * @param targetFieldType
     *            The FieldType of the field to convert to static text.
     *
     * @throws Exception
     *             when an error occurs
     */
    public static void convertFieldsToStaticText(CompositeNode<Node> compositeNode, int targetFieldType) throws Exception {
        String originalNodeText = compositeNode.toString(SaveFormat.TEXT); // ExSkip
        FieldsHelper helper = new FieldsHelper(targetFieldType);
        compositeNode.accept(helper);

        assert (originalNodeText.equals(
                compositeNode.toString(SaveFormat.TEXT))) : "Error: Text of the node converted differs from the original"; // ExSkip
        for (Node node : (Iterable<Node>) compositeNode.getChildNodes(NodeType.ANY, true)) {
            assert (!((node instanceof FieldChar) && (((FieldChar) node)
                    .getFieldType() == targetFieldType))) : "Error: A field node that should be removed still remains."; // ExSkip
        }
    }

    private FieldsHelper(int targetFieldType) {
        mTargetFieldType = targetFieldType;
    }

    @Override
    public int visitFieldStart(FieldStart fieldStart) {
        // We must keep track of the starts and ends of fields incase of any nested
        // fields.
        if (fieldStart.getFieldType() == mTargetFieldType) {
            mFieldDepth++;
            fieldStart.remove();
        } else {
            // This removes the field start if it's inside a field that is being converted.
            CheckDepthAndRemoveNode(fieldStart);
        }

        return VisitorAction.CONTINUE;
    }

    @Override
    public int visitFieldSeparator(FieldSeparator fieldSeparator) {
        // When visiting a field separator we should decrease the depth level.
        if (fieldSeparator.getFieldType() == mTargetFieldType) {
            mFieldDepth--;
            fieldSeparator.remove();
        } else {
            // This removes the field separator if it's inside a field that is being
            // converted.
            CheckDepthAndRemoveNode(fieldSeparator);
        }

        return VisitorAction.CONTINUE;
    }

    @Override
    public int visitFieldEnd(FieldEnd fieldEnd) {
        if (fieldEnd.getFieldType() == mTargetFieldType) {
            fieldEnd.remove();
        } else {
            CheckDepthAndRemoveNode(fieldEnd); // This removes the field end if it's inside a field that is
            // being
            // converted.
        }

        return VisitorAction.CONTINUE;
    }

    @Override
    public int visitRun(Run run) {
        // Remove the run if it is between the FieldStart and FieldSeparator of the
        // field being converted.
        CheckDepthAndRemoveNode(run);

        return VisitorAction.CONTINUE;
    }

    @Override
    public int visitParagraphEnd(Paragraph paragraph) {
        if (mFieldDepth > 0) {
            // The field code that is being converted continues onto another paragraph. We
            // need to copy the remaining content from this paragraph onto the next
            // paragraph.
            Node nextParagraph = paragraph.getNextSibling();

            // Skip ahead to the next available paragraph.
            while ((nextParagraph != null) && (nextParagraph.getNodeType() != NodeType.PARAGRAPH)) {
                nextParagraph = nextParagraph.getNextSibling();
            }

            // Copy all of the nodes over. Keep a list of these nodes so we know not to
            // remove them.
            while (paragraph.hasChildNodes()) {
                mNodesToSkip.add(paragraph.getLastChild());
                ((Paragraph) nextParagraph).prependChild(paragraph.getLastChild());
            }

            paragraph.remove();
        }

        return VisitorAction.CONTINUE;
    }

    @Override
    public int visitTableStart(Table table) {
        CheckDepthAndRemoveNode(table);

        return VisitorAction.CONTINUE;
    }

    /**
     * Checks whether the node is inside a field or should be skipped and then removes it if necessary.
     */
    private void CheckDepthAndRemoveNode(Node node) {
        if ((mFieldDepth > 0) && !mNodesToSkip.contains(node)) {
            node.remove();
        }
    }
}

@DirkSteinkamp Thank you for additional information. If the goal of the code is simply convert field to static text, then there is much simpler way. You can use Field.unlink() method to achieve this. For example in your document you have FORMTEXT, to unlink only this type of fields, you can use this:

Document doc = new Document("C:\\Temp\\in.docx");

for (Field f : doc.getRange().getFields())
{
    if (f.getType() == FieldType.FIELD_FORM_TEXT_INPUT)
        f.unlink();
}
1 Like

Thank you very much for you exceptional support!
That sounds very good, and I’ll try that next week.
Have a nice weekend!

1 Like

@alexey.noskov I checked your proposed solution in my program, and it works perfectly! Thank you very much, again! :slight_smile:

1 Like