Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ import quickfix.Group;</xsl:if>
</xsl:if>
</xsl:template>

<!-- New: ensure recursion continues inside component definitions (no group ancestor needed) -->
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions "no group ancestor needed" but this template will match ANY component in group-delimeter mode, not just those without a group ancestor. The group//component template at line 200 is more specific and will take precedence for components that are descendants of groups, so this template will handle components at position 1 that are either:

  1. Direct children of the group being processed (when recursing through component definitions)
  2. Nested within other components

Consider clarifying the comment to: "Recursively process components when they are not direct descendants of a group element in the XML (e.g., when referenced through component definitions)"

Suggested change
<!-- New: ensure recursion continues inside component definitions (no group ancestor needed) -->
<!-- Recursively process components when they are not direct descendants of a group element in the XML (e.g., when referenced through component definitions) -->

Copilot uses AI. Check for mistakes.
<xsl:template mode="group-delimeter" match="component">
<xsl:if test="position() = 1">
<xsl:variable name="name" select="@name"/>
<xsl:apply-templates select="/fix/components/component[@name=$name]/*[name(.)='field' or name(.)='group' or name(.)='component']"
mode="group-delimeter"/>
</xsl:if>
</xsl:template>

<!-- Find the component numbers and order -->

<xsl:template mode="component-field-numbers" match="field">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.quickfixj.codegenerator;

import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Test;

/**
* Verifies that the code generator sets the correct group delimiter when the
* first element of a repeating group is a component that nests other components
* before the first concrete field appears.
*
* This covers the regression fixed in PR #1089 where the XSL must recurse into
* deeply nested components to locate the actual first field (delimiter).
*/
public class NestedGroupDelimiterTest {

private final File outputDirectory = new File("./target/test-output/");
private final File dictFile = new File("./src/test/resources/NESTED_FIX.xml");
private final File schemaDirectory = new File("./src/main/resources/org/quickfixj/codegenerator");

@Before
public void setup() throws IOException {
if (outputDirectory.exists()) {
FileUtils.cleanDirectory(outputDirectory);
} else {
outputDirectory.mkdirs();
}
}

@Test
public void generatesGroupWithDeeplyNestedDelimiter() throws Exception {
MessageCodeGenerator generator = new MessageCodeGenerator();
MessageCodeGenerator.Task task = new MessageCodeGenerator.Task();

task.setSpecification(dictFile);
task.setName(dictFile.getName());
task.setTransformDirectory(schemaDirectory);
task.setMessagePackage("quickfix.fixZZ");
task.setOutputBaseDirectory(outputDirectory);
task.setFieldPackage("quickfix.field");
task.setUtcTimestampPrecision(null);
task.setOverwrite(true);
task.setOrderedFields(true);
task.setDecimalGenerated(true);

try {
generator.generate(task);
} catch (CodeGenerationException e) {
// Surface with context for easier debugging in CI
throw new AssertionError("Code generation failed: " + e.getMessage(), e);
}

// Read the generated message class and assert the group constructor uses FieldA (1001) as delimiter
File generated = new File(outputDirectory, "quickfix/fixZZ/NestedGroupTest.java");
assertTrue("Expected generated message class not found: " + generated.getAbsolutePath(), generated.exists());

String java = FileUtils.readFileToString(generated, StandardCharsets.UTF_8);

// The group name is NoOuterGrp with counter tag 1000. The delimiter should be FieldA (1001),
// which is the first field reachable by recursing into components (InnerComponent -> DeepComponent -> FieldA)
// The generated constructor should therefore look like: super(1000, 1001, ORDER)
assertTrue(
"Group constructor should use 1001 as delimiter for NoOuterGrp (deeply nested first field)",
java.contains("public static class NoOuterGrp") &&
java.contains("super(1000, 1001, ORDER)")
);
}
}
31 changes: 31 additions & 0 deletions quickfixj-codegenerator/src/test/resources/NESTED_FIX.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<fix major="4" type="FIX" minor="4">
<header>
</header>
<messages>
<message name="NestedGroupTest" msgtype="ZZ" msgcat="app">
<group name="NoOuterGrp" required="N">
<component name="InnerComponent" required="N"/>
</group>
</message>
</messages>
<components>
<component name="InnerComponent">
<component name="DeepComponent"/>
</component>
<component name="DeepComponent">
<!-- The first concrete field deep inside the component tree -->
<field name="FieldA" required="N"/>
<field name="FieldB" required="N"/>
</component>
</components>
<fields>
<!-- Group counter field -->
<field number="1000" name="NoOuterGrp" type="NUMINGROUP"/>
<!-- Deepest first field that should become the group delimiter -->
<field number="1001" name="FieldA" type="STRING"/>
<field number="1002" name="FieldB" type="STRING"/>
</fields>
<trailer>
</trailer>
</fix>