Skip to content

Commit 735a2a4

Browse files
#429: Added a peek function to the factory that decides whether .xml files are ReqM2 files (#469)
#429: Added a peek function to the factory that decides whether .xml files are ReqM2 files Co-authored-by: Christoph Pirkl <[email protected]>
1 parent d0da212 commit 735a2a4

File tree

8 files changed

+184
-16
lines changed

8 files changed

+184
-16
lines changed

.github/workflows/broken_links_checker.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ jobs:
1515
- name: Configure broken links checker
1616
run: |
1717
mkdir -p ./target
18-
echo '{ "aliveStatusCodes": [429, 200] }' > ./target/broken_links_checker.json
18+
echo '{
19+
"aliveStatusCodes": [429, 200],
20+
"ignorePatterns": [
21+
{
22+
# The Autosar TLS certificate cannot be validated from GitHub Actions.
23+
"pattern": "^https://www.autosar.org/"
24+
}
25+
]
26+
}' > ./target/broken_links_checker.json
1927
- uses: tcort/github-action-markdown-link-check@v1
2028
with:
2129
use-quiet-mode: "yes"

doc/changes/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changes
22

3+
+ [4.2.1](changes_4.2.1.md)
34
* [4.2.0](changes_4.2.0.md)
45
* [4.1.0](changes_4.1.0.md)
56
* [4.0.2](changes_4.0.2.md)

doc/changes/changes_4.2.1.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# OpenFastTrace 4.2.1, released 2025-09-14
2+
3+
Code name: Peek before you guess
4+
5+
## Summary
6+
7+
This is a bugfix release. It addresses the problem that ReqM2 uses a generic `.xml` file suffix which led OpenFastTrace to treat any XML file as a ReqM2 file based solely on the extension. To avoid false detections, this version adds a peek function to the file type detection so that we no longer rely only on the file type/extension (#429).
8+
9+
## Bugfixes
10+
11+
* #429: Add content peek to file type detection so `.xml` no longer implies ReqM2; prevents misclassification of arbitrary XML files

doc/spec/design.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
## Acknowledgments
99

10-
This documents structure is derived from the "[arc42][bib.arc42]" architectural template by Dr. Gernot Starke, Dr. Peter Hruschka.
10+
This document's structure is derived from the "[arc42][bib.arc42]" architectural template by Dr. Gernot Starke, Dr. Peter Hruschka.
1111

1212
If you build your own modifications based on this document, please keep the attrbiutions.
1313

@@ -174,9 +174,25 @@ Needs: impl, itest
174174

175175
## Import
176176

177-
Depending on the source format a variety of [importers](#importers) takes care of reading the input [specification items](#specification-item). Each importer emits events which an [import event listener](#import-event-listener) consumes.
177+
Depending on the source format, a variety of [importers](#importers) takes care of reading the input [specification items](#specification-item). Each importer emits events which an [import event listener](#import-event-listener) consumes.
178178

179-
Common parts of the import like filtering out unnecessary items or attributes are handled by the listener.
179+
The listener handles Common parts of the import like filtering out unnecessary items or attributes.
180+
181+
A factory for importers decides which importer to use. Usually, by file extension.
182+
183+
### ReqM2 File Detection
184+
`dsn~import.reqm2-file-detection~1`
185+
186+
The `SpecobjectImporterFactory` detects ReqM2 files either
187+
188+
1. via the file extension `.oreqm` or
189+
2. via the file extension `.xml` and the presence of the string `<specdocument` within the first 4096 bytes of the file.
190+
191+
Covers:
192+
193+
* `req~import.reqm2-file-detection~1`
194+
195+
Needs: impl, utest
180196

181197
### Selective Artifact Type Import
182198

doc/spec/system_requirements.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Needs: dsn
255255

256256
#### Common Requirements for Lightweight Markup Import
257257

258-
Typical OFT specification are written in a lightweight markup language like [Markdown](#markdown-import) or [ReStructured Text](#restructured-text-rst-import). Before we go into the specifics, this section discusses the common requirements.
258+
Typical OFT specifications are written in a lightweight markup language like [Markdown](#markdown-import) or [ReStructured Text](#restructured-text-rst-import). Before we go into the specifics, this section discusses the common requirements.
259259

260260
##### Disabling OFT Parsing for Parts of a Markup File
261261
`req~disabling-oft-parsing-for-parts-of-a-markup-file~1`
@@ -295,7 +295,7 @@ Markdown focuses on content over formatting by giving the document structure lik
295295

296296
OFT defines a Markdown format that we call "Requirement-Enhanced Markdown" which is a superset of the regular Markdown. Any Markdown renderer can render this format without understanding it. The additional structural definitions tell OFT which part of the text is a specification item.
297297

298-
For backward compatibility OFT supports a variant of this format that was introduced at Elektrobit. This format is a little bit closer to ReqM2, the predecessor that sparked the OFT idea. We recommend using standard OFT Markdown format in new documents though since this format is cleaner.
298+
For backward compatibility OFT supports a variant of this format that was introduced at Elektrobit. This format is a little bit closer to ReqM2, the predecessor that sparked the OFT idea. We recommend using the standard OFT Markdown format in new documents, though, since this format is cleaner.
299299

300300
##### Markdown Standard Syntax
301301
`req~markdown-standard-syntax~1`
@@ -319,7 +319,7 @@ The Markdown outline -- a table of contents created from the heading structure b
319319

320320
Rationale:
321321

322-
In long specification document the outline is the primary means of navigating the document. Only if the outline can be read easily, it is useful for authoring specification documents.
322+
In long specification documents the outline is the primary means of navigating the document. Only if the outline can be read easily, it is useful for authoring specification documents.
323323

324324
Covers:
325325

@@ -368,6 +368,24 @@ Covers:
368368

369369
Needs: dsn
370370

371+
#### ReqM2 Format
372+
373+
ReqM2 is a markup format developed at Elektrobit. ReqM2 files traditionally use the `.xml` file extension, a better way is to use `.oreqm` instead since that identifies the files uniquely.
374+
375+
##### ReqM2 File Detection
376+
`req~import.reqm2-file-detection~1`
377+
378+
OFT considers a file to be a ReqM2 file if it either
379+
380+
1. has a the `.oreqm` extension or
381+
2. has a the `.xml` extension and the header indicates that it is a ReqM2 file.
382+
383+
Covers:
384+
385+
* [feat~reqm2-import~1](#reqm2-import)
386+
387+
Needs: dsn
388+
371389
### Tracing
372390

373391
#### Outgoing Coverage Link Status
@@ -467,13 +485,13 @@ Usually the responsibility of document authors or coders when it comes to tracin
467485

468486
If the users try to run a regular trace without feeding in the artifacts all the way to the bottom level of the tracing chain, the coverage check will always report errors because of missing lower level coverage.
469487

470-
To mitigate the situation OFT allows users to ignore required coverage for selected artifact types.
488+
To mitigate the situation, OFT allows users to ignore required coverage for selected artifact types.
471489

472490
Example:
473491

474-
Kim is a software architect and it is her job to cover the system requirements coming from Steve in her software architecture. Kim wants to make sure she did not forget to cover a system requirement and uses OFT to trace the two documents. The system requirement specification uses the artifact types `feat` and `req` where `req` covers the `feat` artifacts in the same document. Kim's architecture uses the artifact type `sysarch` which covers `req` and requires a detailed design `dsn`.
492+
Kim is a software architect, and it is her job to cover the system requirements coming from Steve in her software architecture. Kim wants to make sure she did not forget to cover a system requirement and uses OFT to trace the two documents. The system requirement specification uses the artifact types `feat` and `req` where `req` covers the `feat` artifacts in the same document. Kim's architecture uses the artifact type `sysarch` which covers `req` and requires a detailed design `dsn`.
475493

476-
Obviously the detailed design is missing at the point when Kim runs the trace. To mitigate this situation Kim configures OFT to ignore all artifacts of type `dsn`, including the needed coverage. This allows Kim to validate coverage towards the system requirement without needing the detailed design document.
494+
Obviously, the detailed design is missing at the point when Kim runs the trace. To mitigate this situation, Kim configures OFT to ignore all artifacts of type `dsn`, including the needed coverage. This allows Kim to validate coverage towards the system requirement without needing the detailed design document.
477495

478496
#### Include Only Artifact Types
479497
`req~include-only-artifact-types~1`

importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/SpecobjectImporterFactory.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,71 @@
11
package org.itsallcode.openfasttrace.importer.specobject;
22

3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.util.Locale;
6+
import java.util.logging.Logger;
7+
38
import org.itsallcode.openfasttrace.api.importer.*;
49
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
510
import org.itsallcode.openfasttrace.importer.xmlparser.XmlParserFactory;
611

712
/**
8-
* An {@link ImporterFactory} for XML specobject files.
13+
* An {@link ImporterFactory} for ReqM2/SpecObject XML files.
914
*/
10-
public class SpecobjectImporterFactory extends RegexMatchingImporterFactory
15+
public class SpecobjectImporterFactory extends ImporterFactory
1116
{
17+
private static final Logger LOG = Logger.getLogger(SpecobjectImporterFactory.class.getName());
18+
private static final int PEEK_CHARS = 4096;
19+
1220
private final XmlParserFactory xmlParserFactory;
1321

1422
/**
1523
* Create a new instance.
1624
*/
1725
public SpecobjectImporterFactory()
1826
{
19-
super("(?i).*\\.(xml|oreqm)");
2027
this.xmlParserFactory = new XmlParserFactory();
2128
}
2229

30+
// [impl -> dsn~import.reqm2-file-detection~1]
31+
@Override
32+
public boolean supportsFile(final InputFile file)
33+
{
34+
final String path = file.getPath();
35+
final String lower = path.toLowerCase(Locale.ROOT);
36+
if (lower.endsWith(".oreqm"))
37+
{
38+
return true;
39+
}
40+
else if (lower.endsWith(".xml"))
41+
{
42+
return doesFileContainOreqmHeader(file, path);
43+
} else {
44+
return false;
45+
}
46+
}
47+
48+
private static boolean doesFileContainOreqmHeader(final InputFile file, final String path) {
49+
try (BufferedReader reader = file.createReader())
50+
{
51+
final char[] buf = new char[PEEK_CHARS];
52+
final int read = reader.read(buf);
53+
if (read <= 0)
54+
{
55+
return false;
56+
}
57+
else {
58+
final String header = new String(buf, 0, read);
59+
return header.contains("<specdocument");
60+
}
61+
}
62+
catch (final IOException exception)
63+
{
64+
LOG.fine(() -> "Unable to peek XML file '" + path + "' trying to determine if it contains ReqM2 format: " + exception.getMessage());
65+
return false;
66+
}
67+
}
68+
2369
@Override
2470
public Importer createImporter(final InputFile file, final ImportEventListener listener)
2571
{

importer/specobject/src/test/java/org/itsallcode/openfasttrace/importer/specobject/TestSpecobjectImporterFactory.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
package org.itsallcode.openfasttrace.importer.specobject;
22

33
import static java.util.Arrays.asList;
4+
import static org.hamcrest.MatcherAssert.assertThat;
5+
import static org.hamcrest.Matchers.equalTo;
6+
import static org.mockito.Mockito.when;
47

8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
511
import java.util.List;
612

13+
import org.itsallcode.openfasttrace.api.importer.input.InputFile;
14+
import org.itsallcode.openfasttrace.api.importer.input.RealFileInput;
715
import org.itsallcode.openfasttrace.testutil.importer.ImporterFactoryTestBase;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.io.TempDir;
18+
import org.mockito.Mock;
819

920
/**
1021
* Tests for {@link SpecobjectImporterFactory}
@@ -19,16 +30,73 @@ protected SpecobjectImporterFactory createFactory()
1930
return new SpecobjectImporterFactory();
2031
}
2132

33+
/**
34+
* Only the {@code .oreqm} extension is always supported. That is why we
35+
* can't simply list {@code .xml} here. Whether {@code .xml} is supported
36+
* depends on a successful peek that confirms the file is in ReqM2 format.
37+
*
38+
* @return list of files that need to pass the test for supported files
39+
*/
2240
@Override
2341
protected List<String> getSupportedFilenames()
2442
{
25-
return asList("file.xml", "file.XML", "FILE.xml", "FILE.XML", "file.md.xml");
43+
return asList("file.oreqm", "file.OREQM");
2644
}
2745

2846
@Override
2947
protected List<String> getUnsupportedFilenames()
3048
{
3149
return asList("file.md", "file.xm", "file.ml", "file.1xml", "file.xml1", "file.xml.md",
32-
"file_xml", "filexml");
50+
"file_xml", "filexml", "file_oreqm", "fileoreqm");
51+
}
52+
53+
// [utest ->dsn~import.reqm2-file-detection~1]
54+
@Test
55+
void supportsXmlIfContentLooksLikeReqM2(@TempDir final Path tempDir) throws IOException
56+
{
57+
final Path tempFile = tempDir.resolve("reqm2-specdocument.xml");
58+
Files.writeString(tempFile, """
59+
<?xml version="1.0"?>
60+
<specdocument>
61+
<specobjects doctype="REQ"/>
62+
</specdocument>
63+
""");
64+
final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile));
65+
assertThat(supported, equalTo(true));
66+
}
67+
68+
// [utest ->dsn~import.reqm2-file-detection~1]
69+
@Test
70+
void doesNotSupportXmlIfContentIsGeneric(@TempDir final Path tempDir) throws IOException
71+
{
72+
final Path tempFile = tempDir.resolve("generic.xml");
73+
Files.writeString(tempFile, """
74+
<?xml version="1.0"?>
75+
<root>
76+
<child/>
77+
</root>"
78+
""");
79+
final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile));
80+
assertThat(supported, equalTo(false));
81+
}
82+
83+
// [utest ->dsn~import.reqm2-file-detection~1]
84+
@Test
85+
void givenEmptyFileWhenCheckingReqM2HeaderThenDoesNotClaimSupport(@TempDir final Path tempDir) throws IOException
86+
{
87+
final Path tempFile = tempDir.resolve("empty.xml");
88+
Files.writeString(tempFile, "");
89+
final boolean supported = createFactory().supportsFile(RealFileInput.forPath(tempFile));
90+
assertThat(supported, equalTo(false));
91+
}
92+
93+
// [utest ->dsn~import.reqm2-file-detection~1]
94+
@Test
95+
void givenFileWhenIOExceptionOccursThenDoesNotClaimSupport(@Mock InputFile mockInputFile) throws IOException
96+
{
97+
when(mockInputFile.getPath()).thenReturn("/irrelevant/path/to/file.xml");
98+
when(mockInputFile.createReader()).thenThrow(new IOException("This is an expected test exception"));
99+
final boolean supported = createFactory().supportsFile(mockInputFile);
100+
assertThat(supported, equalTo(false));
33101
}
34102
}

parent/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<description>Free requirement tracking suite</description>
1111
<url>https://github.com/itsallcode/openfasttrace</url>
1212
<properties>
13-
<revision>4.2.0</revision>
13+
<revision>4.2.1</revision>
1414
<java.version>17</java.version>
1515
<junit.version>5.12.2</junit.version>
1616
<maven.surefire.version>3.5.3</maven.surefire.version>

0 commit comments

Comments
 (0)