From ff6fb92dc3b7ab0ce9c4d7a7cc4729d4f961da44 Mon Sep 17 00:00:00 2001 From: abhinavmir Date: Tue, 16 Jun 2026 15:45:23 -0700 Subject: [PATCH] Support custom mapping files in X12Reader X12Reader was limited to the standards enumerated in FileType. Add an X12Mapping interface (transaction definition + expected ANSI version) so callers can parse files against a mapping that is not built in. - FileType now implements X12Mapping. - CustomX12Mapping builds an X12Mapping from a user-supplied mapping XML stream and version string, reusing the same secured XStream loader. - X12Reader gains constructors accepting any X12Mapping (File, InputStream, Reader, with optional Charset); the existing FileType constructors delegate to them, so the public API is unchanged. Adds tests parsing a file through a custom mapping and verifying the result matches the built-in FileType, plus a version-mismatch rejection test. README documents the new usage. --- README.md | 15 +++ .../imsweb/x12/mapping/CustomX12Mapping.java | 40 +++++++ .../com/imsweb/x12/mapping/X12Mapping.java | 47 ++++++++ .../java/com/imsweb/x12/reader/X12Reader.java | 107 +++++++++++++----- .../com/imsweb/x12/reader/X12ReaderTest.java | 43 +++++++ 5 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/imsweb/x12/mapping/CustomX12Mapping.java create mode 100644 src/main/java/com/imsweb/x12/mapping/X12Mapping.java diff --git a/README.md b/README.md index 4b0e396..91dd540 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,21 @@ There may be errors in the structure that are severe enough that they prevent pr List errors = reader.getFatalErrors(); ``` +### Custom mapping files + +If you need to parse a standard that is not covered by a built-in `FileType`, you can supply your own mapping +definition XML (following the same structure as the files in the `mapping` directory). Wrap it in a +`CustomX12Mapping` with the expected ANSI version (the value of the GS08 element) and pass it to the reader: + +```java +try (InputStream mappingStream = new FileInputStream("/path/271.5010.X279.A1.xml")) { + X12Mapping mapping = new CustomX12Mapping("005010X279A1", mappingStream); + X12Reader reader = new X12Reader(mapping, new File("/path/file.txt")); +} +``` + +All `X12Reader` constructors accept either a built-in `FileType` or any `X12Mapping`. + ## Accessing Data You can access the data from the file using: diff --git a/src/main/java/com/imsweb/x12/mapping/CustomX12Mapping.java b/src/main/java/com/imsweb/x12/mapping/CustomX12Mapping.java new file mode 100644 index 0000000..6a28bc4 --- /dev/null +++ b/src/main/java/com/imsweb/x12/mapping/CustomX12Mapping.java @@ -0,0 +1,40 @@ +package com.imsweb.x12.mapping; + +import java.io.InputStream; + +/** + * An {@link X12Mapping} built from a user-supplied mapping definition XML document. Use this to + * parse X12 files whose specification is not one of the built-in {@code X12Reader.FileType} values. + *

+ * The mapping XML must follow the same structure as the built-in mapping files (see the + * {@code mapping} resources, e.g. {@code 999.5010.xml}). + */ +public class CustomX12Mapping implements X12Mapping { + + private final TransactionDefinition _definition; + private final String _version; + + /** + * @param version the ANSI version identifier expected in the GS08 element (e.g. {@code 005010X279A1}) + * @param mapping an input stream to the mapping definition XML; it is read fully by this constructor + * but not closed (the caller retains ownership) + */ + public CustomX12Mapping(String version, InputStream mapping) { + if (version == null) + throw new IllegalArgumentException("version cannot be null"); + if (mapping == null) + throw new IllegalArgumentException("mapping cannot be null"); + _version = version; + _definition = X12Mapping.load(mapping); + } + + @Override + public TransactionDefinition getTransactionDefinition() { + return _definition; + } + + @Override + public String getVersion() { + return _version; + } +} diff --git a/src/main/java/com/imsweb/x12/mapping/X12Mapping.java b/src/main/java/com/imsweb/x12/mapping/X12Mapping.java new file mode 100644 index 0000000..98b7a3b --- /dev/null +++ b/src/main/java/com/imsweb/x12/mapping/X12Mapping.java @@ -0,0 +1,47 @@ +package com.imsweb.x12.mapping; + +import java.io.InputStream; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.StaxDriver; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.WildcardTypePermission; + +/** + * Describes an X12 transaction mapping used by {@code X12Reader}: the parsed transaction + * definition and the ANSI version identifier (the value of the GS08 element) expected for + * files using this mapping. + *

+ * Implement this interface (or use {@link CustomX12Mapping}) to parse X12 files against a + * mapping that is not one of the built-in {@code X12Reader.FileType} values. + */ +public interface X12Mapping { + + /** + * @return the transaction definition that drives parsing + */ + TransactionDefinition getTransactionDefinition(); + + /** + * @return the ANSI version identifier expected in the GS08 element for this mapping + */ + String getVersion(); + + /** + * Parses a mapping definition XML document into a {@link TransactionDefinition}, using the + * same secured XStream configuration as the built-in mappings. + * @param mapping an input stream to the mapping definition XML; the caller is responsible for closing it + * @return the parsed transaction definition + */ + static TransactionDefinition load(InputStream mapping) { + XStream xstream = new XStream(new StaxDriver()); + xstream.autodetectAnnotations(true); + xstream.alias("transaction", TransactionDefinition.class); + + // setup proper security by limiting what classes can be loaded by XStream + xstream.addPermission(NoTypePermission.NONE); + xstream.addPermission(new WildcardTypePermission(new String[] {"com.imsweb.x12.**"})); + + return (TransactionDefinition)xstream.fromXML(mapping); + } +} diff --git a/src/main/java/com/imsweb/x12/reader/X12Reader.java b/src/main/java/com/imsweb/x12/reader/X12Reader.java index 48c1f6d..7c0a109 100644 --- a/src/main/java/com/imsweb/x12/reader/X12Reader.java +++ b/src/main/java/com/imsweb/x12/reader/X12Reader.java @@ -20,11 +20,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import com.thoughtworks.xstream.XStream; -import com.thoughtworks.xstream.io.xml.StaxDriver; -import com.thoughtworks.xstream.security.NoTypePermission; -import com.thoughtworks.xstream.security.WildcardTypePermission; - import com.imsweb.x12.Loop; import com.imsweb.x12.Segment; import com.imsweb.x12.Separators; @@ -34,6 +29,7 @@ import com.imsweb.x12.mapping.SegmentDefinition; import com.imsweb.x12.mapping.TransactionDefinition; import com.imsweb.x12.mapping.TransactionDefinition.Usage; +import com.imsweb.x12.mapping.X12Mapping; public class X12Reader { @@ -63,12 +59,12 @@ public class X12Reader { private final Map>> _childLoopTracker = new HashMap<>(); private Separators _separators; TransactionDefinition _definition; - private final FileType _type; + private final X12Mapping _mapping; /** * All supported X12 file definitions */ - public enum FileType { + public enum FileType implements X12Mapping { ANSI834_5010_X220("mapping/834.5010.X220.A1.xml"), ANSI835_5010_X221("mapping/835.5010.X221.A1.xml"), ANSI835_4010_X091("mapping/835.4010.X091.A1.xml"), @@ -96,17 +92,18 @@ public enum FileType { * @return a TransactionDefinition */ public synchronized TransactionDefinition getDefinition() { - return _DEFINITIONS.computeIfAbsent(_mapping, k -> { - XStream xstream = new XStream(new StaxDriver()); - xstream.autodetectAnnotations(true); - xstream.alias("transaction", TransactionDefinition.class); + return _DEFINITIONS.computeIfAbsent(_mapping, k -> + X12Mapping.load(Thread.currentThread().getContextClassLoader().getResourceAsStream(_mapping))); + } - // setup proper security by limiting what classes can be loaded by XStream - xstream.addPermission(NoTypePermission.NONE); - xstream.addPermission(new WildcardTypePermission(new String[] {"com.imsweb.x12.**"})); + @Override + public TransactionDefinition getTransactionDefinition() { + return getDefinition(); + } - return (TransactionDefinition)xstream.fromXML(Thread.currentThread().getContextClassLoader().getResourceAsStream(_mapping)); - }); + @Override + public String getVersion() { + return _TYPES.get(this); } } @@ -133,8 +130,7 @@ public synchronized TransactionDefinition getDefinition() { * @throws IOException if there was an error reading the input file */ public X12Reader(FileType type, File file) throws IOException { - this._type = type; - parse(new BufferedReader(new InputStreamReader(new FileInputStream(file), Charset.defaultCharset()))); + this((X12Mapping)type, file); } /** @@ -145,8 +141,7 @@ public X12Reader(FileType type, File file) throws IOException { * @throws IOException if there was an error reading the input file */ public X12Reader(FileType type, File file, Charset charset) throws IOException { - this._type = type; - parse(new BufferedReader(new InputStreamReader(new FileInputStream(file), charset))); + this((X12Mapping)type, file, charset); } /** @@ -156,8 +151,7 @@ public X12Reader(FileType type, File file, Charset charset) throws IOException { * @throws IOException if there was an error reading the input file */ public X12Reader(FileType type, InputStream input) throws IOException { - this._type = type; - parse(new BufferedReader(new InputStreamReader(input, Charset.defaultCharset()))); + this((X12Mapping)type, input); } /** @@ -168,8 +162,7 @@ public X12Reader(FileType type, InputStream input) throws IOException { * @throws IOException if there was an error reading the input file */ public X12Reader(FileType type, InputStream input, Charset charset) throws IOException { - this._type = type; - parse(new BufferedReader(new InputStreamReader(input, charset))); + this((X12Mapping)type, input, charset); } /** @@ -179,7 +172,63 @@ public X12Reader(FileType type, InputStream input, Charset charset) throws IOExc * @throws IOException if there was an error reading the input file */ public X12Reader(FileType type, Reader reader) throws IOException { - this._type = type; + this((X12Mapping)type, reader); + } + + /** + * Constructs an X12Reader using a File and a custom mapping + * @param mapping the X12 mapping describing the file's transaction definition and version + * @param file a File object representing the input file + * @throws IOException if there was an error reading the input file + */ + public X12Reader(X12Mapping mapping, File file) throws IOException { + this._mapping = mapping; + parse(new BufferedReader(new InputStreamReader(new FileInputStream(file), Charset.defaultCharset()))); + } + + /** + * Constructs an X12Reader using a File and a custom mapping + * @param mapping the X12 mapping describing the file's transaction definition and version + * @param file a File object representing the input file + * @param charset character encoding + * @throws IOException if there was an error reading the input file + */ + public X12Reader(X12Mapping mapping, File file, Charset charset) throws IOException { + this._mapping = mapping; + parse(new BufferedReader(new InputStreamReader(new FileInputStream(file), charset))); + } + + /** + * Constructs an X12Reader using an InputStream and a custom mapping, with default character encoding + * @param mapping the X12 mapping describing the file's transaction definition and version + * @param input an InputStream to an input file + * @throws IOException if there was an error reading the input file + */ + public X12Reader(X12Mapping mapping, InputStream input) throws IOException { + this._mapping = mapping; + parse(new BufferedReader(new InputStreamReader(input, Charset.defaultCharset()))); + } + + /** + * Constructs an X12Reader using an InputStream and a custom mapping + * @param mapping the X12 mapping describing the file's transaction definition and version + * @param input an InputStream to an input file + * @param charset character encoding + * @throws IOException if there was an error reading the input file + */ + public X12Reader(X12Mapping mapping, InputStream input, Charset charset) throws IOException { + this._mapping = mapping; + parse(new BufferedReader(new InputStreamReader(input, charset))); + } + + /** + * Constructs an X12Reader using a Reader and a custom mapping + * @param mapping the X12 mapping describing the file's transaction definition and version + * @param reader a Reader pointing to an input file + * @throws IOException if there was an error reading the input file + */ + public X12Reader(X12Mapping mapping, Reader reader) throws IOException { + this._mapping = mapping; // the Reader must support mark; if it does not, wrap the reader in a BufferedReader if (!reader.markSupported()) parse(new BufferedReader(reader)); @@ -239,7 +288,7 @@ private void parse(Reader reader) throws IOException { Loop lastLoopStored = null; // parse _definition file - _definition = _type.getDefinition(); + _definition = _mapping.getTransactionDefinition(); // cache definitions of loop starting segments getLoopConfiguration(_definition.getLoop(), null); @@ -392,7 +441,7 @@ else if (loop.getLoop() != null) } private boolean checkVersionsAreConsistent(Separators separators, Reader reader) throws IOException { - if (reader == null || separators == null || _type == null) + if (reader == null || separators == null || _mapping == null) return false; char segmentSeparator = separators.getSegment(); @@ -414,10 +463,10 @@ private boolean checkVersionsAreConsistent(Separators separators, Reader reader) } reader.reset(); - boolean result = _TYPES.get(_type).equals(version); + boolean result = _mapping.getVersion().equals(version); if (!result) - _errors.add("ANSI version " + version + " not consistent with version specified " + _type); + _errors.add("ANSI version " + version + " not consistent with version specified " + _mapping.getVersion()); return result; } diff --git a/src/test/java/com/imsweb/x12/reader/X12ReaderTest.java b/src/test/java/com/imsweb/x12/reader/X12ReaderTest.java index 4302012..547831e 100644 --- a/src/test/java/com/imsweb/x12/reader/X12ReaderTest.java +++ b/src/test/java/com/imsweb/x12/reader/X12ReaderTest.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -15,6 +16,7 @@ import com.imsweb.x12.Element; import com.imsweb.x12.Loop; +import com.imsweb.x12.mapping.CustomX12Mapping; import com.imsweb.x12.mapping.TransactionDefinition; import com.imsweb.x12.reader.X12Reader.FileType; @@ -1478,6 +1480,47 @@ void test277CARejected() throws Exception { assertEquals("85", statusCodeElement.getSubValues().get(2), "Should be able to see a approval status code - EIC"); } + @Test + void testCustomMapping() throws Exception { + // Parse a 270 file using a user-supplied mapping rather than the built-in FileType. The mapping + // XML and version are provided by the caller, so any spec not covered by FileType can be parsed. + URL data = this.getClass().getResource("/x270_271/x270.txt"); + assertNotNull(data); + + try (InputStream mappingStream = Thread.currentThread().getContextClassLoader() + .getResourceAsStream("mapping/270.4010.X092.A1.xml")) { + assertNotNull(mappingStream); + CustomX12Mapping mapping = new CustomX12Mapping("004010X092A1", mappingStream); + + X12Reader reader = new X12Reader(mapping, new File(data.getFile())); + assertTrue(reader.getFatalErrors().isEmpty()); + List loops = reader.getLoops(); + assertEquals(1, loops.size()); + assertEquals(1, loops.get(0).getLoops().size()); + + // result must match parsing the same file through the built-in FileType + X12Reader builtIn = new X12Reader(FileType.ANSI270_4010_X092, new File(data.getFile())); + assertEquals(builtIn.getLoops().get(0).toString(), loops.get(0).toString()); + } + } + + @Test + void testCustomMappingVersionMismatch() throws Exception { + URL data = this.getClass().getResource("/x270_271/x270.txt"); + assertNotNull(data); + + try (InputStream mappingStream = Thread.currentThread().getContextClassLoader() + .getResourceAsStream("mapping/270.4010.X092.A1.xml")) { + assertNotNull(mappingStream); + // wrong version: the file is 004010X092A1, so parsing must be rejected + CustomX12Mapping mapping = new CustomX12Mapping("005010X279A1", mappingStream); + + X12Reader reader = new X12Reader(mapping, new File(data.getFile())); + assertTrue(reader.getLoops().isEmpty()); + assertFalse(reader.getErrors().isEmpty()); + } + } + @Test void test999Accepted() throws Exception { URL url = this.getClass().getResource("/837_5010/x12_999_accepted.txt");